Skip to content

Commit

Permalink
feat: initial paysg integration
Browse files Browse the repository at this point in the history
  • Loading branch information
ogp-weeloong committed Nov 9, 2023
1 parent 9c89d29 commit 0828c1f
Show file tree
Hide file tree
Showing 8 changed files with 258 additions and 0 deletions.
2 changes: 2 additions & 0 deletions packages/backend/src/apps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { IApp } from '@plumber/types'
import customApiApp from './custom-api'
import delayApp from './delay'
import formsgApp from './formsg'
import paysgApp from './paysg'
import postmanApp from './postman'
import schedulerApp from './scheduler'
import slackApp from './slack'
Expand All @@ -16,6 +17,7 @@ const apps: Record<string, IApp> = {
[customApiApp.key]: customApiApp,
[delayApp.key]: delayApp,
[formsgApp.key]: formsgApp,
[paysgApp.key]: paysgApp,
[postmanApp.key]: postmanApp,
[schedulerApp.key]: schedulerApp,
[slackApp.key]: slackApp,
Expand Down
164 changes: 164 additions & 0 deletions packages/backend/src/apps/paysg/actions/create-payment/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import type { IJSONArray, IJSONObject, IRawAction } from '@plumber/types'

import getApiBaseUrl from '../../common/get-api-base-url'

type CreatePaymentPayload = {
reference_id: string
payer_name: string
payer_address: string
payer_identifier: string
payer_email: string
description: string
amount_in_cents: number
metadata: Record<string, string>
due_date?: string
return_url?: string
}

function constructPayload(parameters: IJSONObject): CreatePaymentPayload {
const payload: CreatePaymentPayload = {
reference_id: parameters.referenceId as string,
payer_name: parameters.payerName as string,
payer_address: parameters.payerAddress as string,
payer_identifier: parameters.payerIdentifier as string,
payer_email: parameters.payerEmail as string,
description: parameters.description as string,
amount_in_cents: Number(parameters.paymentAmountCents),
metadata: Object.create(null),
}

if (parameters.dueDate) {
payload['due_date'] = parameters.dueDate as string
}

if (parameters.returnUrl) {
payload['return_url'] = parameters.returnUrl as string
}

const metadata = parameters.metadata as IJSONArray | null
if (metadata?.length) {
for (const metadatum of metadata) {
const { key, value } = metadatum as { key: string; value: string }
payload['metadata'][key] = value
}
}

return payload
}

const action: IRawAction = {
name: 'Create Payment',
key: 'createPayment',
description: 'Create a new PaySG payment',
arguments: [
{
label: 'Reference ID',
key: 'referenceId',
type: 'string' as const,
required: true,
variables: true,
},
{
label: 'Payer Name',
key: 'payerName',
type: 'string' as const,
required: true,
variables: true,
},
{
label: 'Payer Address',
key: 'payerAddress',
type: 'string' as const,
required: true,
variables: true,
},
{
label: 'Payer Identifier',
description: 'e.g. NRIC',
key: 'payerIdentifier',
type: 'string' as const,
required: true,
variables: true,
},
{
label: 'Payer Email',
key: 'payerEmail',
type: 'string' as const,
required: true,
variables: true,
},
{
label: 'Description',
key: 'description',
type: 'string' as const,
required: true,
variables: true,
},
{
label: 'Payment amount (in cents)',
key: 'paymentAmountCents',
type: 'string' as const,
required: true,
variables: true,
},
{
label: 'Due Date',
description: 'Must be formatted as DD-MM-YYYY (e.g. 31-DEC-2023)',
key: 'dueDate',
type: 'string' as const,
required: false,
variables: true,
},
{
label: 'Return URL',
key: 'returnUrl',
type: 'string' as const,
required: false,
variables: true,
},
{
label: 'Metadata',
key: 'metadata',
type: 'multirow' as const,
required: false,
subFields: [
{
placeholder: 'Key',
key: 'key',
type: 'string' as const,
required: true,
variables: true,
},
{
placeholder: 'Value',
key: 'value',
type: 'string' as const,
required: true,
variables: true,
},
],
},
],

async run($) {
const apiKey = $.auth.data.apiKey as string
const baseUrl = getApiBaseUrl(apiKey)
const paymentServiceId = $.auth.data.paymentServiceId as string
const payload = constructPayload($.step.parameters)

const response = await $.http.post(
`/v1/payment-services/${paymentServiceId}/payments`,
payload,
{
baseURL: baseUrl,
headers: {
'x-api-key': apiKey,
},
},
)

$.setActionItem({ raw: { ...response.data } })
},
}

export default action
3 changes: 3 additions & 0 deletions packages/backend/src/apps/paysg/actions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import createPayment from './create-payment'

export default [createPayment]
34 changes: 34 additions & 0 deletions packages/backend/src/apps/paysg/auth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import isStillVerified from './is-still-verified'
import verifyCredentials from './verify-credentials'

export default {
fields: [
{
key: 'screenName',
label: 'Label',
type: 'string' as const,
required: true,
readOnly: false,
},
{
key: 'paymentServiceId',
label: 'Payment Service ID',
type: 'string' as const,
required: true,
readOnly: false,
clickToCopy: false,
},
{
key: 'apiKey',
label: 'API key',
type: 'string' as const,
required: true,
readOnly: false,
clickToCopy: false,
autoComplete: 'off' as const,
},
],

verifyCredentials,
isStillVerified,
}
10 changes: 10 additions & 0 deletions packages/backend/src/apps/paysg/auth/is-still-verified.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { IGlobalVariable } from '@plumber/types'

import verifyCredentials from './verify-credentials'

export default async function isStillVerified(
$: IGlobalVariable,
): Promise<boolean> {
await verifyCredentials($)
return true
}
9 changes: 9 additions & 0 deletions packages/backend/src/apps/paysg/auth/verify-credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { IGlobalVariable } from '@plumber/types'

export default async function verifyCredentials(
$: IGlobalVariable,
): Promise<void> {
if (!$.auth.data?.apiKey) {
throw new Error('Invalid PaySG API key')
}
}
17 changes: 17 additions & 0 deletions packages/backend/src/apps/paysg/common/get-api-base-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const STAGING_PREFIX = 'paysg_stag_'
const STAGING_BASE_URL = 'https://api-staging.pay.gov.sg'

const LIVE_PREFIX = 'paysg_live_'
const LIVE_BASE_URL = 'https://api.pay.gov.sg'

export default function getApiBaseUrl(apiKey: string): string {
if (apiKey.startsWith(LIVE_PREFIX)) {
return LIVE_BASE_URL
}

if (apiKey.startsWith(STAGING_PREFIX)) {
return STAGING_BASE_URL
}

throw new Error('API key has unrecognized prefix!')
}
19 changes: 19 additions & 0 deletions packages/backend/src/apps/paysg/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { IApp } from '@plumber/types'

import actions from './actions'
import auth from './auth'

const app: IApp = {
name: 'PaySG',
key: 'paysg',
iconUrl: '{BASE_URL}/apps/paysg/assets/favicon.svg',
authDocUrl: 'https://guide.plumber.gov.sg/user-guides/actions/paysg',
supportsConnections: true,
baseUrl: '',
apiBaseUrl: '',
primaryColor: '000000',
auth,
actions,
}

export default app

0 comments on commit 0828c1f

Please sign in to comment.