merchant-backoffice

ZZZ: Inactive/Deprecated
Log | Files | Refs | Submodules | README

commit 5002a444a7d207295457cf6c21e5192c5c49c59c
parent 4577c84b9f552315f131b58631490d0e76e8559a
Author: Sebastian <sebasjm@gmail.com>
Date:   Fri, 12 Feb 2021 02:43:11 -0300

amount regex and duration fields

Diffstat:
Msrc/components/yup/YupInput.tsx | 40++++++++++++++++++++++++----------------
Msrc/constants.ts | 2+-
Msrc/i18n/index.ts | 12++++++++++++
Msrc/routes/instances/CreateModal.tsx | 34+++-------------------------------
Msrc/routes/instances/UpdateModal.tsx | 24++++--------------------
Asrc/schemas/index.ts | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtests/functions/regex.test.ts | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
7 files changed, 193 insertions(+), 90 deletions(-)

diff --git a/src/components/yup/YupInput.tsx b/src/components/yup/YupInput.tsx @@ -3,30 +3,38 @@ import { Text, useText } from "preact-i18n"; interface Props { name: string; - value: any; + object: any; info: any; errors: any; valueHandler: any; } -export default function YupInput({ name, info, value, errors, valueHandler }: Props): VNode { - const dict = useText({placeholder: `fields.instance.${name}.placeholder`}) +function convert(object: any, name: string, type?: string): any { + switch (type) { + case 'duration': return object[name]?.d_ms; + default: return object[name]; + } +} + +export default function YupInput({ name, info, object, errors, valueHandler }: Props): VNode { + const dict = useText({ placeholder: `fields.instance.${name}.placeholder` }) + const value = convert(object, name, info.meta?.type) return <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"><Text id={`fields.instance.${name}.label`} /></label> - </div> - <div class="field-body"> - <div class="field"> - <p class="control is-expanded has-icons-left"> - <input class="input" type="text" - placeholder={dict['placeholder']} readonly={info?.meta?.readonly} - name={name} value={value[name]} + <div class="field-label is-normal"> + <label class="label"><Text id={`fields.instance.${name}.label`} /></label> + </div> + <div class="field-body"> + <div class="field"> + <p class="control is-expanded has-icons-left"> + <input class="input" type="text" + placeholder={dict['placeholder']} readonly={info?.meta?.readonly} + name={name} value={value} onChange={(e): void => valueHandler((prev: any) => ({ ...prev, [name]: e.currentTarget.value }))} /> - <Text id={`fields.instance.${name}.help`} /> - </p> - {errors[name] ? <p class="help is-danger"><Text id={`validation.${errors[name].type}`} fields={errors[name].params}>{errors[name].message}</Text></p> : null} + <Text id={`fields.instance.${name}.help`} /> + </p> + {errors[name] ? <p class="help is-danger"><Text id={`validation.${errors[name].type}`} fields={errors[name].params}>{errors[name].message}</Text></p> : null} + </div> </div> </div> -</div> } diff --git a/src/constants.ts b/src/constants.ts @@ -1,2 +1,2 @@ export const PAYTO_REGEX=/^payto:\/\/[a-zA-Z][a-zA-Z0-9-.]*(\/[a-zA-Z0-9\-\.\~\(\)@_%:!$&'*+,;=]*)*\??((amount|receiver-name|sender-name|instruction|message)=[a-zA-Z0-9\-\.\~\(\)@_%:!$'*+,;=]*&?)*$/ - +export const AMOUNT_REGEX=/^[a-zA-Z][a-zA-Z]*:[0-9][0-9,]*\.?[0-9,]*$/ diff --git a/src/i18n/index.ts b/src/i18n/index.ts @@ -66,6 +66,12 @@ export default { default_wire_fee_amortization: { label: 'Amortización de pago', }, + default_pay_delay: { + label: 'Tiempo de espera de pago' + }, + default_wire_transfer_delay: { + label: 'Tiempo de espera de transferencia bancaria' + }, }, }, validation: { @@ -149,6 +155,12 @@ export default { default_wire_fee_amortization: { label: 'Max fee amortization', }, + default_pay_delay: { + label: 'Pay delay' + }, + default_wire_transfer_delay: { + label: 'Wire transfer delay' + }, } }, validation: { diff --git a/src/routes/instances/CreateModal.tsx b/src/routes/instances/CreateModal.tsx @@ -1,38 +1,10 @@ import { h, VNode } from "preact"; import { useState } from "preact/hooks"; -import { Duration, MerchantBackend } from "../../declaration"; +import { MerchantBackend } from "../../declaration"; import * as yup from 'yup'; import ConfirmModal from "../../components/modal"; import YupInput from "../../components/yup/YupInput" - -function stringToArray(this: yup.AnySchema, current: any, original: string): string[] { - if (this.isType(current)) return current; - return original.split(',').filter(s => s.length > 0) -} -function numberToDuration(this: yup.AnySchema, current: any, original: string): Duration { - if (this.isType(current)) return current; - const d_ms = parseInt(original, 10) - return { d_ms } -} -/* -validations - * delays size - * payto-uri format - * currency -*/ - -const schema = yup.object().shape({ - id: yup.string().required().label("Id"), - name: yup.string().required().label("Name"), - payto_uris: yup.array().of(yup.string()) - .min(1).label("Payment Method") - .transform(stringToArray), - default_max_deposit_fee: yup.string().required().label("Max Deposit Fee"), - default_max_wire_fee: yup.string().required().label("Max Wire"), - default_wire_fee_amortization: yup.number().required().label("WireFee Amortization"), - // default_pay_delay: yup.number().required().label("Pay delay").transform(numberToDuration), - // default_wire_transfer_delay: yup.number().required().label("Wire transfer Delay").transform(numberToDuration), -}); +import { InstanceCreateSchema as schema } from '../../schemas' interface Props { onCancel: () => void; @@ -61,7 +33,7 @@ export default function CreateModal({ onCancel, onConfirm }: Props): VNode { return <ConfirmModal description="create_instance" active onConfirm={submit} onCancel={onCancel}> {Object.keys(schema.fields).map(f => { const info = schema.fields[f].describe() - return <YupInput name={f} info={info} errors={errors} value={value} valueHandler={valueHandler} /> + return <YupInput name={f} info={info} errors={errors} object={value} valueHandler={valueHandler} /> })} </ConfirmModal> diff --git a/src/routes/instances/UpdateModal.tsx b/src/routes/instances/UpdateModal.tsx @@ -4,23 +4,7 @@ import { MerchantBackend } from "../../declaration"; import * as yup from 'yup'; import ConfirmModal from '../../components/modal' import YupInput from "../../components/yup/YupInput"; -import { PAYTO_REGEX } from "../../constants"; - -function stringToArray(this: yup.AnySchema, current: any, original: string): string[] { - if (this.isType(current)) return current; - return original.split(',').filter(s => s.length > 0) -} - -const schema = yup.object().shape({ - name: yup.string().required(), - payto_uris: yup.array().of(yup.string()).min(1) - .transform(stringToArray).test('payto','{path} is not valid', (values): boolean => !!values && values.filter( v => v && PAYTO_REGEX.test(v) ).length > 0 ), - default_max_deposit_fee: yup.string().required(), - default_max_wire_fee: yup.string().required(), - default_wire_fee_amortization: yup.number().required(), - // default_pay_delay: yup.number().required().label("Pay delay").transform(numberToDuration), - // default_wire_transfer_delay: yup.number().required().label("Wire transfer Delay").transform(numberToDuration), -}); +import { InstanceUpdateSchema as schema } from '../../schemas' interface Props { element: MerchantBackend.Instances.QueryInstancesResponse | null; @@ -33,7 +17,7 @@ interface KeyValue { } export default function UpdateModal({ element, onCancel, onConfirm }: Props): VNode { - const copy: any = !element ? {} : Object.keys(schema.fields).reduce((prev,cur) => ({...prev, [cur]: (element as any)[cur] }), {}) + const copy: any = !element ? {} : Object.keys(schema.fields).reduce((prev, cur) => ({ ...prev, [cur]: (element as any)[cur] }), {}) const [value, valueHandler] = useState(copy) const [errors, setErrors] = useState<KeyValue>({}) @@ -42,7 +26,7 @@ export default function UpdateModal({ element, onCancel, onConfirm }: Props): VN try { schema.validateSync(value, { abortEarly: false }) - onConfirm({...schema.cast(value), address: {}, jurisdiction: {}, default_wire_transfer_delay: { d_ms: 6000 }, default_pay_delay: { d_ms: 3000 }} as MerchantBackend.Instances.InstanceReconfigurationMessage); + onConfirm({ ...schema.cast(value), address: {}, jurisdiction: {} } as MerchantBackend.Instances.InstanceReconfigurationMessage); } catch (err) { const errors = err.inner as yup.ValidationError[] const pathMessages = errors.reduce((prev, cur) => !cur.path ? prev : ({ ...prev, [cur.path]: { type: cur.type, params: cur.params, message: cur.message } }), {}) @@ -54,7 +38,7 @@ export default function UpdateModal({ element, onCancel, onConfirm }: Props): VN {Object.keys(schema.fields).map(f => { const info = schema.fields[f].describe() - return <YupInput name={f} info={info} errors={errors} value={value} valueHandler={valueHandler} /> + return <YupInput name={f} info={info} errors={errors} object={value} valueHandler={valueHandler} /> })} diff --git a/src/schemas/index.ts b/src/schemas/index.ts @@ -0,0 +1,85 @@ +import * as yup from 'yup'; +import { AMOUNT_REGEX, PAYTO_REGEX } from "../constants"; +import { Duration } from '../declaration'; + +yup.setLocale({ + mixed: { + default: 'field_invalid', + }, + number: { + min: ({ min }) => ({ key: 'field_too_short', values: { min } }), + max: ({ max }) => ({ key: 'field_too_big', values: { max } }), + }, +}); + +function stringToArray(this: yup.AnySchema, current: any, original: string): string[] { + if (this.isType(current)) return current; + return original.split(',').filter(s => s.length > 0) +} + +function listOfPayToUrisAreValid(values?: (string | undefined)[]): boolean { + return !!values && values.filter(v => v && PAYTO_REGEX.test(v)).length > 0; +} + +function numberToDuration(this: yup.AnySchema, current: any, original: string): Duration { + if (this.isType(current)) return current; + const d_ms = parseInt(original, 10) + return { d_ms } +} + +function currencyWithAmountIsValid(value?: string): boolean { + return !!value && AMOUNT_REGEX.test(value) +} + +export const InstanceUpdateSchema = yup.object().shape({ + name: yup.string().required(), + payto_uris: yup.array().of(yup.string()) + .min(1) + .transform(stringToArray) + .test('payto', '{path} is not valid', listOfPayToUrisAreValid), + default_max_deposit_fee: yup.string() + .required() + .test('amount', '{path} is not valid', currencyWithAmountIsValid), + default_max_wire_fee: yup.string() + .required() + .test('amount', '{path} is not valid', currencyWithAmountIsValid), + default_wire_fee_amortization: yup.number() + .required(), + default_pay_delay: yup.object() + .shape({d_ms: yup.number() }) + .required() + .meta({type:'duration'}) + .transform(numberToDuration), + default_wire_transfer_delay: yup.object() + .shape({d_ms: yup.number() }) + .required() + .meta({type:'duration'}) + .transform(numberToDuration), +}); + +export const InstanceCreateSchema = yup.object().shape({ + id: yup.string().required(), + name: yup.string().required(), + payto_uris: yup.array().of(yup.string()) + .min(1) + .transform(stringToArray) + .test('payto', '{path} is not valid', listOfPayToUrisAreValid), + default_max_deposit_fee: yup.string() + .required() + .test('amount', '{path} is not valid', currencyWithAmountIsValid), + default_max_wire_fee: yup.string() + .required() + .test('amount', '{path} is not valid', currencyWithAmountIsValid), + default_wire_fee_amortization: yup.number() + .required(), + default_pay_delay: yup.object() + .shape({d_ms: yup.number() }) + .required() + .meta({type:'duration'}) + .transform(numberToDuration), + default_wire_transfer_delay: yup.object() + .shape({d_ms: yup.number() }) + .required() + .meta({type:'duration'}) + .transform(numberToDuration), +}); diff --git a/tests/functions/regex.test.ts b/tests/functions/regex.test.ts @@ -1,25 +1,66 @@ -import { PAYTO_REGEX } from "../../src/constants"; +import { AMOUNT_REGEX, PAYTO_REGEX } from "../../src/constants"; -const valids = [ - 'payto://iban/DE75512108001245126199?amount=EUR:200.0&message=hello', - 'payto://ach/122000661/1234', - 'payto://upi/alice@example.com?receiver-name=Alice&amount=INR:200', - 'payto://void/?amount=EUR:10.5', - 'payto://ilp/g.acme.bob' -] +describe('payto uri format', () => { + const valids = [ + 'payto://iban/DE75512108001245126199?amount=EUR:200.0&message=hello', + 'payto://ach/122000661/1234', + 'payto://upi/alice@example.com?receiver-name=Alice&amount=INR:200', + 'payto://void/?amount=EUR:10.5', + 'payto://ilp/g.acme.bob' + ] + + test('should be valid', () => { + valids.forEach(v => expect(v).toMatch(PAYTO_REGEX)) + }); + + const invalids = [ + // has two question marks + 'payto://iban/DE75?512108001245126199?amount=EUR:200.0&message=hello', + // has a space + 'payto://ach /122000661/1234', + // has a space + 'payto://upi/alice@ example.com?receiver-name=Alice&amount=INR:200', + // invalid field name (mount instead of amount) + 'payto://void/?mount=EUR:10.5', + // payto:// is incomplete + 'payto: //ilp/g.acme.bob' + ] + + test('should not be valid', () => { + invalids.forEach(v => expect(v).not.toMatch(PAYTO_REGEX)) + }); +}) -test('should be valid', () => { - valids.forEach(v => expect(v).toMatch(PAYTO_REGEX)) -}); +describe('amount format', () => { + const valids = [ + 'ARS:10', + 'COL:10.2', + 'UY:1,000.2', + 'ARS:10.123,123', + 'ARS:1,000,000', + 'ARSCOL:10', + 'THISISTHEMOTHERCOIN:1,000,000.123,123', + ] + + test('should be valid', () => { + valids.forEach(v => expect(v).toMatch(AMOUNT_REGEX)) + }); + + const invalids = [ + //no currency name + ':10', + //use . instead of , + 'ARS:1.000.000', + //currency name with numbers + '1ARS:10', + //currency name with numbers + 'AR5:10', + //missing value + 'USD:', + ] + + test('should not be valid', () => { + invalids.forEach(v => expect(v).not.toMatch(AMOUNT_REGEX)) + }); -const invalids = [ - 'payto://iban/DE75?512108001245126199?amount=EUR:200.0&message=hello', - 'payto://ach/122000661 /1234', - 'payto://upi/alice@ example.com?receiver-name=Alice&amount=INR:200', - 'payto://void/?mount=EUR:10.5', - 'payto: //ilp/g.acme.bob' -] - -test('should not be valid', () => { - invalids.forEach(v => expect(v).not.toMatch(PAYTO_REGEX)) -}); +}) +\ No newline at end of file