Skip to content

Commit a19dbd9

Browse files
committed
feat: encapsulate pdf render data generation
1 parent e8f49ab commit a19dbd9

File tree

7 files changed

+235
-32
lines changed

7 files changed

+235
-32
lines changed

src/app/modules/submission/__tests__/submission.service.spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ const MOCK_AUTOREPLY_DATA = [
9191
{
9292
question: 'Email',
9393
answerTemplate: ['[email protected]'],
94+
fieldType: BasicField.Email,
9495
},
9596
]
9697
const AUTOREPLY_OPTIONS_1: AutoReplyOptions = {
@@ -456,6 +457,7 @@ describe('submission.service', () => {
456457
submission: MOCK_SUBMISSION,
457458
attachments: MOCK_ATTACHMENTS,
458459
responsesData: MOCK_AUTOREPLY_DATA,
460+
isUseLambdaOutput: false,
459461
})
460462

461463
const expectedAutoReplyData = [
@@ -497,6 +499,7 @@ describe('submission.service', () => {
497499
submission: MOCK_SUBMISSION,
498500
attachments: MOCK_ATTACHMENTS,
499501
responsesData: MOCK_AUTOREPLY_DATA,
502+
isUseLambdaOutput: false,
500503
})
501504

502505
expect(MockMailService.sendAutoReplyEmails).not.toHaveBeenCalled()
@@ -542,6 +545,7 @@ describe('submission.service', () => {
542545
submission: MOCK_SUBMISSION,
543546
attachments: MOCK_ATTACHMENTS,
544547
responsesData: MOCK_AUTOREPLY_DATA,
548+
isUseLambdaOutput: false,
545549
})
546550

547551
expect(MockMailService.sendAutoReplyEmails).not.toHaveBeenCalled()
@@ -593,6 +597,7 @@ describe('submission.service', () => {
593597
submission: MOCK_SUBMISSION,
594598
attachments: MOCK_ATTACHMENTS,
595599
responsesData: MOCK_AUTOREPLY_DATA,
600+
isUseLambdaOutput: false,
596601
})
597602

598603
const expectedAutoReplyData = [EXPECTED_AUTOREPLY_DATA_1]
@@ -656,6 +661,7 @@ describe('submission.service', () => {
656661
submission: MOCK_SUBMISSION,
657662
attachments: MOCK_ATTACHMENTS,
658663
responsesData: undefined,
664+
isUseLambdaOutput: false,
659665
})
660666

661667
const expectedAutoReplyData = [
@@ -722,6 +728,7 @@ describe('submission.service', () => {
722728
submission: MOCK_SUBMISSION,
723729
attachments: undefined,
724730
responsesData: MOCK_AUTOREPLY_DATA,
731+
isUseLambdaOutput: false,
725732
})
726733

727734
const expectedAutoReplyData = [
@@ -781,6 +788,7 @@ describe('submission.service', () => {
781788
submission: MOCK_SUBMISSION,
782789
attachments: MOCK_ATTACHMENTS,
783790
responsesData: MOCK_AUTOREPLY_DATA,
791+
isUseLambdaOutput: false,
784792
})
785793

786794
const expectedAutoReplyData = [
@@ -850,6 +858,7 @@ describe('submission.service', () => {
850858
submission: MOCK_SUBMISSION,
851859
attachments: MOCK_ATTACHMENTS,
852860
responsesData: MOCK_AUTOREPLY_DATA,
861+
isUseLambdaOutput: false,
853862
})
854863

855864
const expectedAutoReplyData = [

src/app/modules/submission/multirespondent-submission/multirespondent-submission.service.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -486,10 +486,6 @@ const sendMrfRespondentCopyEmails = ({
486486
InvalidWorkflowTypeError | MailSendError | AutoreplyPdfGenerationError
487487
> => {
488488
const submissionId: string = submission.id
489-
const submissionTime = moment(submission.created)
490-
.tz('Asia/Singapore')
491-
.format('ddd, DD MMM YYYY hh:mm:ss A')
492-
const formUrl: string = `${config.app.appUrl}/${form._id}`
493489

494490
const formQuestionAnswers = getQuestionTitleAnswerString({
495491
formFields: form.form_fields,
@@ -504,20 +500,20 @@ const sendMrfRespondentCopyEmails = ({
504500
const hasFormSummary = respondentCopyRecipientData.some(
505501
(autoReplyMailData) => autoReplyMailData.includeFormSummary,
506502
)
507-
const renderData: AutoreplySummaryRenderData = {
503+
const autoReplyData = {
508504
refNo: submissionId,
509505
formTitle: form.title,
510-
submissionTime: submissionTime,
511-
formData: pdfFormData,
512-
formUrl: formUrl,
506+
submissionDateTime: submission.created || new Date(),
507+
responsesData: pdfFormData,
508+
formUrl: `${config.app.appUrl}/${form._id}`,
513509
}
514510

515511
// Step 1: generate PDF if needed
516512
const pdfResult: ResultAsync<
517513
Mail.Attachment | undefined,
518514
AutoreplyPdfGenerationError
519515
> = hasFormSummary
520-
? generateAutoreplyPdf(renderData, true).map((pdfBuffer) => ({
516+
? generateAutoreplyPdf(autoReplyData, true).map((pdfBuffer) => ({
521517
filename: 'response.pdf',
522518
content: Buffer.copyBytesFrom(pdfBuffer),
523519
}))

src/app/services/mail/__tests__/mail.service.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { cloneDeep } from 'lodash'
22
import moment from 'moment-timezone'
33
import { err, ok, okAsync } from 'neverthrow'
44
import Mail, { Attachment } from 'nodemailer/lib/mailer'
5-
import { FormResponseMode, PaymentChannel } from 'shared/types'
5+
import { BasicField, FormResponseMode, PaymentChannel } from 'shared/types'
66

77
import { MailSendError } from 'src/app/services/mail/mail.errors'
88
import { MailService } from 'src/app/services/mail/mail.service'
@@ -716,6 +716,7 @@ describe('mail.service', () => {
716716
{
717717
question: 'some question',
718718
answerTemplate: ['some answer template'],
719+
fieldType: BasicField.ShortText,
719720
},
720721
],
721722
attachments: ['something'] as Attachment[],
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { ObjectId } from 'bson'
2+
import * as ConvertHtmlToPdf from '../../../utils/convert-html-to-pdf'
3+
import { generateAutoreplyPdf, safeRenderFileForTest } from '../mail.utils'
4+
import { BasicField } from 'shared/types'
5+
import { SIGNATURE_CAPTURED_STRING } from 'shared/utils/signature'
6+
7+
jest.mock('../../../utils/convert-html-to-pdf.ts')
8+
const MockConvertHtmlToPdf = jest.mocked(ConvertHtmlToPdf)
9+
10+
const MOCK_PDF_BUFFER = Buffer.from('mock pdf buffer')
11+
const MOCK_SUBMISSION_DATE_TIME = new Date('2025-01-01T00:00:00.000Z')
12+
13+
describe('mail.utils', () => {
14+
const AUTOREPLY_PDF_TEMPLATE_FILEPATH = `${__dirname}/../../../views/templates/submit-form-summary-pdf.server.view.html`
15+
beforeEach(() => {
16+
jest.clearAllMocks()
17+
MockConvertHtmlToPdf.generatePdfFromHtml.mockResolvedValue(MOCK_PDF_BUFFER)
18+
})
19+
20+
describe('generateAutoreplyPdf', () => {
21+
it('should generate a pdf from html', async () => {
22+
// Arrange
23+
const mockFormId = new ObjectId().toHexString()
24+
const mockSubmissionId = new ObjectId().toHexString()
25+
const autoReplyData = {
26+
refNo: mockSubmissionId,
27+
formTitle: 'Test Form',
28+
submissionDateTime: MOCK_SUBMISSION_DATE_TIME,
29+
responsesData: [],
30+
formUrl: `https://form.gov.sg/${mockFormId}`,
31+
}
32+
33+
// Act
34+
const result = await generateAutoreplyPdf(autoReplyData, true)
35+
36+
// Assert
37+
expect(MockConvertHtmlToPdf.generatePdfFromHtml).toHaveBeenCalledOnce()
38+
expect(result._unsafeUnwrap()).toEqual(MOCK_PDF_BUFFER)
39+
})
40+
41+
it('should generate pdfRender data correctly for signature field', async () => {
42+
// Arrange
43+
const mockFormId = new ObjectId().toHexString()
44+
const mockSubmissionId = new ObjectId().toHexString()
45+
const MOCK_SIGNATURE_PNG_DATAURI = 'datauri://signature.png'
46+
const autoReplyData = {
47+
refNo: mockSubmissionId,
48+
formTitle: 'Test Form',
49+
submissionDateTime: MOCK_SUBMISSION_DATE_TIME,
50+
responsesData: [
51+
{
52+
question: 'Signature',
53+
answer: MOCK_SIGNATURE_PNG_DATAURI,
54+
answerTemplate: [SIGNATURE_CAPTURED_STRING],
55+
fieldType: BasicField.Signature,
56+
},
57+
],
58+
formUrl: `https://form.gov.sg/${mockFormId}`,
59+
}
60+
61+
const formattedSubmissionTimeString = 'Wed, 01 Jan 2025 08:00:00 AM'
62+
63+
const pdfRenderData = {
64+
refNo: mockSubmissionId,
65+
formTitle: 'Test Form',
66+
submissionTime: formattedSubmissionTimeString,
67+
formData: [
68+
{
69+
question: 'Signature',
70+
answer: MOCK_SIGNATURE_PNG_DATAURI, // should be defined for signature fields
71+
answerTemplate: [SIGNATURE_CAPTURED_STRING],
72+
},
73+
],
74+
formUrl: `https://form.gov.sg/${mockFormId}`,
75+
}
76+
77+
const expectedHtml = await safeRenderFileForTest(
78+
AUTOREPLY_PDF_TEMPLATE_FILEPATH,
79+
pdfRenderData,
80+
)
81+
82+
// Act
83+
const result = await generateAutoreplyPdf(autoReplyData, true)
84+
85+
// Assert
86+
expect(MockConvertHtmlToPdf.generatePdfFromHtml).toHaveBeenCalledOnce()
87+
expect(MockConvertHtmlToPdf.generatePdfFromHtml).toHaveBeenCalledWith(
88+
expectedHtml._unsafeUnwrap(),
89+
true,
90+
)
91+
expect(result._unsafeUnwrap()).toEqual(MOCK_PDF_BUFFER)
92+
})
93+
94+
it('should generate pdfRender data correctly for non-signature fields', async () => {
95+
// Arrange
96+
const mockFormId = new ObjectId().toHexString()
97+
const mockSubmissionId = new ObjectId().toHexString()
98+
const autoReplyData = {
99+
refNo: mockSubmissionId,
100+
formTitle: 'Test Form',
101+
submissionDateTime: MOCK_SUBMISSION_DATE_TIME,
102+
responsesData: [
103+
{
104+
question: 'Table field',
105+
answer: 'Table answer',
106+
answerTemplate: ['Table answer'],
107+
fieldType: BasicField.Table,
108+
},
109+
],
110+
formUrl: `https://form.gov.sg/${mockFormId}`,
111+
}
112+
113+
const formattedSubmissionTimeString = 'Wed, 01 Jan 2025 08:00:00 AM'
114+
115+
const pdfRenderData = {
116+
refNo: mockSubmissionId,
117+
formTitle: 'Test Form',
118+
submissionTime: formattedSubmissionTimeString,
119+
formData: [
120+
{
121+
question: 'Table field',
122+
answer: undefined, // should be undefined for non-signature fields
123+
answerTemplate: ['Table answer'],
124+
},
125+
],
126+
formUrl: `https://form.gov.sg/${mockFormId}`,
127+
}
128+
const expectedHtml = await safeRenderFileForTest(
129+
AUTOREPLY_PDF_TEMPLATE_FILEPATH,
130+
pdfRenderData,
131+
)
132+
133+
// Act
134+
const result = await generateAutoreplyPdf(autoReplyData, true)
135+
136+
// Assert
137+
expect(MockConvertHtmlToPdf.generatePdfFromHtml).toHaveBeenCalledOnce()
138+
expect(MockConvertHtmlToPdf.generatePdfFromHtml).toHaveBeenCalledWith(
139+
expectedHtml._unsafeUnwrap(),
140+
true,
141+
)
142+
expect(result._unsafeUnwrap()).toEqual(MOCK_PDF_BUFFER)
143+
})
144+
})
145+
})

src/app/services/mail/mail.service.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -798,13 +798,21 @@ export class MailService {
798798
> => {
799799
// Data to render both the submission details mail HTML body and PDF.
800800

801-
const renderData: AutoreplySummaryRenderData = {
801+
const autoReplyData = {
802+
refNo: submission.id,
803+
formTitle: form.title,
804+
submissionDateTime: submission.created || new Date(),
805+
responsesData,
806+
formUrl: `${this.#appUrl}/${form._id}`,
807+
}
808+
809+
const renderData: AutoreplySummaryRenderData = {
802810
refNo: submission.id,
803811
formTitle: form.title,
804812
submissionTime: moment(submission.created)
805813
.tz('Asia/Singapore')
806814
.format('ddd, DD MMM YYYY hh:mm:ss A'),
807-
formData: responsesData,
815+
formData: responsesData,
808816
formUrl: `${this.#appUrl}/${form._id}`,
809817
}
810818

@@ -824,7 +832,7 @@ export class MailService {
824832
!isPaymentEnabled
825833
) {
826834
const pdfBufferResult = await generateAutoreplyPdf(
827-
renderData,
835+
autoReplyData,
828836
isUseLambdaOutput,
829837
)
830838
if (pdfBufferResult.isErr()) {

src/app/services/mail/mail.types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Mail from 'nodemailer/lib/mailer'
22
import { OperationOptions } from 'retry'
33

4-
import { AutoReplyOptions } from '../../../../shared/types'
4+
import { AutoReplyOptions, BasicField } from '../../../../shared/types'
55
import {
66
EmailAdminDataField,
77
IFormSchema,
@@ -31,6 +31,7 @@ export type SendAutoReplyEmailsArgs = {
3131
attachments?: Mail.Attachment[]
3232
responsesData: (Pick<EmailAdminDataField, 'question' | 'answerTemplate'> & {
3333
answer?: EmailAdminDataField['answer']
34+
fieldType: BasicField
3435
})[]
3536
autoReplyMailDatas: AutoReplyMailData[]
3637
isUseLambdaOutput: boolean

0 commit comments

Comments
 (0)