taler-typescript-core

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

commit 10836e529b9d5c4ee04f8ba71862d7e471fbcff2
parent 8ee287e65d5cfa28cc78f2bfedee6b6055140b1c
Author: Sebastian <sebasjm@gmail.com>
Date:   Wed, 16 Apr 2025 19:33:12 -0300

fixes from QC

Diffstat:
Mpackages/aml-backoffice-ui/src/Routing.tsx | 14+++++++++++++-
Mpackages/aml-backoffice-ui/src/hooks/decision-request.ts | 14++++----------
Mpackages/aml-backoffice-ui/src/pages/Dashboard.tsx | 12++++++------
Mpackages/aml-backoffice-ui/src/pages/Search.tsx | 36++++++++++++++++++++++++++----------
Mpackages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx | 18++++++++++++++----
Mpackages/aml-backoffice-ui/src/pages/decision/Justification.tsx | 56+++++++++++++++++++++++++++++++++-----------------------
Mpackages/aml-backoffice-ui/src/pages/decision/Properties.tsx | 11+++++------
Mpackages/aml-backoffice-ui/src/pages/decision/Rules.tsx | 157++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mpackages/aml-backoffice-ui/src/pages/decision/Summary.tsx | 2+-
Mpackages/web-util/src/forms/forms-ui.tsx | 14++++++++++----
Mpackages/web-util/src/hooks/useForm.ts | 13++++++++++---
11 files changed, 218 insertions(+), 129 deletions(-)

