Skip to content

Commit 046525f

Browse files
authored
feat: add payment forms subscription action to PaySG integration (#884)
## Problem The PaySG team wants to add a new action to the existing PaySG integration. This PR implements this action. For background, the PaySG team is implementing a feature where payers can sign up for a subscription where they will be recurringly billed on a monthly basis. This action allows PaySG admins to add a FormSG form in front of their subscription signup page. (Basically, identical to the previous PR #870 except that after submitting the FormSG form, payers will get directed to a subscription signup page rather than a payment page). As such, the action looks almost identical to the previous PR, with the exception of the associated PaySG API endpoint and some of the data fields. ## Solution The action is implemented in this PR, under the PaySG integration folder. ## Screenshots <img width="849" alt="image" src="https://github.com/user-attachments/assets/62ce3d27-8cfd-409f-be9f-f476c4221224" /> ## Tests For both PaySG staging and production - [ ] Create a new payment form in PaySG. Set up the plumber pipe with relevant fields. Now, go to the PaySG payment link and ensure that the subscription page works as intended. - [ ] Ensure that the payment form still works, even if all the optional fields are missing.
1 parent d16c3ab commit 046525f

File tree

4 files changed

+203
-2
lines changed

4 files changed

+203
-2
lines changed
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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 for subscription',
12+
key: 'createPaymentFormSubmissionSubscription',
13+
description:
14+
'Create a new submission for a payment form, and initiate a subscription',
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+
placeholder: 'e.g. https://pay.gov.sg/forms/aBC123Def456HijK789LMn',
23+
},
24+
{
25+
label: 'FormSG Form ID',
26+
description: 'Input the variable corresponding to form ID here',
27+
key: 'formsg_form_id',
28+
type: 'string' as const,
29+
required: true,
30+
variables: true,
31+
},
32+
{
33+
label: 'FormSG Submission ID',
34+
key: 'formsg_submission_id',
35+
type: 'string' as const,
36+
required: true,
37+
variables: true,
38+
},
39+
{
40+
label: 'FormSG Reference Field Answer',
41+
key: 'nonce',
42+
type: 'string' as const,
43+
required: true,
44+
variables: true,
45+
},
46+
{
47+
label: 'Payer Name',
48+
key: 'payer_name',
49+
type: 'string' as const,
50+
required: true,
51+
variables: true,
52+
},
53+
{
54+
label: 'Payer Email',
55+
key: 'payer_email',
56+
type: 'string' as const,
57+
required: true,
58+
variables: true,
59+
},
60+
{
61+
label: 'Payer Address',
62+
key: 'payer_address',
63+
type: 'string' as const,
64+
required: false,
65+
variables: true,
66+
},
67+
{
68+
label: 'Payer Identifier',
69+
key: 'payer_identifier',
70+
type: 'string' as const,
71+
required: false,
72+
variables: true,
73+
},
74+
{
75+
label: 'Amount (in cents)',
76+
key: 'amount_in_cents',
77+
type: 'string' as const,
78+
required: true,
79+
variables: true,
80+
},
81+
{
82+
label: 'Frequency',
83+
key: 'frequency',
84+
type: 'dropdown',
85+
options: [
86+
{
87+
label: 'Monthly',
88+
value: 'monthly',
89+
},
90+
],
91+
showOptionValue: false,
92+
value: 'monthly',
93+
required: true,
94+
},
95+
{
96+
label: 'Description',
97+
key: 'description',
98+
type: 'string' as const,
99+
required: true,
100+
variables: true,
101+
},
102+
{
103+
label: 'Additional responses',
104+
description: 'These will be included in reports exported from PaySG',
105+
key: 'responses',
106+
type: 'multirow' as const,
107+
required: false,
108+
subFields: [
109+
{
110+
placeholder: 'Question',
111+
key: 'question',
112+
type: 'string' as const,
113+
required: true,
114+
variables: true,
115+
},
116+
{
117+
placeholder: 'Answer',
118+
key: 'answer',
119+
type: 'string' as const,
120+
required: true,
121+
variables: true,
122+
},
123+
],
124+
},
125+
],
126+
127+
async run($) {
128+
const paymentServiceId = $.auth.data.paymentServiceId as string
129+
130+
try {
131+
const { formId, ...body } = requestSchema.parse($.step.parameters)
132+
133+
await $.http.post(
134+
`/v1/payment-services/:paymentServiceId/forms/:formId/subscription-submissions`,
135+
body,
136+
{
137+
urlPathParams: {
138+
paymentServiceId,
139+
formId,
140+
},
141+
},
142+
)
143+
$.setActionItem({ raw: { success: true } })
144+
} catch (error) {
145+
if (error instanceof ZodError) {
146+
const firstError = fromZodError(error).details[0]
147+
148+
throw new StepError(
149+
`${firstError.message} under set up step`,
150+
GenericSolution.ReconfigureInvalidField,
151+
$.step.position,
152+
$.app.name,
153+
)
154+
}
155+
156+
throw error
157+
}
158+
},
159+
}
160+
161+
export default action
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import emailValidator from 'email-validator'
2+
import { z } from 'zod'
3+
4+
import { requestSchema as createPaymentFormSubmissionRequestSchema } from '../create-payment-form-submission/schema'
5+
6+
export const requestSchema = createPaymentFormSubmissionRequestSchema.extend({
7+
description: z
8+
.string({ invalid_type_error: 'Empty description' })
9+
.trim()
10+
.min(1, { message: 'Specify a description' })
11+
.max(500, { message: 'Description cannot be more than 500 characters' }),
12+
frequency: z.literal('monthly'),
13+
payer_email: z
14+
.string({ invalid_type_error: 'Empty payer email' })
15+
.trim()
16+
.max(255, { message: 'Payer email cannot be more than 255 characters' })
17+
.transform((value, context) => {
18+
if (!emailValidator.validate(value)) {
19+
context.addIssue({
20+
code: z.ZodIssueCode.custom,
21+
message: 'Invalid payer email',
22+
})
23+
return z.NEVER
24+
}
25+
26+
return value
27+
}),
28+
payer_address: z
29+
.string()
30+
.trim()
31+
.transform((value) => value || undefined)
32+
.optional(),
33+
payer_identifier: z
34+
.string()
35+
.trim()
36+
.transform((value) => value || undefined)
37+
.optional(),
38+
})

packages/backend/src/apps/paysg/actions/create-payment-form-submission/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import StepError, { GenericSolution } from '@/errors/step'
88
import { requestSchema } from './schema'
99

1010
const action: IRawAction = {
11-
name: 'Create payment form submission',
11+
name: 'Create payment form submission for one-time payment',
1212
key: 'createPaymentFormSubmission',
1313
description:
14-
'Create a new submission for a payment form, and initiate a payment request',
14+
'Create a new submission for a payment form, and initiate a one-time payment',
1515
arguments: [
1616
{
1717
label: 'Payment Form Link',

packages/backend/src/apps/paysg/actions/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import createPayment from './create-payment'
22
import createPaymentFormSubmission from './create-payment-form-submission'
3+
import createPaymentFormSubmissionSubscription from './create-payment-form-submission-subscription'
34
import getPayment from './get-payment'
45
import sendEmail from './send-email'
56

@@ -8,4 +9,5 @@ export default [
89
getPayment,
910
sendEmail,
1011
createPaymentFormSubmission,
12+
createPaymentFormSubmissionSubscription,
1113
]

0 commit comments

Comments
 (0)