From 6948a6b9ab51cea3b2b715d3385956d6d3ac3071 Mon Sep 17 00:00:00 2001 From: Sacha Al Himdani Date: Tue, 12 May 2026 15:06:28 +0200 Subject: [PATCH] Tailor signature certificate email copy for approvals Store the per-signature email subject as text on the electronic_signatures row at creation time, mirroring the consent_text pattern. The document approval service sets "Your approved - Certificate of Completion"; other callers default to "Your signed <Name> - Certificate of Completion". The certificate worker uses signature.email_subject as the email subject, falling back to the default format when the column is empty. Signed-off-by: Sacha Al Himdani <sacha@getprobo.com> --- packages/emails/emails.go | 11 +++++------ .../src/ElectronicSignatureCertificate.tsx | 10 ++++------ .../electronic-signature-certificate.txt | 2 +- pkg/coredata/electronic_signature.go | 12 +++++++----- pkg/coredata/migrations/20260515T140410Z.sql | 19 +++++++++++++++++++ pkg/esign/completion_certificate_worker.go | 7 ++++++- pkg/esign/service.go | 14 ++++++++++++++ pkg/probo/document_approval_service.go | 1 + 8 files changed, 57 insertions(+), 19 deletions(-) create mode 100644 pkg/coredata/migrations/20260515T140410Z.sql diff --git a/packages/emails/emails.go b/packages/emails/emails.go index 19c0bf915..29db869f8 100644 --- a/packages/emails/emails.go +++ b/packages/emails/emails.go @@ -219,7 +219,6 @@ const ( subjectTrustCenterAccess = "Compliance Page Access Invitation - %s" subjectTrustCenterDocumentAccessRejected = "Compliance Page Document Access Rejected - %s" subjectMagicLink = "Connect to %s" - subjectElectronicSignatureCertificate = "Your signed %s - Certificate of Completion" subjectMailingListSubscription = "%s – Confirm Your Compliance Updates Subscription" subjectMailingListUnsubscription = "%s – You've been unsubscribed" subjectMailingListUpdates = "%s – %s" @@ -525,25 +524,25 @@ func (p *Presenter) RenderMagicLink(ctx context.Context, magicLinkUrlPath string return fmt.Sprintf(subjectMagicLink, organizationName), textBody, htmlBody, err } -func (p *Presenter) RenderElectronicSignatureCertificate(ctx context.Context, signerName string, documentName string) (subject string, textBody string, htmlBody *string, err error) { +func (p *Presenter) RenderElectronicSignatureCertificate(ctx context.Context, signerName string, documentName string, subject string) (textBody string, htmlBody *string, err error) { vars, err := p.getCommonVariables(ctx) if err != nil { - return "", "", nil, fmt.Errorf("cannot get common variables: %w", err) + return "", nil, fmt.Errorf("cannot get common variables: %w", err) } data := struct { *CommonVariables SignerName string DocumentName string + Subject string }{ CommonVariables: vars, SignerName: signerName, DocumentName: documentName, + Subject: subject, } - textBody, htmlBody, err = renderEmail(electronicSignatureCertificateTextTemplate, electronicSignatureCertificateHTMLTemplate, data) - - return fmt.Sprintf(subjectElectronicSignatureCertificate, documentName), textBody, htmlBody, err + return renderEmail(electronicSignatureCertificateTextTemplate, electronicSignatureCertificateHTMLTemplate, data) } func (p *Presenter) RenderMailingListSubscription(ctx context.Context, organizationName string, confirmURL string, unsubscribeURL string) (subject string, textBody string, htmlBody *string, err error) { diff --git a/packages/emails/src/ElectronicSignatureCertificate.tsx b/packages/emails/src/ElectronicSignatureCertificate.tsx index f4707fe2a..763d995d6 100644 --- a/packages/emails/src/ElectronicSignatureCertificate.tsx +++ b/packages/emails/src/ElectronicSignatureCertificate.tsx @@ -21,13 +21,11 @@ import EmailLayout, { export const ElectronicSignatureCertificate = () => { return ( - <EmailLayout - subject={`Your signed ${"{{.DocumentName}}"} — Certificate of Completion`} - > + <EmailLayout subject={'{{.Subject}}'}> <Text style={bodyText}> - Your <strong>{"{{.DocumentName}}"}</strong> has been signed - electronically. A Certificate of Completion is attached to this email - as a PDF document. + A Certificate of Completion for{" "} + <strong>{"{{.DocumentName}}"}</strong> is attached to this email as a + PDF document. </Text> <Text style={bodyText}> diff --git a/packages/emails/templates/electronic-signature-certificate.txt b/packages/emails/templates/electronic-signature-certificate.txt index 2d92244e6..2db29a113 100644 --- a/packages/emails/templates/electronic-signature-certificate.txt +++ b/packages/emails/templates/electronic-signature-certificate.txt @@ -2,7 +2,7 @@ Hi {{.RecipientFullName}}, -Your {{.DocumentName}} has been signed electronically. A Certificate of Completion is attached to this email as a PDF document. +A Certificate of Completion for {{.DocumentName}} is attached to this email as a PDF document. The certificate contains a complete record of the signing event, including the integrity seal, timestamp, and audit trail. diff --git a/pkg/coredata/electronic_signature.go b/pkg/coredata/electronic_signature.go index 0294e9313..71697a3d9 100644 --- a/pkg/coredata/electronic_signature.go +++ b/pkg/coredata/electronic_signature.go @@ -39,6 +39,7 @@ type ElectronicSignature struct { FileID gid.GID `db:"file_id"` SignerEmail string `db:"signer_email"` ConsentText string `db:"consent_text"` + EmailSubject string `db:"email_subject"` SignerFullName *string `db:"signer_full_name"` SignerIPAddress *string `db:"signer_ip_address"` SignerUserAgent *string `db:"signer_user_agent"` @@ -85,11 +86,11 @@ func (es *ElectronicSignature) Insert( q := ` INSERT INTO electronic_signatures ( id, tenant_id, organization_id, status, document_type, document_name, file_id, - signer_email, consent_text, seal_version, attempt_count, max_attempts, + signer_email, consent_text, email_subject, seal_version, attempt_count, max_attempts, created_at, updated_at ) VALUES ( @id, @tenant_id, @organization_id, @status, @document_type, @document_name, @file_id, - @signer_email, @consent_text, @seal_version, @attempt_count, @max_attempts, + @signer_email, @consent_text, @email_subject, @seal_version, @attempt_count, @max_attempts, @created_at, @updated_at ) ` @@ -103,6 +104,7 @@ INSERT INTO electronic_signatures ( "file_id": es.FileID, "signer_email": es.SignerEmail, "consent_text": es.ConsentText, + "email_subject": es.EmailSubject, "seal_version": es.SealVersion, "attempt_count": es.AttemptCount, "max_attempts": es.MaxAttempts, @@ -187,7 +189,7 @@ func (es *ElectronicSignature) LoadByID( q := ` SELECT id, tenant_id, organization_id, status, document_type, document_name, file_id, - signer_email, consent_text, signer_full_name, signer_ip_address, + signer_email, consent_text, email_subject, signer_full_name, signer_ip_address, signer_user_agent, file_hash, seal, seal_version, tsa_token, signed_at, certificate_file_id, certificate_processing_started_at, attempt_count, max_attempts, last_attempted_at, last_error, @@ -227,7 +229,7 @@ func (es *ElectronicSignature) LoadNextAcceptedForUpdateSkipLocked( q := ` SELECT id, tenant_id, organization_id, status, document_type, document_name, file_id, - signer_email, consent_text, signer_full_name, signer_ip_address, + signer_email, consent_text, email_subject, signer_full_name, signer_ip_address, signer_user_agent, file_hash, seal, seal_version, tsa_token, signed_at, certificate_file_id, certificate_processing_started_at, attempt_count, max_attempts, last_attempted_at, last_error, @@ -265,7 +267,7 @@ func (es *ElectronicSignature) LoadNextCompletedWithoutCertificateForUpdate( q := ` SELECT id, tenant_id, organization_id, status, document_type, document_name, file_id, - signer_email, consent_text, signer_full_name, signer_ip_address, + signer_email, consent_text, email_subject, signer_full_name, signer_ip_address, signer_user_agent, file_hash, seal, seal_version, tsa_token, signed_at, certificate_file_id, certificate_processing_started_at, attempt_count, max_attempts, last_attempted_at, last_error, diff --git a/pkg/coredata/migrations/20260515T140410Z.sql b/pkg/coredata/migrations/20260515T140410Z.sql new file mode 100644 index 000000000..085f97bb6 --- /dev/null +++ b/pkg/coredata/migrations/20260515T140410Z.sql @@ -0,0 +1,19 @@ +-- Copyright (c) 2026 Probo Inc <hello@getprobo.com>. +-- +-- Permission to use, copy, modify, and/or distribute this software for any +-- purpose with or without fee is hereby granted, provided that the above +-- copyright notice and this permission notice appear in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +-- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +-- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +-- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +-- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +-- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +-- PERFORMANCE OF THIS SOFTWARE. + +ALTER TABLE electronic_signatures + ADD COLUMN email_subject TEXT NOT NULL DEFAULT ''; + +ALTER TABLE electronic_signatures + ALTER COLUMN email_subject DROP DEFAULT; diff --git a/pkg/esign/completion_certificate_worker.go b/pkg/esign/completion_certificate_worker.go index 9afc5a0f8..ac925e1a8 100644 --- a/pkg/esign/completion_certificate_worker.go +++ b/pkg/esign/completion_certificate_worker.go @@ -271,7 +271,12 @@ func (h *completionCertificateHandler) generateCertificate( docName = signature.DocumentType.DisplayName() } - subject, textBody, htmlBody, err := emailPresenter.RenderElectronicSignatureCertificate(ctx, ref.UnrefOrZero(signature.SignerFullName), docName) + subject := signature.EmailSubject + if subject == "" { + subject = fmt.Sprintf("Your signed %s - Certificate of Completion", docName) + } + + textBody, htmlBody, err := emailPresenter.RenderElectronicSignatureCertificate(ctx, ref.UnrefOrZero(signature.SignerFullName), docName, subject) if err != nil { return nil, nil, fmt.Errorf("cannot render email: %w", err) } diff --git a/pkg/esign/service.go b/pkg/esign/service.go index 39fa60269..3033dc935 100644 --- a/pkg/esign/service.go +++ b/pkg/esign/service.go @@ -52,6 +52,7 @@ type ( FileID gid.GID SignerEmail mail.Addr ConsentText string // optional; required when DocumentType == OTHER + EmailSubject string } AcceptSignatureRequest struct { @@ -72,6 +73,7 @@ type ( SignerIPAddr string SignerUA string ConsentText string + EmailSubject string } RecordEventRequest struct { @@ -161,6 +163,16 @@ func (s *Service) CreateSignature( } } + emailSubject := req.EmailSubject + if emailSubject == "" { + docName := req.DocumentType.DisplayName() + if req.DocumentName != nil && *req.DocumentName != "" { + docName = *req.DocumentName + } + + emailSubject = fmt.Sprintf("Your signed %s - Certificate of Completion", docName) + } + now := time.Now() scope := coredata.NewScopeFromObjectID(req.OrganizationID) @@ -180,6 +192,7 @@ func (s *Service) CreateSignature( FileID: stampedFileID, SignerEmail: req.SignerEmail.String(), ConsentText: consentText, + EmailSubject: emailSubject, SealVersion: 1, AttemptCount: 0, MaxAttempts: 10, @@ -209,6 +222,7 @@ func (s *Service) CreateAndAcceptSignature( FileID: req.FileID, SignerEmail: req.SignerEmail, ConsentText: req.ConsentText, + EmailSubject: req.EmailSubject, }, ) if err != nil { diff --git a/pkg/probo/document_approval_service.go b/pkg/probo/document_approval_service.go index 418fce9ae..8ddbba149 100644 --- a/pkg/probo/document_approval_service.go +++ b/pkg/probo/document_approval_service.go @@ -365,6 +365,7 @@ func (s *DocumentApprovalService) Approve( SignerIPAddr: req.SignerIPAddr, SignerUA: req.SignerUA, ConsentText: "By clicking Approve, I consent to approve this document electronically and agree that my electronic signature has the same legal validity as a handwritten signature.", + EmailSubject: fmt.Sprintf("Your approved %s - Certificate of Completion", document.Title), }, ) if err != nil {