diff --git a/packages/aml-backoffice-ui/src/Routing.tsx b/packages/aml-backoffice-ui/src/Routing.tsx @@ -314,7 +314,19 @@ function PrivateRouting(): VNode { return <Accounts routeToCaseById={privatePages.caseDetails} />; } case "search": { - return <Search />; + return ( + <Search + onNewDecision={(request, account, payto) => { + updateRequest(request); + navigateTo( + privatePages.decideNew.url({ + cid: account, + payto: payto, + }), + ); + }} + /> + ); } case "statsDownload": { return <div>not yet implemented</div>; diff --git a/packages/aml-backoffice-ui/src/hooks/decision-request.ts b/packages/aml-backoffice-ui/src/hooks/decision-request.ts @@ -80,7 +80,7 @@ export interface DecisionRequest { /** * If given all the information, this account need to be investigated */ - keep_investigating: boolean; + keep_investigating: boolean | undefined; /** * Description of the decision */ @@ -126,15 +126,9 @@ export const codecForDecisionRequest = (): Codec<DecisionRequest> => "triggering_events", codecOptional(codecForList(codecForString())), ) - .property( - "keep_investigating", - codecOptionalDefault(codecForBoolean(), false), - ) + .property("keep_investigating", codecOptional(codecForBoolean())) .property("new_measures", codecOptional(codecForList(codecForString()))) - .property( - "onExpire_measure", - codecOptional(codecForString()), - ) + .property("onExpire_measure", codecOptional(codecForString())) .build("DecisionRequest"); export const DECISION_REQUEST_EMPTY: DecisionRequest = { @@ -146,7 +140,7 @@ export const DECISION_REQUEST_EMPTY: DecisionRequest = { accountName: undefined, triggering_events: undefined, justification: undefined, - keep_investigating: false, + keep_investigating: undefined, new_measures: undefined, properties: undefined, rules: undefined, diff --git a/packages/aml-backoffice-ui/src/pages/Dashboard.tsx b/packages/aml-backoffice-ui/src/pages/Dashboard.tsx @@ -266,9 +266,9 @@ function labelForEvent_tops( case TOPS_AmlEventsName.ACCOUNT_CLOSED_HIGH_RISK: return i18n.str`High risk account removed`; case TOPS_AmlEventsName.ACCOUNT_OPENED_DOMESTIC_PEP: - return i18n.str`Account from dometic PEP incorporated`; + return i18n.str`Account from domestic PEP incorporated`; case TOPS_AmlEventsName.ACCOUNT_CLOSED_DOMESTIC_PEP: - return i18n.str`Account from dometic PEP removed`; + return i18n.str`Account from domestic PEP removed`; case TOPS_AmlEventsName.ACCOUNT_OPENED_FOREIGN_PEP: return i18n.str`Account from foreign PEP incorporated`; case TOPS_AmlEventsName.ACCOUNT_CLOSED_FOREIGN_PEP: @@ -278,9 +278,9 @@ function labelForEvent_tops( case TOPS_AmlEventsName.ACCOUNT_CLOSED_HR_COUNTRY: return i18n.str`Account from high-risk country removed`; case TOPS_AmlEventsName.ACCOUNT_OPENED_INT_ORG_PEP: - return i18n.str`Account from dometic PEP incorporated`; + return i18n.str`Account from domestic PEP incorporated`; case TOPS_AmlEventsName.ACCOUNT_CLOSED_INT_ORG_PEP: - return i18n.str`Account from dometic PEP removed`; + return i18n.str`Account from domestic PEP removed`; case TOPS_AmlEventsName.ACCOUNT_MROS_REPORTED_SUSPICION_SIMPLE: return i18n.str`MROS reported by obligation`; case TOPS_AmlEventsName.ACCOUNT_MROS_REPORTED_SUSPICION_SUBSTANTIATED: @@ -342,9 +342,9 @@ function descriptionForEvent_tops( case TOPS_AmlEventsName.ACCOUNT_CLOSED_HIGH_RISK: return i18n.str`High risk account removed`; case TOPS_AmlEventsName.ACCOUNT_OPENED_DOMESTIC_PEP: - return i18n.str`Account from dometic PEP incorporated`; + return i18n.str`Account from domestic PEP incorporated`; case TOPS_AmlEventsName.ACCOUNT_CLOSED_DOMESTIC_PEP: - return i18n.str`Account from dometic PEP removed`; + return i18n.str`Account from domestic PEP removed`; case TOPS_AmlEventsName.ACCOUNT_OPENED_FOREIGN_PEP: return i18n.str`Account from foreign PEP incorporated`; case TOPS_AmlEventsName.ACCOUNT_CLOSED_FOREIGN_PEP: diff --git a/packages/aml-backoffice-ui/src/pages/Search.tsx b/packages/aml-backoffice-ui/src/pages/Search.tsx @@ -52,8 +52,13 @@ import { privatePages } from "../Routing.js"; import { Pagination, ToInvestigateIcon } from "./Cases.js"; import { HandleAccountNotReady } from "./HandleAccountNotReady.js"; import { Officer } from "./Officer.js"; +import { DECISION_REQUEST_EMPTY, DecisionRequest } from "../hooks/decision-request.js"; -export function Search() { +export function Search({ + onNewDecision, +}: { + onNewDecision: (d: DecisionRequest, account: string, payto: string) => void; +}) { const officer = useOfficer(); const { i18n } = useTranslationContext(); @@ -110,12 +115,20 @@ export function Search() { } } })()} - {!paytoUri ? undefined : <ShowResult payto={paytoUri} />} + {!paytoUri ? undefined : ( + <ShowResult payto={paytoUri} onNewDecision={onNewDecision} /> + )} </div> ); } -function ShowResult({ payto }: { payto: PaytoUri }): VNode { +function ShowResult({ + payto, + onNewDecision, +}: { + payto: PaytoUri; + onNewDecision: (d: DecisionRequest, account: string, payto: string) => void; +}): VNode { const paytoStr = stringifyPaytoUri(payto); const account = encodeCrock(hashNormalizedPaytoUri(paytoStr)); const { i18n } = useTranslationContext(); @@ -279,17 +292,20 @@ function ShowResult({ payto }: { payto: PaytoUri }): VNode { There is no history known for this account yet. </i18n.Translate> &nbsp; - <a - href={privatePages.decideNew.url({ - cid: account, - payto: encodeCrockForURI(paytoStr), - })} + <button + // href={privatePages.decideNew.url({ + // cid: account, + // payto: encodeCrockForURI(paytoStr), + // })} + onClick={async () => { + onNewDecision(DECISION_REQUEST_EMPTY, account, encodeCrockForURI(paytoStr)); + }} class="text-indigo-600 hover:text-indigo-900" > <i18n.Translate> You can make a decision for this account anyway. </i18n.Translate> - </a> + </button> </Attention> </div> ); @@ -580,7 +596,7 @@ const walletFields: (i18n: InternationalizationAPI) => UIFormElementConfig[] = ( placeholder: i18n.str`abcdef1235`, validator(value) { return value && value.length !== 32 - ? i18n.str`Should be 16 characters` + ? i18n.str`Should be 32 characters` : undefined; }, }, diff --git a/packages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx b/packages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx @@ -20,6 +20,7 @@ import { TranslatedString } from "@gnu-taler/taler-util"; import { + CopyButton, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; @@ -71,7 +72,7 @@ const STEPS_ORDER_MAP = STEPS_ORDER.reduce( ); export function isRulesCompleted(request: DecisionRequest): boolean { - return request.rules !== undefined && request.deadline !== undefined && (AbsoluteTime.isNever(request.deadline) || !!request.onExpire_measure); + return request.rules !== undefined && request.deadline !== undefined; } export function isAttributesCompleted(request: DecisionRequest): boolean { return request.attributes === undefined || request.attributes.errors === undefined; @@ -110,7 +111,7 @@ export function AmlDecisionRequestWizard({ const content = (function () { switch (stepOrDefault) { case "rules": - return <Rules account={account} />; + return <Rules account={account} newPayto={newPayto} />; case "properties": return <Properties account={account} />; case "events": @@ -118,7 +119,7 @@ export function AmlDecisionRequestWizard({ case "measures": return <Measures />; case "justification": - return <Justification newPayto={newPayto} />; + return <Justification account={account} newPayto={newPayto} />; case "attributes": return <Attributes formId={formId}/>; case "summary": @@ -128,7 +129,16 @@ export function AmlDecisionRequestWizard({ })(); return ( - <div> + <div class="min-w-60"> + + <header class="flex items-center justify-between border-b border-white/5 px-4 py-4 sm:px-6 sm:py-6 lg:px-8"> + <h1 class="text-base font-semibold leading-7 text-black"> + <i18n.Translate>Decision for account: </i18n.Translate> + </h1> + <div>{account}</div> + <CopyButton class="" getContent={() => account} /> + </header> + <WizardSteps step={stepOrDefault} onMove={onMove} newAccount={!!newPayto} /> <button disabled={!STEPS_ORDER_MAP[stepOrDefault].prev} diff --git a/packages/aml-backoffice-ui/src/pages/decision/Justification.tsx b/packages/aml-backoffice-ui/src/pages/decision/Justification.tsx @@ -1,42 +1,53 @@ -import { - AbsoluteTime, - Duration, - MeasureInformation, - PaytoString, - TalerError, -} from "@gnu-taler/taler-util"; +import { AmlDecision, PaytoString, TalerError } from "@gnu-taler/taler-util"; import { FormDesign, FormUI, InternationalizationAPI, + Loading, onComponentUnload, - UIHandlerId, useForm, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { useCurrentDecisionRequest } from "../../hooks/decision-request.js"; -import { useServerMeasures } from "../../hooks/server-info.js"; +import { useAccountActiveDecision } from "../../hooks/decisions.js"; /** * Mark for further investigation and explain decision * @param param0 * @returns */ -export function Justification({ newPayto }: { newPayto?: PaytoString }): VNode { +export function Justification({ + account, + newPayto, +}: { + account: string; + newPayto?: PaytoString; +}): VNode { + const isNewAccount = !!newPayto; + + const activeDecision = useAccountActiveDecision(isNewAccount ? undefined :account); + const info = + !activeDecision || + activeDecision instanceof TalerError || + activeDecision.type === "fail" + ? undefined + : activeDecision.body; + + if (!info && !isNewAccount) { + return <Loading />; + } + + return <JustificationForm isNewAccount={isNewAccount} info={info}/> +} + +function JustificationForm({info, isNewAccount}:{info: AmlDecision | undefined, isNewAccount: boolean}):VNode { const { i18n } = useTranslationContext(); const [request, _, updateRequest] = useCurrentDecisionRequest(); - const measures = useServerMeasures(); - const measureList = - !measures || measures instanceof TalerError || measures.type === "fail" - ? [] - : Object.entries(measures.body.roots).map(([id, mi]) => ({ id, ...mi })); - - const unknownAccount = !!newPayto - const design = formDesign(i18n, measureList, unknownAccount); + const design = formDesign(i18n, isNewAccount); const form = useForm<FormType>(design, { - investigate: request.keep_investigating, + investigate: request.keep_investigating ?? info?.to_investigate ?? false, justification: request.justification, accountName: request.accountName, }); @@ -46,7 +57,7 @@ export function Justification({ newPayto }: { newPayto?: PaytoString }): VNode { ...request, keep_investigating: !!form.status.result.investigate, justification: form.status.result.justification ?? "", - accountName: form.status.result.justification ?? "", + accountName: form.status.result.accountName ?? "", }); }); @@ -66,8 +77,7 @@ type FormType = { const formDesign = ( i18n: InternationalizationAPI, - mi: (MeasureInformation & { id: string })[], - unknownAccount: boolean + unknownAccount: boolean, ): FormDesign<FormType> => ({ type: "single-column", fields: [ @@ -87,7 +97,7 @@ const formDesign = ( type: "text", label: i18n.str`Account holder`, required: true, - help:i18n.str`Full name of the account holder`, + help: i18n.str`Full name of the account holder`, hidden: !unknownAccount, }, ], diff --git a/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx b/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx @@ -32,6 +32,7 @@ import { import { useAccountActiveDecision } from "../../hooks/decisions.js"; import { usePreferences } from "../../hooks/preferences.js"; import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js"; +import { DEFAULT_LIMITS_WHEN_NEW_ACCOUNT } from "./Rules.js"; /** * Update account properties @@ -68,16 +69,14 @@ export function Properties({ account }: { account: string }): VNode { (pref.testingDialect ? undefined : config.config.aml_spa_dialect) ?? AmlSpaDialect.TESTING; - const calculatedProps = !lastDecision - ? undefined - : calculatePropertiesBasedOnState( - lastDecision.limits, - lastDecision.properties ?? {}, + const calculatedProps = calculatePropertiesBasedOnState( + lastDecision?.limits ?? DEFAULT_LIMITS_WHEN_NEW_ACCOUNT, + lastDecision?.properties ?? {}, request, dialect, ); - const merged = !calculatedProps ? {} : Object.entries(calculatedProps).reduce( + const merged = Object.entries(calculatedProps).reduce( (prev, [key, value]) => { if (prev[key] === undefined) { prev[key] = !!value; diff --git a/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx b/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx @@ -9,6 +9,7 @@ import { LegitimizationRuleSet, LimitOperationType, MeasureInformation, + PaytoString, TalerError, TranslatedString, } from "@gnu-taler/taler-util"; @@ -31,17 +32,30 @@ import { ShowDecisionLimitInfo } from "../CaseDetails.js"; import { RulesInfo } from "../RulesInfo.js"; const DEFAULT_MEASURE_IF_NONE = ["VERBOTEN"]; +export const DEFAULT_LIMITS_WHEN_NEW_ACCOUNT: LegitimizationRuleSet = { + custom_measures: {}, + expiration_time: AbsoluteTime.toProtocolTimestamp(AbsoluteTime.never()), + rules:[], +} + /** * Defined new limits for the account * @param param0 * @returns */ -export function Rules({ account }: { account: string }): VNode { - const activeDecision = useAccountActiveDecision(account); - +export function Rules({ + account, + newPayto, +}: { + account: string; + newPayto?: PaytoString; +}): VNode { const { i18n } = useTranslationContext(); const { config } = useExchangeApiContext(); + + const isNewAccount = !!newPayto; + // const [request, updateRequestField, updateRequest] = // useCurrentDecisionRequest(); const measures = useServerMeasures(); @@ -51,6 +65,9 @@ export function Rules({ account }: { account: string }): VNode { ? undefined : measures.body.roots; + const activeDecision = useAccountActiveDecision( + isNewAccount ? undefined : account, + ); const info = !activeDecision || activeDecision instanceof TalerError || @@ -58,7 +75,7 @@ export function Rules({ account }: { account: string }): VNode { ? undefined : activeDecision.body; - if (!info) { + if (!info && !isNewAccount) { return <Loading />; } @@ -67,23 +84,31 @@ export function Rules({ account }: { account: string }): VNode { <UpdateRulesForm rootMeasures={rootMeasures} config={config.config} - limits={info.limits} + limits={info?.limits ?? DEFAULT_LIMITS_WHEN_NEW_ACCOUNT} /> <div> <h2 class="mt-4 mb-2"> <i18n.Translate>Current active rules</i18n.Translate> </h2> - <ShowDecisionLimitInfo - fixed - since={AbsoluteTime.fromProtocolTimestamp(info.decision_time)} - until={AbsoluteTime.fromProtocolTimestamp( - info.limits.expiration_time, - )} - rules={info.limits.rules} - startOpen - measure={info.limits.successor_measure ?? ""} - /> + {info === undefined ? ( + <p> + <i18n.Translate> + There are no rules for this account. + </i18n.Translate> + </p> + ) : ( + <ShowDecisionLimitInfo + fixed + since={AbsoluteTime.fromProtocolTimestamp(info.decision_time)} + until={AbsoluteTime.fromProtocolTimestamp( + info.limits.expiration_time, + )} + rules={info.limits.rules} + startOpen + measure={info.limits.successor_measure ?? ""} + /> + )} </div> </div> ); @@ -184,10 +209,7 @@ function UpdateRulesForm({ </button> <button onClick={() => { - updateRequestField( - "rules", - FREEZE_PLAN(config.currency), - ); + updateRequestField("rules", FREEZE_PLAN(config.currency)); }} 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" > @@ -195,10 +217,7 @@ function UpdateRulesForm({ </button> <button onClick={() => { - updateRequestField( - "rules", - BASIC_PLAN(config.currency), - ); + updateRequestField("rules", BASIC_PLAN(config.currency)); }} 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" > @@ -206,10 +225,7 @@ function UpdateRulesForm({ </button> <button onClick={() => { - updateRequestField( - "rules", - PREMIUM_PLAN(config.currency), - ); + updateRequestField("rules", PREMIUM_PLAN(config.currency)); }} 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" > @@ -503,7 +519,9 @@ const BASIC_PLAN: (currency: string) => KycRule[] = (currency) => [ fraction: 0, value: 1000, }), - timeframe: Duration.toTalerProtocolDuration(Duration.fromSpec({months:1})), + timeframe: Duration.toTalerProtocolDuration( + Duration.fromSpec({ months: 1 }), + ), }, { display_priority: 1, @@ -514,7 +532,9 @@ const BASIC_PLAN: (currency: string) => KycRule[] = (currency) => [ fraction: 0, value: 1000, }), - timeframe: Duration.toTalerProtocolDuration(Duration.fromSpec({months:1})), + timeframe: Duration.toTalerProtocolDuration( + Duration.fromSpec({ months: 1 }), + ), }, { display_priority: 1, @@ -523,9 +543,11 @@ const BASIC_PLAN: (currency: string) => KycRule[] = (currency) => [ threshold: Amounts.stringify({ currency, fraction: 0, - value: 5*1000, + value: 5 * 1000, }), - timeframe: Duration.toTalerProtocolDuration(Duration.fromSpec({months:1})), + timeframe: Duration.toTalerProtocolDuration( + Duration.fromSpec({ months: 1 }), + ), }, { display_priority: 1, @@ -534,9 +556,11 @@ const BASIC_PLAN: (currency: string) => KycRule[] = (currency) => [ threshold: Amounts.stringify({ currency, fraction: 0, - value: 50*1000, + value: 50 * 1000, }), - timeframe: Duration.toTalerProtocolDuration(Duration.fromSpec({years:1})), + timeframe: Duration.toTalerProtocolDuration( + Duration.fromSpec({ years: 1 }), + ), }, { display_priority: 1, @@ -545,9 +569,11 @@ const BASIC_PLAN: (currency: string) => KycRule[] = (currency) => [ threshold: Amounts.stringify({ currency, fraction: 0, - value: 5*1000, + value: 5 * 1000, }), - timeframe: Duration.toTalerProtocolDuration(Duration.fromSpec({months:1})), + timeframe: Duration.toTalerProtocolDuration( + Duration.fromSpec({ months: 1 }), + ), }, { display_priority: 1, @@ -556,11 +582,12 @@ const BASIC_PLAN: (currency: string) => KycRule[] = (currency) => [ threshold: Amounts.stringify({ currency, fraction: 0, - value: 50*1000, + value: 50 * 1000, }), - timeframe: Duration.toTalerProtocolDuration(Duration.fromSpec({years:1})), + timeframe: Duration.toTalerProtocolDuration( + Duration.fromSpec({ years: 1 }), + ), }, - ]; const PREMIUM_PLAN: (currency: string) => KycRule[] = (currency) => [ @@ -571,7 +598,7 @@ const PREMIUM_PLAN: (currency: string) => KycRule[] = (currency) => [ threshold: Amounts.stringify({ currency, fraction: 0, - value: 10*1000, + value: 10 * 1000, }), timeframe: Duration.toTalerProtocolDuration(Duration.getForever()), }, @@ -595,7 +622,9 @@ const PREMIUM_PLAN: (currency: string) => KycRule[] = (currency) => [ fraction: 0, value: 1000, }), - timeframe: Duration.toTalerProtocolDuration(Duration.fromSpec({months:1})), + timeframe: Duration.toTalerProtocolDuration( + Duration.fromSpec({ months: 1 }), + ), }, { display_priority: 1, @@ -606,7 +635,9 @@ const PREMIUM_PLAN: (currency: string) => KycRule[] = (currency) => [ fraction: 0, value: 1000, }), - timeframe: Duration.toTalerProtocolDuration(Duration.fromSpec({months:1})), + timeframe: Duration.toTalerProtocolDuration( + Duration.fromSpec({ months: 1 }), + ), }, { display_priority: 1, @@ -615,9 +646,11 @@ const PREMIUM_PLAN: (currency: string) => KycRule[] = (currency) => [ threshold: Amounts.stringify({ currency, fraction: 0, - value: 15*1000, + value: 15 * 1000, }), - timeframe: Duration.toTalerProtocolDuration(Duration.fromSpec({months:1})), + timeframe: Duration.toTalerProtocolDuration( + Duration.fromSpec({ months: 1 }), + ), }, { display_priority: 1, @@ -626,9 +659,11 @@ const PREMIUM_PLAN: (currency: string) => KycRule[] = (currency) => [ threshold: Amounts.stringify({ currency, fraction: 0, - value: 150*1000, + value: 150 * 1000, }), - timeframe: Duration.toTalerProtocolDuration(Duration.fromSpec({years:1})), + timeframe: Duration.toTalerProtocolDuration( + Duration.fromSpec({ years: 1 }), + ), }, { display_priority: 1, @@ -637,9 +672,11 @@ const PREMIUM_PLAN: (currency: string) => KycRule[] = (currency) => [ threshold: Amounts.stringify({ currency, fraction: 0, - value: 15*1000, + value: 15 * 1000, }), - timeframe: Duration.toTalerProtocolDuration(Duration.fromSpec({months:1})), + timeframe: Duration.toTalerProtocolDuration( + Duration.fromSpec({ months: 1 }), + ), }, { display_priority: 1, @@ -648,20 +685,19 @@ const PREMIUM_PLAN: (currency: string) => KycRule[] = (currency) => [ threshold: Amounts.stringify({ currency, fraction: 0, - value: 150*1000, + value: 150 * 1000, }), - timeframe: Duration.toTalerProtocolDuration(Duration.fromSpec({years:1})), + timeframe: Duration.toTalerProtocolDuration( + Duration.fromSpec({ years: 1 }), + ), }, ]; -const FREEZE_PLAN: (currency: string) => KycRule[] = (currency) => Object.values(LimitOperationType).map((operation_type) => ({ - display_priority: 1, - measures: ["VERBOTEN"], - operation_type, - threshold: Amounts.stringify( - Amounts.zeroOfCurrency(currency), - ), - timeframe: Duration.toTalerProtocolDuration( - Duration.getForever(), - ), -})) -\ No newline at end of file +const FREEZE_PLAN: (currency: string) => KycRule[] = (currency) => + Object.values(LimitOperationType).map((operation_type) => ({ + display_priority: 1, + measures: ["VERBOTEN"], + operation_type, + threshold: Amounts.stringify(Amounts.zeroOfCurrency(currency)), + timeframe: Duration.toTalerProtocolDuration(Duration.getForever()), + })); diff --git a/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx b/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx @@ -119,7 +119,7 @@ export function Summary({ ), justification: decision.justification!, payto_uri: !fullPayto ? undefined : stringifyPaytoUri(fullPayto), - keep_investigating: decision.keep_investigating, + keep_investigating: decision.keep_investigating ?? false, new_rules: { expiration_time: AbsoluteTime.toProtocolTimestamp( decision.deadline!, diff --git a/packages/web-util/src/forms/forms-ui.tsx b/packages/web-util/src/forms/forms-ui.tsx @@ -313,7 +313,7 @@ export function ErrorsSummary<T>({ > <div class="px-4 sm:px-0"> <h3 class="text-base/7 font-semibold text-gray-900"> - <i18n.Translate>Errors summary</i18n.Translate> + <i18n.Translate>Missing fields</i18n.Translate> </h3> </div> @@ -397,9 +397,15 @@ export function ErrorsSummary<T>({ }} class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0 odd:bg-white even:bg-gray-100 cursor-pointer" > - <dt class="underline pl-4 text-sm/6 font-medium text-gray-900"> - {errHandler.label} - </dt> + {errHandler.section ? ( + <dt class="underline pl-4 text-sm/6 font-medium text-gray-900"> + {errHandler.section}: {errHandler.label} + </dt> + ) : ( + <dt class="underline pl-4 text-sm/6 font-medium text-gray-900"> + {errHandler.label} + </dt> + )} <dd class="underline flex text-sm/6 text-red-700 sm:col-span-2 sm:mt-0"> {errHandler.message} </dd> diff --git a/packages/web-util/src/hooks/useForm.ts b/packages/web-util/src/hooks/useForm.ts @@ -100,6 +100,7 @@ export type RecursivePartial<T> = { export type ErrorAndLabel = { message: TranslatedString; label: TranslatedString; + section: TranslatedString | undefined; }; export type FormErrors<T> = { @@ -223,6 +224,7 @@ function checkFormFieldIsValid( formElement: UIFormElementConfig, currentValue: string | undefined, i18n: InternationalizationAPI, + secitonTitle: string | undefined, ): ErrorAndLabel | undefined { if (!("id" in formElement)) { return undefined; @@ -232,6 +234,7 @@ function checkFormFieldIsValid( return { label: formElement.label as TranslatedString, message: i18n.str`required`, + section: secitonTitle as TranslatedString, }; } else if (formElement.validator) { try { @@ -240,6 +243,7 @@ function checkFormFieldIsValid( return { label: formElement.label as TranslatedString, message, + section: secitonTitle as TranslatedString, }; } } catch (e) { @@ -251,6 +255,7 @@ function checkFormFieldIsValid( return { label: formElement.label as TranslatedString, message, + section: secitonTitle as TranslatedString, }; } } @@ -278,6 +283,7 @@ function constructFormHandler<T>( formElement: UIFormElementConfig, hiddenSection: boolean | undefined, handlerUiPath: string, + secitonTitle: string | undefined, ): void { if (!("id" in formElement)) { return undefined; @@ -296,7 +302,7 @@ function constructFormHandler<T>( (formElement.hide && formElement.hide(currentValue, result)); const currentError: ErrorAndLabel | undefined = !hidden - ? checkFormFieldIsValid(formElement, currentValue, i18n) + ? checkFormFieldIsValid(formElement, currentValue, i18n, secitonTitle) : undefined; if (currentError !== undefined) { @@ -329,15 +335,16 @@ function constructFormHandler<T>( if (hidden) { model.hiddenSections.add(`${secIndex}`); } + sec.fields.forEach((f, fieldIndex) => - createFieldHandler(f, hidden, `${secIndex}.${fieldIndex}`), + createFieldHandler(f, hidden, `${secIndex}.${fieldIndex}`, sec.title), ); }); break; } case "single-column": { design.fields.forEach((f, fieldIndex) => - createFieldHandler(f, undefined, `root.${fieldIndex}`), + createFieldHandler(f, undefined, `root.${fieldIndex}`, undefined), ); break; }