taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 4748841815f035fbfccfc89b599abde8b48cc789
parent 589a013eadfccb22a2b8dd4b77785c46743c69cb
Author: Sebastian <sebasjm@gmail.com>
Date:   Thu, 13 Mar 2025 11:01:51 -0300

wip: add forms to the wizard

Diffstat:
Mpackages/aml-backoffice-ui/src/Routing.tsx | 23++---------------------
Mpackages/aml-backoffice-ui/src/hooks/decision-request.ts | 19+++++++++++++++++--
Mpackages/aml-backoffice-ui/src/pages/CaseDetails.tsx | 16+++++-----------
Mpackages/aml-backoffice-ui/src/pages/CaseUpdate.tsx | 23+++--------------------
Mpackages/aml-backoffice-ui/src/pages/Cases.tsx | 2+-
Mpackages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx | 3+--
Mpackages/aml-backoffice-ui/src/pages/Transfers.tsx | 2+-
Mpackages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx | 14++++++++++++--
Apackages/aml-backoffice-ui/src/pages/decision/Information.tsx | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/aml-backoffice-ui/src/pages/decision/Properties.tsx | 9+++++----
Mpackages/aml-backoffice-ui/src/pages/decision/Summary.tsx | 1+
Mpackages/taler-util/src/types-taler-exchange.ts | 16++++++++--------
Mpackages/web-util/src/forms/gana/taler_form_attributes.ts | 46++++++++++++++++++++++++++++++++++++++++++----
13 files changed, 192 insertions(+), 76 deletions(-)

