taler-typescript-core

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

commit cbca95c72f78bcf9da71c67aa68285a8b339833e
parent 5b07bb1a5ed3ccb4fbbf63a91e43e63757215b1e
Author: Sebastian <sebasjm@gmail.com>
Date:   Wed, 16 Apr 2025 11:09:05 -0300

fix #9739

Diffstat:
Mpackages/aml-backoffice-ui/src/pages/CaseDetails.tsx | 147+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mpackages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx | 5+++--
Mpackages/aml-backoffice-ui/src/pages/decision/Justification.tsx | 18------------------
Mpackages/aml-backoffice-ui/src/pages/decision/Rules.tsx | 529+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mpackages/aml-backoffice-ui/src/pages/decision/Summary.tsx | 1+
5 files changed, 507 insertions(+), 193 deletions(-)

diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx @@ -132,7 +132,7 @@ export function CaseDetails({ onNewDecision, }: { onNewDecision: (d: DecisionRequest) => void; - routeToShowCollectedInfo: RouteDefinition<{cid:string,rowId:string}>; + routeToShowCollectedInfo: RouteDefinition<{ cid: string; rowId: string }>; account: string; }) { const [selected, setSelected] = useState<AbsoluteTime | undefined>(undefined); //AbsoluteTime.now()); @@ -256,6 +256,7 @@ export function CaseDetails({ justification={activeDecision.justification} rules={activeDecision.limits.rules} startOpen + measure={activeDecision.limits.successor_measure ?? ""} /> </div> )} @@ -273,6 +274,7 @@ export function CaseDetails({ )} justification={d.justification} rules={d.limits.rules} + measure={d.limits.successor_measure ?? ""} /> ); })} @@ -486,6 +488,7 @@ function SubmitNewDecision({ )} rules={decision.request.new_rules.rules} startOpen + measure={decision.request.new_rules.successor_measure ?? ""} /> </div> ); @@ -580,6 +583,7 @@ export function ShowDecisionLimitInfo({ startOpen, justification, fixed, + measure, }: { since: AbsoluteTime; until: AbsoluteTime; @@ -587,6 +591,7 @@ export function ShowDecisionLimitInfo({ rules: KycRule[]; startOpen?: boolean; fixed?: boolean; + measure: string; }): VNode { const { i18n } = useTranslationContext(); const [opened, setOpened] = useState(startOpen ?? false); @@ -625,6 +630,16 @@ export function ShowDecisionLimitInfo({ <Time format="dd/MM/yyyy HH:mm:ss" timestamp={until} /> </div> </div> + {AbsoluteTime.isNever(until) ? undefined : ( + <div class="flex rounded-md shadow-sm border-0 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600"> + <div class="pointer-events-none p-2 bg-gray-300 inset-y-0 flex items-center "> + <i18n.Translate>Successor measure</i18n.Translate> + </div> + <div class="p-2 bg-gray-50 rounded-md rounded-l-none data-[left=true]:text-left text-gray-900 placeholder:text-gray-50 sm:text-sm sm:leading-6"> + {measure} + </div> + </div> + )} {fixed ? ( <Fragment /> ) : ( @@ -731,8 +746,8 @@ function ShowTimeline({ account, routeToShowCollectedInfo, }: { - account: string, - routeToShowCollectedInfo: RouteDefinition<{cid:string,rowId:string}>; + account: string; + routeToShowCollectedInfo: RouteDefinition<{ cid: string; rowId: string }>; history: TalerExchangeApi.KycAttributeCollectionEvent[]; }): VNode { const { i18n } = useTranslationContext(); @@ -746,78 +761,76 @@ function ShowTimeline({ | undefined; return ( - <a href={routeToShowCollectedInfo.url({cid: account, rowId: String(e.rowid)})}> - - <li - key={idx} - class="hover:bg-gray-200 p-2 rounded" + <a + href={routeToShowCollectedInfo.url({ + cid: account, + rowId: String(e.rowid), + })} > - <div class="relative pb-3"> - <span class="absolute left-3 top-5 -ml-px h-full w-1 bg-gray-200"></span> - <div class="relative flex space-x-3"> - {/* <ArrowDownCircleIcon class="h-8 w-8 text-green-700" /> */} - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - stroke-width="1.5" - stroke="currentColor" - class="w-6 h-6" - > - <path - stroke-linecap="round" - stroke-linejoin="round" - d="M9 12.75l3 3m0 0l3-3m-3 3v-7.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" - /> - </svg> - {!formId ? undefined : ( - <div> - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - stroke-width="1.5" - stroke="currentColor" - class="size-6" - > - <path - stroke-linecap="round" - stroke-linejoin="round" - d="M12 7.5h1.5m-1.5 3h1.5m-7.5 3h7.5m-7.5 3h7.5m3-9h3.375c.621 0 1.125.504 1.125 1.125V18a2.25 2.25 0 0 1-2.25 2.25M16.5 7.5V18a2.25 2.25 0 0 0 2.25 2.25M16.5 7.5V4.875c0-.621-.504-1.125-1.125-1.125H4.125C3.504 3.75 3 4.254 3 4.875V18a2.25 2.25 0 0 0 2.25 2.25h13.5M6 7.5h3v3H6v-3Z" - /> - </svg> - <span>{formId}</span> - </div> - )} - <div class="flex min-w-0 flex-1 justify-between space-x-4 pt-1.5"> - <div class="whitespace-nowrap text-right text-sm text-gray-500"> - {e.collection_time.t_s === "never" ? ( - "never" - ) : ( - <time - dateTime={format( - e.collection_time.t_s * 1000, - "dd MMM yyyy", - )} + <li key={idx} class="hover:bg-gray-200 p-2 rounded"> + <div class="relative pb-3"> + <span class="absolute left-3 top-5 -ml-px h-full w-1 bg-gray-200"></span> + <div class="relative flex space-x-3"> + {/* <ArrowDownCircleIcon class="h-8 w-8 text-green-700" /> */} + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-6 h-6" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M9 12.75l3 3m0 0l3-3m-3 3v-7.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" + /> + </svg> + {!formId ? undefined : ( + <div> + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="size-6" > - {format( - e.collection_time.t_s * 1000, - "dd MMM yyyy HH:mm:ss", - )} - </time> - )} + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M12 7.5h1.5m-1.5 3h1.5m-7.5 3h7.5m-7.5 3h7.5m3-9h3.375c.621 0 1.125.504 1.125 1.125V18a2.25 2.25 0 0 1-2.25 2.25M16.5 7.5V18a2.25 2.25 0 0 0 2.25 2.25M16.5 7.5V4.875c0-.621-.504-1.125-1.125-1.125H4.125C3.504 3.75 3 4.254 3 4.875V18a2.25 2.25 0 0 0 2.25 2.25h13.5M6 7.5h3v3H6v-3Z" + /> + </svg> + <span>{formId}</span> + </div> + )} + <div class="flex min-w-0 flex-1 justify-between space-x-4 pt-1.5"> + <div class="whitespace-nowrap text-right text-sm text-gray-500"> + {e.collection_time.t_s === "never" ? ( + "never" + ) : ( + <time + dateTime={format( + e.collection_time.t_s * 1000, + "dd MMM yyyy", + )} + > + {format( + e.collection_time.t_s * 1000, + "dd MMM yyyy HH:mm:ss", + )} + </time> + )} + </div> </div> </div> </div> - </div> - </li> + </li> </a> - ); })} - <li - class="hover:bg-gray-200 p-2 rounded" - > + <li class="hover:bg-gray-200 p-2 rounded"> <div class="flex min-w-0 flex-1 justify-between space-x-4 pt-1.5"> <svg xmlns="http://www.w3.org/2000/svg" diff --git a/packages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx b/packages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx @@ -14,6 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { + AbsoluteTime, assertUnreachable, PaytoString, TranslatedString @@ -70,7 +71,7 @@ const STEPS_ORDER_MAP = STEPS_ORDER.reduce( ); export function isRulesCompleted(request: DecisionRequest): boolean { - return request.rules !== undefined && request.deadline !== undefined; + return request.rules !== undefined && request.deadline !== undefined && (AbsoluteTime.isNever(request.deadline) || !!request.onExpire_measure); } export function isAttributesCompleted(request: DecisionRequest): boolean { return request.attributes === undefined || request.attributes.errors === undefined; @@ -105,7 +106,7 @@ export function AmlDecisionRequestWizard({ onMove: (n: WizardSteps | undefined) => void; }): VNode { const { i18n } = useTranslationContext(); - const stepOrDefault = step ?? "attributes"; + const stepOrDefault = step ?? STEPS_ORDER[0]; const content = (function () { switch (stepOrDefault) { case "rules": diff --git a/packages/aml-backoffice-ui/src/pages/decision/Justification.tsx b/packages/aml-backoffice-ui/src/pages/decision/Justification.tsx @@ -35,14 +35,9 @@ export function Justification({ newPayto }: { newPayto?: PaytoString }): VNode { const unknownAccount = !!newPayto const design = formDesign(i18n, measureList, unknownAccount); - const expMeasres: string = !request.onExpire_measure - ? "" - : request.onExpire_measure; - const form = useForm<FormType>(design, { investigate: request.keep_investigating, justification: request.justification, - measure: expMeasres, accountName: request.accountName, }); @@ -51,7 +46,6 @@ export function Justification({ newPayto }: { newPayto?: PaytoString }): VNode { ...request, keep_investigating: !!form.status.result.investigate, justification: form.status.result.justification ?? "", - onExpire_measure: form.status.result.measure ?? "", accountName: form.status.result.justification ?? "", }); }); @@ -96,17 +90,5 @@ const formDesign = ( help:i18n.str`Full name of the account holder`, hidden: !unknownAccount, }, - { - type: "selectOne", - choices: mi.map((m) => { - return { - value: m.id, - label: m.id, - }; - }), - id: "measure", - label: i18n.str`Successor measure`, - help: i18n.str`Measure taken automatically upon expiration of the current decision.`, - }, ], }); diff --git a/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx b/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx @@ -2,8 +2,11 @@ import { AbsoluteTime, AmountJson, Amounts, + AvailableMeasureSummary, Duration, + ExchangeVersionResponse, KycRule, + LegitimizationRuleSet, LimitOperationType, MeasureInformation, TalerError, @@ -13,13 +16,14 @@ import { FormDesign, FormUI, InternationalizationAPI, + Loading, onComponentUnload, - UIHandlerId, useExchangeApiContext, useForm, useTranslationContext, } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; import { useCurrentDecisionRequest } from "../../hooks/decision-request.js"; import { useAccountActiveDecision } from "../../hooks/decisions.js"; import { useServerMeasures } from "../../hooks/server-info.js"; @@ -38,18 +42,14 @@ export function Rules({ account }: { account: string }): VNode { const { i18n } = useTranslationContext(); const { config } = useExchangeApiContext(); - const [request, updateRequestField, updateRequest] = useCurrentDecisionRequest(); + // const [request, updateRequestField, updateRequest] = + // useCurrentDecisionRequest(); const measures = useServerMeasures(); - - const measureList = + // const [changeRules, setChangeRules] = useState(true); // useState(false); + const rootMeasures = !measures || measures instanceof TalerError || measures.type === "fail" - ? [] - : Object.entries(measures.body.roots).map(([id, mi]) => ({ id, ...mi })); - const design = formDesign(i18n, config.config.currency, measureList); - - const form = useForm<FormType>(design, { - expiration: request.deadline, - }); + ? undefined + : measures.body.roots; const info = !activeDecision || @@ -58,20 +58,91 @@ export function Rules({ account }: { account: string }): VNode { ? undefined : activeDecision.body; - onComponentUnload(() => { - if (!request.rules) { - updateRequestField("rules", []); - } else { - updateRequest({ - ...request, - deadline: - (form.status.result.expiration as AbsoluteTime) ?? AbsoluteTime.never(), - }); + if (!info) { + return <Loading />; + } + + return ( + <div> + <UpdateRulesForm + rootMeasures={rootMeasures} + config={config.config} + limits={info.limits} + /> + + <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 ?? ""} + /> + </div> + </div> + ); +} + +function UpdateRulesForm({ + config, + limits, + rootMeasures, +}: { + config: ExchangeVersionResponse; + limits: LegitimizationRuleSet; + rootMeasures: AvailableMeasureSummary["roots"] | undefined; +}): VNode { + const { i18n } = useTranslationContext(); + const [request, updateRequestField, updateRequest] = + useCurrentDecisionRequest(); + const [showAddRuleForm, setShowAddRuleForm] = useState(false); + const measureList = !rootMeasures + ? [] + : Object.entries(rootMeasures).map(([id, mi]) => ({ id, ...mi })); - } + const ruleFormDesign = ruleFormDesignTemplate( + i18n, + config.currency, + measureList, + ); + + const ruleForm = useForm<RuleFormType>(ruleFormDesign, {}); + + const expirationFormDesign = expirationFormDesignTemplate(i18n, measureList); + + const expirationForm = useForm<ExpirationFormType>(expirationFormDesign, { + expiration: + request.deadline ?? + AbsoluteTime.fromProtocolTimestamp(limits.expiration_time), + measure: request.onExpire_measure ?? limits.successor_measure, }); - function addNewRule(nr: FormType) { + const currentRules = !request.rules ? limits.rules : request.rules; + + onComponentUnload(() => { + const deadline = + expirationForm.status.status === "fail" + ? undefined + : expirationForm.status.result.expiration; + const doesntExpire = !deadline || AbsoluteTime.isNever(deadline); + updateRequest({ + ...request, + rules: currentRules, + deadline, + onExpire_measure: + expirationForm.status.status === "fail" || doesntExpire + ? "" + : expirationForm.status.result.measure, + }); + }); + + function addNewRule(nr: RuleFormType) { const result = !request.rules ? [] : [...request.rules]; const clean = (nr.measures ?? []).filter((m) => !!m); const measures = !clean.length ? DEFAULT_MEASURE_IF_NONE : clean; @@ -88,44 +159,34 @@ export function Rules({ account }: { account: string }): VNode { }); updateRequestField("rules", result); } - return ( <div> <h2 class="mt-4 mb-2"> - <i18n.Translate>Add a new rule</i18n.Translate> + <i18n.Translate>New rules</i18n.Translate> </h2> - <FormUI design={design} model={form.model} /> + <RulesInfo + rules={currentRules} + onRemove={(r, idx) => { + const nr = [...currentRules]; + nr.splice(idx, 1); + updateRequestField("rules", nr); + }} + /> <button - disabled={form.status.status === "fail"} onClick={() => { - addNewRule(form.status.result as FormType); + updateRequestField("rules", limits.rules); }} 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> + <i18n.Translate>Reset rules</i18n.Translate> </button> - - <h2 class="mt-4 mb-2"> - <i18n.Translate>New rules</i18n.Translate> - </h2> - <button onClick={() => { updateRequestField( "rules", - Object.values(LimitOperationType).map((operation_type) => ({ - display_priority: 1, - measures: ["VERBOTEN"], - operation_type, - threshold: Amounts.stringify( - Amounts.zeroOfCurrency(config.config.currency), - ), - timeframe: Duration.toTalerProtocolDuration( - Duration.getForever(), - ), - })), + 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" @@ -136,24 +197,7 @@ export function Rules({ account }: { account: string }): VNode { onClick={() => { updateRequestField( "rules", - Object.values(LimitOperationType).map((operation_type) => ({ - display_priority: 1, - measures: ["VERBOTEN"], - operation_type, - threshold: Amounts.stringify({ - currency: config.config.currency, - fraction: 0, - value: 100, - }), - timeframe: Duration.toTalerProtocolDuration( - operation_type === LimitOperationType.transaction || - operation_type === LimitOperationType.balance - ? Duration.getForever() - : Duration.fromSpec({ - months: 1, - }), - ), - })), + 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" @@ -164,68 +208,92 @@ export function Rules({ account }: { account: string }): VNode { onClick={() => { updateRequestField( "rules", - Object.values(LimitOperationType).map((operation_type) => ({ - display_priority: 1, - measures: ["VERBOTEN"], - operation_type, - threshold: Amounts.stringify({ - currency: config.config.currency, - fraction: 0, - value: 12000, - }), - timeframe: Duration.toTalerProtocolDuration( - operation_type === LimitOperationType.transaction || - operation_type === LimitOperationType.balance - ? Duration.getForever() - : Duration.fromSpec({ - months: 1, - }), - ), - })), + 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" > <i18n.Translate>Premium</i18n.Translate> </button> - <RulesInfo - rules={request.rules ?? []} - onRemove={(r, idx) => { - const nr = !request.rules ? [] : [...request.rules]; - nr.splice(idx, 1); - updateRequestField("rules", nr); + <button + onClick={() => { + setShowAddRuleForm(true); }} - /> - - {!info ? undefined : ( - <div> + 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 new rule</i18n.Translate> + </button> + {!showAddRuleForm ? ( + <Fragment> + <h2 class="mt-4 mb-2"> + <i18n.Translate>On expiration behavior</i18n.Translate> + </h2> + <FormUI design={expirationFormDesign} model={expirationForm.model} /> + <button + onClick={() => { + expirationForm.model + .getHandlerForAttributeKey("measure") + .onChange(limits.successor_measure ?? ""); + }} + 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>Reset measure</i18n.Translate> + </button> + <button + onClick={() => { + const c = + expirationForm.model.getHandlerForAttributeKey("expiration"); + c.onChange( + AbsoluteTime.fromProtocolTimestamp(limits.expiration_time), + ); + }} + 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>Reset expiration</i18n.Translate> + </button> + </Fragment> + ) : ( + <Fragment> <h2 class="mt-4 mb-2"> - <i18n.Translate>Current rules</i18n.Translate> + <i18n.Translate>New rule form</i18n.Translate> </h2> - <ShowDecisionLimitInfo - fixed - since={AbsoluteTime.fromProtocolTimestamp(info.decision_time)} - until={AbsoluteTime.fromProtocolTimestamp( - info.limits.expiration_time, - )} - rules={info.limits.rules} - startOpen - /> - </div> + <FormUI design={ruleFormDesign} model={ruleForm.model} /> + + <button + disabled={ruleForm.status.status === "fail"} + onClick={() => { + addNewRule(ruleForm.status.result as RuleFormType); + }} + 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> + <button + onClick={() => { + setShowAddRuleForm(false); + }} + class="relative disabled:bg-gray-100 disabled:text-gray-500 ml-3 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0" + > + <i18n.Translate>Cancel</i18n.Translate> + </button> + </Fragment> )} </div> ); } -type FormType = { +type RuleFormType = { operation_type: LimitOperationType; threshold: AmountJson; timeframe: Duration; exposed: boolean; - expiration: AbsoluteTime; measures: string[]; all: boolean; }; +type ExpirationFormType = { + expiration: AbsoluteTime; + measure: string | undefined; +}; function labelForOperationType( op: LimitOperationType, @@ -251,7 +319,7 @@ function labelForOperationType( } } -const formDesign = ( +const ruleFormDesignTemplate = ( i18n: InternationalizationAPI, currency: string, mi: (MeasureInformation & { id: string })[], @@ -339,13 +407,261 @@ const formDesign = ( }, ], }, + ], +}); +const expirationFormDesignTemplate = ( + i18n: InternationalizationAPI, + mi: (MeasureInformation & { id: string })[], +): FormDesign<KycRule> => ({ + type: "single-column", + fields: [ + { + type: "choiceHorizontal", + label: i18n.str`Expiration`, + help: i18n.str`Predefined shortcuts`, + id: "expiration", + choices: [ + { + label: i18n.str`In a week`, + value: AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromSpec({ days: 7 }), + ) as any, + }, + { + label: i18n.str`In a month`, + value: AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromSpec({ months: 1 }), + ) as any, + }, + { + label: i18n.str`In a year`, + value: AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromSpec({ years: 1 }), + ) as any, + }, + { + label: i18n.str`Never`, + value: AbsoluteTime.never(), + }, + ], + }, { id: "expiration", type: "absoluteTimeText", placeholder: "dd/MM/yyyy", pattern: "dd/MM/yyyy", - label: i18n.str`Expiration`, + label: i18n.str`Expiration date`, help: i18n.str`For how long this rules will last`, }, + { + type: "selectOne", + choices: mi.map((m) => { + return { + value: m.id, + label: m.id, + }; + }), + id: "measure", + label: i18n.str`Successor measure`, + help: i18n.str`Measure taken automatically upon expiration of the current decision.`, + }, ], }); + +const BASIC_PLAN: (currency: string) => KycRule[] = (currency) => [ + { + display_priority: 1, + measures: ["VERBOTEN"], + operation_type: LimitOperationType.balance, + threshold: Amounts.stringify({ + currency, + fraction: 0, + value: 10000, + }), + timeframe: Duration.toTalerProtocolDuration(Duration.getForever()), + }, + { + display_priority: 1, + measures: ["VERBOTEN"], + operation_type: LimitOperationType.transaction, + threshold: Amounts.stringify({ + currency, + fraction: 0, + value: 1000, + }), + timeframe: Duration.toTalerProtocolDuration(Duration.getForever()), + }, + { + display_priority: 1, + measures: ["VERBOTEN"], + operation_type: LimitOperationType.withdraw, + threshold: Amounts.stringify({ + currency, + fraction: 0, + value: 1000, + }), + timeframe: Duration.toTalerProtocolDuration(Duration.fromSpec({months:1})), + }, + { + display_priority: 1, + measures: ["VERBOTEN"], + operation_type: LimitOperationType.merge, + threshold: Amounts.stringify({ + currency, + fraction: 0, + value: 1000, + }), + timeframe: Duration.toTalerProtocolDuration(Duration.fromSpec({months:1})), + }, + { + display_priority: 1, + measures: ["preserve-investigate"], + operation_type: LimitOperationType.deposit, + threshold: Amounts.stringify({ + currency, + fraction: 0, + value: 5*1000, + }), + timeframe: Duration.toTalerProtocolDuration(Duration.fromSpec({months:1})), + }, + { + display_priority: 1, + measures: ["preserve-investigate"], + operation_type: LimitOperationType.deposit, + threshold: Amounts.stringify({ + currency, + fraction: 0, + value: 50*1000, + }), + timeframe: Duration.toTalerProtocolDuration(Duration.fromSpec({years:1})), + }, + { + display_priority: 1, + measures: ["preserve-investigate"], + operation_type: LimitOperationType.aggregate, + threshold: Amounts.stringify({ + currency, + fraction: 0, + value: 5*1000, + }), + timeframe: Duration.toTalerProtocolDuration(Duration.fromSpec({months:1})), + }, + { + display_priority: 1, + measures: ["preserve-investigate"], + operation_type: LimitOperationType.aggregate, + threshold: Amounts.stringify({ + currency, + fraction: 0, + value: 50*1000, + }), + timeframe: Duration.toTalerProtocolDuration(Duration.fromSpec({years:1})), + }, + +]; + +const PREMIUM_PLAN: (currency: string) => KycRule[] = (currency) => [ + { + display_priority: 1, + measures: ["VERBOTEN"], + operation_type: LimitOperationType.balance, + threshold: Amounts.stringify({ + currency, + fraction: 0, + value: 10*1000, + }), + timeframe: Duration.toTalerProtocolDuration(Duration.getForever()), + }, + { + display_priority: 1, + measures: ["VERBOTEN"], + operation_type: LimitOperationType.transaction, + threshold: Amounts.stringify({ + currency, + fraction: 0, + value: 1000, + }), + timeframe: Duration.toTalerProtocolDuration(Duration.getForever()), + }, + { + display_priority: 1, + measures: ["VERBOTEN"], + operation_type: LimitOperationType.withdraw, + threshold: Amounts.stringify({ + currency, + fraction: 0, + value: 1000, + }), + timeframe: Duration.toTalerProtocolDuration(Duration.fromSpec({months:1})), + }, + { + display_priority: 1, + measures: ["VERBOTEN"], + operation_type: LimitOperationType.merge, + threshold: Amounts.stringify({ + currency, + fraction: 0, + value: 1000, + }), + timeframe: Duration.toTalerProtocolDuration(Duration.fromSpec({months:1})), + }, + { + display_priority: 1, + measures: ["preserve-investigate"], + operation_type: LimitOperationType.deposit, + threshold: Amounts.stringify({ + currency, + fraction: 0, + value: 15*1000, + }), + timeframe: Duration.toTalerProtocolDuration(Duration.fromSpec({months:1})), + }, + { + display_priority: 1, + measures: ["preserve-investigate"], + operation_type: LimitOperationType.deposit, + threshold: Amounts.stringify({ + currency, + fraction: 0, + value: 150*1000, + }), + timeframe: Duration.toTalerProtocolDuration(Duration.fromSpec({years:1})), + }, + { + display_priority: 1, + measures: ["preserve-investigate"], + operation_type: LimitOperationType.aggregate, + threshold: Amounts.stringify({ + currency, + fraction: 0, + value: 15*1000, + }), + timeframe: Duration.toTalerProtocolDuration(Duration.fromSpec({months:1})), + }, + { + display_priority: 1, + measures: ["preserve-investigate"], + operation_type: LimitOperationType.aggregate, + threshold: Amounts.stringify({ + currency, + fraction: 0, + value: 150*1000, + }), + 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 diff --git a/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx b/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx @@ -202,6 +202,7 @@ export function Summary({ until={decision.deadline!} rules={decision.rules!} startOpen + measure={decision.onExpire_measure ?? ""} /> </div> )}