From 567ce54e22135525e850a8ea8f6802f9d1b6a520 Mon Sep 17 00:00:00 2001 From: Justyn Oh Date: Wed, 12 Apr 2023 16:36:57 +0800 Subject: [PATCH 1/3] feat: perform post-submission actions when payment is completed --- shared/types/payment.ts | 1 + src/app/models/payment.server.model.ts | 1 + .../__tests__/admin-form.controller.spec.ts | 6 +- .../form/admin-form/admin-form.controller.ts | 13 +- src/app/modules/payments/payments.service.ts | 151 ++++++++++-------- .../encrypt-submission.controller.ts | 34 +--- .../encrypt-submission.service.ts | 73 +++++++++ .../encrypt-submission.utils.ts | 19 ++- .../modules/submission/submission.utils.ts | 15 -- 9 files changed, 188 insertions(+), 125 deletions(-) diff --git a/shared/types/payment.ts b/shared/types/payment.ts index e9c44f980a..ee49170a86 100644 --- a/shared/types/payment.ts +++ b/shared/types/payment.ts @@ -35,6 +35,7 @@ export type Payment = { email: string amount: number paymentIntentId: string + responses: any[] // Will be typed as FilteredResponse[] // Payment status tracking webhookLog: Stripe.Event[] diff --git a/src/app/models/payment.server.model.ts b/src/app/models/payment.server.model.ts index d8a3fce513..dcbab4bdd9 100644 --- a/src/app/models/payment.server.model.ts +++ b/src/app/models/payment.server.model.ts @@ -28,6 +28,7 @@ const PaymentSchema = new Schema( type: String, required: true, }, + responses: [], webhookLog: [], status: { diff --git a/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts b/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts index 9f76c891ed..c1399d7a03 100644 --- a/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts +++ b/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts @@ -6141,11 +6141,9 @@ describe('admin-form.controller', () => { responses: MOCK_RESPONSES, form: MOCK_FORM, encryptedContent: MOCK_ENCRYPTED_CONTENT, - } as IncomingEncryptSubmission), - ) - MockSubmissionUtils.extractEmailConfirmationDataFromIncomingSubmission.mockReturnValue( - [], + } as unknown as IncomingEncryptSubmission), ) + MockSubmissionUtils.extractEmailConfirmationData.mockReturnValue([]) MockEncryptSubmissionService.createEncryptSubmissionWithoutSave.mockReturnValue( MOCK_SUBMISSION, ) diff --git a/src/app/modules/form/admin-form/admin-form.controller.ts b/src/app/modules/form/admin-form/admin-form.controller.ts index d8ff56fbe0..c3495db18e 100644 --- a/src/app/modules/form/admin-form/admin-form.controller.ts +++ b/src/app/modules/form/admin-form/admin-form.controller.ts @@ -74,10 +74,7 @@ import * as EncryptSubmissionService from '../../submission/encrypt-submission/e import { mapRouteError as mapEncryptSubmissionError } from '../../submission/encrypt-submission/encrypt-submission.utils' import IncomingEncryptSubmission from '../../submission/encrypt-submission/IncomingEncryptSubmission.class' import * as SubmissionService from '../../submission/submission.service' -import { - extractEmailConfirmationData, - extractEmailConfirmationDataFromIncomingSubmission, -} from '../../submission/submission.utils' +import { extractEmailConfirmationData } from '../../submission/submission.utils' import * as UserService from '../../user/user.service' import { PrivateFormError } from '../form.errors' import * as FormService from '../form.service' @@ -1491,10 +1488,10 @@ export const submitEncryptPreview: ControllerHandler< void SubmissionService.sendEmailConfirmations({ form, submission, - recipientData: - extractEmailConfirmationDataFromIncomingSubmission( - incomingSubmission, - ), + recipientData: extractEmailConfirmationData( + incomingSubmission.responses, + form.form_fields, + ), }) // Return the reply early to the submitter diff --git a/src/app/modules/payments/payments.service.ts b/src/app/modules/payments/payments.service.ts index e9a164ccf0..7213810cc2 100644 --- a/src/app/modules/payments/payments.service.ts +++ b/src/app/modules/payments/payments.service.ts @@ -7,6 +7,8 @@ import { createLoggerWithLabel } from '../../config/logger' import getPaymentModel from '../../models/payment.server.model' import { getMongoErrorMessage } from '../../utils/handle-mongo-error' import { DatabaseError } from '../core/core.errors' +import { performEncryptPostSubmissionActions } from '../submission/encrypt-submission/encrypt-submission.service' +import { isSubmissionEncryptMode } from '../submission/encrypt-submission/encrypt-submission.utils' import { PendingSubmissionNotFoundError } from '../submission/submission.errors' import * as SubmissionService from '../submission/submission.service' @@ -130,6 +132,7 @@ export const findPaymentBySubmissionId = ( * @requires paymentId must reference a payment document such that payment.completedPayment is undefined * * @param paymentId payment id of the payment to be confirmed + * @param paymentDate date of the charge success * @param receiptUrl the payment's receipt URL * @param transactionFee the transaction fee associated with the payment * @@ -164,83 +167,95 @@ export const confirmPaymentPendingSubmission = ( error, }) return new DatabaseError(getMongoErrorMessage(error)) - }).andThen((session) => { - session.startTransaction({ - readPreference: 'primary', - readConcern: { level: 'snapshot' }, - writeConcern: { w: 'majority' }, - }) + }) + .andThen((session) => { + session.startTransaction({ + readPreference: 'primary', + readConcern: { level: 'snapshot' }, + writeConcern: { w: 'majority' }, + }) - return ( - // Step 1: Retrieve the payment by payment id and check that the payment - // has not already been confirmed. - findPaymentById(paymentId, session) - .andThen((payment) => - payment.completedPayment - ? errAsync(new PaymentAlreadyConfirmedError()) - : okAsync(payment), - ) - .andThen((payment) => - // Step 2: Copy the pending submission to the submissions collection - SubmissionService.copyPendingSubmissionToSubmissions( - payment.pendingSubmissionId, - session, - ).andThen((submission) => { - // Step 3: Update the payment document with the metadata showing that - // the payment is complete and save it - payment.completedPayment = { - submissionId: submission._id, - paymentDate, - receiptUrl, - transactionFee, - } + return ( + // Step 1: Retrieve the payment by payment id and check that the payment + // has not already been confirmed. + findPaymentById(paymentId, session) + .andThen((payment) => + payment.completedPayment + ? errAsync(new PaymentAlreadyConfirmedError()) + : okAsync(payment), + ) + .andThen((payment) => + // Step 2: Copy the pending submission to the submissions collection + SubmissionService.copyPendingSubmissionToSubmissions( + payment.pendingSubmissionId, + session, + ).andThen((submission) => { + // Step 3: Update the payment document with the metadata showing that + // the payment is complete and save it + payment.completedPayment = { + submissionId: submission._id, + paymentDate, + receiptUrl, + transactionFee, + } + return ResultAsync.fromPromise( + payment.save({ session }), + (error) => { + logger.error({ + message: 'Database error while saving payment document', + meta: logMeta, + error, + }) + return new DatabaseError(getMongoErrorMessage(error)) + }, + ).andThen((payment) => okAsync({ payment, submission })) + }), + ) + // Finally: Commit or abort depending on whether an error was caught, + // then end the session + .andThen(({ payment, submission }) => { return ResultAsync.fromPromise( - payment.save({ session }), + session.commitTransaction(), (error) => { logger.error({ - message: 'Database error while saving payment document', + message: 'Database error while committing transaction', meta: logMeta, error, }) return new DatabaseError(getMongoErrorMessage(error)) }, - ) - }), - ) - // Finally: Commit or abort depending on whether an error was caught, - // then end the session - .andThen((payment) => { - return ResultAsync.fromPromise( - session.commitTransaction(), - (error) => { - logger.error({ - message: 'Database error while committing transaction', - meta: logMeta, - error, - }) - return new DatabaseError(getMongoErrorMessage(error)) - }, - ).andThen(() => { - session.endSession() - return okAsync(payment) + ).andThen(() => { + session.endSession() + return okAsync({ payment, submission }) + }) }) - }) - .orElse((err) => { - return ResultAsync.fromPromise( - session.abortTransaction(), - (error) => { - logger.error({ - message: 'Database error while aborting transaction', - meta: logMeta, - error, - }) - return new DatabaseError(getMongoErrorMessage(error)) - }, - ).andThen(() => { - session.endSession() - return errAsync(err) + .orElse((err) => { + return ResultAsync.fromPromise( + session.abortTransaction(), + (error) => { + logger.error({ + message: 'Database error while aborting transaction', + meta: logMeta, + error, + }) + return new DatabaseError(getMongoErrorMessage(error)) + }, + ).andThen(() => { + session.endSession() + return errAsync(err) + }) }) - }) - ) - }) + ) + }) + .andThen(({ payment, submission }) => { + if (isSubmissionEncryptMode(submission)) { + return performEncryptPostSubmissionActions( + submission, + payment.responses, + ) + .andThen(() => okAsync(payment)) + .orElse(() => okAsync(payment)) + } + return okAsync(payment) + }) } diff --git a/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts b/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts index cfd344d87d..88c3985a26 100644 --- a/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts +++ b/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts @@ -41,10 +41,7 @@ import { SgidService } from '../../sgid/sgid.service' import { getOidcService } from '../../spcp/spcp.oidc.service' import { getPopulatedUserById } from '../../user/user.service' import * as VerifiedContentService from '../../verified-content/verified-content.service' -import { WebhookFactory } from '../../webhook/webhook.factory' import * as EncryptSubmissionMiddleware from '../encrypt-submission/encrypt-submission.middleware' -import { sendEmailConfirmations } from '../submission.service' -import { extractEmailConfirmationDataFromIncomingSubmission } from '../submission.utils' import { addPaymentDataStream, @@ -54,6 +51,7 @@ import { getSubmissionMetadata, getSubmissionMetadataList, getSubmissionPaymentDto, + performEncryptPostSubmissionActions, transformAttachmentMetasToSignedUrls, transformAttachmentMetaStream, uploadAttachments, @@ -391,6 +389,7 @@ const submitEncryptModeForm: ControllerHandler< const payment = new Payment({ amount, email: paymentReceiptEmail, + responses: incomingSubmission.responses, }) const paymentId = payment.id @@ -571,18 +570,6 @@ const submitEncryptModeForm: ControllerHandler< }, }) - // Fire webhooks if available - // To avoid being coupled to latency of receiving system, - // do not await on webhook - const webhookUrl = form.webhook?.url - if (webhookUrl) { - void WebhookFactory.sendInitialWebhook( - submission, - webhookUrl, - !!form.webhook?.isRetryEnabled, - ) - } - // Send success back to client res.json({ message: 'Form submission successful.', @@ -590,21 +577,10 @@ const submitEncryptModeForm: ControllerHandler< timestamp: (submission.created || new Date()).getTime(), }) - // Send Email Confirmations - return sendEmailConfirmations({ - form, + return await performEncryptPostSubmissionActions( submission, - recipientData: - extractEmailConfirmationDataFromIncomingSubmission(incomingSubmission), - }).mapErr((error) => { - logger.error({ - message: 'Error while sending email confirmations', - meta: { - action: 'sendEmailAutoReplies', - }, - error, - }) - }) + incomingSubmission.responses, + ) } export const handleEncryptedSubmission = [ diff --git a/src/app/modules/submission/encrypt-submission/encrypt-submission.service.ts b/src/app/modules/submission/encrypt-submission/encrypt-submission.service.ts index 5ca34376ea..2dad1c1406 100644 --- a/src/app/modules/submission/encrypt-submission/encrypt-submission.service.ts +++ b/src/app/modules/submission/encrypt-submission/encrypt-submission.service.ts @@ -13,6 +13,7 @@ import { SubmissionPaymentDto, } from '../../../../../shared/types' import { + FieldResponse, IEncryptedSubmissionSchema, IPopulatedEncryptedForm, IPopulatedForm, @@ -28,15 +29,25 @@ import { AttachmentUploadError, DatabaseError, MalformedParametersError, + PossibleDatabaseError, } from '../../core/core.errors' import { CreatePresignedUrlError } from '../../form/admin-form/admin-form.errors' +import { FormNotFoundError } from '../../form/form.errors' +import * as FormService from '../../form/form.service' import { isFormEncryptMode } from '../../form/form.utils' import { PaymentNotFoundError } from '../../payments/payments.errors' import * as PaymentsService from '../../payments/payments.service' +import { + WebhookPushToQueueError, + WebhookValidationError, +} from '../../webhook/webhook.errors' +import { WebhookFactory } from '../../webhook/webhook.factory' import { ResponseModeError, SubmissionNotFoundError, } from '../submission.errors' +import { sendEmailConfirmations } from '../submission.service' +import { extractEmailConfirmationData } from '../submission.utils' import { AttachmentMetadata, @@ -448,3 +459,65 @@ export const createEncryptSubmissionWithoutSave = ({ version, }) } + +/** + * Performs the post-submission actions for encrypt submissions. This is to be + * called when the submission is completed + * @param submission the completed submission + * @param responses the verified field responses sent with the original submission request + * @returns ok(true) if all actions were completed successfully + * @returns err(FormNotFoundError) if the form or form admin does not exist + * @returns err(ResponseModeError) if the form is not encrypt mode + * @returns err(WebhookValidationError) if the webhook URL failed validation + * @returns err(WebhookPushToQueueError) if the webhook was failed to be pushed to SQS + * @returns err(SubmissionNotFoundError) if there was an error updating the submission with the webhook record + * @returns err(PossibleDatabaseError) if error occurs whilst querying the database + */ +export const performEncryptPostSubmissionActions = ( + submission: IEncryptedSubmissionSchema, + responses: FieldResponse[], +): ResultAsync< + true, + | FormNotFoundError + | ResponseModeError + | WebhookValidationError + | WebhookPushToQueueError + | SubmissionNotFoundError + | PossibleDatabaseError +> => { + return FormService.retrieveFullFormById(submission.form) + .andThen(checkFormIsEncryptMode) + .andThen((form) => { + // Fire webhooks if available + // To avoid being coupled to latency of receiving system, + // do not await on webhook + const webhookUrl = form.webhook?.url + if (!webhookUrl) return okAsync(form) + + return WebhookFactory.sendInitialWebhook( + submission, + webhookUrl, + !!form.webhook?.isRetryEnabled, + ).andThen(() => okAsync(form)) + }) + .andThen((form) => { + // Send Email Confirmations + return sendEmailConfirmations({ + form, + submission, + recipientData: extractEmailConfirmationData( + responses, + form.form_fields, + ), + }).mapErr((error) => { + logger.error({ + message: 'Error while sending email confirmations', + meta: { + action: 'sendEmailAutoReplies', + }, + error, + }) + return error + }) + }) +} diff --git a/src/app/modules/submission/encrypt-submission/encrypt-submission.utils.ts b/src/app/modules/submission/encrypt-submission/encrypt-submission.utils.ts index b2b268fdaf..068de5bf74 100644 --- a/src/app/modules/submission/encrypt-submission/encrypt-submission.utils.ts +++ b/src/app/modules/submission/encrypt-submission/encrypt-submission.utils.ts @@ -4,8 +4,14 @@ import moment from 'moment-timezone' import { StorageModeSubmissionDto, SubmissionPaymentDto, + SubmissionType, } from '../../../../../shared/types' -import { MapRouteErrors, SubmissionData } from '../../../../types' +import { + IEncryptedSubmissionSchema, + ISubmissionSchema, + MapRouteErrors, + SubmissionData, +} from '../../../../types' import { MapRouteError } from '../../../../types/routing' import { createLoggerWithLabel } from '../../../config/logger' import { MalformedVerifiedContentError } from '../../../modules/verified-content/verified-content.errors' @@ -199,6 +205,17 @@ const errorMapper: MapRouteError = ( export const mapRouteError: MapRouteErrors = genericMapRouteErrorTransform(errorMapper) +/** + * Typeguard to check if given submission is an encrypt mode submission. + * @param submission the submission to check + * @returns true if submission is encrypt mode submission, false otherwise. + */ +export const isSubmissionEncryptMode = ( + submission: ISubmissionSchema, +): submission is IEncryptedSubmissionSchema => { + return submission.submissionType === SubmissionType.Encrypt +} + /** * Creates and returns an EncryptedSubmissionDto object from submissionData and * attachment presigned urls. diff --git a/src/app/modules/submission/submission.utils.ts b/src/app/modules/submission/submission.utils.ts index 3c5ed92328..d1d31d8803 100644 --- a/src/app/modules/submission/submission.utils.ts +++ b/src/app/modules/submission/submission.utils.ts @@ -10,7 +10,6 @@ import { import { FieldResponse, FormFieldSchema, IFormDocument } from '../../../types' import { AutoReplyMailData } from '../../services/mail/mail.types' -import { IncomingSubmission } from './IncomingSubmission.class' import { ConflictError } from './submission.errors' import { FilteredResponse } from './submission.types' @@ -81,7 +80,6 @@ export const getFormFieldModeFilter = ( * @param formFields Fields from form object * @returns Array of data for email confirmations */ -// TODO: Migrate to extractEmailConfirmationDataFromIncomingSubmission export const extractEmailConfirmationData = ( responses: FieldResponse[], formFields: FormFieldSchema[] | undefined, @@ -110,19 +108,6 @@ export const extractEmailConfirmationData = ( }, []) } -/** - * Extracts response data to be sent in email confirmations - * @param responses Responses from form filler - * @param formFields Fields from form object - * @returns Array of data for email confirmations - */ -export const extractEmailConfirmationDataFromIncomingSubmission = ( - incomingSubmission: IncomingSubmission, -): AutoReplyMailData[] => { - const { responses, form } = incomingSubmission - return extractEmailConfirmationData(responses, form.form_fields) -} - /** * Filter allowed form field responses from given responses and return the * array of responses with duplicates removed. From 6d076a809998cf850e71de27e8246dbff26a373a Mon Sep 17 00:00:00 2001 From: Justyn Oh Date: Mon, 17 Apr 2023 15:04:56 +0800 Subject: [PATCH 2/3] feat: delete email responses from payment document --- src/app/modules/payments/payments.service.ts | 187 ++++++++++--------- 1 file changed, 102 insertions(+), 85 deletions(-) diff --git a/src/app/modules/payments/payments.service.ts b/src/app/modules/payments/payments.service.ts index 7213810cc2..d9c612c70e 100644 --- a/src/app/modules/payments/payments.service.ts +++ b/src/app/modules/payments/payments.service.ts @@ -160,102 +160,119 @@ export const confirmPaymentPendingSubmission = ( } // Step 0: Set up the session and start the transaction - return ResultAsync.fromPromise(mongoose.startSession(), (error) => { - logger.error({ - message: 'Database error while starting mongoose session', - meta: logMeta, - error, - }) - return new DatabaseError(getMongoErrorMessage(error)) - }) - .andThen((session) => { - session.startTransaction({ - readPreference: 'primary', - readConcern: { level: 'snapshot' }, - writeConcern: { w: 'majority' }, + return ( + ResultAsync.fromPromise(mongoose.startSession(), (error) => { + logger.error({ + message: 'Database error while starting mongoose session', + meta: logMeta, + error, }) + return new DatabaseError(getMongoErrorMessage(error)) + }) + .andThen((session) => { + session.startTransaction({ + readPreference: 'primary', + readConcern: { level: 'snapshot' }, + writeConcern: { w: 'majority' }, + }) - return ( - // Step 1: Retrieve the payment by payment id and check that the payment - // has not already been confirmed. - findPaymentById(paymentId, session) - .andThen((payment) => - payment.completedPayment - ? errAsync(new PaymentAlreadyConfirmedError()) - : okAsync(payment), - ) - .andThen((payment) => - // Step 2: Copy the pending submission to the submissions collection - SubmissionService.copyPendingSubmissionToSubmissions( - payment.pendingSubmissionId, - session, - ).andThen((submission) => { - // Step 3: Update the payment document with the metadata showing that - // the payment is complete and save it - payment.completedPayment = { - submissionId: submission._id, - paymentDate, - receiptUrl, - transactionFee, - } + return ( + // Step 1: Retrieve the payment by payment id and check that the payment + // has not already been confirmed. + findPaymentById(paymentId, session) + .andThen((payment) => + payment.completedPayment + ? errAsync(new PaymentAlreadyConfirmedError()) + : okAsync(payment), + ) + .andThen((payment) => + // Step 2: Copy the pending submission to the submissions collection + SubmissionService.copyPendingSubmissionToSubmissions( + payment.pendingSubmissionId, + session, + ).andThen((submission) => { + // Step 3: Update the payment document with the metadata showing that + // the payment is complete and save it + payment.completedPayment = { + submissionId: submission._id, + paymentDate, + receiptUrl, + transactionFee, + } + return ResultAsync.fromPromise( + payment.save({ session }), + (error) => { + logger.error({ + message: 'Database error while saving payment document', + meta: logMeta, + error, + }) + return new DatabaseError(getMongoErrorMessage(error)) + }, + ).andThen(() => okAsync(submission)) + }), + ) + // Finally: Commit or abort depending on whether an error was caught, + // then end the session + .andThen((submission) => { return ResultAsync.fromPromise( - payment.save({ session }), + session.commitTransaction(), (error) => { logger.error({ - message: 'Database error while saving payment document', + message: 'Database error while committing transaction', meta: logMeta, error, }) return new DatabaseError(getMongoErrorMessage(error)) }, - ).andThen((payment) => okAsync({ payment, submission })) - }), - ) - // Finally: Commit or abort depending on whether an error was caught, - // then end the session - .andThen(({ payment, submission }) => { - return ResultAsync.fromPromise( - session.commitTransaction(), - (error) => { - logger.error({ - message: 'Database error while committing transaction', - meta: logMeta, - error, - }) - return new DatabaseError(getMongoErrorMessage(error)) - }, - ).andThen(() => { - session.endSession() - return okAsync({ payment, submission }) + ).andThen(() => { + session.endSession() + return okAsync(submission) + }) }) - }) - .orElse((err) => { - return ResultAsync.fromPromise( - session.abortTransaction(), - (error) => { - logger.error({ - message: 'Database error while aborting transaction', - meta: logMeta, - error, - }) - return new DatabaseError(getMongoErrorMessage(error)) - }, - ).andThen(() => { - session.endSession() - return errAsync(err) + .orElse((err) => { + return ResultAsync.fromPromise( + session.abortTransaction(), + (error) => { + logger.error({ + message: 'Database error while aborting transaction', + meta: logMeta, + error, + }) + return new DatabaseError(getMongoErrorMessage(error)) + }, + ).andThen(() => { + session.endSession() + return errAsync(err) + }) }) - }) - ) - }) - .andThen(({ payment, submission }) => { - if (isSubmissionEncryptMode(submission)) { - return performEncryptPostSubmissionActions( - submission, - payment.responses, ) - .andThen(() => okAsync(payment)) - .orElse(() => okAsync(payment)) - } - return okAsync(payment) - }) + }) + // Post-submission: fire webhooks and send email confirmations. + .andThen((submission) => + findPaymentById(paymentId).andThen((payment) => { + if (isSubmissionEncryptMode(submission)) { + return performEncryptPostSubmissionActions( + submission, + payment.responses, + ) + .andThen(() => { + // If successfully sent email confirmations, delete response data from payment document. + payment.responses = [] + return ResultAsync.fromPromise(payment.save(), (error) => { + logger.error({ + message: + 'Database error while deleting responses from payment', + meta: logMeta, + error, + }) + return new DatabaseError(getMongoErrorMessage(error)) + }).andThen(() => okAsync(payment)) + }) + .orElse(() => okAsync(payment)) + } + return okAsync(payment) + }), + ) + ) } From 46f2e8c5158b81c7b219c3bffe236040c6e6a3c8 Mon Sep 17 00:00:00 2001 From: Justyn Oh Date: Mon, 17 Apr 2023 15:55:14 +0800 Subject: [PATCH 3/3] chore: remove responses field from shared payment type --- shared/types/payment.ts | 1 - src/types/payment.ts | 8 +++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/shared/types/payment.ts b/shared/types/payment.ts index ee49170a86..e9c44f980a 100644 --- a/shared/types/payment.ts +++ b/shared/types/payment.ts @@ -35,7 +35,6 @@ export type Payment = { email: string amount: number paymentIntentId: string - responses: any[] // Will be typed as FilteredResponse[] // Payment status tracking webhookLog: Stripe.Event[] diff --git a/src/types/payment.ts b/src/types/payment.ts index 4e60a31d5a..efcb6d773f 100644 --- a/src/types/payment.ts +++ b/src/types/payment.ts @@ -3,7 +3,13 @@ import Stripe from 'stripe' import { Payment } from '../../shared/types/payment' -export interface IPaymentSchema extends Payment, Document {} +export interface IPaymentSchema extends Payment, Document { + /** + * Additional field to store responses for sending email confirmations post-payment. + * Will be used to store FilteredResponse[], allows for population. + */ + responses: any[] +} export type IPaymentModel = Model