diff --git a/packages/aml-backoffice-ui/src/Routing.tsx b/packages/aml-backoffice-ui/src/Routing.tsx @@ -28,7 +28,7 @@ import { ExchangeAmlFrame } from "./ExchangeAmlFrame.js"; import { useCurrentDecisionRequest } from "./hooks/decision-request.js"; import { useOfficer } from "./hooks/officer.js"; import { CaseDetails } from "./pages/CaseDetails.js"; -import { CaseUpdate, SelectForm } from "./pages/CaseUpdate.js"; +import { CaseUpdate } from "./pages/CaseUpdate.js"; import { Accounts } from "./pages/Cases.js"; import { Dashboard } from "./pages/Dashboard.js"; import { HandleAccountNotReady } from "./pages/HandleAccountNotReady.js"; @@ -125,17 +125,9 @@ export const privatePages = { transfers: urlPattern(/\/transfers/, () => "#/transfers"), accounts: urlPattern(/\/accounts/, () => "#/accounts"), caseUpdate: urlPattern<{ cid: string; type: string }>( - /\/case\/(?<cid>[a-zA-Z0-9]+)\/new\/(?<type>[a-zA-Z0-9_.]+)/, + /\/case\/(?<cid>[a-zA-Z0-9]+)\/new\/(?<type>[a-zA-Z0-9_\-.]+)/, ({ cid, type }) => `#/case/${cid}/new/${type}`, ), - caseNew: urlPattern<{ cid: string }>( - /\/case\/(?<cid>[a-zA-Z0-9]+)\/new/, - ({ cid }) => `#/case/${cid}/new`, - ), - // caseDetailsNewAccount: urlPattern<{ cid: string; payto: string }>( - // /\/case\/(?<cid>[a-zA-Z0-9]+)\/(?<payto>[a-zA-Z0-9]+)/, - // ({ cid, payto }) => `#/case/${cid}/${payto}`, - // ), caseDetails: urlPattern<{ cid: string }>( /\/case\/(?<cid>[a-zA-Z0-9]+)/, ({ cid }) => `#/case/${cid}`, @@ -261,17 +253,6 @@ function PrivateRouting(): VNode { /> ); } - // case "caseDetailsNewAccount": { - // return ( - // <CaseDetails - // account={location.values.cid} - // paytoString={decodeCrockFromURI(location.values.payto)} - // /> - // ); - // } - case "caseNew": { - return <SelectForm account={location.values.cid} />; - } case "accounts": { return <Accounts routeToCaseById={privatePages.caseDetails} />; } diff --git a/packages/aml-backoffice-ui/src/hooks/decision-request.ts b/packages/aml-backoffice-ui/src/hooks/decision-request.ts @@ -17,27 +17,34 @@ import { AbsoluteTime, Codec, + Duration, KycRule, - MeasureInformation, buildCodecForObject, codecForAbsoluteTime, codecForAny, codecForBoolean, + codecForDuration, + codecForDurationMs, codecForKycRules, codecForList, codecForMap, - codecForMeasureInformation, codecForString, codecOptional, codecOptionalDefault, } from "@gnu-taler/taler-util"; import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; +export interface ExtraInformation { + data: object; + expiration: AbsoluteTime; +} + export interface DecisionRequest { rules: KycRule[] | undefined; new_measures: string[] | undefined; deadline: AbsoluteTime | undefined; onExpire_measures: string[] | undefined; + information: ExtraInformation | undefined; properties: Record<string, boolean> | undefined; custom_properties: Record<string, any> | undefined; custom_events: string[] | undefined; @@ -46,11 +53,18 @@ export interface DecisionRequest { justification: string | undefined; } +export const codecForExtraInformation = (): Codec<ExtraInformation> => + buildCodecForObject<ExtraInformation>() + .property("expiration", codecForAbsoluteTime) + .property("data", codecForAny()) + .build("ExtraInformation"); + export const codecForDecisionRequest = (): Codec<DecisionRequest> => buildCodecForObject<DecisionRequest>() .property("rules", codecOptional(codecForList(codecForKycRules()))) .property("deadline", codecOptional(codecForAbsoluteTime)) .property("properties", codecOptional(codecForMap(codecForBoolean()))) + .property("information", codecOptional(codecForExtraInformation())) .property("custom_properties", codecForAny()) .property("justification", codecOptional(codecForString())) .property("custom_events", codecOptional(codecForList(codecForString()))) @@ -75,6 +89,7 @@ const defaultDecisionRequest: DecisionRequest = { keep_investigating: false, new_measures: undefined, properties: undefined, + information: undefined, custom_properties: undefined, rules: undefined, }; diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx @@ -24,7 +24,6 @@ import { codecForNumber, codecForString, codecOptional, - CurrencySpecification, HttpStatusCode, KycRule, LimitOperationType, @@ -56,18 +55,16 @@ import { format } from "date-fns"; import { Fragment, h, Ref, VNode } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js"; -import { useUiFormsContext } from "../context/ui-forms.js"; -import { preloadedForms } from "../forms/index.js"; import { useAccountInformation } from "../hooks/account.js"; +import { CustomMeasures, useCustomMeasures } from "../hooks/custom-measures.js"; import { DecisionRequest } from "../hooks/decision-request.js"; import { useAccountDecisions } from "../hooks/decisions.js"; import { useOfficer } from "../hooks/officer.js"; +import { useServerMeasures } from "../hooks/server-info.js"; import { CurrentMeasureTable, MeasureInfo, Mesaures } from "./MeasuresTable.js"; import { Officer } from "./Officer.js"; -import { ShowConsolidated } from "./ShowConsolidated.js"; -import { useServerMeasures } from "../hooks/server-info.js"; import { RulesInfo } from "./RulesInfo.js"; -import { CustomMeasures, useCustomMeasures } from "../hooks/custom-measures.js"; +import { ShowConsolidated } from "./ShowConsolidated.js"; export type AmlEvent = | AmlFormEvent @@ -116,7 +113,6 @@ function selectSooner(a: WithTime, b: WithTime) { export function getEventsFromAmlHistory( events: TalerExchangeApi.KycAttributeCollectionEvent[], i18n: InternationalizationAPI, - forms: FormMetadata[], ): AmlEvent[] { const ke = events.map((event) => { return { @@ -157,9 +153,6 @@ export function CaseDetails({ const details = useAccountInformation(account); const history = useAccountDecisions(account); - const { forms } = useUiFormsContext(); - - const allForms = [...forms, ...preloadedForms(i18n)]; if (!details || !history) { return <Loading />; } @@ -197,7 +190,7 @@ export function CaseDetails({ ? history.body : history.body.filter((d) => d.rowid !== activeDecision.rowid); - const events = getEventsFromAmlHistory(accountDetails, i18n, allForms); + const events = getEventsFromAmlHistory(accountDetails, i18n); function ShortcutActionButtons(): VNode { return ( @@ -210,6 +203,7 @@ export function CaseDetails({ onExpire_measures: undefined, custom_events: undefined, inhibit_events: undefined, + information: undefined, justification: undefined, keep_investigating: false, new_measures: undefined, diff --git a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx @@ -41,23 +41,6 @@ import { useOfficer } from "../hooks/officer.js"; import { Justification } from "./CaseDetails.js"; import { HandleAccountNotReady } from "./HandleAccountNotReady.js"; -function searchForm( - i18n: InternationalizationAPI, - forms: FormMetadata[], - formId: string, -): FormMetadata | undefined { - { - const found = forms.find((v) => v.id === formId); - if (found) return found; - } - { - const pf = preloadedForms(i18n); - const found = pf.find((v) => v.id === formId); - if (found) return found; - } - return undefined; -} - type FormType = { when: AbsoluteTime; state: TalerExchangeApi.AmlState; @@ -91,7 +74,7 @@ export function CaseUpdate({ if (officer.state !== "ready") { return <HandleAccountNotReady officer={officer} />; } - const theForm = searchForm(i18n, forms, formId); + const theForm: any = undefined; //searchForm(i18n, forms, formId); if (!theForm) { return <div>form with id {formId} not found</div>; } @@ -156,7 +139,7 @@ export function CaseUpdate({ <Fragment> <LocalNotificationBanner notification={notification} /> <div class="space-y-10 divide-y -mt-5 divide-gray-900/10"> - <FormUI design={theForm.config} handler={form} /> + <FormUI design={theForm.config} handler={form.handler} /> </div> <div class="mt-6 flex items-center justify-end gap-x-6"> @@ -179,7 +162,7 @@ export function CaseUpdate({ ); } -export function SelectForm({ account }: { account: string }) { +function SelectForm({ account }: { account: string }) { const { i18n } = useTranslationContext(); const { forms } = useUiFormsContext(); const pf = preloadedForms(i18n); diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx @@ -133,7 +133,7 @@ export function Accounts({ </h1> <p class="mt-2 text-sm text-gray-700 w-80"> <i18n.Translate> - A list of all the known accounts by the exchange. + A list of all the accounts known to the exchange. </i18n.Translate> </p> </div> diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx @@ -44,7 +44,7 @@ const nullTranslator: InternationalizationAPI = { }; export const WithEmptyHistory = tests.createExample(TestedComponent, { - history: getEventsFromAmlHistory([], nullTranslator, []), + history: getEventsFromAmlHistory([], nullTranslator), until: AbsoluteTime.now(), }); @@ -66,7 +66,6 @@ export const WithSomeEvents = tests.createExample(TestedComponent, { }, ], nullTranslator, - [], ), until: AbsoluteTime.now(), }); diff --git a/packages/aml-backoffice-ui/src/pages/Transfers.tsx b/packages/aml-backoffice-ui/src/pages/Transfers.tsx @@ -151,7 +151,7 @@ export function Transfers({ <Attention type="low" title={i18n.str`No transfers yet.`}> <i18n.Translate> - There are no transfer reported by the exchange with the current + There are no transfer reported to the exchange with the current threshold. </i18n.Translate> </Attention> diff --git a/packages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx b/packages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx @@ -35,8 +35,10 @@ import { Rules } from "./Rules.js"; import { Measures } from "./Measures.js"; import { Justification } from "./Justification.js"; import { Summary } from "./Summary.js"; +import { Information } from "./Information.js"; export type WizardSteps = + | "information" // submit more information | "rules" // define the limits | "measures" // define a new form/challenge | "properties" // define account information @@ -45,10 +47,11 @@ export type WizardSteps = | "summary"; const STEPS_ORDER: WizardSteps[] = [ - "rules", - "measures", + "information", "properties", "events", + "rules", + "measures", "justification", "summary", ]; @@ -108,6 +111,8 @@ export function AmlDecisionRequestWizard({ return <Measures />; case "justification": return <Justification />; + case "information": + return <Information />; case "summary": return <Summary account={account} onMove={onMove} />; } @@ -155,6 +160,11 @@ function WizardSteps({ isCompleted: (r: DecisionRequest) => boolean; }; } = { + information: { + label: i18n.str`Information`, + description: i18n.str`Add more inforamtion to the account`, + isCompleted: isRulesCompleted, + }, rules: { label: i18n.str`Rules`, description: i18n.str`Set the limit of the operations`, diff --git a/packages/aml-backoffice-ui/src/pages/decision/Information.tsx b/packages/aml-backoffice-ui/src/pages/decision/Information.tsx @@ -0,0 +1,94 @@ +import { + AbsoluteTime, + Duration, + MeasureInformation, +} from "@gnu-taler/taler-util"; +import { + FormDesign, + FormMetadata, + FormUI, + InternationalizationAPI, + onComponentUnload, + UIHandlerId, + useForm, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { preloadedForms } from "../../forms/index.js"; +import { useCurrentDecisionRequest } from "../../hooks/decision-request.js"; +import { useUiFormsContext } from "../../context/ui-forms.js"; +import { useState } from "preact/compat"; + +/** + * Mark for further investigation and explain decision + * @param param0 + * @returns + */ +export function Information({}: {}): VNode { + const { i18n } = useTranslationContext(); + const [request, _, updateRequest] = useCurrentDecisionRequest(); + + const FORM_ID = request.information?.data ?? {}; + const [formId, setFormId] = useState<string>(); + + const { forms } = useUiFormsContext(); + + const theForm = !formId ? undefined : searchForm(i18n, forms, formId); + if (!theForm) { + return <div>form with id {formId} not found</div>; + } + + const form = useForm<FormType>(theForm.config, { + data: request.information?.data, + expiration: request.information?.expiration, + }); + + onComponentUnload(() => { + updateRequest({ + ...request, + }); + }); + + return ( + <div> + <FormUI design={theForm.config} handler={form.handler} /> + </div> + ); +} + +type FormType = { + data: Record<string, any> | undefined; + expiration: AbsoluteTime | undefined; +}; + +const formDesign = ( + i18n: InternationalizationAPI, + mi: (MeasureInformation & { id: string })[], +): FormDesign<FormType> => ({ + type: "single-column", + fields: [ + { + id: "justification" as UIHandlerId, + type: "textArea", + required: true, + label: i18n.str`Justification`, + }, + ], +}); + +function searchForm( + i18n: InternationalizationAPI, + forms: FormMetadata[], + formId: string, +): FormMetadata | undefined { + { + const found = forms.find((v) => v.id === formId); + if (found) return found; + } + { + const pf = preloadedForms(i18n); + const found = pf.find((v) => v.id === formId); + if (found) return found; + } + return undefined; +} diff --git a/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx b/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx @@ -20,15 +20,13 @@ import { useTranslationContext, } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; +import { useAccountInformation } from "../../hooks/account.js"; import { DecisionRequest, useCurrentDecisionRequest, } from "../../hooks/decision-request.js"; import { useAccountActiveDecision } from "../../hooks/decisions.js"; import { usePreferences } from "../../hooks/preferences.js"; -import { useAccountInformation } from "../../hooks/account.js"; -import { getConsolidated } from "../ShowConsolidated.js"; -import { getEventsFromAmlHistory } from "../CaseDetails.js"; /** * Update account properites @@ -276,7 +274,10 @@ function calculatePropertiesBasedOnState( const since = AbsoluteTime.fromProtocolTimestamp(cur.collection_time); Object.entries(cur.attributes ?? {}).forEach(([key, value]) => { const v: any = value; - prev[key] = { value: "text" in (v as any) ? v.text : value, since }; + prev[key] = { + value: typeof v === "object" && "text" in v ? v.text : value, + since, + }; }); return prev; }, diff --git a/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx b/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx @@ -81,6 +81,7 @@ export function Summary({ custom_properties: undefined, deadline: undefined, inhibit_events: undefined, + information: undefined, justification: undefined, keep_investigating: false, new_measures: undefined, diff --git a/packages/taler-util/src/types-taler-exchange.ts b/packages/taler-util/src/types-taler-exchange.ts @@ -1996,15 +1996,15 @@ export interface AmlDecisionRequest { */ decision_time: Timestamp; - /** - * When do the attributes expire? - */ - attributes_expiration?: Timestamp; - - /** - * Attributes set by the AML officer. - */ + // KYC attributes uploaded by the AML officer + // The object *must* contain high-entropy salt, + // as the hash of the attributes will be + // stored in plain text. attributes?: Object; + + // Expiration timestamp of the attributes. + // Mandatory if attributes are present. + attributes_expiration?: Timestamp; } export interface KycRule { diff --git a/packages/web-util/src/forms/gana/taler_form_attributes.ts b/packages/web-util/src/forms/gana/taler_form_attributes.ts @@ -1856,13 +1856,13 @@ export namespace TalerFormAttributes { description: "Income and assets, liabilities (estimated)", } as FormFieldInfo, /** - * Description: Nature, amount and currency of the involved assets. + * Description: Amount of the involved assets. * * GANA Type: Amount */ BIZREL_ORIGIN_AMOUNT: { id: "BIZREL_ORIGIN_AMOUNT" as UIHandlerId, - description: "Nature, amount and currency of the involved assets.", + description: "Amount of the involved assets.", } as FormFieldInfo, /** * Description: @@ -1883,6 +1883,15 @@ export namespace TalerFormAttributes { description: "", } as FormFieldInfo, /** + * Description: Currency of the involved assets. + * + * GANA Type: Amount + */ + BIZREL_ORIGIN_CURRENCY: { + id: "BIZREL_ORIGIN_CURRENCY" as UIHandlerId, + description: "Currency of the involved assets.", + } as FormFieldInfo, + /** * Description: Detail description of the origings * * GANA Type: Paragraph @@ -1892,13 +1901,13 @@ export namespace TalerFormAttributes { description: "Detail description of the origings", } as FormFieldInfo, /** - * Description: + * Description: Nature of the involved assets. * * GANA Type: String */ BIZREL_ORIGIN_NATURE: { id: "BIZREL_ORIGIN_NATURE" as UIHandlerId, - description: "", + description: "Nature of the involved assets.", } as FormFieldInfo, /** * Description: Profession, business activities, etc. (former, current, potentially planned) @@ -2020,6 +2029,35 @@ export namespace TalerFormAttributes { description: "Contracing partner signature,", } as FormFieldInfo, } as const; + export const FormMetadata = { + /** + * Description: Name of the form completed by the user. + * + * GANA Type: String + */ + FORM_ID: { + id: "FORM_ID" as UIHandlerId, + description: "Name of the form completed by the user.", + } as FormFieldInfo, + /** + * Description: High entropy value used in forms where hash is going to be stored in plain text. + * + * GANA Type: String + */ + FORM_SALT: { + id: "FORM_SALT" as UIHandlerId, + description: "High entropy value used in forms where hash is going to be stored in plain text.", + } as FormFieldInfo, + /** + * Description: Version of the form completed by the user. + * + * GANA Type: Number + */ + FORM_VERSION: { + id: "FORM_VERSION" as UIHandlerId, + description: "Version of the form completed by the user.", + } as FormFieldInfo, + } as const; export const VQF_902_9_identity = { /** * Description: