commit 795dde8b1c5ed6f9c6439b05af7ee20c5ceb99fc parent a124711373d95e1c4f505e1d3e676bd07223d7cb Author: Sebastian <sebasjm@gmail.com> Date: Tue, 28 Jan 2025 09:28:22 -0300 new measure form Diffstat:
14 files changed, 883 insertions(+), 354 deletions(-)
diff --git a/packages/aml-backoffice-ui/src/Routing.tsx b/packages/aml-backoffice-ui/src/Routing.tsx @@ -37,13 +37,14 @@ import { CaseDetails } from "./pages/CaseDetails.js"; import { CaseUpdate, SelectForm } from "./pages/CaseUpdate.js"; import { HandleAccountNotReady } from "./pages/HandleAccountNotReady.js"; import { Search } from "./pages/Search.js"; -import { Measures } from "./pages/Measures.js"; +import { MeasureList } from "./pages/MeasureList.js"; import { AmlDecisionRequestWizard, WizardSteps, } from "./pages/decision/AmlDecisionRequestWizard.js"; import { useCurrentDecisionRequest } from "./hooks/decision-request.js"; import { Dashboard } from "./pages/Dashboard.js"; +import { NewMeasure } from "./pages/NewMeasure.js"; export function Routing(): VNode { const session = useOfficer(); @@ -122,6 +123,7 @@ export const privatePages = { /\/decide_new\/(?<cid>[a-zA-Z0-9]+)\/(?<payto>[a-zA-Z0-9]+)/, ({ cid, payto }) => `#/decide_new/${cid}/${payto}`, ), + measuresNew: urlPattern(/\/measures\/new/, () => "#/measures/new"), measures: urlPattern(/\/measures/, () => "#/measures"), search: urlPattern(/\/search/, () => "#/search"), investigation: urlPattern(/\/investigation/, () => "#/investigation"), @@ -162,7 +164,10 @@ function PrivateRouting(): VNode { return <Officer />; } case "measures": { - return <Measures />; + return <MeasureList routeToNew={privatePages.measuresNew} />; + } + case "measuresNew": { + return <NewMeasure />; } case "decideNew": { return ( diff --git a/packages/aml-backoffice-ui/src/hooks/custom-measures.ts b/packages/aml-backoffice-ui/src/hooks/custom-measures.ts @@ -15,23 +15,13 @@ */ import { - AbsoluteTime, Codec, - KycRule, - MeasureInformation, buildCodecForObject, - codecForAbsoluteTime, codecForAny, - codecForBoolean, codecForConstString, codecForEither, - codecForEmptyObject, - codecForKycRules, codecForList, - codecForMap, - codecForMeasureInformation, codecForString, - codecOptional, codecOptionalDefault, } from "@gnu-taler/taler-util"; import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx @@ -723,7 +723,7 @@ function ShowMesaureInfo({ nextMeasures }: { nextMeasures: string[] }): VNode { const allMeasures = computeAvailableMesaures( measures.body, cm, - filteredMeasures, + (m) => filteredMeasures.indexOf(m.name) === -1, ); if (!filteredMeasures.length) { @@ -1441,7 +1441,7 @@ export function ShowMeasuresToSelect({ export function computeAvailableMesaures( serverMeasures: TalerExchangeApi.AvailableMeasureSummary | undefined, customMeasures?: Readonly<CustomMeasures>, - filter?: string[], + skpiFilter?: (m: MeasureInfo) => boolean, ): Mesaures { const init: Mesaures = { forms: [], procedures: [] }; if (!serverMeasures) { @@ -1449,28 +1449,30 @@ export function computeAvailableMesaures( } const server = Object.entries(serverMeasures.roots).reduce( (prev, [key, value]) => { - if (filter !== undefined && filter.indexOf(key) === -1) { - // if filter has been given and the measure is not in the list - // then skip - return prev; - } if (value.check_name !== "SKIP") { - prev.forms.push({ + const r: MeasureInfo = { type: "form", name: key, context: value.context, + programName: value.prog_name, program: serverMeasures.programs[value.prog_name], + checkName: value.check_name, check: serverMeasures.checks[value.check_name], custom: false, - }); + }; + if (skpiFilter && skpiFilter(r)) return prev; // skip + prev.forms.push(r); } else { - prev.procedures.push({ + const r: MeasureInfo = { type: "procedure", name: key, context: value.context, + programName: value.prog_name, program: serverMeasures.programs[value.prog_name], custom: false, - }); + }; + if (skpiFilter && skpiFilter(r)) return prev; // skip + prev.procedures.push(r); } return prev; }, @@ -1482,28 +1484,30 @@ export function computeAvailableMesaures( } const serverAndCustom = customMeasures.measures.reduce((prev, value) => { - if (filter !== undefined && filter.indexOf(value.name) === -1) { - // if filter has been given and the measure is not in the list - // then skip - return prev; - } if (value.check_name !== "SKIP") { - prev.forms.push({ + const r: MeasureInfo = { type: "form", name: value.name, context: value.context, + programName: value.program, program: serverMeasures.programs[value.program], + checkName: value.check_name, check: serverMeasures.checks[value.check_name], custom: true, - }); + }; + if (skpiFilter && skpiFilter(r)) return prev; // skip + prev.forms.push(r); } else { - prev.procedures.push({ + const r: MeasureInfo = { type: "procedure", name: value.name, context: value.context, + programName: value.program, program: serverMeasures.programs[value.program], custom: true, - }); + }; + if (skpiFilter && skpiFilter(r)) return prev; // skip + prev.procedures.push(r); } return prev; }, server); diff --git a/packages/aml-backoffice-ui/src/pages/MeasureList.tsx b/packages/aml-backoffice-ui/src/pages/MeasureList.tsx @@ -0,0 +1,98 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { + assertUnreachable, + HttpStatusCode, + TalerError, +} from "@gnu-taler/taler-util"; +import { + Attention, + Loading, + RouteDefinition, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, h } from "preact"; +import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js"; +import { useCustomMeasures } from "../hooks/custom-measures.js"; +import { useServerMeasures } from "../hooks/server-info.js"; +import { computeAvailableMesaures } from "./CaseDetails.js"; +import { CurrentMeasureTable } from "./MeasuresTable.js"; +import { Officer } from "./Officer.js"; + +export function MeasureList({ routeToNew }: { routeToNew: RouteDefinition }) { + const { i18n } = useTranslationContext(); + + const measures = useServerMeasures(); + const [custom] = useCustomMeasures(); + + if (!measures) { + return <Loading />; + } + if (measures instanceof TalerError) { + return <ErrorLoadingWithDebug error={measures} />; + } + + if (measures.type === "fail") { + switch (measures.case) { + // case HttpStatusCode.Unauthorized: + case HttpStatusCode.Forbidden: + return ( + <Fragment> + <Attention type="danger" title={i18n.str`Operation denied`}> + <i18n.Translate> + This account signature is wrong, contact administrator or create + a new one. + </i18n.Translate> + </Attention> + <Officer /> + </Fragment> + ); + default: + assertUnreachable(measures.case); + } + } + + const ms = computeAvailableMesaures(measures.body, custom); + + return ( + <div> + <div class="px-4 sm:px-6 lg:px-8"> + <div class="sm:flex sm:items-center"> + <div class="sm:flex-auto"> + <h1 class="text-base font-semibold text-gray-900"> + <i18n.Translate>Measures</i18n.Translate> + </h1> + <p class="mt-2 text-sm text-gray-700"> + <i18n.Translate> + A list of all the pre-define measures in your that can used. + </i18n.Translate> + </p> + </div> + <div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none"> + <a + href={routeToNew.url({})} + class="block rounded-md bg-indigo-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + > + <i18n.Translate>Add custom measure</i18n.Translate> + </a> + </div> + </div> + + <CurrentMeasureTable measures={ms} /> + </div> + </div> + ); +} diff --git a/packages/aml-backoffice-ui/src/pages/Measures.tsx b/packages/aml-backoffice-ui/src/pages/Measures.tsx @@ -1,97 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022-2024 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -import { - assertUnreachable, - HttpStatusCode, - TalerError, -} from "@gnu-taler/taler-util"; -import { - Attention, - Loading, - useTranslationContext, -} from "@gnu-taler/web-util/browser"; -import { Fragment, h } from "preact"; -import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js"; -import { useCustomMeasures } from "../hooks/custom-measures.js"; -import { useServerMeasures } from "../hooks/server-info.js"; -import { CurrentMeasureTable, Mesaures } from "./MeasuresTable.js"; -import { Officer } from "./Officer.js"; -import { computeAvailableMesaures } from "./CaseDetails.js"; - -export function Measures({}: {}) { - const { i18n } = useTranslationContext(); - - const measures = useServerMeasures(); - const [custom] = useCustomMeasures(); - - if (!measures) { - return <Loading />; - } - if (measures instanceof TalerError) { - return <ErrorLoadingWithDebug error={measures} />; - } - - if (measures.type === "fail") { - switch (measures.case) { - // case HttpStatusCode.Unauthorized: - case HttpStatusCode.Forbidden: - return ( - <Fragment> - <Attention type="danger" title={i18n.str`Operation denied`}> - <i18n.Translate> - This account signature is wrong, contact administrator or create - a new one. - </i18n.Translate> - </Attention> - <Officer /> - </Fragment> - ); - default: - assertUnreachable(measures.case); - } - } - - const ms = computeAvailableMesaures(measures.body, custom); - - return ( - <div> - <div class="px-4 sm:px-6 lg:px-8"> - <div class="sm:flex sm:items-center"> - <div class="sm:flex-auto"> - <h1 class="text-base font-semibold text-gray-900"> - <i18n.Translate>Measures</i18n.Translate> - </h1> - <p class="mt-2 text-sm text-gray-700"> - <i18n.Translate> - A list of all the pre-define measures in your that can used. - </i18n.Translate> - </p> - </div> - <div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none"> - <button - type="button" - class="block rounded-md bg-indigo-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" - > - <i18n.Translate>Add custom measure</i18n.Translate> - </button> - </div> - </div> - - <CurrentMeasureTable measures={ms} /> - </div> - </div> - ); -} diff --git a/packages/aml-backoffice-ui/src/pages/MeasuresTable.tsx b/packages/aml-backoffice-ui/src/pages/MeasuresTable.tsx @@ -25,6 +25,7 @@ export type MeasureInfo = ProcedureMeasure | FormMeasure; export type ProcedureMeasure = { type: "procedure"; name: string; + programName: string; program: AmlProgramRequirement; context?: object; custom: boolean; @@ -32,6 +33,8 @@ export type ProcedureMeasure = { export type FormMeasure = { type: "form"; name: string; + programName: string; + checkName: string; program: AmlProgramRequirement; check: KycCheckInformation; context?: object; @@ -53,94 +56,97 @@ export function CurrentMeasureTable({ <Fragment> {!measures.forms.length ? undefined : ( <div class="mt-4 flow-root"> - <div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"> - <div class="sm:flex sm:items-center"> - <div class="sm:flex-auto"> - <h1 class="text-base font-semibold text-gray-900"> - <i18n.Translate>Forms</i18n.Translate> - </h1> - </div> + <div class="sm:flex sm:items-center"> + <div class="sm:flex-auto"> + <h1 class="text-base font-semibold text-gray-900"> + <i18n.Translate>Gather information</i18n.Translate> + </h1> + <p class="mt-2 text-sm text-gray-700"> + <i18n.Translate> + These measures will ask the customer for information. + </i18n.Translate> + </p> </div> + </div> - <div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"> - <div class="overflow-hidden shadow ring-1 ring-black/5 sm:rounded-lg"> - <table class="min-w-full divide-y divide-gray-300"> - <thead class="bg-gray-50"> - <tr> - {onSelect ? ( - <th scope="col" class="relative p-2 "> - <span class="sr-only">Select</span> - </th> - ) : ( - <Fragment /> - )} - <th - scope="col" - class="p-2 text-left text-sm font-semibold text-gray-900 sm:pl-6" - > - <i18n.Translate>Name</i18n.Translate> - </th> - <th - scope="col" - class="p-2 text-left text-sm font-semibold text-gray-900" - > - <i18n.Translate>Check</i18n.Translate> - </th> - <th - scope="col" - class="p-2 text-left text-sm font-semibold text-gray-900" - > - <i18n.Translate>Program</i18n.Translate> + <div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"> + <div class="overflow-hidden shadow ring-1 ring-black/5 sm:rounded-lg"> + <table class="min-w-full divide-y divide-gray-300"> + <thead class="bg-gray-50"> + <tr> + {onSelect ? ( + <th scope="col" class="relative p-2 "> + <span class="sr-only">Select</span> </th> - <th - scope="col" - class="p-2 text-left text-sm font-semibold text-gray-900" - > - <i18n.Translate>Context</i18n.Translate> - </th> - </tr> - </thead> - <tbody class="divide-y divide-gray-200 bg-white"> - {measures.forms.map((m) => { - // if ( - // m.context && - // "internal" in m.context && - // m.context.internal - // ) { - // return <Fragment />; - // } - return ( - <tr> - {onSelect ? ( - <td class="relative whitespace-nowrap p-2 text-right text-sm font-medium "> - <button - onClick={() => onSelect(m)} - class="rounded-md w-fit border-0 p-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700" - > - <i18n.Translate>Select</i18n.Translate> - </button> - </td> - ) : ( - <Fragment /> - )} - <td class="whitespace-nowrap p-2 text-sm font-medium text-gray-900 sm:pl-6"> - {m.name} - </td> - <td class="whitespace-nowrap p-2 text-sm text-gray-500"> - {m.check?.description ?? ""} - </td> - <td class="whitespace-nowrap p-2 text-sm text-gray-500"> - {m.program.description} + ) : ( + <Fragment /> + )} + <th + scope="col" + class="p-2 text-left text-sm font-semibold text-gray-900 sm:pl-6" + > + <i18n.Translate>Name</i18n.Translate> + </th> + <th + scope="col" + class="p-2 text-left text-sm font-semibold text-gray-900" + > + <i18n.Translate>Check</i18n.Translate> + </th> + <th + scope="col" + class="p-2 text-left text-sm font-semibold text-gray-900" + > + <i18n.Translate>Program</i18n.Translate> + </th> + <th + scope="col" + class="p-2 text-left text-sm font-semibold text-gray-900" + > + <i18n.Translate>Context</i18n.Translate> + </th> + </tr> + </thead> + <tbody class="divide-y divide-gray-200 bg-white"> + {measures.forms.map((m) => { + // if ( + // m.context && + // "internal" in m.context && + // m.context.internal + // ) { + // return <Fragment />; + // } + return ( + <tr> + {onSelect ? ( + <td class="relative whitespace-nowrap p-2 text-right text-sm font-medium "> + <button + onClick={() => onSelect(m)} + class="rounded-md w-fit border-0 p-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700" + > + <i18n.Translate>Select</i18n.Translate> + </button> </td> - <td class="whitespace-nowrap p-2 text-sm text-gray-500"> - {Object.keys(m.context ?? {}).join(", ")} - </td> - </tr> - ); - })} - </tbody> - </table> - </div> + ) : ( + <Fragment /> + )} + <td class="whitespace-nowrap p-2 text-sm font-medium text-gray-900 sm:pl-6"> + {m.name} + </td> + <td class="whitespace-nowrap p-2 text-sm text-gray-500"> + {m.check?.description ?? ""} + </td> + <td class="whitespace-nowrap p-2 text-sm text-gray-500"> + {m.program.description} + </td> + <td class="whitespace-nowrap p-2 text-sm text-gray-500"> + {Object.keys(m.context ?? {}).join(", ")} + </td> + </tr> + ); + })} + </tbody> + </table> </div> </div> </div> @@ -148,85 +154,89 @@ export function CurrentMeasureTable({ {!measures.procedures.length ? undefined : ( <div class="mt-4 flow-root"> - <div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"> - <div class="sm:flex sm:items-center"> - <div class="sm:flex-auto"> - <h1 class="text-base font-semibold text-gray-900"> - <i18n.Translate>Procedures</i18n.Translate> - </h1> - </div> + <div class="sm:flex sm:items-center"> + <div class="sm:flex-auto"> + <h1 class="text-base font-semibold text-gray-900"> + <i18n.Translate>Procedures</i18n.Translate> + </h1> + <p class="mt-2 text-sm text-gray-700"> + <i18n.Translate> + These measures will be triggered immediatly without customer + interaction. + </i18n.Translate> + </p> </div> + </div> - <div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"> - <div class="overflow-hidden shadow ring-1 ring-black/5 sm:rounded-lg"> - <table class="min-w-full divide-y divide-gray-300"> - <thead class="bg-gray-50"> - <tr> - {onSelect ? ( - <th scope="col" class="relative p-2 "> - <span class="sr-only">Select</span> - </th> - ) : ( - <Fragment /> - )} - <th - scope="col" - class="p-2 text-left text-sm font-semibold text-gray-900 sm:pl-6" - > - <i18n.Translate>Name</i18n.Translate> - </th> - <th - scope="col" - class="p-2 text-left text-sm font-semibold text-gray-900" - > - <i18n.Translate>Program</i18n.Translate> + <div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"> + <div class="overflow-hidden shadow ring-1 ring-black/5 sm:rounded-lg"> + <table class="min-w-full divide-y divide-gray-300"> + <thead class="bg-gray-50"> + <tr> + {onSelect ? ( + <th scope="col" class="relative p-2 "> + <span class="sr-only">Select</span> </th> - <th - scope="col" - class="p-2 text-left text-sm font-semibold text-gray-900" - > - <i18n.Translate>Context</i18n.Translate> - </th> - </tr> - </thead> - <tbody class="divide-y divide-gray-200 bg-white"> - {measures.procedures.map((m) => { - // if ( - // m.context && - // "internal" in m.context && - // m.context.internal - // ) { - // return <Fragment />; - // } - return ( - <tr> - {onSelect ? ( - <td class="relative whitespace-nowrap p-2 text-right text-sm font-medium "> - <button - onClick={() => onSelect(m)} - class="rounded-md w-fit border-0 p-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700" - > - <i18n.Translate>Select</i18n.Translate> - </button> - </td> - ) : ( - <Fragment /> - )} - <td class="whitespace-nowrap p-2 text-sm font-medium text-gray-900 sm:pl-6"> - {m.name} - </td> - <td class="whitespace-nowrap p-2 text-sm text-gray-500"> - {m.program.description} - </td> - <td class="whitespace-nowrap p-2 text-sm text-gray-500"> - {Object.keys(m.context ?? {}).join(", ")} + ) : ( + <Fragment /> + )} + <th + scope="col" + class="p-2 text-left text-sm font-semibold text-gray-900 sm:pl-6" + > + <i18n.Translate>Name</i18n.Translate> + </th> + <th + scope="col" + class="p-2 text-left text-sm font-semibold text-gray-900" + > + <i18n.Translate>Program</i18n.Translate> + </th> + <th + scope="col" + class="p-2 text-left text-sm font-semibold text-gray-900" + > + <i18n.Translate>Context</i18n.Translate> + </th> + </tr> + </thead> + <tbody class="divide-y divide-gray-200 bg-white"> + {measures.procedures.map((m) => { + // if ( + // m.context && + // "internal" in m.context && + // m.context.internal + // ) { + // return <Fragment />; + // } + return ( + <tr> + {onSelect ? ( + <td class="relative whitespace-nowrap p-2 text-right text-sm font-medium "> + <button + onClick={() => onSelect(m)} + class="rounded-md w-fit border-0 p-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700" + > + <i18n.Translate>Select</i18n.Translate> + </button> </td> - </tr> - ); - })} - </tbody> - </table> - </div> + ) : ( + <Fragment /> + )} + <td class="whitespace-nowrap p-2 text-sm font-medium text-gray-900 sm:pl-6"> + {m.name} + </td> + <td class="whitespace-nowrap p-2 text-sm text-gray-500"> + {m.program.description} + </td> + <td class="whitespace-nowrap p-2 text-sm text-gray-500"> + {Object.keys(m.context ?? {}).join(", ")} + </td> + </tr> + ); + })} + </tbody> + </table> </div> </div> </div> diff --git a/packages/aml-backoffice-ui/src/pages/NewMeasure.tsx b/packages/aml-backoffice-ui/src/pages/NewMeasure.tsx @@ -0,0 +1,428 @@ +import { + AmlProgramRequirement, + AvailableMeasureSummary, + KycCheckInformation, + KycRule, + TalerError, + TranslatedString, +} from "@gnu-taler/taler-util"; +import { + FormDesign, + FormErrors, + FormUI, + InternationalizationAPI, + UIHandlerId, + undefinedIfEmpty, + useForm, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { useCustomMeasures } from "../hooks/custom-measures.js"; +import { useServerMeasures } from "../hooks/server-info.js"; +import { computeAvailableMesaures } from "./CaseDetails.js"; +import { CurrentMeasureTable } from "./MeasuresTable.js"; + +/** + * Defined new limits for the account + * @param param0 + * @returns + */ +export function NewMeasure({}: {}): VNode { + const { i18n } = useTranslationContext(); + const [custom, updateCustom] = useCustomMeasures(); + const measures = useServerMeasures(); + // const [rules, setRules] = useState<KycRule[]>([]); + const names = + !measures || measures instanceof TalerError || measures.type === "fail" + ? { measures: [], programs: [], checks: [] } + : { + // measures: Object.entries(measures.body.roots).map(([key,value]) => ({ + // key, value: value. + // })), + programs: Object.entries(measures.body.programs).map( + ([key, value]) => ({ + key, + value, + }), + ), + checks: Object.entries(measures.body.checks).map(([key, value]) => ({ + key, + value, + })), + }; + + const design = formDesign(i18n, names.programs, names.checks); + + const summary = + !measures || measures instanceof TalerError || measures.type === "fail" + ? undefined + : measures.body; + + const form = useForm<FormType>( + design, + { + program: "check-tos", + // check: "form-accept-tos", + check: "askEmail", + context: [ + { + key: "domain", + value: "taler.net", + }, + ], + }, + (f) => { + if (!summary) return undefined; + return undefinedIfEmpty<FormErrors<FormType>>({ + name: !f.name + ? i18n.str`required` + : summary.roots[f.name] + ? i18n.str`already exist` + : undefined, + program: !f.program + ? i18n.str`required` + : programAndCheckMatch(i18n, summary, f.program, f.check) ?? + undefined, + check: checkAndcontextMatch( + i18n, + summary, + f.check, + (f.context ?? []) as { + key: string; + value: string; + }[], + ), + context: checkAndcontextMatch( + i18n, + summary, + f.check, + (f.context ?? []) as { + key: string; + value: string; + }[], + ) as any, + }); + }, + ); + + function addNewRule(nr: FormType) { + const measures = !custom.measures ? [] : [...custom.measures]; + const idx = measures.findIndex((m) => m.name === nr.name); + if (idx !== -1) { + measures.splice(idx, 1); + } + measures.push({ + check_name: nr.check, + context: nr.context, + name: nr.name, + program: nr.program, + type: !nr.check ? "procedure" : "form", + }); + updateCustom("measures", measures); + } + + if (!summary) { + return <div>loading...</div>; + } + + const name = !form.status.result ? undefined : form.status.result.name; + + const program = + !form.status.result || !form.status.result.program + ? undefined + : { + ...summary.programs[form.status.result.program], + name: form.status.result.program, + }; + + const check = + !form.status.result || !form.status.result.check + ? undefined + : { + ...summary.checks[form.status.result.check], + name: form.status.result.check, + }; + + const context = + !form.status.result || !form.status.result.context + ? [] + : (form.status.result.context as { key: string; value: string }[]); + + const related = computeAvailableMesaures(summary, custom, (m) => { + if (name && m.name === name) { + return false; + } + if (program && m.programName === program.name) { + return false; + } + if (m.type === "form" && check && m.checkName === check.name) { + return false; + } + return true; + }); + + const haveRelated = related.forms.length > 0 || related.procedures.length > 0; + + return ( + <div> + <h2 class="mt-4 mb-2"> + <i18n.Translate>Add measure</i18n.Translate> + </h2> + + <FormUI design={design} handler={form.handler} /> + + <button + disabled={form.status.status === "fail"} + onClick={() => { + addNewRule(form.status.result as FormType); + }} + class="m-4 rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700 disabled:bg-gray-600" + > + <i18n.Translate>Add</i18n.Translate> + </button> + + <h2 class="mt-4 mb-2"> + <i18n.Translate>Description</i18n.Translate> + </h2> + + {!program ? undefined : ( + <div class="rounded-lg bg-gray-150 ring-1 shadow-lg border-indigo-700 border ring-gray-900/5 "> + <dl class="flex flex-wrap"> + <div class="flex-auto pt-4 pl-4 bg-indigo-600 rounded-t-lg"> + <dt class="text-sm/6 text-white"> + <i18n.Translate>Program</i18n.Translate> + </dt> + <dd class="mt-1 text-base font-semibold text-white"> + {program.name} + </dd> + </div> + <div class="mt-4 flex w-full flex-none gap-x-4 px-6"> + <dt class="flex-none text-gray-500"> + <i18n.Translate>Description</i18n.Translate> + </dt> + <dd class="text-sm/6 "> + <i18n.Translate>{program.description}</i18n.Translate> + </dd> + </div> + <div class="mt-2 flex w-full flex-none gap-x-4 border-t border-gray-900/5 px-6 pt-2"> + <dt class="flex-none text-gray-500"> + <i18n.Translate>Context</i18n.Translate> + </dt> + <dd class="text-sm/6 font-medium text-gray-900"> + <pre>{program.context.join(",")}</pre> + </dd> + </div> + <div class="mt-4 flex w-full flex-none gap-x-4 px-6"> + <dt class="flex-none text-gray-500"> + <i18n.Translate>Inputs</i18n.Translate> + </dt> + <dd class="text-sm/6 "> + <pre class="whitespace-pre-wrap"> + {program.inputs.join(",")} + </pre> + </dd> + </div> + </dl> + <div class="px-4 pb-2"></div> + </div> + )} + {!check ? undefined : ( + <div class="mt-6 rounded-lg bg-gray-150 ring-1 shadow-lg border-indigo-700 border ring-gray-900/5 "> + <dl class="flex flex-wrap"> + <div class="flex-auto pt-4 pl-4 bg-indigo-600 rounded-t-lg"> + <dt class="text-sm/6 text-white"> + <i18n.Translate>Check</i18n.Translate> + </dt> + <dd class="mt-1 text-base font-semibold text-white"> + {check.name} + </dd> + </div> + <div class="mt-4 flex w-full flex-none gap-x-4 px-6"> + <dt class="flex-none text-gray-500">Description</dt> + <dd class="text-sm/6 "> + <i18n.Translate>{check.description}</i18n.Translate> + </dd> + </div> + <div class="mt-2 flex w-full flex-none gap-x-4 border-t border-gray-900/5 px-6 pt-2"> + <dt class="flex-none text-gray-500"> + <i18n.Translate>Output</i18n.Translate> + </dt> + <dd class="text-sm/6 font-medium "> + <pre class="whitespace-break-spaces"> + {check.outputs.join(", ")} + </pre> + </dd> + </div> + <div class="mt-4 flex w-full flex-none gap-x-4 px-6"> + <dt class="flex-none text-gray-500"> + <i18n.Translate>Requires</i18n.Translate> + </dt> + <dd class="text-sm/6 "> + <pre>{check.requires.join(",")}</pre> + </dd> + </div> + <div class="mt-4 flex w-full flex-none gap-x-4 px-6"> + <dt class="flex-none text-gray-500"> + <i18n.Translate>Fallback</i18n.Translate> + </dt> + <dd class="text-sm/6 "> + <pre>{check.fallback}</pre> + </dd> + </div> + </dl> + <div class="px-4 pb-2"></div> + </div> + )} + {!context || !context.length ? undefined : ( + <div class="mt-6 rounded-lg bg-gray-150 ring-1 shadow-lg border-indigo-700 border ring-gray-900/5 "> + <dl class="flex flex-wrap"> + <div class="flex-auto pt-4 pl-4 bg-indigo-600 rounded-t-lg"> + <dt class="text-sm/6 text-white"> + <i18n.Translate>Context</i18n.Translate> + </dt> + <dd class="mt-1 text-base font-semibold text-white"></dd> + </div> + {context.map(({ key, value }) => { + return ( + <div key={key} class="mt-4 flex w-full flex-none gap-x-4 px-6"> + <dt class="flex-none text-gray-500">{key}</dt> + <dd class="text-sm/6 "> + <i18n.Translate>{value}</i18n.Translate> + </dd> + </div> + ); + })} + </dl> + <div class="px-4 pb-2"></div> + </div> + )} + + {!haveRelated ? undefined : ( + <div class="px-4 mt-4"> + <div class="sm:flex sm:items-center"> + <div class="sm:flex-auto"> + <h1 class="text-base font-semibold text-gray-900"> + <i18n.Translate>Related measures</i18n.Translate> + </h1> + <p class="mt-2 text-sm text-gray-700"> + <i18n.Translate> + This measures share checks or programs + </i18n.Translate> + </p> + </div> + </div> + + <CurrentMeasureTable measures={related} /> + </div> + )} + </div> + ); +} + +type FormType = { + name: string; + program: string; + check: string; + context: { key: string; value: string }[]; +}; + +const formDesign = ( + i18n: InternationalizationAPI, + programs: { key: string; value: AmlProgramRequirement }[], + checks: { key: string; value: KycCheckInformation }[], +): FormDesign<KycRule> => ({ + type: "single-column", + fields: [ + { + id: "name" as UIHandlerId, + type: "text", + required: true, + label: i18n.str`Name`, + }, + { + type: "selectOne", + id: "program" as UIHandlerId, + label: i18n.str`Program`, + choices: programs.map((m) => { + return { + value: m.key, + label: m.key, + }; + }), + }, + { + type: "selectOne", + id: "check" as UIHandlerId, + label: i18n.str`Check`, + choices: checks.map((m) => { + return { + value: m.key, + label: m.key, + }; + }), + }, + { + type: "array", + id: "context" as UIHandlerId, + label: i18n.str`Context`, + labelFieldId: "key" as UIHandlerId, + fields: [ + { + type: "text", + id: "key" as UIHandlerId, + label: i18n.str`Key`, + }, + { + type: "text", + id: "value" as UIHandlerId, + label: i18n.str`Value`, + }, + ], + }, + ], +}); + +function programAndCheckMatch( + i18n: InternationalizationAPI, + summary: AvailableMeasureSummary, + progName: string, + checkName: string | undefined, +): TranslatedString | undefined { + const program = summary.programs[progName]; + if (checkName === undefined) { + if (program.inputs.length > 0) { + return i18n.str`There are unsatisfied inputs: ${program.inputs.join( + ", ", + )}`; + } + return undefined; + } + const check = summary.checks[checkName]; + const missing = program.inputs.filter((d) => { + return check.outputs.indexOf(d) === -1; + }); + if (missing.length > 0) { + return i18n.str`There are missing inputs: ${missing.join(", ")}`; + } + return; +} + +function checkAndcontextMatch( + i18n: InternationalizationAPI, + summary: AvailableMeasureSummary, + checkName: string | undefined, + context: { key: string; value: string }[], +): TranslatedString | undefined { + if (checkName === undefined) { + return undefined; + } + const check = summary.checks[checkName]; + const output = context.map((d) => d.key); + const missing = check.requires.filter((d) => { + return output.indexOf(d) === -1; + }); + if (missing.length > 0) { + return i18n.str`There are missing requirements: ${missing.join(", ")}`; + } + return; +} diff --git a/packages/aml-backoffice-ui/src/pages/RulesInfo.tsx b/packages/aml-backoffice-ui/src/pages/RulesInfo.tsx @@ -52,7 +52,6 @@ export function RulesInfo({ }; const sorted = [...rules].sort((a, b) => { - console.log(a.operation_type); // to prevent iterate again we are using this sort function // to save present operation type OPERATION_TYPE_MISSING[a.operation_type] = false; @@ -64,11 +63,9 @@ export function RulesInfo({ OPERATION_TYPE_MISSING[rules[0].operation_type] = false; } - console.log(OPERATION_TYPE_MISSING); const missing = Object.entries(OPERATION_TYPE_MISSING) .filter(([key, value]) => !!value) .map(([key]) => key) as LimitOperationType[]; - console.log(missing); const hasActions = !!onEdit || !!onRemove; diff --git a/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx b/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx @@ -15,6 +15,10 @@ import { useCurrentDecisionRequest } from "../../hooks/decision-request.js"; import { ShowMeasuresToSelect } from "../CaseDetails.js"; import { useServerMeasures } from "../../hooks/server-info.js"; import { useMemo } from "preact/hooks"; +import { + CustomMeasures, + useCustomMeasures, +} from "../../hooks/custom-measures.js"; /** * Ask for more information, define new paths to proceed @@ -25,6 +29,7 @@ export function Measures({}: {}): VNode { const { i18n } = useTranslationContext(); const [request, _, updateRequest] = useCurrentDecisionRequest(); const measures = useServerMeasures(); + const [custom] = useCustomMeasures(); const measureList = !measures || measures instanceof TalerError || measures.type === "fail" ? [] @@ -32,9 +37,12 @@ export function Measures({}: {}): VNode { const nm = !request.new_measures ? [] : request.new_measures; - const initValue = useMemo<FormType>(() => ({ measures: nm }), [nm]); + const initValue = useMemo<FormType>( + () => ({ measures: nm }), + [request.new_measures], + ); - const design = formDesign(i18n, measureList); + const design = formDesign(i18n, measureList, custom); const form = useForm<FormType>(design, initValue); onComponentUnload(() => { @@ -59,6 +67,7 @@ type FormType = { function formDesign( i18n: InternationalizationAPI, mi: (MeasureInformation & { id: string })[], + cm: CustomMeasures, ): FormDesign<FormType> { return { type: "single-column", @@ -66,12 +75,20 @@ function formDesign( { type: "selectMultiple", unique: true, - choices: mi.map((m) => { - return { - value: m.id, - label: m.id, - }; - }), + choices: [ + ...mi.map((m) => { + return { + value: m.id, + label: m.id, + }; + }), + ...cm.measures.map((m) => { + return { + value: m.name, + label: m.name, + }; + }), + ], id: "measures" as UIHandlerId, label: i18n.str`Active measures`, help: i18n.str`Measures that the customer will need to satisfy while the rules are active.`, diff --git a/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx b/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx @@ -49,7 +49,6 @@ export function Properties({}: {}): VNode { custom_properties: (form.status.result.custom ?? []).reduce( (prev, cur) => { if (!cur || !cur.name || !cur.value) return prev; - console.log(cur); prev[cur.name] = cur.value; return prev; }, @@ -119,32 +118,25 @@ export function propertiesByDialect( case AmlSpaDialect.TESTING: { return [ { - id: "ACCOUNT_PEP" satisfies keyof TalerFormAttributes.AccountProperties_Testing as UIHandlerId, + id: "ACCOUNT_PEP" satisfies keyof TalerFormAttributes.AccountProperties as UIHandlerId, label: i18n.str`Public exposed person?`, // gana_type: "Boolean", type: "toggle", required: true, }, { - id: "ACCOUNT_BUSINESS_DOMAIN" satisfies keyof TalerFormAttributes.AccountProperties_Testing as UIHandlerId, + id: "ACCOUNT_BUSINESS_DOMAIN" satisfies keyof TalerFormAttributes.AccountProperties as UIHandlerId, label: i18n.str`Business domain`, // gana_type: "Text", type: "text", required: true, }, - { - id: "ACCOUNT_FROZEN" satisfies keyof TalerFormAttributes.AccountProperties_Testing as UIHandlerId, - label: i18n.str`Is frozen?`, - // gana_type: "Boolean", - type: "toggle", - required: true, - }, ]; } case AmlSpaDialect.GLS: { return [ { - id: "ACCOUNT_REPORTED" satisfies keyof TalerFormAttributes.AccountProperties_GLS as UIHandlerId, + id: "ACCOUNT_REPORTED" satisfies keyof TalerFormAttributes.AccountProperties as UIHandlerId, label: i18n.str`Is PEP`, // gana_type: "Boolean", type: "toggle", @@ -155,42 +147,42 @@ export function propertiesByDialect( case AmlSpaDialect.TOPS: { return [ { - id: "ACCOUNT_FROZEN" satisfies keyof TalerFormAttributes.AccountProperties_TOPS as UIHandlerId, + id: "ACCOUNT_FROZEN" satisfies keyof TalerFormAttributes.AccountProperties as UIHandlerId, label: i18n.str`Frozen?`, // gana_type: "Boolean", type: "toggle", required: true, }, { - id: "ACCOUNT_HIGH_RISK" satisfies keyof TalerFormAttributes.AccountProperties_TOPS as UIHandlerId, + id: "ACCOUNT_HIGH_RISK" satisfies keyof TalerFormAttributes.AccountProperties as UIHandlerId, label: i18n.str`High risk?`, // gana_type: "Boolean", type: "toggle", required: true, }, { - id: "ACCOUNT_PEP" satisfies keyof TalerFormAttributes.AccountProperties_TOPS as UIHandlerId, + id: "ACCOUNT_PEP" satisfies keyof TalerFormAttributes.AccountProperties as UIHandlerId, label: i18n.str`Public exposed person?`, // gana_type: "Boolean", type: "toggle", required: true, }, { - id: "ACCOUNT_REPORTED" satisfies keyof TalerFormAttributes.AccountProperties_TOPS as UIHandlerId, + id: "ACCOUNT_REPORTED" satisfies keyof TalerFormAttributes.AccountProperties as UIHandlerId, label: i18n.str`Is reported to authorities?`, // gana_type: "Boolean", type: "toggle", required: true, }, { - id: "ACCOUNT_SANCTIONED" satisfies keyof TalerFormAttributes.AccountProperties_TOPS as UIHandlerId, + id: "ACCOUNT_SANCTIONED" satisfies keyof TalerFormAttributes.AccountProperties as UIHandlerId, label: i18n.str`Is PEP`, // gana_type: "Boolean", type: "toggle", required: true, }, { - id: "ACCOUNT_BUSINESS_DOMAIN" satisfies keyof TalerFormAttributes.AccountProperties_TOPS as UIHandlerId, + id: "ACCOUNT_BUSINESS_DOMAIN" satisfies keyof TalerFormAttributes.AccountProperties as UIHandlerId, label: i18n.str`Business domain`, // gana_type: "Boolean", type: "text", diff --git a/packages/aml-backoffice-ui/src/pages/decision/aml-events.ts b/packages/aml-backoffice-ui/src/pages/decision/aml-events.ts @@ -34,7 +34,7 @@ export const AML_EVENTS_INFO: EventMapInfo = { dialect: [AmlSpaDialect.TESTING], shouldBeTriggered(req, dialect) { if (!req.properties) return false; - return !!(req.properties as TalerFormAttributes.AccountProperties_TOPS) + return !!(req.properties as TalerFormAttributes.AccountProperties) .ACCOUNT_FROZEN; }, }, @@ -42,7 +42,7 @@ export const AML_EVENTS_INFO: EventMapInfo = { dialect: [AmlSpaDialect.GLS, AmlSpaDialect.TOPS, AmlSpaDialect.TESTING], shouldBeTriggered(req, dialect) { if (!req.properties) return false; - return !!(req.properties as TalerFormAttributes.AccountProperties_TOPS) + return !!(req.properties as TalerFormAttributes.AccountProperties) .ACCOUNT_PEP; }, }, diff --git a/packages/web-util/src/forms/fields/InputSelectOne.tsx b/packages/web-util/src/forms/fields/InputSelectOne.tsx @@ -11,7 +11,7 @@ export function InputSelectOne<T extends object, K extends keyof T>( } & UIFormProps<T, K>, ): VNode { const { label, choices, placeholder, tooltip, required } = props; - const { value, onChange } = + const { value, onChange, error } = props.handler ?? noHandlerPropsAndNoContextForField(props.name); const [filter, setFilter] = useState<string | undefined>(undefined); @@ -46,7 +46,6 @@ export function InputSelectOne<T extends object, K extends keyof T>( }} class="group relative h-5 w-5 rounded-sm hover:bg-gray-500/20" > - <span class="sr-only">Remove</span> <svg viewBox="0 0 14 14" class="h-5 w-5 stroke-gray-700/50 group-hover:stroke-gray-700/75" @@ -135,6 +134,11 @@ export function InputSelectOne<T extends object, K extends keyof T>( )} </div> )} + {error && ( + <p class="mt-2 text-sm text-red-600" id="email-error"> + {error} + </p> + )} </div> ); } diff --git a/packages/web-util/src/forms/gana/GLS_Onboarding.ts b/packages/web-util/src/forms/gana/GLS_Onboarding.ts @@ -13,29 +13,134 @@ export function GLS_Onboarding( type: "double-column", sections: [ { - title: i18n.str`This form was completed by`, + title: i18n.str`Personal individual information`, fields: [ { - id: "TAX_IS_ACTIVE" satisfies keyof TalerFormAttributes.GLS_Onboarding as UIHandlerId, - label: i18n.str`Is tax active?`, - // gana_type: "AbsoluteDateTime", - type: "toggle", + id: "PERSON_FULL_NAME" satisfies keyof TalerFormAttributes.GLS_Onboarding as UIHandlerId, + label: i18n.str`Fullname`, + // gana_type: "String", + type: "text", + }, + { + id: "PERSON_LAST_NAME" satisfies keyof TalerFormAttributes.GLS_Onboarding as UIHandlerId, + label: i18n.str`Last name`, + // gana_type: "String", + type: "text", + required: true, + }, + { + id: "PERSON_LAST_NAME" satisfies keyof TalerFormAttributes.GLS_Onboarding as UIHandlerId, + label: i18n.str`Last name`, + // gana_type: "String", + type: "text", + required: true, + }, + { + id: "PERSON_NATIONAL_ID" satisfies keyof TalerFormAttributes.GLS_Onboarding as UIHandlerId, + label: i18n.str`National ID Number`, + // gana_type: "String", + type: "text", + required: true, + }, + { + id: "PERSON_DATE_OF_BIRTH" satisfies keyof TalerFormAttributes.GLS_Onboarding as UIHandlerId, + label: i18n.str`Date of birth`, + // gana_type: "String", + type: "text", + required: true, + }, + { + id: "PERSON_NATIONAL_ID_SCAN" satisfies keyof TalerFormAttributes.GLS_Onboarding as UIHandlerId, + label: i18n.str`National ID Photo`, + // gana_type: "File", + type: "file", + required: true, }, ], }, { - title: i18n.str`Information on customer`, - description: i18n.str`The customer is the person with whom the member concludes the contract with regard to the financial service provided (civil law). Does the member act as director of a domiciliary company, this domiciliary company is the customer.`, + title: i18n.str`Business company information`, fields: [ { - id: "PERSON_FULL_NAME" satisfies keyof TalerFormAttributes.GLS_Onboarding as UIHandlerId, - label: i18n.str`Full name`, + id: "BUSINESS_DISPLAY_NAME" satisfies keyof TalerFormAttributes.GLS_Onboarding as UIHandlerId, + label: i18n.str`Name of the company`, // gana_type: "String", type: "text", required: true, }, ], }, + { + title: i18n.str`Contact information`, + fields: [ + { + id: "CONTACT_DNS_DOMAIN" satisfies keyof TalerFormAttributes.GLS_Onboarding as UIHandlerId, + label: i18n.str`Hostname`, + // gana_type: "Hostname", + type: "text", + required: true, + }, + { + id: "CONTACT_WEB_DOMAIN" satisfies keyof TalerFormAttributes.GLS_Onboarding as UIHandlerId, + label: i18n.str`Web site`, + // gana_type: "Hostname", + type: "text", + required: true, + }, + { + id: "CONTACT_EMAIL" satisfies keyof TalerFormAttributes.GLS_Onboarding as UIHandlerId, + label: i18n.str`Email`, + // gana_type: "Hostname", + type: "text", + required: true, + }, + { + id: "CONTACT_PHONE" satisfies keyof TalerFormAttributes.GLS_Onboarding as UIHandlerId, + label: i18n.str`Phone`, + // gana_type: "Phone", + type: "text", + required: true, + }, + ], + }, + { + title: i18n.str`Location information`, + fields: [ + { + id: "ADDRESS_COUNTRY" satisfies keyof TalerFormAttributes.GLS_Onboarding as UIHandlerId, + label: i18n.str`Country`, + // gana_type: "Hostname", + type: "text", + required: true, + }, + { + id: "ADDRESS_STREET_NAME" satisfies keyof TalerFormAttributes.GLS_Onboarding as UIHandlerId, + label: i18n.str`Street`, + // gana_type: "Hostname", + type: "text", + required: true, + }, + ], + }, + { + title: i18n.str`Tax information`, + fields: [ + { + id: "TAX_COUNTRY" satisfies keyof TalerFormAttributes.GLS_Onboarding as UIHandlerId, + label: i18n.str`Country`, + // gana_type: "Hostname", + type: "text", + required: true, + }, + { + id: "TAX_ID" satisfies keyof TalerFormAttributes.GLS_Onboarding as UIHandlerId, + label: i18n.str`ID`, + // gana_type: "Hostname", + type: "text", + required: true, + }, + ], + }, ], }; } diff --git a/packages/web-util/src/forms/gana/taler_form_attributes.ts b/packages/web-util/src/forms/gana/taler_form_attributes.ts @@ -1319,24 +1319,7 @@ export namespace TalerFormAttributes { TAX_IS_USA_LAW?: Boolean; } export interface GLS_BusinessRepresentative {} - export interface AccountProperties_Testing { - /** - * Business domain of the account owner. - * Required: false - */ - ACCOUNT_BUSINESS_DOMAIN?: String; - /** - * True if this is a politically exposed account. - * Required: false - */ - ACCOUNT_PEP?: Boolean; - /** - * Is the client's account currently frozen? - * Required: false - */ - ACCOUNT_FROZEN?: Boolean; - } - export interface AccountProperties_TOPS { + export interface AccountProperties { /** * Business domain of the account owner. * Required: false @@ -1368,11 +1351,4 @@ export namespace TalerFormAttributes { */ ACCOUNT_SANCTIONED?: Boolean; } - export interface AccountProperties_GLS { - /** - * Was the client's account reported to the authorities? - * Required: false - */ - ACCOUNT_REPORTED?: Boolean; - } }