Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: perform post-submission actions when payment is completed #6089

Merged
merged 3 commits into from
Apr 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/app/models/payment.server.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const PaymentSchema = new Schema<IPaymentSchema, IPaymentModel>(
type: String,
required: true,
},
responses: [],

webhookLog: [],
status: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down
13 changes: 5 additions & 8 deletions src/app/modules/form/admin-form/admin-form.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down
194 changes: 113 additions & 81 deletions src/app/modules/payments/payments.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -157,90 +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 ResultAsync.fromPromise(
payment.save({ session }),
(error) => {
logger.error({
message: 'Database error while saving payment document',
meta: logMeta,
error,
})
return new DatabaseError(getMongoErrorMessage(error))
},
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),
)
}),
)
// 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,
.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(
session.commitTransaction(),
(error) => {
logger.error({
message: 'Database error while committing transaction',
meta: logMeta,
error,
})
return new DatabaseError(getMongoErrorMessage(error))
},
).andThen(() => {
session.endSession()
return okAsync(submission)
})
return new DatabaseError(getMongoErrorMessage(error))
},
).andThen(() => {
session.endSession()
return okAsync(payment)
})
})
.orElse((err) => {
return ResultAsync.fromPromise(
session.abortTransaction(),
(error) => {
logger.error({
message: 'Database error while aborting transaction',
meta: logMeta,
error,
})
.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)
})
return new DatabaseError(getMongoErrorMessage(error))
},
).andThen(() => {
session.endSession()
return errAsync(err)
})
})
)
})
})
)
})
// 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)
}),
)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -54,6 +51,7 @@ import {
getSubmissionMetadata,
getSubmissionMetadataList,
getSubmissionPaymentDto,
performEncryptPostSubmissionActions,
transformAttachmentMetasToSignedUrls,
transformAttachmentMetaStream,
uploadAttachments,
Expand Down Expand Up @@ -391,6 +389,7 @@ const submitEncryptModeForm: ControllerHandler<
const payment = new Payment({
amount,
email: paymentReceiptEmail,
responses: incomingSubmission.responses,
})
const paymentId = payment.id

Expand Down Expand Up @@ -571,40 +570,17 @@ 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.',
submissionId,
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 = [
Expand Down
Loading