Skip to content

Commit ca3d531

Browse files
committed
feat: add payment forms action to paysg integration
1 parent b337bde commit ca3d531

File tree

4 files changed

+225
-2
lines changed

4 files changed

+225
-2
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import type { IRawAction } from '@plumber/types'
2+
3+
import { ZodError } from 'zod'
4+
import { fromZodError } from 'zod-validation-error'
5+
6+
import StepError, { GenericSolution } from '@/errors/step'
7+
8+
import { requestSchema } from './schema'
9+
10+
const action: IRawAction = {
11+
name: 'Create payment form submission',
12+
key: 'createPaymentFormSubmission',
13+
description:
14+
'Create a new submission for a payment form, and initiate a payment request',
15+
arguments: [
16+
{
17+
label: 'Payment Form Link',
18+
description: 'This can be found on your PaySG payment services dashboard',
19+
key: 'formId',
20+
type: 'string' as const,
21+
required: true,
22+
},
23+
{
24+
label: 'FormSG Form ID',
25+
description: 'Input the variable corresponding to form ID here',
26+
key: 'formsg_form_id',
27+
type: 'string' as const,
28+
required: true,
29+
variables: true,
30+
},
31+
{
32+
label: 'FormSG Submission ID',
33+
key: 'formsg_submission_id',
34+
type: 'string' as const,
35+
required: true,
36+
variables: true,
37+
},
38+
{
39+
label: 'FormSG Reference Field Answer',
40+
key: 'nonce',
41+
type: 'string' as const,
42+
required: true,
43+
variables: true,
44+
},
45+
{
46+
label: 'Payer Name',
47+
key: 'payer_name',
48+
type: 'string' as const,
49+
required: true,
50+
variables: true,
51+
},
52+
{
53+
label: 'Payer Email',
54+
key: 'payer_email',
55+
type: 'string' as const,
56+
required: false,
57+
variables: true,
58+
},
59+
{
60+
label: 'Amount (in cents)',
61+
key: 'amount_in_cents',
62+
type: 'string' as const,
63+
required: true,
64+
variables: true,
65+
},
66+
{
67+
label: 'Description',
68+
key: 'description',
69+
type: 'string' as const,
70+
required: false,
71+
variables: true,
72+
},
73+
{
74+
label: 'Responses',
75+
key: 'responses',
76+
type: 'multirow' as const,
77+
required: false,
78+
subFields: [
79+
{
80+
label: 'Question',
81+
key: 'question',
82+
type: 'string' as const,
83+
required: true,
84+
variables: true,
85+
},
86+
{
87+
label: 'Answer',
88+
key: 'answer',
89+
type: 'string' as const,
90+
required: true,
91+
variables: true,
92+
},
93+
],
94+
},
95+
],
96+
97+
async run($) {
98+
const paymentServiceId = $.auth.data.paymentServiceId as string
99+
100+
try {
101+
const { formId, ...body } = requestSchema.parse($.step.parameters)
102+
103+
await $.http.post(
104+
`/v1/payment-services/:paymentServiceId/forms/:formId/submissions`,
105+
body,
106+
{
107+
urlPathParams: {
108+
paymentServiceId,
109+
formId,
110+
},
111+
},
112+
)
113+
$.setActionItem({ raw: { success: true } })
114+
} catch (error) {
115+
if (error instanceof ZodError) {
116+
const firstError = fromZodError(error).details[0]
117+
118+
throw new StepError(
119+
`${firstError.message} at "${firstError.path}"`,
120+
GenericSolution.ReconfigureInvalidField,
121+
$.step.position,
122+
$.app.name,
123+
)
124+
}
125+
126+
throw error
127+
}
128+
},
129+
}
130+
131+
export default action
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import emailValidator from 'email-validator'
2+
import { z } from 'zod'
3+
4+
import { FORM_ID_LENGTH } from '@/apps/formsg/common/constants'
5+
6+
import { normalizeSpecialChars } from '../create-payment/normalize-special-chars'
7+
8+
const PAYMENT_FORM_LINK_REGEX = /^https:\/\/(staging.)?pay.gov.sg\/forms\/(.*)$/
9+
10+
export const requestSchema = z.object({
11+
formId: z
12+
.string()
13+
.trim()
14+
.min(1, { message: 'Specify a form link' })
15+
.transform((value, context) => {
16+
const match = value.match(PAYMENT_FORM_LINK_REGEX)
17+
18+
if (!match || !match[0]) {
19+
context.addIssue({
20+
code: z.ZodIssueCode.custom,
21+
message: 'Invalid payment form link',
22+
})
23+
return z.NEVER
24+
}
25+
26+
return match[2]
27+
}),
28+
formsg_form_id: z
29+
.string()
30+
.trim()
31+
.length(FORM_ID_LENGTH, { message: 'Specify a valid FormSG form ID' }),
32+
formsg_submission_id: z
33+
.string()
34+
.trim()
35+
.min(1, { message: 'Specify a submission ID' }),
36+
nonce: z.string().trim().min(1, { message: 'Specify a nonce field' }),
37+
payer_name: z
38+
.string()
39+
.trim()
40+
.min(1, { message: 'Empty payer name' })
41+
.max(255, { message: 'Payer name cannot be more than 255 characters' })
42+
.transform((value) => normalizeSpecialChars(value)),
43+
payer_email: z
44+
.string()
45+
.trim()
46+
.max(255, { message: 'Payer email cannot be more than 255 characters' })
47+
.transform((value, context) => {
48+
if (!value || value.length === 0) {
49+
return undefined
50+
}
51+
if (!emailValidator.validate(value)) {
52+
context.addIssue({
53+
code: z.ZodIssueCode.custom,
54+
message: 'Invalid payer email',
55+
})
56+
return z.NEVER
57+
}
58+
59+
return value
60+
})
61+
.optional(),
62+
amount_in_cents: z
63+
.string()
64+
.trim()
65+
.min(1, { message: 'Empty payment amount' })
66+
.pipe(
67+
z.coerce
68+
.number()
69+
.int('Payment amount must be round number')
70+
.min(50, { message: 'Payment amount must be larger than 50 cents' })
71+
.max(99999999, {
72+
message: 'Payment amount cannot be larger than $999999.99',
73+
}),
74+
),
75+
description: z
76+
.string()
77+
.trim()
78+
.max(500, { message: 'Description cannot be more than 500 characters' })
79+
.optional(),
80+
responses: z.array(
81+
z.object({
82+
question: z.string().trim().min(1, { message: 'Empty question' }),
83+
answer: z.string().trim().min(1, { message: 'Empty answer' }),
84+
}),
85+
),
86+
})

packages/backend/src/apps/paysg/actions/create-payment/schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export const requestSchema = z
6767
.string()
6868
.trim()
6969
.min(1, { message: 'Empty description' })
70-
.max(500, { message: 'Payer email cannot be more than 500 characters' }),
70+
.max(500, { message: 'Description cannot be more than 500 characters' }),
7171
paymentAmountCents: z
7272
.string()
7373
.trim()
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import createPayment from './create-payment'
2+
import createPaymentFormSubmission from './create-payment-form-submission'
23
import getPayment from './get-payment'
34
import sendEmail from './send-email'
45

5-
export default [createPayment, getPayment, sendEmail]
6+
export default [
7+
createPayment,
8+
getPayment,
9+
sendEmail,
10+
createPaymentFormSubmission,
11+
]

0 commit comments

Comments
 (0)