Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
const fields = [
{
key: 'telephoneNumber',
label: 'Telephone number',
type: 'tel',
required: true,
placeholder: '1234567890',
order: 4,
minLength: 9,
pattern: '^[0-9]+$'
},
{
key: 'businessName',
label: 'Business name',
type: 'text',
required: true,
placeholder: 'Enter your business name',
order: 3,
minLength: 3,
maxLength: 40
},
{
key: 'deviceId',
label: 'Device ID',
type: 'number',
required: true,
placeholder: 'Enter your device ID',
order: 6,
min: 2,
max: 15
},
{
key: 'acceptTerms',
label: 'Accept Terms and Conditions',
type: 'checkbox',
required: true,
order: 7,
mustEqual: true
},
{
key: 'fiscalYear',
label: 'Fiscal Year',
type: 'date',
placeholder: 'YYYY-MM-DD',
order: 5
}
]

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = async function (knex) {
await knex.schema.alterTable('field_definitions', (table) => {
table.integer('minLength').nullable()
table.string('pattern').nullable()
table.integer('min').nullable()
table.integer('max').nullable()
table.boolean('mustEqual').nullable()
})

return knex('field_definitions').insert(fields)
}

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = async function (knex) {
const keys = fields.map((f) => f.key)
await knex('field_definitions').whereIn('key', keys).del()

return knex.schema.alterTable('field_definitions', (table) => {
table.dropColumn('minLength')
table.dropColumn('pattern')
table.dropColumn('min')
table.dropColumn('max')
table.dropColumn('mustEqual')
})
}
28 changes: 27 additions & 1 deletion packages/wallet/backend/src/terminal/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,39 @@ export class TerminalController {
constructor(private terminalService: TerminalService) {}

getOnboardingFormDefinition = async (
_req: Request,
req: Request,
res: Response,
next: NextFunction
) => {
try {
const full = String(req.query?.full) === 'true'

if (full) {
const formDefinition =
await this.terminalService.getAllOnboardingFormDefinitions()

return res.status(200).json(toSuccessResponse(formDefinition))
}

const minimalKeys = ['merchantCategoryCode', 'contactEmail']
const formDefinition =
await this.terminalService.getOnboardingFormDefinition()
const filtered = formDefinition.filter((f) => minimalKeys.includes(f.key))

return res.status(200).json(toSuccessResponse(filtered))
} catch (error) {
next(error)
}
}

getAllOnboardingFormDefinition = async (
_req: Request,
res: Response,
next: NextFunction
) => {
try {
const formDefinition =
await this.terminalService.getAllOnboardingFormDefinitions()
res.status(200).json(toSuccessResponse(formDefinition))
} catch (error) {
next(error)
Expand Down
5 changes: 5 additions & 0 deletions packages/wallet/backend/src/terminal/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ export class FieldDefinitions extends BaseModel {
public validation?: Validation
public format?: string
public maxLength?: number
public minLength?: number
public pattern?: string
public min?: number
public max?: number
public mustEqual?: boolean

static relationMappings = () => ({
options: {
Expand Down
66 changes: 66 additions & 0 deletions packages/wallet/backend/src/terminal/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,70 @@ export class TerminalService {
return mapped as FieldDefinitions
})
}

async getAllOnboardingFormDefinitions(): Promise<FieldDefinitions[]> {
const fields = await FieldDefinitions.query()
.withGraphFetched('options')
.orderBy('order', 'asc')

this.logger.debug('Returning merchant onboarding form definition', {
fields: fields
})

return fields.map((field) => {
const mapped = {
key: field.key,
label: field.label,
type: field.type,
required: field.required,
order: field.order
} as Partial<FieldDefinitions>

if (field.description) mapped.description = field.description
if (field.placeholder) mapped.placeholder = field.placeholder
if (field.format)
mapped.validation = {
...(mapped.validation || {}),
format: field.format
}
if (field.maxLength)
mapped.validation = {
...(mapped.validation || {}),
maxLength: field.maxLength
}
if (field.minLength)
mapped.validation = {
...(mapped.validation || {}),
minLength: field.minLength
}
if (field.pattern)
mapped.validation = {
...(mapped.validation || {}),
pattern: field.pattern
}
if (field.min)
mapped.validation = {
...(mapped.validation || {}),
min: field.min
}
if (field.max)
mapped.validation = {
...(mapped.validation || {}),
max: field.max
}
if (field.mustEqual)
mapped.validation = {
...(mapped.validation || {}),
mustEqual: field.mustEqual
}
if (field.options?.length) {
mapped.options = field.options.map((opt) => ({
...(opt.value && { value: opt.value }),
...(opt.label && { label: opt.label })
})) as FieldDefinitions['options']
}

return mapped as FieldDefinitions
})
}
}
109 changes: 102 additions & 7 deletions packages/wallet/backend/tests/terminal/controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import { FieldDefinitions } from '@/terminal/model'

describe('Terminal Controller', () => {
const mockTerminalService = {
getOnboardingFormDefinition: jest.fn()
getOnboardingFormDefinition: jest.fn(),
getAllOnboardingFormDefinitions: jest.fn()
}

let terminalController: TerminalController
Expand All @@ -20,18 +21,19 @@ describe('Terminal Controller', () => {
const next = jest.fn()

beforeEach(() => {
jest.clearAllMocks()
terminalController = new TerminalController(
mockTerminalService as unknown as TerminalService
)
req = createRequest()
res = createResponse()
})

it('should return onboarding form definition', async () => {
it('should return the minimal onboarding fields by default', async () => {
const formDefinition = [
{
key: 'mockContactEmail',
label: 'Mock contact email',
key: 'contactEmail',
label: 'Contact email',
description: 'We use this to send onboarding confirmation.',
type: 'email',
required: true,
Expand All @@ -42,8 +44,8 @@ describe('Terminal Controller', () => {
},
{
id: '1acf7723-e1cd-44e7-a5db-3f614ce045ac',
key: 'mockMerchantCategoryCode',
label: 'Mock merchant category',
key: 'merchantCategoryCode',
label: 'Merchant category',
type: 'select',
required: true,
order: 1
Expand All @@ -57,10 +59,103 @@ describe('Terminal Controller', () => {
await terminalController.getOnboardingFormDefinition(req, res, next)

expect(mockTerminalService.getOnboardingFormDefinition).toHaveBeenCalled()
expect(
mockTerminalService.getAllOnboardingFormDefinitions
).not.toHaveBeenCalled()
expect(res.statusCode).toBe(200)
const responseBody = res._getJSONData()
expect(responseBody).toMatchObject({ success: true })
expect(responseBody.result).toEqual(
expect.arrayContaining([
expect.objectContaining({ key: 'contactEmail' }),
expect.objectContaining({ key: 'merchantCategoryCode' })
])
)
expect(responseBody.result).toHaveLength(2)
expect(next).not.toHaveBeenCalled()
})

it('should return the full onboarding form', async () => {
const fullFormDefinition = [
{
key: 'mockContactEmail',
label: 'Mock contact email',
description: 'We use this to send onboarding confirmation.',
type: 'email',
required: true,
placeholder: 'me@interledger.org',
order: 2,
format: 'email',
maxLength: 255
},
{
id: '1acf7723-e1cd-44e7-a5db-3f614ce045ac',
key: 'mockMerchantCategoryCode',
label: 'Mock merchant category',
type: 'select',
required: true,
order: 1
},
{
key: 'mockBusinessName',
label: 'Mock business name',
type: 'text',
required: true,
placeholder: 'Enter your business name',
order: 3,
minLength: 3,
maxLength: 40
},
{
key: 'mockTelephoneNumber',
label: 'Mock telephone number',
type: 'tel',
required: true,
placeholder: '1234567890',
order: 4,
minLength: 9,
pattern: '^[0-9]+$'
},
{
key: 'deviceId',
label: 'Device ID',
type: 'number',
required: true,
placeholder: 'Enter your device ID',
order: 6,
min: 2,
max: 15
},
{
key: 'acceptTerms',
label: 'Accept Terms and Conditions',
type: 'checkbox',
required: true,
order: 7,
mustEqual: true
},
{
key: 'fiscalYear',
label: 'Fiscal Year',
type: 'date',
placeholder: 'YYYY-MM-DD',
order: 5
}
] as unknown as FieldDefinitions[]

mockTerminalService.getAllOnboardingFormDefinitions.mockResolvedValue(
fullFormDefinition
)

await terminalController.getAllOnboardingFormDefinition(req, res, next)

expect(
mockTerminalService.getAllOnboardingFormDefinitions
).toHaveBeenCalled()
expect(res.statusCode).toBe(200)
expect(res._getJSONData()).toMatchObject({
success: true,
result: formDefinition
result: fullFormDefinition
})
expect(next).not.toHaveBeenCalled()
})
Expand Down
Loading
Loading