commit 977787d6a827174f86976c978c0c4c60907f79df
parent 8f329518e9ecd8ef14a85a6619b51b2e29aa160f
Author: Sebastian <sebasjm@gmail.com>
Date: Sun, 26 Jan 2025 08:14:39 -0300
fix measure format, shortcut buttons
Diffstat:
6 files changed, 389 insertions(+), 287 deletions(-)
diff --git a/packages/aml-backoffice-ui/src/hooks/decision-request.ts b/packages/aml-backoffice-ui/src/hooks/decision-request.ts
@@ -35,9 +35,9 @@ import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
export interface DecisionRequest {
rules: KycRule[] | undefined;
- new_measures: string | undefined;
+ new_measures: string[] | undefined;
deadline: AbsoluteTime | undefined;
- onExpire_measures: string | undefined;
+ onExpire_measures: string[] | undefined;
properties: Record<string, any> | undefined;
custom_properties: Record<string, any> | undefined;
custom_events: string[] | undefined;
@@ -59,8 +59,11 @@ export const codecForDecisionRequest = (): Codec<DecisionRequest> =>
"keep_investigating",
codecOptionalDefault(codecForBoolean(), false),
)
- .property("new_measures", codecOptional(codecForString()))
- .property("onExpire_measures", codecOptional(codecForString()))
+ .property("new_measures", codecOptional(codecForList(codecForString())))
+ .property(
+ "onExpire_measures",
+ codecOptional(codecForList(codecForString())),
+ )
.build("DecisionRequest");
const defaultDecisionRequest: DecisionRequest = {
diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
@@ -52,7 +52,7 @@ import {
useLocalNotificationHandler,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
-import { format, formatDuration, intervalToDuration } from "date-fns";
+import { format } from "date-fns";
import { Fragment, h, Ref, VNode } from "preact";
import { useState } from "preact/hooks";
import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
@@ -66,6 +66,7 @@ import { CurrentMeasureTable, MeasureInfo } from "./MeasuresTable.js";
import { Officer } from "./Officer.js";
import { ShowConsolidated } from "./ShowConsolidated.js";
import { useServerMeasures } from "../hooks/server-info.js";
+import { RulesInfo } from "./RulesInfo.js";
export type AmlEvent =
| AmlFormEvent
@@ -797,169 +798,6 @@ function ShowMesaureInfo({
);
}
-export function RulesInfo({
- rules,
- onEdit,
- onRemove,
-}: {
- rules: KycRule[];
- onEdit?: (k: KycRule, idx: number) => void;
- onRemove?: (k: KycRule, idx: number) => void;
-}): VNode {
- const { i18n } = useTranslationContext();
- const { config } = useExchangeApiContext();
-
- if (!rules.length) {
- return (
- <Attention
- title={i18n.str`There are no rules for operations`}
- type="warning"
- />
- );
- }
-
- const balanceLimitIdx = rules.findIndex(
- (r) => r.operation_type === "BALANCE",
- );
- const balanceLimit = rules[balanceLimitIdx];
-
- const hasActions = !!onEdit || !!onRemove;
-
- return (
- <Fragment>
- <div class="">
- <div class="flex mt-2 rounded-md w-fit shadow-sm border-0 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600">
- <div class="whitespace-nowrap pointer-events-none bg-gray-200 inset-y-0 items-center px-3 flex">
- <i18n.Translate>Max balance</i18n.Translate>
- </div>
- <div class="p-2 disabled:bg-gray-200 text-right rounded-md rounded-l-none data-[left=true]:text-left py-1.5 pl-3 text-gray-900 placeholder:text-gray-400 sm:text-sm sm:leading-6">
- {!balanceLimit ? (
- <i18n.Translate>Unlimited</i18n.Translate>
- ) : (
- <RenderAmount
- value={Amounts.parseOrThrow(balanceLimit.threshold)}
- spec={config.config.currency_specification}
- />
- )}
- </div>
- </div>
- </div>
- <div class="">
- <table class="min-w-full divide-y divide-gray-300">
- <thead class="bg-gray-50">
- <tr>
- <th
- scope="col"
- class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6"
- >
- <i18n.Translate>Operation</i18n.Translate>
- </th>
- <th
- scope="col"
- class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
- >
- <i18n.Translate>Timeframe</i18n.Translate>
- </th>
- <th
- scope="col"
- class="relative py-3.5 pl-3 pr-4 sm:pr-6 text-right"
- >
- <i18n.Translate>Amount</i18n.Translate>
- </th>
- <th
- scope="col"
- class="relative py-3.5 pl-3 pr-4 sm:pr-6 text-right"
- >
- <i18n.Translate>Measures</i18n.Translate>
- </th>
- {!hasActions ? undefined : (
- <th
- scope="col"
- class="relative py-3.5 pl-3 pr-4 sm:pr-6 text-right"
- >
- <i18n.Translate>Actions</i18n.Translate>
- </th>
- )}
- </tr>
- </thead>
- <tbody class="divide-y divide-gray-200">
- {rules.map((r, idx) => {
- if (r.operation_type === "BALANCE") return;
- return (
- <tr>
- <td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6 text-left">
- {r.operation_type}
- </td>
- <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
- {r.timeframe.d_us === "forever" ? (
- <i18n.Translate>Forever</i18n.Translate>
- ) : (
- formatDuration(
- intervalToDuration({
- start: 0,
- end: r.timeframe.d_us / 1000,
- }),
- )
- )}
- </td>
- <td class=" relative whitespace-nowrap py-4 pl-3 pr-4 text-sm font-medium sm:pr-6 text-right">
- <RenderAmount
- value={Amounts.parseOrThrow(r.threshold)}
- spec={config.config.currency_specification}
- />
- </td>
- <td class=" relative whitespace-nowrap py-4 pl-3 pr-4 text-sm font-medium sm:pr-6 text-right">
- {r.measures}
- </td>
- {!hasActions ? undefined : (
- <td class="relative flex justify-end whitespace-nowrap py-4 pl-3 pr-4 text-sm font-medium sm:pr-6">
- {!onEdit ? undefined : (
- <button onClick={() => onEdit(r, idx)}>
- <svg
- xmlns="http://www.w3.org/2000/svg"
- fill="none"
- viewBox="0 0 24 24"
- stroke-width="1.5"
- stroke="currentColor"
- class="size-6 text-green-700"
- >
- <path
- stroke-linecap="round"
- stroke-linejoin="round"
- d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"
- />
- </svg>
- </button>
- )}
- {!onRemove ? undefined : (
- <button onClick={() => onRemove(r, idx)}>
- <svg
- xmlns="http://www.w3.org/2000/svg"
- fill="none"
- viewBox="0 0 24 24"
- stroke-width="1.5"
- stroke="currentColor"
- class="size-6 text-red-700"
- >
- <path
- stroke-linecap="round"
- stroke-linejoin="round"
- d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
- />
- </svg>
- </button>
- )}
- </td>
- )}
- </tr>
- );
- })}
- </tbody>
- </table>
- </div>
- </Fragment>
- );
-}
export function ShowDecisionLimitInfo({
rules,
since,
@@ -1094,58 +932,27 @@ export function ShowDecisionLimitInfo({
);
}
-export function RenderAmount({
- value,
- spec,
- negative,
- withColor,
- hideSmall,
-}: {
- spec: CurrencySpecification;
- value: AmountJson;
- hideSmall?: boolean;
- negative?: boolean;
- withColor?: boolean;
-}): VNode {
- const neg = !!negative; // convert to true or false
-
- const { currency, normal, small } = Amounts.stringifyValueWithSpec(
- value,
- spec,
- );
-
- return (
- <span
- data-negative={withColor ? neg : undefined}
- class="whitespace-nowrap data-[negative=false]:text-green-600 data-[negative=true]:text-red-600"
- >
- {negative ? "- " : undefined}
- {currency} {normal}{" "}
- {!hideSmall && small && <sup class="-ml-1">{small}</sup>}
- </span>
- );
-}
-
function AmlStateBadge({ state }: { state: TalerExchangeApi.AmlState }): VNode {
+ const { i18n } = useTranslationContext();
switch (state) {
case TalerExchangeApi.AmlState.normal: {
return (
<span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20">
- Normal
+ <i18n.Translate>Normal</i18n.Translate>
</span>
);
}
case TalerExchangeApi.AmlState.pending: {
return (
<span class="inline-flex items-center rounded-md bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-700 ring-1 ring-inset ring-green-600/20">
- Pending
+ <i18n.Translate>Pending</i18n.Translate>
</span>
);
}
case TalerExchangeApi.AmlState.frozen: {
return (
<span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-green-600/20">
- Frozen
+ <i18n.Translate>Frozen</i18n.Translate>
</span>
);
}
diff --git a/packages/aml-backoffice-ui/src/pages/RulesInfo.tsx b/packages/aml-backoffice-ui/src/pages/RulesInfo.tsx
@@ -0,0 +1,236 @@
+import {
+ amountFractionalBase,
+ AmountJson,
+ Amounts,
+ assertUnreachable,
+ CurrencySpecification,
+ KycRule,
+ LimitOperationType,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ useExchangeApiContext,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { formatDuration, intervalToDuration } from "date-fns";
+import { Fragment, h, VNode } from "preact";
+
+export function RulesInfo({
+ rules,
+ onEdit,
+ onRemove,
+}: {
+ rules: KycRule[];
+ onEdit?: (k: KycRule, idx: number) => void;
+ onRemove?: (k: KycRule, idx: number) => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const { config } = useExchangeApiContext();
+
+ if (!rules.length) {
+ return (
+ <Attention
+ title={i18n.str`There are no rules for operations`}
+ type="warning"
+ />
+ );
+ }
+
+ const sorted = [...rules].sort(sortKycRules);
+
+ const hasActions = !!onEdit || !!onRemove;
+
+ return (
+ <Fragment>
+ <div class="">
+ <table class="min-w-full divide-y divide-gray-300">
+ <thead class="bg-gray-50">
+ <tr>
+ <th
+ scope="col"
+ class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6"
+ >
+ <i18n.Translate>Operation</i18n.Translate>
+ </th>
+ <th
+ scope="col"
+ class="relative py-3.5 pl-3 pr-4 sm:pr-6 text-right"
+ >
+ <i18n.Translate>Threshold</i18n.Translate>
+ </th>
+ <th
+ scope="col"
+ class="relative py-3.5 pl-3 pr-4 sm:pr-6 text-right"
+ >
+ <i18n.Translate>Escalation</i18n.Translate>
+ </th>
+ {!hasActions ? undefined : (
+ <th
+ scope="col"
+ class="relative py-3.5 pl-3 pr-4 sm:pr-6 text-right"
+ >
+ <i18n.Translate>Actions</i18n.Translate>
+ </th>
+ )}
+ </tr>
+ </thead>
+ <tbody class="divide-y divide-gray-200">
+ {sorted.map((r, idx) => {
+ return (
+ <tr>
+ <td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6 text-left">
+ {r.operation_type}
+ </td>
+ <td class=" relative whitespace-nowrap py-4 pl-3 pr-4 text-sm font-medium sm:pr-6 text-right">
+ {r.timeframe.d_us === "forever" ? (
+ <RenderAmount
+ value={Amounts.parseOrThrow(r.threshold)}
+ spec={config.config.currency_specification}
+ />
+ ) : (
+ <i18n.Translate context="threshold">
+ <RenderAmount
+ value={Amounts.parseOrThrow(r.threshold)}
+ spec={config.config.currency_specification}
+ />
+ every{" "}
+ {formatDuration(
+ intervalToDuration({
+ start: 0,
+ end: r.timeframe.d_us / 1000,
+ }),
+ )}
+ </i18n.Translate>
+ )}
+ </td>
+ <td class=" relative whitespace-nowrap py-4 pl-3 pr-4 text-sm font-medium sm:pr-6 text-right">
+ {r.measures}
+ </td>
+ {!hasActions ? undefined : (
+ <td class="relative flex justify-end whitespace-nowrap py-4 pl-3 pr-4 text-sm font-medium sm:pr-6">
+ {!onEdit ? undefined : (
+ <button onClick={() => onEdit(r, idx)}>
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="size-6 text-green-700"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"
+ />
+ </svg>
+ </button>
+ )}
+ {!onRemove ? undefined : (
+ <button onClick={() => onRemove(r, idx)}>
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="size-6 text-red-700"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
+ />
+ </svg>
+ </button>
+ )}
+ </td>
+ )}
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ </Fragment>
+ );
+}
+
+function RenderAmount({
+ value,
+ spec,
+ negative,
+ withColor,
+ hideSmall,
+}: {
+ spec: CurrencySpecification;
+ value: AmountJson;
+ hideSmall?: boolean;
+ negative?: boolean;
+ withColor?: boolean;
+}): VNode {
+ const neg = !!negative; // convert to true or false
+
+ const { currency, normal, small } = Amounts.stringifyValueWithSpec(
+ value,
+ spec,
+ );
+
+ return (
+ <span
+ data-negative={withColor ? neg : undefined}
+ class="whitespace-nowrap data-[negative=false]:text-green-600 data-[negative=true]:text-red-600"
+ >
+ {negative ? "- " : undefined}
+ {currency} {normal}{" "}
+ {!hideSmall && small && <sup class="-ml-1">{small}</sup>}
+ </span>
+ );
+}
+
+export function rate(a: AmountJson, b: number): number {
+ const af = toFloat(a);
+ const bf = b;
+ if (bf === 0) return 0;
+ return af / bf;
+}
+
+function toFloat(amount: AmountJson): number {
+ return amount.value + amount.fraction / amountFractionalBase;
+}
+
+const OPERATION_TYPE_ORDER = {
+ [LimitOperationType.balance]: 1,
+ [LimitOperationType.transaction]: 2,
+ [LimitOperationType.withdraw]: 3,
+ [LimitOperationType.deposit]: 4,
+ [LimitOperationType.aggregate]: 5,
+ [LimitOperationType.close]: 6,
+ [LimitOperationType.refund]: 7,
+ [LimitOperationType.merge]: 8,
+} as const;
+
+/**
+ * Operation follows OPERATION_TYPE_ORDER.
+ * Then operations with timeframe "forever" means they are not reset, like balance. Go first.
+ * Then operations with high throughput first.
+ * @param a
+ * @param b
+ * @returns
+ */
+function sortKycRules(a: KycRule, b: KycRule): number {
+ const op =
+ OPERATION_TYPE_ORDER[a.operation_type] -
+ OPERATION_TYPE_ORDER[b.operation_type];
+ if (op !== 0) return op;
+ const at = a.timeframe;
+ const bt = b.timeframe;
+ if (at.d_us === "forever" || bt.d_us === "forever") {
+ if (at.d_us === "forever") return -1;
+ if (bt.d_us === "forever") return 1;
+ return Amounts.cmp(a.threshold, b.threshold);
+ }
+ const as = rate(Amounts.parseOrThrow(a.threshold), at.d_us);
+ const bs = rate(Amounts.parseOrThrow(a.threshold), bt.d_us);
+ return bs - as;
+}
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Justification.tsx b/packages/aml-backoffice-ui/src/pages/decision/Justification.tsx
@@ -1,4 +1,10 @@
import {
+ AbsoluteTime,
+ Duration,
+ MeasureInformation,
+ TalerError,
+} from "@gnu-taler/taler-util";
+import {
FormDesign,
FormUI,
InternationalizationAPI,
@@ -9,18 +15,6 @@ import {
} from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
import { useCurrentDecisionRequest } from "../../hooks/decision-request.js";
-import {
- AbsoluteTime,
- Duration,
- MeasureInformation,
- TalerError,
-} from "@gnu-taler/taler-util";
-import {
- deserializeMeasures,
- measureArrayField,
- MeasurePath,
- serializeMeasures,
-} from "./Measures.js";
import { useServerMeasures } from "../../hooks/server-info.js";
/**
@@ -38,9 +32,9 @@ export function Justification({}: {}): VNode {
: Object.entries(measures.body.roots).map(([id, mi]) => ({ id, ...mi }));
const design = formDesign(i18n, measureList);
- const expMeasres: MeasurePath[] = !request.onExpire_measures
+ const expMeasres: string[] = !request.onExpire_measures
? []
- : deserializeMeasures(request.onExpire_measures);
+ : request.onExpire_measures;
const form = useForm<FormType>(design, {
investigate: request.keep_investigating,
@@ -54,9 +48,7 @@ export function Justification({}: {}): VNode {
...request,
keep_investigating: !!form.status.result.investigate,
justification: form.status.result.justification ?? "",
- onExpire_measures: serializeMeasures(
- (form.status.result.measures ?? []) as MeasurePath[],
- ),
+ onExpire_measures: (form.status.result.measures ?? []) as string[],
deadline:
(form.status.result.expiration as AbsoluteTime) ?? AbsoluteTime.never(),
@@ -75,7 +67,7 @@ type FormType = {
justification: string;
investigate: boolean;
expiration: AbsoluteTime;
- measures: MeasurePath[];
+ measures: string[];
};
const formDesign = (
@@ -122,6 +114,17 @@ const formDesign = (
pattern: "dd/MM/yyyy",
label: i18n.str`Expiration`,
},
- measureArrayField(i18n, mi),
+ {
+ type: "selectMultiple",
+ choices: mi.map((m) => {
+ return {
+ value: m.id,
+ label: m.id,
+ };
+ }),
+ id: "measures" as UIHandlerId,
+ label: i18n.str`Expiration measure`,
+ help: i18n.str`Measures that the customer will need to satisfy after expiration.`,
+ },
],
});
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx b/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx
@@ -15,24 +15,6 @@ import { useCurrentDecisionRequest } from "../../hooks/decision-request.js";
import { ShowMeasuresToSelect } from "../CaseDetails.js";
import { useServerMeasures } from "../../hooks/server-info.js";
-export function serializeMeasures(
- paths?: RecursivePartial<MeasurePath[]>,
-): string {
- if (!paths) return "";
- return paths
- .map((p) => {
- if (!p?.steps) return "";
- return p.steps.join("+");
- })
- .join(" ");
-}
-export function deserializeMeasures(
- measures: string | undefined,
-): MeasurePath[] {
- if (!measures) return [];
- return measures.split(" ").map((path) => ({ steps: path.split("+") }));
-}
-
/**
* Ask for more information, define new paths to proceed
* @param param0
@@ -48,24 +30,16 @@ export function Measures({}: {}): VNode {
: Object.entries(measures.body.roots).map(([id, mi]) => ({ id, ...mi }));
const initValue: FormType = !request.new_measures
- ? { paths: [] }
- : { paths: deserializeMeasures(request.new_measures) };
+ ? { measures: [] }
+ : { measures: request.new_measures };
const design = formDesign(i18n, measureList);
const form = useForm<FormType>(design, initValue);
onComponentUnload(() => {
- const r = !form.status.result.paths
- ? []
- : (form.status.result.paths.map(
- (path) => path?.steps ?? [],
- ) as string[][]);
-
updateRequest({
...request,
- new_measures: serializeMeasures(
- (form.status.result.paths ?? []) as MeasurePath[],
- ),
+ new_measures: (form.status.result.measures ?? []) as string[],
});
});
@@ -77,22 +51,16 @@ export function Measures({}: {}): VNode {
);
}
-export type MeasurePath = { steps: string[] };
-
type FormType = {
- paths: MeasurePath[];
+ measures: string[];
};
-export function measureArrayField(
+function formDesign(
i18n: InternationalizationAPI,
mi: (MeasureInformation & { id: string })[],
-): UIFormElementConfig {
+): FormDesign<FormType> {
return {
- type: "array",
- id: "paths" as UIHandlerId,
- label: i18n.str`Measures`,
- help: i18n.str`For every entry the customer will have a different path to satify checks.`,
- labelFieldId: "steps" as UIHandlerId,
+ type: "single-column",
fields: [
{
type: "selectMultiple",
@@ -102,20 +70,10 @@ export function measureArrayField(
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.`,
+ id: "measures" as UIHandlerId,
+ label: i18n.str`Active measures`,
+ help: i18n.str`Measures that the customer will need to satisfy while the current rules are active.`,
},
],
};
}
-
-function formDesign(
- i18n: InternationalizationAPI,
- mi: (MeasureInformation & { id: string })[],
-): FormDesign<FormType> {
- return {
- type: "single-column",
- fields: [measureArrayField(i18n, mi)],
- };
-}
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx b/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx
@@ -22,9 +22,9 @@ import {
import { h, VNode } from "preact";
import { useCurrentDecisionRequest } from "../../hooks/decision-request.js";
import { useAccountActiveDecision } from "../../hooks/decisions.js";
-import { RulesInfo, ShowDecisionLimitInfo } from "../CaseDetails.js";
-import { measureArrayField, serializeMeasures } from "./Measures.js";
import { useServerMeasures } from "../../hooks/server-info.js";
+import { ShowDecisionLimitInfo } from "../CaseDetails.js";
+import { RulesInfo } from "../RulesInfo.js";
/**
* Defined new limits for the account
@@ -62,12 +62,16 @@ export function Rules({ account }: { account?: string }): VNode {
function addNewRule(nr: FormType) {
const result = !request.rules ? [] : [...request.rules];
+ const clean = (nr.measures ?? []).filter((m) => !m);
+ const measures = !clean.length ? ["VERBOTEN"] : clean;
result.push({
timeframe: Duration.toTalerProtocolDuration(nr.timeframe),
threshold: Amounts.stringify(nr.threshold),
operation_type: nr.operation_type,
display_priority: 1,
- measures: [serializeMeasures(nr.paths)], // FIXME: change how server expect new measures
+ exposed: nr.exposed,
+ is_and_combinator: nr.all,
+ measures,
});
updateRequest("rules", result);
}
@@ -83,7 +87,6 @@ export function Rules({ account }: { account?: string }): VNode {
<button
disabled={form.status.status === "fail"}
onClick={() => {
- console.log(form);
addNewRule(form.status.result as FormType);
}}
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"
@@ -95,13 +98,85 @@ export function Rules({ account }: { account?: string }): VNode {
<i18n.Translate>New rules</i18n.Translate>
</h2>
+ <button
+ onClick={() => {
+ updateRequest(
+ "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(),
+ ),
+ })),
+ );
+ }}
+ 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>Freeze account</i18n.Translate>
+ </button>
+ <button
+ onClick={() => {
+ updateRequest(
+ "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,
+ }),
+ ),
+ })),
+ );
+ }}
+ 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>Basic plan</i18n.Translate>
+ </button>
+ <button
+ onClick={() => {
+ updateRequest(
+ "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,
+ }),
+ ),
+ })),
+ );
+ }}
+ 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 ?? []}
- // onEdit={(r, idx) => {
- // const nr = !request.rules ? [] : [...request.rules];
- // nr.splice(idx, 1);
- // updateRequest("rules", nr);
- // }}
onRemove={(r, idx) => {
const nr = !request.rules ? [] : [...request.rules];
nr.splice(idx, 1);
@@ -134,7 +209,8 @@ type FormType = {
threshold: AmountJson;
timeframe: Duration;
exposed: boolean;
- paths: { steps: Array<string> }[];
+ measures: string[];
+ all: boolean;
};
function labelForOperationType(
@@ -189,16 +265,35 @@ const formDesign = (
},
{
id: "timeframe" as UIHandlerId,
- type: "duration",
+ type: "durationText",
required: true,
+ placeholder: "1Y 2M 3D 4h 5m 6s",
label: i18n.str`Timeframe`,
+ help: `Use YMDhms next to a number as a unit for Year, Month, Day, hour, minute and seconds.`,
},
{
id: "exposed" as UIHandlerId,
type: "toggle",
label: i18n.str`Exposed`,
- help: i18n.str`Is the customer aware of this limit?`,
+ help: i18n.str`Is this limit comunicated to the customer?`,
+ },
+ {
+ type: "selectMultiple",
+ choices: mi.map((m) => {
+ return {
+ value: m.id,
+ label: m.id,
+ };
+ }),
+ id: "measures" as UIHandlerId,
+ label: i18n.str`Esclation measure`,
+ help: i18n.str`Measures that the customer will need to satisfy to apply for a new threshold.`,
+ },
+ {
+ id: "all" as UIHandlerId,
+ type: "toggle",
+ label: i18n.str`All measures`,
+ help: i18n.str`Hint the customer that all measure should be completed`,
},
- measureArrayField(i18n, mi),
],
});