Skip to content

Commit

Permalink
feat(payment): webhook with charge information (#7058)
Browse files Browse the repository at this point in the history
* feat: populate payment fields

* fix: incorrect populate params

* fix: update test cases to include paymentContent

* feat: update paymentFields to be object mapped instead of array

* fix: fully resolve receipt download url

* fix: update import from absolute to relative

* test: add webhook tests for paymentContents

* feat: set empty webhooks for fixed payment

* refactor: add contextual comments for payment webhook event object

* fix: typing on paymetnContent
  • Loading branch information
KenLSM authored Feb 5, 2024
1 parent db5939c commit 3d4d832
Show file tree
Hide file tree
Showing 6 changed files with 285 additions and 19 deletions.
206 changes: 200 additions & 6 deletions src/app/models/__tests__/submission.server.model.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import getSubmissionModel, {
import {
BasicField,
FormAuthType,
PaymentType,
SubmissionType,
WebhookResponse,
} from '../../../../shared/types'
import { ISubmissionSchema } from '../../../../src/types'
import getPaymentModel from '../payment.server.model'

jest.mock('dns', () => ({
promises: {
Expand All @@ -27,6 +29,7 @@ const MockDns = jest.mocked(dns)
const Submission = getSubmissionModel(mongoose)
const EncryptedSubmission = getEncryptSubmissionModel(mongoose)
const EmailSubmission = getEmailSubmissionModel(mongoose)
const PaymentSubmission = getPaymentModel(mongoose)

// TODO: Add more tests for the rest of the submission schema.
describe('Submission Model', () => {
Expand Down Expand Up @@ -265,6 +268,132 @@ describe('Submission Model', () => {
verifiedContent: undefined,
version: 0,
created: submission.created,
paymentContent: {},
},
},
})
})

it('should return the paymentContent when the submission, payment, and webhook URL exist', async () => {
const { form } = await dbHandler.insertEncryptForm({
formOptions: {
webhook: {
url: MOCK_WEBHOOK_URL,
isRetryEnabled: true,
},
},
})
const submission = await EncryptedSubmission.create({
form: form._id,
encryptedContent: MOCK_ENCRYPTED_CONTENT,
version: 0,
})

const MOCK_PAYMENT_INTENT_ID = 'MOCK_PAYMENT_INTENT_ID'
const MOCK_EMAIL = 'MOCK_EMAIL'
const MOCK_PAYMENT_STATUS = 'succeeded'
const payment = await PaymentSubmission.create({
amount: 100,
paymentStatus: 'successful',
submission: submission._id,
gstEnabled: false,
paymentIntentId: MOCK_PAYMENT_INTENT_ID,
email: MOCK_EMAIL,
targetAccountId: 'targetAccountId',
formId: form._id,
pendingSubmissionId: submission._id,
status: MOCK_PAYMENT_STATUS,
})
submission.paymentId = payment._id
await submission.save()

const result = await EncryptedSubmission.retrieveWebhookInfoById(
String(submission._id),
)

expect(result).toEqual({
webhookUrl: MOCK_WEBHOOK_URL,
isRetryEnabled: true,
webhookView: {
data: {
attachmentDownloadUrls: {},
formId: String(form._id),
submissionId: String(submission._id),
encryptedContent: MOCK_ENCRYPTED_CONTENT,
verifiedContent: undefined,
version: 0,
created: submission.created,
paymentContent: {
amount: '1.00',
dateTime: '-',
payer: MOCK_EMAIL,
paymentIntent: MOCK_PAYMENT_INTENT_ID,
productService: '-',
status: MOCK_PAYMENT_STATUS,
transactionFee: '-',
type: 'payment_charge',
url: expect.stringContaining(
`api/v3/payments/${form._id}/${payment._id}/invoice/download`,
),
},
},
},
})
})

it('should not return the paymentContent when the payment type is Fixed', async () => {
const { form } = await dbHandler.insertEncryptForm({
formOptions: {
webhook: {
url: MOCK_WEBHOOK_URL,
isRetryEnabled: true,
},
},
})
const submission = await EncryptedSubmission.create({
form: form._id,
encryptedContent: MOCK_ENCRYPTED_CONTENT,
version: 0,
})

const MOCK_PAYMENT_INTENT_ID = 'MOCK_PAYMENT_INTENT_ID'
const MOCK_EMAIL = 'MOCK_EMAIL'
const MOCK_PAYMENT_STATUS = 'succeeded'
const payment = await PaymentSubmission.create({
amount: 100,
paymentStatus: 'successful',
submission: submission._id,
gstEnabled: false,
paymentIntentId: MOCK_PAYMENT_INTENT_ID,
email: MOCK_EMAIL,
targetAccountId: 'targetAccountId',
formId: form._id,
pendingSubmissionId: submission._id,
status: MOCK_PAYMENT_STATUS,
payment_fields_snapshot: {
payment_type: PaymentType.Fixed,
},
})
submission.paymentId = payment._id
await submission.save()

const result = await EncryptedSubmission.retrieveWebhookInfoById(
String(submission._id),
)

expect(result).toEqual({
webhookUrl: MOCK_WEBHOOK_URL,
isRetryEnabled: true,
webhookView: {
data: {
attachmentDownloadUrls: {},
formId: String(form._id),
submissionId: String(submission._id),
encryptedContent: MOCK_ENCRYPTED_CONTENT,
verifiedContent: undefined,
version: 0,
created: submission.created,
paymentContent: {},
},
},
})
Expand Down Expand Up @@ -318,6 +447,7 @@ describe('Submission Model', () => {
verifiedContent: undefined,
version: 0,
created: submission.created,
paymentContent: {},
},
},
})
Expand Down Expand Up @@ -354,6 +484,7 @@ describe('Submission Model', () => {
verifiedContent: undefined,
version: 0,
created: submission.created,
paymentContent: {},
},
},
})
Expand All @@ -365,7 +496,7 @@ describe('Submission Model', () => {
// Arrange
const formCounts = [4, 2, 4]
const formIdsAndCounts = times(formCounts.length, (it) => ({
_id: mongoose.Types.ObjectId(),
_id: new mongoose.Types.ObjectId(),
count: formCounts[it],
}))
const submissionPromises: Promise<ISubmissionSchema>[] = []
Expand Down Expand Up @@ -401,7 +532,7 @@ describe('Submission Model', () => {
// Arrange
const formCounts = [1, 1, 2]
const formIdsAndCounts = times(formCounts.length, (it) => ({
_id: mongoose.Types.ObjectId(),
_id: new mongoose.Types.ObjectId(),
count: formCounts[it],
}))
const submissionPromises: Promise<ISubmissionSchema>[] = []
Expand Down Expand Up @@ -436,6 +567,66 @@ describe('Submission Model', () => {

describe('Methods', () => {
describe('getWebhookView', () => {
it('should returnt non-null view with paymentContent when submission has paymentId', async () => {
const formId = new ObjectId()

const submission = await EncryptedSubmission.create({
submissionType: SubmissionType.Encrypt,
form: formId,
encryptedContent: MOCK_ENCRYPTED_CONTENT,
version: 1,
authType: FormAuthType.NIL,
myInfoFields: [],
webhookResponses: [],
})

const MOCK_PAYMENT_INTENT_ID = 'MOCK_PAYMENT_INTENT_ID'
const MOCK_EMAIL = 'MOCK_EMAIL'
const MOCK_PAYMENT_STATUS = 'succeeded'
const payment = await PaymentSubmission.create({
amount: 100,
paymentStatus: 'successful',
submission: submission._id,
gstEnabled: false,
paymentIntentId: MOCK_PAYMENT_INTENT_ID,
email: MOCK_EMAIL,
targetAccountId: 'targetAccountId',
formId: formId,
pendingSubmissionId: submission._id,
status: MOCK_PAYMENT_STATUS,
})
submission.paymentId = payment._id
await submission.save()

// Act
const actualWebhookView = await submission.getWebhookView()

// Assert
expect(actualWebhookView).toEqual({
data: {
formId: expect.any(String),
submissionId: expect.any(String),
created: expect.any(Date),
encryptedContent: MOCK_ENCRYPTED_CONTENT,
verifiedContent: undefined,
attachmentDownloadUrls: {},
version: 1,
paymentContent: {
amount: '1.00',
dateTime: '-',
payer: MOCK_EMAIL,
paymentIntent: MOCK_PAYMENT_INTENT_ID,
productService: '-',
status: MOCK_PAYMENT_STATUS,
transactionFee: '-',
type: 'payment_charge',
url: expect.stringContaining(
`api/v3/payments/${formId}/${payment._id}/invoice/download`,
),
},
},
})
})
it('should return non-null view with encryptedSubmission type when submission has no verified content', async () => {
// Arrange
const formId = new ObjectId()
Expand All @@ -451,7 +642,7 @@ describe('Submission Model', () => {
})

// Act
const actualWebhookView = submission.getWebhookView()
const actualWebhookView = await submission.getWebhookView()

// Assert
expect(actualWebhookView).toEqual({
Expand All @@ -463,6 +654,7 @@ describe('Submission Model', () => {
verifiedContent: undefined,
attachmentDownloadUrls: {},
version: 1,
paymentContent: {},
},
})
})
Expand All @@ -483,7 +675,7 @@ describe('Submission Model', () => {
})

// Act
const actualWebhookView = submission.getWebhookView()
const actualWebhookView = await submission.getWebhookView()

// Assert
expect(actualWebhookView).toEqual({
Expand All @@ -495,6 +687,7 @@ describe('Submission Model', () => {
encryptedContent: MOCK_ENCRYPTED_CONTENT,
verifiedContent: MOCK_VERIFIED_CONTENT,
version: 1,
paymentContent: {},
},
})
})
Expand Down Expand Up @@ -526,7 +719,7 @@ describe('Submission Model', () => {
).populate('form', 'webhook')

// Act
const actualWebhookView = populatedSubmission!.getWebhookView()
const actualWebhookView = await populatedSubmission!.getWebhookView()

// Assert
expect(actualWebhookView).toEqual({
Expand All @@ -538,6 +731,7 @@ describe('Submission Model', () => {
encryptedContent: MOCK_ENCRYPTED_CONTENT,
verifiedContent: MOCK_VERIFIED_CONTENT,
version: 1,
paymentContent: {},
},
})
})
Expand All @@ -559,7 +753,7 @@ describe('Submission Model', () => {
})

// Act
const actualWebhookView = submission.getWebhookView()
const actualWebhookView = await submission.getWebhookView()

// Assert
expect(actualWebhookView).toBeNull()
Expand Down
27 changes: 21 additions & 6 deletions src/app/models/submission.server.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
WebhookData,
WebhookView,
} from '../../types'
import { getPaymentWebhookEventObject } from '../modules/payments/payment.service.utils'
import { createQueryWithDateParam } from '../utils/date'

import { FORM_SCHEMA_ID } from './form.server.model'
Expand Down Expand Up @@ -144,8 +145,8 @@ export const EmailSubmissionSchema = new Schema<IEmailSubmissionSchema>({
/**
* Returns null as email submission does not have a webhook view
*/
EmailSubmissionSchema.methods.getWebhookView = function (): null {
return null
EmailSubmissionSchema.methods.getWebhookView = function (): Promise<null> {
return Promise.resolve(null)
}

const webhookResponseSchema = new Schema<IWebhookResponseSchema>(
Expand Down Expand Up @@ -209,16 +210,23 @@ export const EncryptSubmissionSchema = new Schema<
* Returns an object which represents the encrypted submission
* which will be posted to the webhook URL.
*/
EncryptSubmissionSchema.methods.getWebhookView = function (
EncryptSubmissionSchema.methods.getWebhookView = async function (
this: IEncryptedSubmissionSchema | IPopulatedWebhookSubmission,
): WebhookView {
): Promise<WebhookView> {
const formId = this.populated('form')
? String((this as IPopulatedWebhookSubmission).form._id)
: String(this.form)
const attachmentRecords = Object.fromEntries(
this.attachmentMetadata ?? new Map(),
)

if (this.paymentId) {
await (this as IPopulatedWebhookSubmission).populate('paymentId')
}
const paymentContent = this.populated('paymentId')
? getPaymentWebhookEventObject(this.paymentId)
: {}

const webhookData: WebhookData = {
formId,
submissionId: String(this._id),
Expand All @@ -227,6 +235,7 @@ EncryptSubmissionSchema.methods.getWebhookView = function (
version: this.version,
created: this.created,
attachmentDownloadUrls: attachmentRecords,
paymentContent,
}

return {
Expand All @@ -250,13 +259,19 @@ EncryptSubmissionSchema.statics.retrieveWebhookInfoById = function (
submissionId: string,
): Promise<SubmissionWebhookInfo | null> {
return this.findById(submissionId)
.populate('form', 'webhook')
.populate([{ path: 'form', select: 'webhook' }, { path: 'paymentId' }])
.then((populatedSubmission: IPopulatedWebhookSubmission | null) => {
if (!populatedSubmission) return null
const webhookView = populatedSubmission.getWebhookView()
return Promise.all([populatedSubmission, webhookView])
})
.then((arr) => {
if (!arr) return null
const [populatedSubmission, webhookView] = arr
return {
webhookUrl: populatedSubmission.form.webhook?.url ?? '',
isRetryEnabled: !!populatedSubmission.form.webhook?.isRetryEnabled,
webhookView: populatedSubmission.getWebhookView(),
webhookView,
}
})
}
Expand Down
Loading

0 comments on commit 3d4d832

Please sign in to comment.