Skip to content

Commit fecaa0b

Browse files
authored
Release v1.36.0 (#886)
* feat: add payment forms subscription action to PaySG integration * chore: change email to plumber support form * chore: bold email otp
2 parents 8fffd81 + 6ef7326 commit fecaa0b

File tree

12 files changed

+227
-20
lines changed

12 files changed

+227
-20
lines changed

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,5 +105,5 @@
105105
"tsconfig-paths": "^4.2.0",
106106
"type-fest": "4.10.3"
107107
},
108-
"version": "1.35.1"
108+
"version": "1.36.0"
109109
}
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
]

packages/backend/src/graphql/mutations/request-otp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ const requestOtp: MutationResolvers['requestOtp'] = async (_parent, params) => {
4949
// Send otp
5050
await sendEmail({
5151
subject: 'Your OTP for Plumber',
52-
body: `Your OTP is ${otp}. It's valid for ${
52+
body: `Your OTP is <b>${otp}</b>. It's valid for ${
5353
OTP_VALIDITY_IN_MS / 1000 / 60
5454
} minutes.`,
5555
recipient: email,

packages/frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "frontend",
3-
"version": "1.35.1",
3+
"version": "1.36.0",
44
"scripts": {
55
"dev": "wait-on tcp:3000 && vite --host --force",
66
"build": "tsc && vite build --mode=${VITE_MODE:-prod}",

packages/frontend/src/components/ErrorResult/GenericErrorResult.tsx

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Box, Collapse, Text } from '@chakra-ui/react'
55
import { Button, Infobox } from '@opengovsg/design-system-react'
66

77
import JSONViewer from '@/components/JSONViewer'
8+
import { SUPPORT_FORM_LINK } from '@/config/urls'
89

910
interface GenericErrorResultProps {
1011
errorDetails: IJSONObject
@@ -29,16 +30,20 @@ export default function GenericErrorResult(props: GenericErrorResultProps) {
2930
{`Check if you have configured ${
3031
isTestRun ? 'the steps above' : 'this step'
3132
} correctly and retest. If
32-
this error still persists, contact us at [email protected]. `}
33-
<Button
34-
onClick={toggleDropdown}
35-
variant="link"
36-
size="sm"
37-
sx={{ textDecoration: 'underline' }}
38-
>
39-
View error details below.
40-
</Button>
33+
this error still persists, contact us at `}
34+
<Text as="a" href={SUPPORT_FORM_LINK} target="_blank">
35+
{SUPPORT_FORM_LINK}
36+
</Text>
37+
.
4138
</Text>
39+
<Button
40+
onClick={toggleDropdown}
41+
variant="link"
42+
size="sm"
43+
sx={{ textDecoration: 'underline' }}
44+
>
45+
View error details below.
46+
</Button>
4247

4348
<Box>
4449
<Collapse in={isOpen}>

packages/frontend/src/components/ErrorResult/SpecificErrorResult.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from '@opengovsg/design-system-react'
1313

1414
import JSONViewer from '@/components/JSONViewer'
15+
import { SUPPORT_FORM_LINK } from '@/config/urls'
1516
import { RETRY_PARTIAL_STEP } from '@/graphql/mutations/retry-partial-step'
1617
import { GET_EXECUTION_STEPS } from '@/graphql/queries/get-execution-steps'
1718

@@ -21,8 +22,7 @@ interface SpecificErrorResultProps {
2122
executionStepId?: string
2223
}
2324

24-
const contactPlumberMessage =
25-
'If this error still persists, contact us at [email protected].'
25+
const contactPlumberMessage = `If this error still persists, contact us at [${SUPPORT_FORM_LINK}](${SUPPORT_FORM_LINK}).`
2626

2727
export default function SpecificErrorResult(props: SpecificErrorResultProps) {
2828
const { errorDetails, isTestRun, executionStepId } = props

0 commit comments

Comments
 (0)