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 <Title> - 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>
This commit is contained in:
@@ -219,7 +219,6 @@ const (
|
|||||||
subjectTrustCenterAccess = "Compliance Page Access Invitation - %s"
|
subjectTrustCenterAccess = "Compliance Page Access Invitation - %s"
|
||||||
subjectTrustCenterDocumentAccessRejected = "Compliance Page Document Access Rejected - %s"
|
subjectTrustCenterDocumentAccessRejected = "Compliance Page Document Access Rejected - %s"
|
||||||
subjectMagicLink = "Connect to %s"
|
subjectMagicLink = "Connect to %s"
|
||||||
subjectElectronicSignatureCertificate = "Your signed %s - Certificate of Completion"
|
|
||||||
subjectMailingListSubscription = "%s – Confirm Your Compliance Updates Subscription"
|
subjectMailingListSubscription = "%s – Confirm Your Compliance Updates Subscription"
|
||||||
subjectMailingListUnsubscription = "%s – You've been unsubscribed"
|
subjectMailingListUnsubscription = "%s – You've been unsubscribed"
|
||||||
subjectMailingListUpdates = "%s – %s"
|
subjectMailingListUpdates = "%s – %s"
|
||||||
@@ -525,25 +524,25 @@ func (p *Presenter) RenderMagicLink(ctx context.Context, magicLinkUrlPath string
|
|||||||
return fmt.Sprintf(subjectMagicLink, organizationName), textBody, htmlBody, err
|
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)
|
vars, err := p.getCommonVariables(ctx)
|
||||||
if err != nil {
|
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 {
|
data := struct {
|
||||||
*CommonVariables
|
*CommonVariables
|
||||||
SignerName string
|
SignerName string
|
||||||
DocumentName string
|
DocumentName string
|
||||||
|
Subject string
|
||||||
}{
|
}{
|
||||||
CommonVariables: vars,
|
CommonVariables: vars,
|
||||||
SignerName: signerName,
|
SignerName: signerName,
|
||||||
DocumentName: documentName,
|
DocumentName: documentName,
|
||||||
|
Subject: subject,
|
||||||
}
|
}
|
||||||
|
|
||||||
textBody, htmlBody, err = renderEmail(electronicSignatureCertificateTextTemplate, electronicSignatureCertificateHTMLTemplate, data)
|
return renderEmail(electronicSignatureCertificateTextTemplate, electronicSignatureCertificateHTMLTemplate, data)
|
||||||
|
|
||||||
return fmt.Sprintf(subjectElectronicSignatureCertificate, documentName), textBody, htmlBody, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Presenter) RenderMailingListSubscription(ctx context.Context, organizationName string, confirmURL string, unsubscribeURL string) (subject string, textBody string, htmlBody *string, err error) {
|
func (p *Presenter) RenderMailingListSubscription(ctx context.Context, organizationName string, confirmURL string, unsubscribeURL string) (subject string, textBody string, htmlBody *string, err error) {
|
||||||
|
|||||||
@@ -21,13 +21,11 @@ import EmailLayout, {
|
|||||||
|
|
||||||
export const ElectronicSignatureCertificate = () => {
|
export const ElectronicSignatureCertificate = () => {
|
||||||
return (
|
return (
|
||||||
<EmailLayout
|
<EmailLayout subject={'{{.Subject}}'}>
|
||||||
subject={`Your signed ${"{{.DocumentName}}"} — Certificate of Completion`}
|
|
||||||
>
|
|
||||||
<Text style={bodyText}>
|
<Text style={bodyText}>
|
||||||
Your <strong>{"{{.DocumentName}}"}</strong> has been signed
|
A Certificate of Completion for{" "}
|
||||||
electronically. A Certificate of Completion is attached to this email
|
<strong>{"{{.DocumentName}}"}</strong> is attached to this email as a
|
||||||
as a PDF document.
|
PDF document.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text style={bodyText}>
|
<Text style={bodyText}>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Hi {{.RecipientFullName}},
|
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.
|
The certificate contains a complete record of the signing event, including the integrity seal, timestamp, and audit trail.
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ type ElectronicSignature struct {
|
|||||||
FileID gid.GID `db:"file_id"`
|
FileID gid.GID `db:"file_id"`
|
||||||
SignerEmail string `db:"signer_email"`
|
SignerEmail string `db:"signer_email"`
|
||||||
ConsentText string `db:"consent_text"`
|
ConsentText string `db:"consent_text"`
|
||||||
|
EmailSubject string `db:"email_subject"`
|
||||||
SignerFullName *string `db:"signer_full_name"`
|
SignerFullName *string `db:"signer_full_name"`
|
||||||
SignerIPAddress *string `db:"signer_ip_address"`
|
SignerIPAddress *string `db:"signer_ip_address"`
|
||||||
SignerUserAgent *string `db:"signer_user_agent"`
|
SignerUserAgent *string `db:"signer_user_agent"`
|
||||||
@@ -85,11 +86,11 @@ func (es *ElectronicSignature) Insert(
|
|||||||
q := `
|
q := `
|
||||||
INSERT INTO electronic_signatures (
|
INSERT INTO electronic_signatures (
|
||||||
id, tenant_id, organization_id, status, document_type, document_name, file_id,
|
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
|
created_at, updated_at
|
||||||
) VALUES (
|
) VALUES (
|
||||||
@id, @tenant_id, @organization_id, @status, @document_type, @document_name, @file_id,
|
@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
|
@created_at, @updated_at
|
||||||
)
|
)
|
||||||
`
|
`
|
||||||
@@ -103,6 +104,7 @@ INSERT INTO electronic_signatures (
|
|||||||
"file_id": es.FileID,
|
"file_id": es.FileID,
|
||||||
"signer_email": es.SignerEmail,
|
"signer_email": es.SignerEmail,
|
||||||
"consent_text": es.ConsentText,
|
"consent_text": es.ConsentText,
|
||||||
|
"email_subject": es.EmailSubject,
|
||||||
"seal_version": es.SealVersion,
|
"seal_version": es.SealVersion,
|
||||||
"attempt_count": es.AttemptCount,
|
"attempt_count": es.AttemptCount,
|
||||||
"max_attempts": es.MaxAttempts,
|
"max_attempts": es.MaxAttempts,
|
||||||
@@ -187,7 +189,7 @@ func (es *ElectronicSignature) LoadByID(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id, tenant_id, organization_id, status, document_type, document_name, file_id,
|
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,
|
signer_user_agent, file_hash, seal, seal_version, tsa_token, signed_at,
|
||||||
certificate_file_id, certificate_processing_started_at,
|
certificate_file_id, certificate_processing_started_at,
|
||||||
attempt_count, max_attempts, last_attempted_at, last_error,
|
attempt_count, max_attempts, last_attempted_at, last_error,
|
||||||
@@ -227,7 +229,7 @@ func (es *ElectronicSignature) LoadNextAcceptedForUpdateSkipLocked(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id, tenant_id, organization_id, status, document_type, document_name, file_id,
|
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,
|
signer_user_agent, file_hash, seal, seal_version, tsa_token, signed_at,
|
||||||
certificate_file_id, certificate_processing_started_at,
|
certificate_file_id, certificate_processing_started_at,
|
||||||
attempt_count, max_attempts, last_attempted_at, last_error,
|
attempt_count, max_attempts, last_attempted_at, last_error,
|
||||||
@@ -265,7 +267,7 @@ func (es *ElectronicSignature) LoadNextCompletedWithoutCertificateForUpdate(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id, tenant_id, organization_id, status, document_type, document_name, file_id,
|
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,
|
signer_user_agent, file_hash, seal, seal_version, tsa_token, signed_at,
|
||||||
certificate_file_id, certificate_processing_started_at,
|
certificate_file_id, certificate_processing_started_at,
|
||||||
attempt_count, max_attempts, last_attempted_at, last_error,
|
attempt_count, max_attempts, last_attempted_at, last_error,
|
||||||
|
|||||||
19
pkg/coredata/migrations/20260515T140410Z.sql
Normal file
19
pkg/coredata/migrations/20260515T140410Z.sql
Normal file
@@ -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;
|
||||||
@@ -271,7 +271,12 @@ func (h *completionCertificateHandler) generateCertificate(
|
|||||||
docName = signature.DocumentType.DisplayName()
|
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 {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("cannot render email: %w", err)
|
return nil, nil, fmt.Errorf("cannot render email: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ type (
|
|||||||
FileID gid.GID
|
FileID gid.GID
|
||||||
SignerEmail mail.Addr
|
SignerEmail mail.Addr
|
||||||
ConsentText string // optional; required when DocumentType == OTHER
|
ConsentText string // optional; required when DocumentType == OTHER
|
||||||
|
EmailSubject string
|
||||||
}
|
}
|
||||||
|
|
||||||
AcceptSignatureRequest struct {
|
AcceptSignatureRequest struct {
|
||||||
@@ -72,6 +73,7 @@ type (
|
|||||||
SignerIPAddr string
|
SignerIPAddr string
|
||||||
SignerUA string
|
SignerUA string
|
||||||
ConsentText string
|
ConsentText string
|
||||||
|
EmailSubject string
|
||||||
}
|
}
|
||||||
|
|
||||||
RecordEventRequest struct {
|
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()
|
now := time.Now()
|
||||||
scope := coredata.NewScopeFromObjectID(req.OrganizationID)
|
scope := coredata.NewScopeFromObjectID(req.OrganizationID)
|
||||||
|
|
||||||
@@ -180,6 +192,7 @@ func (s *Service) CreateSignature(
|
|||||||
FileID: stampedFileID,
|
FileID: stampedFileID,
|
||||||
SignerEmail: req.SignerEmail.String(),
|
SignerEmail: req.SignerEmail.String(),
|
||||||
ConsentText: consentText,
|
ConsentText: consentText,
|
||||||
|
EmailSubject: emailSubject,
|
||||||
SealVersion: 1,
|
SealVersion: 1,
|
||||||
AttemptCount: 0,
|
AttemptCount: 0,
|
||||||
MaxAttempts: 10,
|
MaxAttempts: 10,
|
||||||
@@ -209,6 +222,7 @@ func (s *Service) CreateAndAcceptSignature(
|
|||||||
FileID: req.FileID,
|
FileID: req.FileID,
|
||||||
SignerEmail: req.SignerEmail,
|
SignerEmail: req.SignerEmail,
|
||||||
ConsentText: req.ConsentText,
|
ConsentText: req.ConsentText,
|
||||||
|
EmailSubject: req.EmailSubject,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -365,6 +365,7 @@ func (s *DocumentApprovalService) Approve(
|
|||||||
SignerIPAddr: req.SignerIPAddr,
|
SignerIPAddr: req.SignerIPAddr,
|
||||||
SignerUA: req.SignerUA,
|
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.",
|
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 {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user