Send framework export by email
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -29,13 +29,15 @@ import (
|
|||||||
|
|
||||||
type (
|
type (
|
||||||
FrameworkExport struct {
|
FrameworkExport struct {
|
||||||
ID gid.GID `db:"id"`
|
ID gid.GID `db:"id"`
|
||||||
FrameworkID gid.GID `db:"framework_id"`
|
FrameworkID gid.GID `db:"framework_id"`
|
||||||
Status FrameworkExportStatus `db:"status"`
|
RecipientEmail string `db:"recipient_email"`
|
||||||
FileID *gid.GID `db:"file_id"`
|
RecipientName string `db:"recipient_name"`
|
||||||
CreatedAt time.Time `db:"created_at"`
|
Status FrameworkExportStatus `db:"status"`
|
||||||
StartedAt *time.Time `db:"started_at"`
|
FileID *gid.GID `db:"file_id"`
|
||||||
CompletedAt *time.Time `db:"completed_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
|
StartedAt *time.Time `db:"started_at"`
|
||||||
|
CompletedAt *time.Time `db:"completed_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
FrameworkExports []*FrameworkExport
|
FrameworkExports []*FrameworkExport
|
||||||
@@ -64,22 +66,28 @@ INSERT INTO framework_exports (
|
|||||||
id,
|
id,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
framework_id,
|
framework_id,
|
||||||
|
recipient_email,
|
||||||
|
recipient_name,
|
||||||
status,
|
status,
|
||||||
created_at
|
created_at
|
||||||
) VALUES (
|
) VALUES (
|
||||||
@id,
|
@id,
|
||||||
@tenant_id,
|
@tenant_id,
|
||||||
@framework_id,
|
@framework_id,
|
||||||
|
@recipient_email,
|
||||||
|
@recipient_name,
|
||||||
@status,
|
@status,
|
||||||
@created_at
|
@created_at
|
||||||
)`
|
)`
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"id": fe.ID,
|
"id": fe.ID,
|
||||||
"tenant_id": scope.GetTenantID(),
|
"tenant_id": scope.GetTenantID(),
|
||||||
"framework_id": fe.FrameworkID,
|
"framework_id": fe.FrameworkID,
|
||||||
"status": fe.Status,
|
"recipient_email": fe.RecipientEmail,
|
||||||
"created_at": fe.CreatedAt,
|
"recipient_name": fe.RecipientName,
|
||||||
|
"status": fe.Status,
|
||||||
|
"created_at": fe.CreatedAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := conn.Exec(ctx, q, args)
|
_, err := conn.Exec(ctx, q, args)
|
||||||
@@ -127,6 +135,8 @@ func (fe *FrameworkExport) LoadNextPendingForUpdateSkipLocked(
|
|||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
framework_id,
|
framework_id,
|
||||||
|
recipient_email,
|
||||||
|
recipient_name,
|
||||||
status,
|
status,
|
||||||
file_id,
|
file_id,
|
||||||
created_at,
|
created_at,
|
||||||
|
|||||||
7
pkg/coredata/migrations/20250911T114906Z.sql
Normal file
7
pkg/coredata/migrations/20250911T114906Z.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
ALTER TABLE framework_exports ADD COLUMN recipient_email CITEXT NOT NULL DEFAULT '';
|
||||||
|
|
||||||
|
ALTER TABLE framework_exports ALTER COLUMN recipient_email DROP DEFAULT;
|
||||||
|
|
||||||
|
ALTER TABLE framework_exports ADD COLUMN recipient_name TEXT NOT NULL DEFAULT '';
|
||||||
|
|
||||||
|
ALTER TABLE framework_exports ALTER COLUMN recipient_name DROP DEFAULT;
|
||||||
@@ -34,7 +34,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
maxStateOfApplicabilityLimit = 10_000
|
maxStateOfApplicabilityLimit = 10_000
|
||||||
|
frameworkExportEmailExpiresIn = 24 * time.Hour
|
||||||
|
frameworkExportEmailSubject = "Your framework export is ready"
|
||||||
|
frameworkExportEmailBody = `
|
||||||
|
Your framework export has been completed successfully.
|
||||||
|
|
||||||
|
You can download the export using the link below:
|
||||||
|
[1] %s
|
||||||
|
|
||||||
|
This link will expire in 24 hours.`
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -71,6 +80,8 @@ type (
|
|||||||
func (s FrameworkService) RequestExport(
|
func (s FrameworkService) RequestExport(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
frameworkID gid.GID,
|
frameworkID gid.GID,
|
||||||
|
recipientEmail string,
|
||||||
|
recipientName string,
|
||||||
) (error, *coredata.FrameworkExport) {
|
) (error, *coredata.FrameworkExport) {
|
||||||
frameworkExport := &coredata.FrameworkExport{}
|
frameworkExport := &coredata.FrameworkExport{}
|
||||||
|
|
||||||
@@ -83,10 +94,12 @@ func (s FrameworkService) RequestExport(
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
frameworkExport = &coredata.FrameworkExport{
|
frameworkExport = &coredata.FrameworkExport{
|
||||||
ID: gid.New(framework.ID.TenantID(), coredata.FrameworkExportEntityType),
|
ID: gid.New(framework.ID.TenantID(), coredata.FrameworkExportEntityType),
|
||||||
FrameworkID: frameworkID,
|
FrameworkID: frameworkID,
|
||||||
Status: coredata.FrameworkExportStatusPending,
|
Status: coredata.FrameworkExportStatusPending,
|
||||||
CreatedAt: now,
|
RecipientEmail: recipientEmail,
|
||||||
|
RecipientName: recipientName,
|
||||||
|
CreatedAt: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := frameworkExport.Insert(ctx, conn, s.svc.scope); err != nil {
|
if err := frameworkExport.Insert(ctx, conn, s.svc.scope); err != nil {
|
||||||
@@ -661,3 +674,65 @@ func (s FrameworkService) StateOfApplicability(ctx context.Context, frameworkID
|
|||||||
|
|
||||||
return output, nil
|
return output, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s FrameworkService) SendFrameworkExportEmail(
|
||||||
|
ctx context.Context,
|
||||||
|
fileID gid.GID,
|
||||||
|
recipientName string,
|
||||||
|
recipientEmail string,
|
||||||
|
) error {
|
||||||
|
return s.svc.pg.WithTx(
|
||||||
|
ctx,
|
||||||
|
func(tx pg.Conn) error {
|
||||||
|
file := &coredata.File{}
|
||||||
|
if err := file.LoadByID(ctx, tx, s.svc.scope, fileID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadURL, err := s.GenerateFrameworkExportDownloadURL(ctx, file)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot generate download URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
email := coredata.NewEmail(
|
||||||
|
recipientName,
|
||||||
|
recipientEmail,
|
||||||
|
frameworkExportEmailSubject,
|
||||||
|
fmt.Sprintf(frameworkExportEmailBody, downloadURL),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := email.Insert(ctx, tx); err != nil {
|
||||||
|
return fmt.Errorf("cannot insert email: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s FrameworkService) GenerateFrameworkExportDownloadURL(
|
||||||
|
ctx context.Context,
|
||||||
|
file *coredata.File,
|
||||||
|
) (string, error) {
|
||||||
|
presignClient := s3.NewPresignClient(s.svc.s3)
|
||||||
|
|
||||||
|
presignedReq, err := presignClient.PresignGetObject(
|
||||||
|
ctx,
|
||||||
|
&s3.GetObjectInput{
|
||||||
|
Bucket: ref.Ref(s.svc.bucket),
|
||||||
|
Key: ref.Ref(file.FileKey),
|
||||||
|
ResponseCacheControl: ref.Ref("max-age=3600, public"),
|
||||||
|
ResponseContentType: ref.Ref(file.MimeType),
|
||||||
|
ResponseContentDisposition: ref.Ref(fmt.Sprintf("attachment; filename=\"%s\"", file.FileName)),
|
||||||
|
},
|
||||||
|
func(opts *s3.PresignOptions) {
|
||||||
|
opts.Expires = frameworkExportEmailExpiresIn
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("cannot presign GetObject request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return presignedReq.URL, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -210,6 +210,23 @@ func (s *Service) ExportFrameworkJob(ctx context.Context) error {
|
|||||||
return fmt.Errorf("cannot build and upload framework export: %w", buildErr)
|
return fmt.Errorf("cannot build and upload framework export: %w", buildErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tenantService := s.WithTenant(scope.GetTenantID())
|
||||||
|
if emailErr := tenantService.Frameworks.SendFrameworkExportEmail(ctx, *fe.FileID, fe.RecipientName, fe.RecipientEmail); emailErr != nil {
|
||||||
|
if err := s.commitFailedExport(ctx, scope, fe); err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"cannot send completion email: %w, and cannot commit failed export: %w",
|
||||||
|
emailErr,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("cannot send completion email: %w", emailErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.commitSuccessfulExport(ctx, scope, fe); err != nil {
|
||||||
|
return fmt.Errorf("cannot commit successful export: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,8 +331,6 @@ func (s *Service) buildAndUploadExport(ctx context.Context, scope coredata.Scope
|
|||||||
}
|
}
|
||||||
|
|
||||||
fe.FileID = &file.ID
|
fe.FileID = &file.ID
|
||||||
fe.CompletedAt = ref.Ref(time.Now())
|
|
||||||
fe.Status = coredata.FrameworkExportStatusCompleted
|
|
||||||
if err := fe.Update(ctx, tx, scope); err != nil {
|
if err := fe.Update(ctx, tx, scope); err != nil {
|
||||||
return fmt.Errorf("cannot update framework export: %w", err)
|
return fmt.Errorf("cannot update framework export: %w", err)
|
||||||
}
|
}
|
||||||
@@ -346,3 +361,19 @@ func (s *Service) commitFailedExport(ctx context.Context, scope coredata.Scoper,
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) commitSuccessfulExport(ctx context.Context, scope coredata.Scoper, fe *coredata.FrameworkExport) error {
|
||||||
|
fe.CompletedAt = ref.Ref(time.Now())
|
||||||
|
fe.Status = coredata.FrameworkExportStatusCompleted
|
||||||
|
|
||||||
|
return s.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
if err := fe.Update(ctx, conn, scope); err != nil {
|
||||||
|
return fmt.Errorf("cannot update framework export: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1626,7 +1626,18 @@ func (r *mutationResolver) GenerateFrameworkStateOfApplicability(ctx context.Con
|
|||||||
// ExportFramework is the resolver for the exportFramework field.
|
// ExportFramework is the resolver for the exportFramework field.
|
||||||
func (r *mutationResolver) ExportFramework(ctx context.Context, input types.ExportFrameworkInput) (*types.ExportFrameworkPayload, error) {
|
func (r *mutationResolver) ExportFramework(ctx context.Context, input types.ExportFrameworkInput) (*types.ExportFrameworkPayload, error) {
|
||||||
prb := r.ProboService(ctx, input.FrameworkID.TenantID())
|
prb := r.ProboService(ctx, input.FrameworkID.TenantID())
|
||||||
err, exportJobID := prb.Frameworks.RequestExport(ctx, input.FrameworkID)
|
|
||||||
|
user := UserFromContext(ctx)
|
||||||
|
if user == nil {
|
||||||
|
panic(fmt.Errorf("user not found"))
|
||||||
|
}
|
||||||
|
|
||||||
|
err, exportJobID := prb.Frameworks.RequestExport(
|
||||||
|
ctx,
|
||||||
|
input.FrameworkID,
|
||||||
|
user.FullName,
|
||||||
|
user.EmailAddress,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot export framework: %w", err)
|
return nil, fmt.Errorf("cannot export framework: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user