Skip to content

Commit 8a246d7

Browse files
committed
feat: setup shared pdf generation
1 parent 1a3244e commit 8a246d7

File tree

4 files changed

+108
-69
lines changed

4 files changed

+108
-69
lines changed

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

Lines changed: 82 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,24 @@ import mongoose from 'mongoose'
33
import { err, ok, okAsync, Result, ResultAsync } from 'neverthrow'
44
import Mail from 'nodemailer/lib/mailer'
55

6-
import { AutoReplyMailData } from 'src/app/services/mail/mail.types'
6+
import { AutoReplyMailData, AutoreplySummaryRenderData } from 'src/app/services/mail/mail.types'
77
import MailService from '../../../services/mail/mail.service'
88
import * as EmailSubmissionService from '../email-submission/email-submission.service'
99

1010
import { featureFlags } from '../../../../../shared/constants'
1111
import {
1212
DateString,
1313
FormResponseMode,
14+
PaymentChannel,
1415
SubmissionType,
1516
} from '../../../../../shared/types'
1617
import {
18+
EmailAdminDataField,
1719
FieldResponse, IEncryptedSubmissionSchema,
1820
IPopulatedEncryptedForm,
1921
IPopulatedForm
2022
} from '../../../../types'
23+
import config from '../../../config/config'
2124
import { createLoggerWithLabel } from '../../../config/logger'
2225
import { getEncryptSubmissionModel } from '../../../models/submission.server.model'
2326
import { createQueryWithDateParam } from '../../../utils/date'
@@ -42,7 +45,9 @@ import {
4245
import { sendEmailConfirmations } from '../submission.service'
4346
import { extractEmailConfirmationData } from '../submission.utils'
4447

48+
import moment from 'moment'
4549
import { AutoreplyPdfGenerationError } from 'src/app/services/mail/mail.errors'
50+
import { generateAutoreplyPdf } from 'src/app/services/mail/mail.utils'
4651
import { MYINFO_PREFIX } from '../email-submission/email-submission.constants'
4752
import { ProcessedFieldResponse } from '../submission.types'
4853
import { CHARTS_MAX_SUBMISSION_RESULTS } from './encrypt-submission.constants'
@@ -136,23 +141,71 @@ export const createEncryptSubmissionWithoutSave = ({
136141
}
137142

138143
const checkIfAdminPdfIsRequired = (): boolean => {
139-
return false
144+
return true
140145
}
141146

142-
const checkIfRespondentFormSummaryIsRequired = (): boolean => {
143-
return false
147+
const checkIfRespondentFormSummaryIsRequired = ({
148+
autoReplyMailDatas,
149+
isPaymentEnabled,
150+
}: {
151+
autoReplyMailDatas: AutoReplyMailData[]
152+
isPaymentEnabled: boolean
153+
}): boolean => {
154+
return !isPaymentEnabled && autoReplyMailDatas.some((data) => data.includeFormSummary)
144155
}
145156

146-
const checkIfPdfGenerationIsRequired = (): boolean => {
147-
return checkIfAdminPdfIsRequired() || checkIfRespondentFormSummaryIsRequired()
157+
const checkIfPdfGenerationIsRequired = ({
158+
isPaymentEnabled,
159+
autoReplyMailDatas,
160+
}: {
161+
isPaymentEnabled: boolean
162+
autoReplyMailDatas: AutoReplyMailData[]
163+
}): boolean => {
164+
return checkIfAdminPdfIsRequired() || checkIfRespondentFormSummaryIsRequired({
165+
isPaymentEnabled,
166+
autoReplyMailDatas,
167+
})
148168
}
149169

150-
const generatePdfAttachmentIfRequired = (): ResultAsync<Mail.Attachment | undefined, AutoreplyPdfGenerationError> => {
151-
if (!checkIfPdfGenerationIsRequired()) {
170+
const generatePdfAttachmentIfRequired = ({
171+
isPaymentEnabled,
172+
autoReplyMailDatas,
173+
submission,
174+
form,
175+
responsesData,
176+
}: {
177+
isPaymentEnabled: boolean
178+
autoReplyMailDatas: AutoReplyMailData[]
179+
submission: IEncryptedSubmissionSchema
180+
form: IPopulatedEncryptedForm
181+
responsesData: (Pick<EmailAdminDataField, 'question' | 'answerTemplate'> & {
182+
answer?: EmailAdminDataField['answer']
183+
})[]
184+
}): ResultAsync<Mail.Attachment | undefined, AutoreplyPdfGenerationError> => {
185+
if (!checkIfPdfGenerationIsRequired({
186+
isPaymentEnabled,
187+
autoReplyMailDatas,
188+
})) {
152189
return okAsync(undefined)
153190
}
154-
155-
return okAsync(undefined)
191+
192+
const renderData: AutoreplySummaryRenderData = {
193+
refNo: submission.id,
194+
formTitle: form.title,
195+
submissionTime: moment(submission.created)
196+
.tz('Asia/Singapore')
197+
.format('ddd, DD MMM YYYY hh:mm:ss A'),
198+
formData: responsesData,
199+
formUrl: `${config.app.appUrl}/${form._id}`,
200+
}
201+
202+
return generateAutoreplyPdf(
203+
renderData,
204+
true,
205+
).map((pdfBuffer) => ({
206+
filename: 'response.pdf',
207+
content: Buffer.copyBytesFrom(pdfBuffer),
208+
}))
156209
}
157210

158211
/**
@@ -269,13 +322,25 @@ export const performEncryptPostSubmissionActions = ({
269322
answer: item.answer,
270323
}))
271324

272-
const pdfAttachmentResult = generatePdfAttachmentIfRequired()
325+
const recipientEmailDatas = [
326+
...extractEmailConfirmationData(responses, form.form_fields),
327+
...respondentCopyEmailData,
328+
]
273329

274-
return pdfAttachmentResult.andThen((pdfAttachment) => {
275-
if (pdfAttachment) {
276-
attachments = [...(attachments ?? []), pdfAttachment]
277-
}
330+
const isPaymentEnabled =
331+
form.responseMode === FormResponseMode.Encrypt &&
332+
form.payments_channel.channel !== PaymentChannel.Unconnected &&
333+
form.payments_field.enabled === true
334+
335+
const pdfAttachmentResult = generatePdfAttachmentIfRequired({
336+
isPaymentEnabled,
337+
autoReplyMailDatas: recipientEmailDatas,
338+
submission,
339+
form,
340+
responsesData: emailData.formData,
341+
})
278342

343+
return pdfAttachmentResult.andThen((pdfAttachment) => {
279344
void MailService.sendSubmissionToAdmin({
280345
replyToEmails: EmailSubmissionService.extractEmailAnswers(emailFields),
281346
form,
@@ -293,11 +358,8 @@ export const performEncryptPostSubmissionActions = ({
293358
submission,
294359
attachments,
295360
responsesData: emailData?.autoReplyData,
296-
recipientData: [
297-
...extractEmailConfirmationData(responses, form.form_fields),
298-
...respondentCopyEmailData,
299-
],
300-
isUseLambdaOutput,
361+
recipientData: recipientEmailDatas,
362+
pdfAttachment,
301363
}).mapErr((error) => {
302364
logger.error({
303365
message: 'Error while sending email confirmations',

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -458,8 +458,8 @@ export type TriggerVirusScanThenDownloadCleanFileChainError =
458458
*/
459459
export const triggerVirusScanThenDownloadCleanFileChain = <
460460
T extends
461-
| ParsedClearAttachmentResponse
462-
| ParsedClearAttachmentFieldResponseV3,
461+
| ParsedClearAttachmentResponse
462+
| ParsedClearAttachmentFieldResponseV3,
463463
>(
464464
response: T,
465465
formId: string,
@@ -593,14 +593,14 @@ export const sendEmailConfirmations = <S extends ISubmissionSchema>({
593593
responsesData = [],
594594
attachments,
595595
recipientData,
596-
isUseLambdaOutput,
596+
pdfAttachment,
597597
}: {
598598
form: IPopulatedForm
599599
submission: S
600600
responsesData?: EmailRespondentConfirmationField[]
601601
attachments?: Mail.Attachment[]
602602
recipientData: AutoReplyMailData[]
603-
isUseLambdaOutput: boolean
603+
pdfAttachment?: Mail.Attachment
604604
}): ResultAsync<true, SendEmailConfirmationError> => {
605605
const logMeta = {
606606
action: 'sendEmailConfirmations',
@@ -616,7 +616,7 @@ export const sendEmailConfirmations = <S extends ISubmissionSchema>({
616616
attachments,
617617
responsesData,
618618
autoReplyMailDatas: recipientData,
619-
isUseLambdaOutput,
619+
pdfAttachment,
620620
})
621621
return ResultAsync.fromPromise(sentEmailsPromise, (error) => {
622622
logger.error({
@@ -1381,8 +1381,8 @@ export type TriggerGuardDutyScanThenDownloadCleanFileChainError =
13811381
*/
13821382
export const triggerGuardDutyScanThenDownloadCleanFileChain = <
13831383
T extends
1384-
| ParsedClearAttachmentResponse
1385-
| ParsedClearAttachmentFieldResponseV3,
1384+
| ParsedClearAttachmentResponse
1385+
| ParsedClearAttachmentFieldResponseV3,
13861386
>(
13871387
response: T,
13881388
formId: string,

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

Lines changed: 18 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { render } from '@react-email/render'
22
import tracer from 'dd-trace'
33
import { get, inRange, isEmpty } from 'lodash'
44
import moment from 'moment-timezone'
5-
import { Result, ResultAsync, err, errAsync, fromPromise, okAsync } from 'neverthrow'
5+
import { Result, ResultAsync, errAsync, fromPromise, okAsync } from 'neverthrow'
66
import Mail from 'nodemailer/lib/mailer'
77
import promiseRetry from 'promise-retry'
88
import validator from 'validator'
@@ -61,14 +61,12 @@ import {
6161
SubmissionToAdminHtmlData,
6262
} from './mail.types'
6363
import {
64-
generateAutoreplyHtml,
65-
generateAutoreplyPdf,
66-
generateIssueReportedNotificationHtml,
64+
generateAutoreplyHtml, generateIssueReportedNotificationHtml,
6765
generateLoginOtpHtml,
6866
generatePaymentConfirmationHtml,
6967
generatePaymentOnboardingHtml,
7068
generateSubmissionToAdminHtml,
71-
isToFieldValid,
69+
isToFieldValid
7270
} from './mail.utils'
7371

7472
const logger = createLoggerWithLabel(module)
@@ -688,6 +686,7 @@ export class MailService {
688686
attachments,
689687
dataCollationData,
690688
formData,
689+
pdfAttachment,
691690
}: {
692691
replyToEmails?: string[]
693692
form: Pick<IFormHasEmailSchema, '_id' | 'title' | 'emails'>
@@ -698,6 +697,7 @@ export class MailService {
698697
question: string
699698
answer: string | number
700699
}[]
700+
pdfAttachment?: Mail.Attachment
701701
}): ResultAsync<true, MailGenerationError | MailSendError> => {
702702
const logMeta = {
703703
action: 'sendSubmissionToAdmin',
@@ -710,6 +710,10 @@ export class MailService {
710710
return okAsync(true)
711711
}
712712

713+
if (pdfAttachment) {
714+
attachments = [...(attachments ?? []), pdfAttachment]
715+
}
716+
713717
logger.info({
714718
message: 'Sending admin notification mail',
715719
meta: logMeta,
@@ -802,7 +806,7 @@ export class MailService {
802806
responsesData,
803807
autoReplyMailDatas,
804808
attachments = [],
805-
isUseLambdaOutput,
809+
pdfAttachment,
806810
}: SendAutoReplyEmailsArgs): Promise<
807811
PromiseSettledResult<
808812
Result<
@@ -813,57 +817,30 @@ export class MailService {
813817
> => {
814818
// Data to render both the submission details mail HTML body and PDF.
815819

816-
const renderData: AutoreplySummaryRenderData = {
820+
const strippedRenderData: AutoreplySummaryRenderData = {
817821
refNo: submission.id,
818822
formTitle: form.title,
819823
submissionTime: moment(submission.created)
820824
.tz('Asia/Singapore')
821825
.format('ddd, DD MMM YYYY hh:mm:ss A'),
822-
formData: responsesData,
826+
// strip answer from renderData to always use answerTemplate for email body responses
827+
formData: responsesData.map(
828+
({ question, answerTemplate }) => ({
829+
question,
830+
answerTemplate,
831+
})),
823832
formUrl: `${this.#appUrl}/${form._id}`,
824833
}
825834

826835
// Create a copy of attachments for attaching of autoreply pdf if needed.
827-
const attachmentsWithAutoreplyPdf = [...attachments]
836+
const attachmentsWithAutoreplyPdf = [...attachments, ...(pdfAttachment ? [pdfAttachment] : [])]
828837
const isEncryptForm = form?.responseMode === FormResponseMode.Encrypt
829838
const encryptFormDef = form as IPopulatedEncryptedForm
830839
const isPaymentEnabled =
831840
isEncryptForm &&
832841
encryptFormDef.payments_channel.channel !== PaymentChannel.Unconnected &&
833842
encryptFormDef.payments_field.enabled === true
834843

835-
// Generate autoreply pdf and append into attachments if any of the mail has
836-
// to include a form summary.
837-
if (
838-
autoReplyMailDatas.some((data) => data.includeFormSummary) &&
839-
!isPaymentEnabled
840-
) {
841-
const pdfBufferResult = await generateAutoreplyPdf(
842-
renderData,
843-
isUseLambdaOutput,
844-
)
845-
if (pdfBufferResult.isErr()) {
846-
return Promise.allSettled([err(pdfBufferResult.error)])
847-
}
848-
attachmentsWithAutoreplyPdf.push({
849-
filename: 'response.pdf',
850-
content: Buffer.copyBytesFrom(pdfBufferResult.value),
851-
})
852-
}
853-
854-
// strip answer from renderData to always use answerTemplate for email body responses
855-
const strippedResponsesData = responsesData.map(
856-
({ question, answerTemplate }) => ({
857-
question,
858-
answerTemplate,
859-
}),
860-
)
861-
862-
const strippedRenderData = {
863-
...renderData,
864-
formData: strippedResponsesData,
865-
}
866-
867844
// Prepare mail sending for each autoreply mail.
868845
return Promise.allSettled(
869846
autoReplyMailDatas.map((mailData, index) => {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export type SendAutoReplyEmailsArgs = {
3333
answer?: EmailAdminDataField['answer']
3434
})[]
3535
autoReplyMailDatas: AutoReplyMailData[]
36-
isUseLambdaOutput: boolean
36+
pdfAttachment?: Mail.Attachment
3737
}
3838

3939
export type MailServiceParams = {

0 commit comments

Comments
 (0)