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 (
|
||||
FrameworkExport struct {
|
||||
ID gid.GID `db:"id"`
|
||||
FrameworkID gid.GID `db:"framework_id"`
|
||||
Status FrameworkExportStatus `db:"status"`
|
||||
FileID *gid.GID `db:"file_id"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
StartedAt *time.Time `db:"started_at"`
|
||||
CompletedAt *time.Time `db:"completed_at"`
|
||||
ID gid.GID `db:"id"`
|
||||
FrameworkID gid.GID `db:"framework_id"`
|
||||
RecipientEmail string `db:"recipient_email"`
|
||||
RecipientName string `db:"recipient_name"`
|
||||
Status FrameworkExportStatus `db:"status"`
|
||||
FileID *gid.GID `db:"file_id"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
StartedAt *time.Time `db:"started_at"`
|
||||
CompletedAt *time.Time `db:"completed_at"`
|
||||
}
|
||||
|
||||
FrameworkExports []*FrameworkExport
|
||||
@@ -64,22 +66,28 @@ INSERT INTO framework_exports (
|
||||
id,
|
||||
tenant_id,
|
||||
framework_id,
|
||||
recipient_email,
|
||||
recipient_name,
|
||||
status,
|
||||
created_at
|
||||
) VALUES (
|
||||
@id,
|
||||
@tenant_id,
|
||||
@framework_id,
|
||||
@recipient_email,
|
||||
@recipient_name,
|
||||
@status,
|
||||
@created_at
|
||||
)`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"id": fe.ID,
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"framework_id": fe.FrameworkID,
|
||||
"status": fe.Status,
|
||||
"created_at": fe.CreatedAt,
|
||||
"id": fe.ID,
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"framework_id": fe.FrameworkID,
|
||||
"recipient_email": fe.RecipientEmail,
|
||||
"recipient_name": fe.RecipientName,
|
||||
"status": fe.Status,
|
||||
"created_at": fe.CreatedAt,
|
||||
}
|
||||
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
@@ -127,6 +135,8 @@ func (fe *FrameworkExport) LoadNextPendingForUpdateSkipLocked(
|
||||
SELECT
|
||||
id,
|
||||
framework_id,
|
||||
recipient_email,
|
||||
recipient_name,
|
||||
status,
|
||||
file_id,
|
||||
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 (
|
||||
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 (
|
||||
@@ -71,6 +80,8 @@ type (
|
||||
func (s FrameworkService) RequestExport(
|
||||
ctx context.Context,
|
||||
frameworkID gid.GID,
|
||||
recipientEmail string,
|
||||
recipientName string,
|
||||
) (error, *coredata.FrameworkExport) {
|
||||
frameworkExport := &coredata.FrameworkExport{}
|
||||
|
||||
@@ -83,10 +94,12 @@ func (s FrameworkService) RequestExport(
|
||||
now := time.Now()
|
||||
|
||||
frameworkExport = &coredata.FrameworkExport{
|
||||
ID: gid.New(framework.ID.TenantID(), coredata.FrameworkExportEntityType),
|
||||
FrameworkID: frameworkID,
|
||||
Status: coredata.FrameworkExportStatusPending,
|
||||
CreatedAt: now,
|
||||
ID: gid.New(framework.ID.TenantID(), coredata.FrameworkExportEntityType),
|
||||
FrameworkID: frameworkID,
|
||||
Status: coredata.FrameworkExportStatusPending,
|
||||
RecipientEmail: recipientEmail,
|
||||
RecipientName: recipientName,
|
||||
CreatedAt: now,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -314,8 +331,6 @@ func (s *Service) buildAndUploadExport(ctx context.Context, scope coredata.Scope
|
||||
}
|
||||
|
||||
fe.FileID = &file.ID
|
||||
fe.CompletedAt = ref.Ref(time.Now())
|
||||
fe.Status = coredata.FrameworkExportStatusCompleted
|
||||
if err := fe.Update(ctx, tx, scope); err != nil {
|
||||
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.
|
||||
func (r *mutationResolver) ExportFramework(ctx context.Context, input types.ExportFrameworkInput) (*types.ExportFrameworkPayload, error) {
|
||||
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 {
|
||||
return nil, fmt.Errorf("cannot export framework: %w", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user