taler-typescript-core

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

commit 140bef3747521b9ab4a92e24dceedfaf1ecc15f9
parent d32df801ef9f61552094d9392c97442f9f4f097c
Author: Sebastian <sebasjm@gmail.com>
Date:   Wed, 15 Jan 2025 12:01:32 -0300

update measures

Diffstat:
Mpackages/aml-backoffice-ui/src/Routing.tsx | 3+++
Mpackages/aml-backoffice-ui/src/forms.json | 528+------------------------------------------------------------------------------
Mpackages/aml-backoffice-ui/src/hooks/account.ts | 8+++++---
Mpackages/aml-backoffice-ui/src/hooks/decision-request.ts | 83++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Mpackages/aml-backoffice-ui/src/hooks/decisions.ts | 63+++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mpackages/aml-backoffice-ui/src/pages/AmlDecisionRequestWizard.tsx | 306+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Mpackages/aml-backoffice-ui/src/pages/CaseDetails.tsx | 4++--
Mpackages/aml-backoffice-ui/src/pages/CreateAccount.tsx | 4++--
8 files changed, 400 insertions(+), 599 deletions(-)

diff --git a/packages/aml-backoffice-ui/src/Routing.tsx b/packages/aml-backoffice-ui/src/Routing.tsx @@ -164,6 +164,7 @@ function PrivateRouting(): VNode { case "decideNew": { return ( <AmlDecisionRequestWizard + account={location.values.cid} onMove={(step) => { if (!step) { navigateTo(privatePages.profile.url({})); @@ -182,6 +183,7 @@ function PrivateRouting(): VNode { case "decide": { return ( <AmlDecisionRequestWizard + account={location.values.cid} onMove={(step) => { if (!step) { navigateTo(privatePages.profile.url({})); @@ -200,6 +202,7 @@ function PrivateRouting(): VNode { case "decideWithStep": { return ( <AmlDecisionRequestWizard + account={location.values.cid} step={location.values.step as WizardSteps} onMove={(step) => { if (!step) { diff --git a/packages/aml-backoffice-ui/src/forms.json b/packages/aml-backoffice-ui/src/forms.json @@ -1,529 +1,3 @@ { - "forms": [ - { - "label": "Information on customer", - "id": "902_1e", - "version": 1, - "config": { - "type": "double-column", - "design": [ - { - "title": "Information on customer", - "description": "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.", - "fields": [ - { - "type": "choiceStacked", - - "name": "customerType", - "id": ".customerType", - "label": "Type of customer", - "help": "Select one and complete the next form", - "required": true, - "choices": [ - { - "label": "Natural person", - "value": "natural" - }, - { - "label": "Legal entity", - "value": "legal" - } - ] - }, - { - "type": "group", - - "label": "Natural customer form", - "name": "algo", - "id": "algo", - "before": "a) Country risk (nationality)", - "after": "a) Country risk (nationality)", - "fields": [ - { - "type": "text", - - "name": "naturalCustomer.fullName", - "id": ".naturalCustomer.fullName", - "label": "Full name", - "required": true - }, - { - "type": "text", - - "name": "naturalCustomer.address", - "id": ".naturalCustomer.address", - "label": "Residential address", - "required": true - }, - { - "type": "integer", - - "name": "naturalCustomer.telephone", - "id": ".naturalCustomer.telephone", - "label": "Telephone" - }, - { - "type": "text", - - "name": "naturalCustomer.email", - "id": ".naturalCustomer.email", - "label": "E-mail" - }, - { - "type": "absoluteTimeText", - - "pattern": "dd/MM/yyyy", - "name": "naturalCustomer.dateOfBirth", - "id": ".naturalCustomer.dateOfBirth", - "label": "Date of birth", - "required": true - }, - { - "type": "text", - - "name": "naturalCustomer.nationality", - "id": ".naturalCustomer.nationality", - "label": "Nationality", - "required": true - }, - { - "type": "text", - - "name": "naturalCustomer.document", - "id": ".naturalCustomer.document", - "label": "Identification document", - "required": true - }, - { - "type": "file", - - "name": "naturalCustomer.documentAttachment", - "id": ".naturalCustomer.documentAttachment", - "label": "Document attachment", - "required": true, - "maxBites": 2097152, - "accept": ".pdf", - "help": "PDF file with max size of 2 mega bytes" - }, - { - "type": "text", - - "name": "naturalCustomer.companyName", - "id": ".naturalCustomer.companyName", - "label": "Company name" - }, - { - "type": "text", - - "name": "naturalCustomer.office", - "id": ".naturalCustomer.office", - "label": "Registered office" - }, - { - "type": "text", - - "name": "naturalCustomer.companyDocument", - "id": ".naturalCustomer.companyDocument", - "label": "Company identification document" - }, - { - "type": "file", - - "name": "naturalCustomer.companyDocumentAttachment", - "id": ".naturalCustomer.companyDocumentAttachment", - "label": "Document attachment", - "required": true, - "maxBites": 2097152, - "accept": ".png", - "help": "PNG file with max size of 2 mega bytes" - } - ] - }, - - { - "type": "group", - - "label": "Natural customer form", - "name": "algo", - "id": "algo", - "before": "a) Country risk (nationality)", - "after": "a) Country risk (nationality)", - "fields": [ - { - "type": "text", - - "name": "legalCustomer.companyName", - "id": ".legalCustomer.companyName", - "label": "Company name", - "required": true - }, - { - "type": "text", - - "name": "legalCustomer.domicile", - "id": ".legalCustomer.domicile", - "label": "Domicile", - "required": true - }, - { - "type": "text", - - "name": "legalCustomer.contactPerson", - "id": ".legalCustomer.contactPerson", - "label": "Contact person" - }, - { - "type": "text", - - "name": "legalCustomer.telephone", - "id": ".legalCustomer.telephone", - "label": "Telephone" - }, - { - "type": "text", - - "name": "legalCustomer.email", - "id": ".legalCustomer.email", - "label": "E-mail" - }, - { - "type": "text", - - "name": "legalCustomer.document", - "id": ".legalCustomer.document", - "label": "Identification document", - "help": "Not older than 12 month" - }, - { - "type": "file", - - "name": "legalCustomer.documentAttachment", - "id": ".legalCustomer.documentAttachment", - "label": "Document attachment", - "required": true, - "maxBites": 2097152, - "accept": ".png", - "help": "PNG file with max size of 2 mega bytes" - } - ] - } - ] - }, - { - "title": "Information on the natural persons who establish the business relationship for legal entities and partnerships", - "description": "For legal entities and partnerships the identity of the natural persons who establish the business relationship must be verified.", - "fields": [ - { - "type": "array", - - "name": "businessEstablisher", - "id": ".businessEstablisher", - "label": "Persons", - "required": true, - "labelFieldId": "fullName", - "placeholder": "this is the placeholder", - "fields": [ - { - "type": "text", - - "name": "fullName", - "id": ".fullName", - "label": "Full name", - "required": true - }, - { - "type": "text", - - "name": "address", - "id": ".address", - "label": "Residential address", - "required": true - }, - { - "type": "absoluteTimeText", - - "pattern": "dd/MM/yyyy", - "name": "dateOfBirth", - "id": ".dateOfBirth", - "label": "Date of birth", - "required": true - }, - - { - "type": "text", - - "name": "nationality", - "id": ".nationality", - "label": "Nationality", - "required": true - }, - { - "type": "text", - - "name": "typeOfAuthorization", - "id": ".typeOfAuthorization", - "label": "Type of authorization (signatory of representation)", - "required": true - }, - { - "type": "file", - - "name": "documentAttachment", - "id": ".documentAttachment", - "label": "Identification document attachment", - "required": true, - "maxBites": 2097152, - "accept": ".pdf", - "help": "PDF file with max size of 2 mega bytes" - }, - { - "type": "choiceStacked", - - "name": "powerOfAttorneyArrangements", - "id": ".powerOfAttorneyArrangements", - "label": "Power of attorney arrangements", - "required": true, - "choices": [ - { - "label": "CR extract", - "value": "cr" - }, - { - "label": "Mandate", - "value": "mandate" - }, - { - "label": "Other", - "value": "other" - } - ] - }, - { - "type": "text", - - "name": "powerOfAttorneyArrangementsOther", - "id": ".powerOfAttorneyArrangementsOther", - "label": "Power of attorney arrangements", - "required": true - } - ], - "labelField": "fullName" - } - ] - }, - { - "title": "Acceptance of business relationship", - "fields": [ - { - "type": "absoluteTimeText", - - "name": "acceptance.when", - "id": ".acceptance.when", - "pattern": "dd/MM/yyyy", - "converterId": "Taler.AbsoluteTime", - "label": "Date (conclusion of contract)" - }, - { - "type": "choiceStacked", - - "name": "acceptance.acceptedBy", - "id": ".acceptance.acceptedBy", - "label": "Accepted by", - "required": true, - "choices": [ - { - "label": "Face-to-face meeting with customer", - "value": "face-to-face" - }, - { - "label": "Correspondence: authenticated copy of identification document obtained", - "value": "correspondence-document" - }, - { - "label": "Correspondence: residential address validated", - "value": "correspondence-address" - } - ] - }, - { - "type": "choiceStacked", - - "name": "acceptance.typeOfCorrespondence", - "id": ".acceptance.typeOfCorrespondence", - "label": "Type of correspondence service", - "choices": [ - { - "label": "to the customer", - "value": "customer" - }, - { - "label": "hold at bank", - "value": "bank" - }, - { - "label": "to the member", - "value": "member" - }, - { - "label": "to a third party", - "value": "third-party" - } - ] - }, - { - "type": "text", - - "name": "acceptance.thirdPartyFullName", - "id": ".acceptance.thirdPartyFullName", - "label": "Third party full name", - "required": true - }, - { - "type": "text", - - "name": "acceptance.thirdPartyAddress", - "id": ".acceptance.thirdPartyAddress", - "label": "Third party address", - "required": true - }, - { - "type": "selectMultiple", - - "name": "acceptance.language", - "id": ".acceptance.language", - "label": "Languages", - "choices": [ - { - "label": "Espanol", - "value": "es" - } - ], - "unique": true - }, - { - "type": "textArea", - - "name": "acceptance.furtherInformation", - "id": ".acceptance.furtherInformation", - "label": "Further information" - } - ] - }, - { - "title": "Information on the beneficial owner of the assets and/or controlling person", - "description": "Establishment of the beneficial owner of the assets and/or controlling person", - "fields": [ - { - "type": "choiceStacked", - - "name": "establishment", - "id": ".establishment", - "label": "The customer is", - "required": true, - "choices": [ - { - "label": "a natural person and there are no doubts that this person is the sole beneficial owner of the assets", - "value": "natural" - }, - { - "label": "a foundation (or a similar construct; incl. underlying companies)", - "value": "foundation" - }, - { - "label": "a trust (incl. underlying companies)", - "value": "trust" - }, - { - "label": "a life insurance policy with separately managed accounts/securities accounts", - "value": "insurance-wrapper" - }, - { - "label": "all other cases", - "value": "other" - } - ] - } - ] - }, - { - "title": "Evaluation with regard to embargo procedures/terrorism lists on establishing the business relationship", - "description": "Verification whether the customer, beneficial owners of the assets, controlling persons, authorized representatives or other involved persons are listed on an embargo/terrorism list (date of verification/result)", - "fields": [ - { - "type": "textArea", - - "name": "embargoEvaluation", - "id": ".embargoEvaluation", - "help": "The evaluation must be made at the beginning of the business relationship and has to be repeated in the case of permanent business relationship every time the according lists are updated.", - "label": "Evaluation" - } - ] - }, - { - "title": "In the case of cash transactions/occasional customers: Information on type and purpose of business relationship", - "description": "These details are only necessary for occasional customers, i.e. money exchange, money and asset transfer or other cash transactions provided that no customer profile (VQF doc. No. 902.5) is created", - "fields": [ - { - "type": "choiceStacked", - - "name": "cashTransactions.typeOfBusiness", - "id": ".cashTransactions.typeOfBusiness", - "label": "Type of business relationship", - "choices": [ - { - "label": "Money exchange", - "value": "money-exchange" - }, - { - "label": "Money and asset transfer", - "value": "money-and-asset-transfer" - }, - { - "label": "Other cash transactions. Specify below", - "value": "other" - } - ] - }, - { - "type": "text", - - "name": "cashTransactions.otherTypeOfBusiness", - "id": ".cashTransactions.otherTypeOfBusiness", - "required": true, - "label": "Specify other cash transactions:" - }, - { - "type": "textArea", - "name": "cashTransactions.purpose", - "id": ".cashTransactions.purpose", - "label": "Purpose of the business relationship (purpose of service requested)" - } - ] - } - ] - } - }, - { - "label": "Example form", - "id": "example", - "version": 1, - "config": { - "type": "double-column", - "design": [ - { - "title": "Boolean inputs", - "fields": [ - { - "type": "toggle", - "name": "yes", - "id": ".yes", - "label": "Yes or no?" - } - ] - } - ] - } - } - ], - "not_yet_supported": [] + "forms": [] } diff --git a/packages/aml-backoffice-ui/src/hooks/account.ts b/packages/aml-backoffice-ui/src/hooks/account.ts @@ -34,9 +34,12 @@ export function revalidateAccountInformation() { { revalidate: true }, ); } -export function useAccountInformation(paytoHash: string) { +export function useAccountInformation(paytoHash?: string) { const officer = useOfficer(); - const session = officer.state === "ready" ? officer.account : undefined; + const session = + officer.state === "ready" && paytoHash !== undefined + ? officer.account + : undefined; const { lib: { exchange: api }, @@ -97,4 +100,3 @@ export function useServerMeasures() { if (error) return error; return undefined; } - diff --git a/packages/aml-backoffice-ui/src/hooks/decision-request.ts b/packages/aml-backoffice-ui/src/hooks/decision-request.ts @@ -16,36 +16,52 @@ import { AbsoluteTime, + AmountJson, Codec, - KycRule, + Duration, MeasureInformation, - PaytoHash, - Timestamp, - TranslatedString, buildCodecForObject, codecForAbsoluteTime, + codecForAmountJson, codecForAny, codecForBoolean, - codecForKycRules, + codecForDurationMs, codecForList, codecForMap, codecForMeasureInformation, codecForString, codecOptional, + codecOptionalDefault, } from "@gnu-taler/taler-util"; -import { - buildStorageKey, - useLocalStorage, - useTranslationContext, -} from "@gnu-taler/web-util/browser"; +import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; + +export type BalanceForm = { + balance: AmountJson | undefined; + balanceUnlimited: boolean; + withdrawalAmount: AmountJson | undefined; + withdrawalTimeframe: Duration | undefined; + withdrawalUnlimited: boolean; + depositAmount: AmountJson | undefined; + depositTimeframe: Duration | undefined; + depositUnlimited: boolean; + closeAmount: AmountJson | undefined; + closeTimeframe: Duration | undefined; + closeUnlimited: boolean; + mergeAmount: AmountJson | undefined; + mergeTimeframe: Duration | undefined; + mergeUnlimited: boolean; + aggregateAmount: AmountJson | undefined; + aggregateTimeframe: Duration | undefined; + aggregateUnlimited: boolean; +}; export interface DecisionRequest { - rules: KycRule[] | undefined; + rules: BalanceForm | undefined; deadline: AbsoluteTime | undefined; properties: object | undefined; custom_events: string[] | undefined; inhibit_events: string[] | undefined; - keep_investigating: boolean | undefined; + keep_investigating: boolean; justification: string | undefined; next_measure: string[][] | undefined; custom_measures: @@ -55,15 +71,51 @@ export interface DecisionRequest { | undefined; } +export const codecForBalanceForm = (): Codec<BalanceForm> => + buildCodecForObject<BalanceForm>() + .property("balance", codecOptional(codecForAmountJson())) + .property( + "balanceUnlimited", + codecOptionalDefault(codecForBoolean(), false), + ) + .property("withdrawalAmount", codecOptional(codecForAmountJson())) + .property("withdrawalTimeframe", codecOptional(codecForDurationMs)) + .property( + "withdrawalUnlimited", + codecOptionalDefault(codecForBoolean(), false), + ) + .property("depositAmount", codecOptional(codecForAmountJson())) + .property("depositTimeframe", codecOptional(codecForDurationMs)) + .property( + "depositUnlimited", + codecOptionalDefault(codecForBoolean(), false), + ) + .property("closeAmount", codecOptional(codecForAmountJson())) + .property("closeTimeframe", codecOptional(codecForDurationMs)) + .property("closeUnlimited", codecOptionalDefault(codecForBoolean(), false)) + .property("mergeAmount", codecOptional(codecForAmountJson())) + .property("mergeTimeframe", codecOptional(codecForDurationMs)) + .property("mergeUnlimited", codecOptionalDefault(codecForBoolean(), false)) + .property("aggregateAmount", codecOptional(codecForAmountJson())) + .property("aggregateTimeframe", codecOptional(codecForDurationMs)) + .property( + "aggregateUnlimited", + codecOptionalDefault(codecForBoolean(), false), + ) + .build("BalanceForm"); + export const codecForDecisionRequest = (): Codec<DecisionRequest> => buildCodecForObject<DecisionRequest>() - .property("rules", codecOptional(codecForList(codecForKycRules()))) + .property("rules", codecOptional(codecForBalanceForm())) .property("deadline", codecOptional(codecForAbsoluteTime)) .property("properties", codecForAny()) .property("justification", codecOptional(codecForString())) .property("custom_events", codecOptional(codecForList(codecForString()))) .property("inhibit_events", codecOptional(codecForList(codecForString()))) - .property("keep_investigating", codecOptional(codecForBoolean())) + .property( + "keep_investigating", + codecOptionalDefault(codecForBoolean(), false), + ) .property( "next_measure", codecOptional(codecForList(codecForList(codecForString()))), @@ -80,7 +132,7 @@ const defaultDecisionRequest: DecisionRequest = { custom_events: undefined, inhibit_events: undefined, justification: undefined, - keep_investigating: undefined, + keep_investigating: false, next_measure: undefined, properties: undefined, rules: undefined, @@ -110,6 +162,7 @@ export function useCurrentDecisionRequest(): [ v: DecisionRequest[T], ) { const newValue = { ...value, [k]: v }; + console.log("===", v, k); update(newValue); } return [value, updateField, update]; diff --git a/packages/aml-backoffice-ui/src/hooks/decisions.ts b/packages/aml-backoffice-ui/src/hooks/decisions.ts @@ -19,6 +19,8 @@ import { useState } from "preact/hooks"; import { OfficerAccount, OperationOk, + opFixedSuccess, + opSuccessFromHttp, TalerExchangeResultByMethod, TalerHttpError, } from "@gnu-taler/taler-util"; @@ -63,12 +65,7 @@ export function useCurrentDecisionsUnderInvestigation() { const { data, error } = useSWR< TalerExchangeResultByMethod<"getAmlDecisions">, TalerHttpError - >( - !session - ? undefined - : [session, offset, "getAmlDecisions"], - fetcher, - ); + >(!session ? undefined : [session, offset, "getAmlDecisions"], fetcher); if (error) return error; if (data === undefined) return undefined; @@ -109,12 +106,7 @@ export function useCurrentDecisions() { const { data, error } = useSWR< TalerExchangeResultByMethod<"getAmlDecisions">, TalerHttpError - >( - !session - ? undefined - : [session, offset, "getAmlDecisions"], - fetcher, - ); + >(!session ? undefined : [session, offset, "getAmlDecisions"], fetcher); if (error) return error; if (data === undefined) return undefined; @@ -176,6 +168,53 @@ export function useAccountDecisions(accountStr: string) { ); } +/** + * @param account + * @param args + * @returns + */ +export function useAccountActiveDecision(accountStr?: string) { + const officer = useOfficer(); + const session = + accountStr !== undefined && officer.state === "ready" + ? officer.account + : undefined; + const { + lib: { exchange: api }, + } = useExchangeApiContext(); + + const [offset, setOffset] = useState<string>(); + + async function fetcher([officer, account, offset]: [ + OfficerAccount, + string, + string | undefined, + ]) { + return await api.getAmlDecisions(officer, { + order: "dec", + offset, + account, + active: true, + limit: PAGINATED_LIST_REQUEST, + }); + } + + const { data, error } = useSWR< + TalerExchangeResultByMethod<"getAmlDecisions">, + TalerHttpError + >( + !session ? undefined : [session, accountStr, offset, "getAmlDecisions"], + fetcher, + ); + + if (error) return error; + if (data === undefined) return undefined; + if (data.type !== "ok") return data; + + if (!data.body.records.length) return opFixedSuccess(undefined); + return opFixedSuccess(data.body.records[0]); +} + type PaginatedResult<T> = OperationOk<T> & { isLastPage: boolean; isFirstPage: boolean; diff --git a/packages/aml-backoffice-ui/src/pages/AmlDecisionRequestWizard.tsx b/packages/aml-backoffice-ui/src/pages/AmlDecisionRequestWizard.tsx @@ -16,15 +16,29 @@ import { AbsoluteTime, assertUnreachable, + MeasureInformation, + TalerError, TranslatedString, } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; import { + FormDesign, + FormUI, + InternationalizationAPI, + UIHandlerId, + useExchangeApiContext, + useForm, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { + BalanceForm, DecisionRequest, useCurrentDecisionRequest, } from "../hooks/decision-request.js"; -import { ShowDecisionLimitInfo } from "./CaseDetails.js"; +import { useAccountActiveDecision } from "../hooks/decisions.js"; +import { ShowDecisionLimitInfo, ShowMeasuresToSelect } from "./CaseDetails.js"; +import { useEffect, useRef } from "preact/hooks"; +import { useServerMeasures } from "../hooks/account.js"; export type WizardSteps = | "rules" // define the limits @@ -58,15 +72,7 @@ const STEPS_ORDER_MAP = STEPS_ORDER.reduce( ); function isRulesCompleted(request: DecisionRequest): boolean { - return ( - request.rules !== undefined && - request.rules.findIndex((r) => r.operation_type === "AGGREGATE") !== -1 && - request.rules.findIndex((r) => r.operation_type === "BALANCE") !== -1 && - request.rules.findIndex((r) => r.operation_type === "CLOSE") !== -1 && - request.rules.findIndex((r) => r.operation_type === "DEPOSIT") !== -1 && - request.rules.findIndex((r) => r.operation_type === "MERGE") !== -1 && - request.rules.findIndex((r) => r.operation_type === "WITHDRAW") !== -1 - ); + return request.rules !== undefined; } function isPropertiesCompleted(request: DecisionRequest): boolean { return request.properties !== undefined; @@ -87,9 +93,11 @@ function isJustificationCompleted(request: DecisionRequest): boolean { } export function AmlDecisionRequestWizard({ + account, step, onMove, }: { + account?: string; step?: WizardSteps; onMove: (n: WizardSteps | undefined) => void; }): VNode { @@ -98,7 +106,7 @@ export function AmlDecisionRequestWizard({ const content = (function () { switch (stepOrDefault) { case "rules": - return <Rules />; + return <Rules account={account} />; case "properties": return <Properties />; case "events": @@ -132,39 +140,199 @@ export function AmlDecisionRequestWizard({ > <i18n.Translate>Next</i18n.Translate> </button> - {/* <button - onClick={() => { - onMove(undefined); - }} - 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" - > - <i18n.Translate>Exit</i18n.Translate> - </button> */} {content} </div> ); } +const rulesForm = ( + i18n: InternationalizationAPI, + currency: string, +): FormDesign<BalanceForm> => ({ + type: "double-column", + sections: [ + { + title: i18n.str`Wallet`, + description: i18n.str`Limit the state of the wallet.`, + fields: [ + { + id: "balance" as UIHandlerId, + type: "amount", + label: i18n.str`Balance`, + currency, + }, + { + id: "balanceUnlimited" as UIHandlerId, + type: "toggle", + label: i18n.str`Unlimited`, + }, + ], + }, + { + title: i18n.str`Operations`, + description: i18n.str`Limit the operation rate.`, + fields: [ + { + type: "group", + label: i18n.str`Withdrawal`, + fields: [ + { + id: "withdrawalAmount" as UIHandlerId, + type: "amount", + label: i18n.str`Amount`, + currency, + }, + { + id: "withdrawalTimeframe" as UIHandlerId, + type: "duration", + label: i18n.str`Timeframe`, + }, + { + id: "withdrawalUnlimited" as UIHandlerId, + type: "toggle", + label: i18n.str`Unlimited`, + }, + ], + }, + { + type: "group", + label: i18n.str`Deposit`, + fields: [ + { + id: "depositAmount" as UIHandlerId, + type: "amount", + label: i18n.str`Amount`, + + currency, + }, + { + id: "depositTimeframe" as UIHandlerId, + type: "duration", + label: i18n.str`Timeframe`, + }, + ], + }, + { + type: "group", + label: i18n.str`Aggregate`, + fields: [ + { + id: "aggregateAmount" as UIHandlerId, + type: "amount", + label: i18n.str`Amount`, + + currency, + }, + { + id: "aggregateTimeframe" as UIHandlerId, + type: "duration", + label: i18n.str`Timeframe`, + }, + ], + }, + { + type: "group", + label: i18n.str`Merge`, + fields: [ + { + id: "mergeAmount" as UIHandlerId, + type: "amount", + label: i18n.str`Amount`, + + currency, + }, + { + id: "mergeTimeframe" as UIHandlerId, + type: "duration", + label: i18n.str`Timeframe`, + }, + ], + }, + { + type: "group", + label: i18n.str`Close`, + fields: [ + { + id: "closeAmount" as UIHandlerId, + type: "amount", + label: i18n.str`Amount`, + + currency, + }, + { + id: "closeTimeframe" as UIHandlerId, + type: "duration", + label: i18n.str`Timeframe`, + }, + ], + }, + ], + }, + ], +}); + +function onComponentUnload(callback: () => void) { + /** + * we use a ref to avoid evaluating the effect function + * on every render and so the unload is called only once + */ + const ref = useRef<typeof callback>(); + ref.current = callback; + + useEffect(() => { + return () => { + ref.current!(); + }; + }, []); +} + /** * Defined new limits for the account * @param param0 * @returns */ -function Rules({}: {}): VNode { - const [request] = useCurrentDecisionRequest(); - if (request.rules) { - return ( - <ShowDecisionLimitInfo - fixed - since={AbsoluteTime.now()} - until={request.deadline ?? AbsoluteTime.never()} - rules={request.rules} - startOpen - /> - ); - } - - return <div></div>; +function Rules({ account }: { account?: string }): VNode { + const activeDecision = useAccountActiveDecision(account); + + const { i18n } = useTranslationContext(); + const { config } = useExchangeApiContext(); + const [request, updateRequest] = useCurrentDecisionRequest(); + const currency = config.config.currency; + const design = rulesForm(i18n, currency); + const form = useForm<BalanceForm>(design, request.rules ?? {}); + + onComponentUnload(() => { + updateRequest("rules", form.status.result as any); + }); + + const info = + !activeDecision || + activeDecision instanceof TalerError || + activeDecision.type === "fail" + ? undefined + : activeDecision.body; + + return ( + <div> + <FormUI design={design} handler={form.handler} /> + {!info ? undefined : ( + <div> + <h2 class="mt-4 mb-2"> + <i18n.Translate>Current limits</i18n.Translate> + </h2> + <ShowDecisionLimitInfo + fixed + since={AbsoluteTime.fromProtocolTimestamp(info.decision_time)} + until={AbsoluteTime.fromProtocolTimestamp( + info.limits.expiration_time, + )} + rules={info.limits.rules} + startOpen + /> + </div> + )} + </div> + ); } /** @@ -189,6 +357,40 @@ function Events({}: {}): VNode { return <div> not yet impltemented: events</div>; } +type MeasureForm = { + paths: { steps: Array<string> }[]; +}; + +const measureForm = ( + i18n: InternationalizationAPI, + mi: (MeasureInformation & { id: string })[], +): FormDesign<MeasureForm> => ({ + type: "single-column", + fields: [ + { + type: "array", + id: "paths" as UIHandlerId, + label: i18n.str`Paths`, + help: i18n.str`For every entry the customer will have a different path to satify checks.`, + labelFieldId: "steps" as UIHandlerId, + fields: [ + { + type: "selectMultiple", + choices: mi.map((m) => { + return { + value: m.id, + label: m.id, + }; + }), + id: "steps" as UIHandlerId, + label: i18n.str`Steps`, + help: i18n.str`The checks that the customer will need to satisfy for this path.`, + }, + ], + }, + ], +}); + /** * Ask for more information, define new paths to proceed * @param param0 @@ -196,8 +398,36 @@ function Events({}: {}): VNode { */ function Measures({}: {}): VNode { const { i18n } = useTranslationContext(); - const [request] = useCurrentDecisionRequest(); - return <div> not yet impltemented: measures</div>; + 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 initValue: MeasureForm = !request.next_measure + ? { paths: [] } + : { paths: request.next_measure.map((steps) => ({ steps })) }; + + const design = measureForm(i18n, measureList); + const form = useForm<MeasureForm>(design, initValue); + + onComponentUnload(() => { + const r = !form.status.result.paths + ? [] + : (form.status.result.paths.map( + (path) => path?.steps ?? [], + ) as string[][]); + updateRequest("next_measure", r); + updateRequest("custom_measures", {}); + }); + + return ( + <div> + <FormUI design={design} handler={form.handler} /> + <ShowMeasuresToSelect /> + </div> + ); } /** diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx @@ -256,7 +256,7 @@ export function CaseDetails({ custom_events: undefined, inhibit_events: undefined, justification: undefined, - keep_investigating: undefined, + keep_investigating: false, next_measure: undefined, properties: undefined, rules: undefined, @@ -1554,7 +1554,7 @@ const FREEZE_RULES: (currency: string) => TalerExchangeApi.KycRule[] = ( }, ]; -function ShowMeasuresToSelect({ +export function ShowMeasuresToSelect({ onSelect, }: { onSelect?: (m: MeasureInfo) => void; diff --git a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx @@ -72,13 +72,13 @@ const createAccountForm = (i18n: InternationalizationAPI): FormDesign => ({ fields: [ { id: "password" as UIHandlerId, - type: "text", + type: "secret", label: i18n.str`Password`, required: true, }, { id: "repeat" as UIHandlerId, - type: "text", + type: "secret", label: i18n.str`Repeat password`, required: true, },