commit cbca95c72f78bcf9da71c67aa68285a8b339833e
parent 5b07bb1a5ed3ccb4fbbf63a91e43e63757215b1e
Author: Sebastian <sebasjm@gmail.com>
Date: Wed, 16 Apr 2025 11:09:05 -0300
fix #9739
Diffstat:
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>
)}