commit 2cbb2d4405890418a612d30619b1eb4a4369a475
parent 9a2cfef15cd3989eaac0691b3a0d3915efa3392d
Author: Sebastian <sebasjm@gmail.com>
Date: Wed, 26 Mar 2025 18:22:35 -0300
calculate events
Diffstat:
9 files changed, 142 insertions(+), 101 deletions(-)
diff --git a/packages/aml-backoffice-ui/src/forms/index.ts b/packages/aml-backoffice-ui/src/forms/index.ts
@@ -29,12 +29,12 @@ import { v1 as simplest } from "./simplest.js";
export const preloadedForms: (
i18n: InternationalizationAPI,
) => Array<FormMetadata> = (i18n) => [
- {
- label: i18n.str`Simple comment`,
- id: "__simple_comment",
- version: 1,
- config: simplest(i18n),
- },
+ // {
+ // label: i18n.str`Simple comment`,
+ // id: "__simple_comment",
+ // version: 1,
+ // config: simplest(i18n, ),
+ // },
form_vqf_902_1_customer(i18n),
{
label: i18n.str`Identification Form (acceptance)`,
diff --git a/packages/aml-backoffice-ui/src/forms/simplest.ts b/packages/aml-backoffice-ui/src/forms/simplest.ts
@@ -29,7 +29,7 @@ export const v1 = (i18n: InternationalizationAPI): DoubleColumnFormDesign => ({
fields: [
{
type: "textArea",
- id: "comment",
+ id: "comment" as UIHandlerId,
label: i18n.str`Comment`,
},
],
@@ -58,7 +58,7 @@ export function resolutionSection(
fields: [
{
type: "choiceHorizontal",
- id: "state",
+ id: "state" as UIHandlerId,
label: i18n.str`New state`,
converterId: "TalerExchangeApi.AmlState",
choices: [
@@ -78,7 +78,7 @@ export function resolutionSection(
},
{
type: "amount",
- id: "threshold",
+ id: "threshold" as UIHandlerId,
currency: "NETZBON",
converterId: "Taler.Amount",
label: i18n.str`New threshold`,
diff --git a/packages/aml-backoffice-ui/src/hooks/decision-request.ts b/packages/aml-backoffice-ui/src/hooks/decision-request.ts
@@ -90,6 +90,10 @@ export interface DecisionRequest {
*/
custom_properties: Record<string, any> | undefined;
/**
+ * Supported events to be triggered
+ */
+ triggering_events: string[] | undefined;
+ /**
* Custom unsupported events to be triggered
*/
custom_events: string[] | undefined;
@@ -113,6 +117,7 @@ export const codecForDecisionRequest = (): Codec<DecisionRequest> =>
.property("custom_properties", codecForAny())
.property("justification", codecOptional(codecForString()))
.property("custom_events", codecOptional(codecForList(codecForString())))
+ .property("triggering_events", codecOptional(codecForList(codecForString())))
.property(
"keep_investigating",
codecOptionalDefault(codecForBoolean(), false),
@@ -131,6 +136,7 @@ const defaultDecisionRequest: DecisionRequest = {
justification: undefined,
keep_investigating: false,
new_measures: undefined,
+ triggering_events: undefined,
properties: undefined,
attributes: undefined,
custom_properties: undefined,
diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
@@ -203,6 +203,7 @@ export function CaseDetails({
onExpire_measures: undefined,
custom_events: undefined,
attributes: undefined,
+ triggering_events: undefined,
justification: undefined,
keep_investigating: false,
new_measures: undefined,
diff --git a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
@@ -71,7 +71,7 @@ export function UnlockAccount(): VNode {
);
const forgetHandler =
- status.status === "fail" || officer.state !== "locked"
+ officer.state === "not-found"
? undefined
: withErrorHandler(
async () => officer.forget(),
diff --git a/packages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx b/packages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx
@@ -44,9 +44,9 @@ export type WizardSteps =
const STEPS_ORDER: WizardSteps[] = [
"attributes",
+ "rules",
"properties",
"events",
- "rules",
"measures",
"justification",
"summary",
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Events.tsx b/packages/aml-backoffice-ui/src/pages/decision/Events.tsx
@@ -1,9 +1,12 @@
import {
+ AbsoluteTime,
AML_EVENTS_INFO,
AmlDecision,
AmlSpaDialect,
assertUnreachable,
GLS_AmlEventsName,
+ HttpStatusCode,
+ LegitimizationRuleSet,
MeasureInformation,
TalerError,
TOPS_AmlEventsName,
@@ -12,7 +15,9 @@ import {
FormDesign,
FormUI,
InternationalizationAPI,
+ Loading,
onComponentUnload,
+ SelectUiChoice,
UIFormElementConfig,
UIHandlerId,
useExchangeApiContext,
@@ -26,9 +31,11 @@ import {
} from "../../hooks/decision-request.js";
import { usePreferences } from "../../hooks/preferences.js";
import { useAccountActiveDecision } from "../../hooks/decisions.js";
+import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
/**
* Trigger additional events
+ *
* @param param0
* @returns
*/
@@ -43,91 +50,129 @@ export function Events({ account }: { account: string }): VNode {
(pref.testingDialect ? undefined : config.config.aml_spa_dialect) ??
AmlSpaDialect.TESTING;
- const lastDecision =
- !activeDecision ||
- activeDecision instanceof TalerError ||
- activeDecision.type === "fail"
- ? undefined
- : activeDecision.body;
-
- const calculatedEvents = calculateEventsBasedOnState(
- lastDecision,
- request,
- i18n,
- dialect,
- );
+ if (!activeDecision) {
+ return <Loading />;
+ }
+ if (activeDecision instanceof TalerError) {
+ return <ErrorLoadingWithDebug error={activeDecision} />;
+ }
+ if (activeDecision.type === "fail") {
+ switch (activeDecision.case) {
+ case HttpStatusCode.Forbidden:
+ case HttpStatusCode.NotFound:
+ case HttpStatusCode.Conflict:
+ return <div>couldn't load the last active decision</div>;
+ default:
+ assertUnreachable(activeDecision);
+ }
+ }
- const design = formDesign(i18n, calculatedEvents.on);
+ function ShowEventForm({ events: calculatedEvents }: { events: Events }) {
+ const design = formDesign(i18n, calculatedEvents);
- const form = useForm<FormType>(design, {
- inhibit: calculatedEvents.on.reduce(
- (prev, cur) => {
- if (cur.type !== "toggle") return prev;
- // const isInhibit =
- // request.inhibit_events !== undefined &&
- // request.inhibit_events.indexOf(cur.id) !== -1;
- prev[cur.id] = !false; // FIXME
- return prev;
- },
- {} as FormType["inhibit"],
- ),
- trigger: !request.custom_events
- ? []
- : request.custom_events.map((name) => ({ name })),
- });
+ const calculated =
+ request.triggering_events === undefined
+ ? calculatedEvents.triggered
+ : request.triggering_events.filter((name) =>
+ calculatedEvents.triggered.includes(name),
+ );
- onComponentUnload(() => {
- updateRequest({
- ...request,
- custom_events: !form.status.result.trigger
+ const rest =
+ request.triggering_events === undefined
? []
- : form.status.result.trigger.map((t) => t?.name!),
- // inhibit_events: Object.entries(form.status.result.inhibit ?? {})
- // .filter(([key, inhibit]) => !inhibit)
- // .map(([key]) => key),
+ : request.triggering_events.filter((name) =>
+ calculatedEvents.rest.includes(name),
+ );
+
+ const form = useForm<FormType>(design, {
+ calculated,
+ rest,
+ });
+
+ onComponentUnload(() => {
+ updateRequest({
+ ...request,
+ custom_events: !form.status.result.custom
+ ? []
+ : form.status.result.custom,
+ triggering_events: [
+ ...(form.status.result.calculated ?? []),
+ ...(form.status.result.rest ?? []),
+ ],
+ });
});
- });
- return (
- <div>
- <FormUI design={design} model={form.model} />
- </div>
+ return (
+ <div>
+ <FormUI design={design} handler={form.handler} />
+ </div>
+ );
+ }
+
+ const events = calculateEventsBasedOnState(
+ activeDecision.body,
+ request,
+ i18n,
+ dialect,
);
+ return <ShowEventForm events={events} />;
}
+type Events = {
+ triggered: string[];
+ rest: string[];
+};
+
type FormType = {
- trigger: { name: string }[];
- inhibit: { [name: string]: boolean };
+ calculated: string[];
+ rest: string[];
+ custom: string[];
};
const formDesign = (
i18n: InternationalizationAPI,
- inhibitEvents: UIFormElementConfig[],
+ events: {
+ triggered: string[];
+ rest: string[];
+ },
): FormDesign<MeasureInformation> => ({
type: "double-column",
sections: [
{
- title: i18n.str`Inhibit default events`,
- description: i18n.str`Here you can prevent events to be triggered by the current status.`,
- fields: !inhibitEvents.length
- ? [
- {
- type: "caption",
- label: i18n.str`No default events calculated.`,
- },
- ]
- : inhibitEvents.map((f) =>
- "id" in f ? { ...f, id: ("inhibit." + f.id) } : f,
- ),
- },
- {
- title: i18n.str`Custom event`,
- description: i18n.str`Add more events to be triggered by this request.`,
+ title: i18n.str`Events`,
+ description: i18n.str`This events will count when this decision is confirmed.`,
fields: [
{
- id: "trigger",
+ type: "selectMultiple",
+ id: "calculated",
+ label: i18n.str`Calculated`,
+ help: i18n.str`Events based on properties and new attributes that should be triggered`,
+ choices: events.triggered.map(
+ (name) =>
+ ({
+ label: name,
+ value: name,
+ }) as SelectUiChoice,
+ ),
+ },
+ {
+ type: "selectMultiple",
+ id: "rest",
+ label: i18n.str`Others`,
+ help: i18n.str`Events that can be triggered manually`,
+ choices: events.rest.map(
+ (name) =>
+ ({
+ label: name,
+ value: name,
+ }) as SelectUiChoice,
+ ),
+ },
+ {
+ id: "custom",
type: "array",
- label: i18n.str`Event list`,
+ label: i18n.str`Custom list`,
+ help: i18n.str`Events that is not yet supported`,
labelFieldId: "name",
fields: [
{
@@ -233,31 +278,21 @@ function calculateEventsBasedOnState(
request: DecisionRequest,
i18n: InternationalizationAPI,
dialect: AmlSpaDialect,
-) {
- const init: {
- on: UIFormElementConfig[];
- off: UIFormElementConfig[];
- } = { on: [], off: [] };
+): Events {
+ const init: Events = { triggered: [], rest: [] };
return Object.entries(AML_EVENTS_INFO).reduce((prev, [name, info]) => {
- const field = {
- id: name,
- type: "toggle",
- required: true,
- label: labelForEvent(name, i18n, dialect),
- } satisfies UIFormElementConfig;
-
if (
- currentState &&
info.shouldBeTriggered(
- currentState.limits,
- currentState.properties ?? {},
- request.properties ?? {},
+ currentState?.limits?.rules,
+ request.rules,
+ currentState?.properties,
+ request.properties,
(request.attributes ?? {}) as Record<string, unknown>,
)
) {
- prev.on.push(field);
+ prev.triggered.push(name);
} else {
- prev.off.push(field);
+ prev.rest.push(name);
}
return prev;
}, init);
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx b/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx
@@ -167,31 +167,26 @@ function propertiesByDialect(
id: TalerFormAttributes.AML_ACCOUNT_OPEN,
label: i18n.str`Is account active for deposit and payments?`,
type: "toggle",
- threeState: true,
},
{
id: TalerFormAttributes.AML_DOMESTIC_PEP,
label: i18n.str`Does account belong to a domestic PEP?`,
type: "toggle",
- threeState: true,
},
{
id: TalerFormAttributes.AML_FOREIGN_PEP,
label: i18n.str`Does account belong to a foreign PEP?`,
type: "toggle",
- threeState: true,
},
{
id: TalerFormAttributes.AML_HIGH_RISK_BUSINESS,
label: i18n.str`Does account belong to a high risk business?`,
type: "toggle",
- threeState: true,
},
{
id: TalerFormAttributes.AML_HIGH_RISK_COUNTRY,
label: i18n.str`Does account belong to a person from a high risk country?`,
type: "toggle",
- threeState: true,
},
// {
// id: TalerFormAttributes.AML_INVESTIGATION_ART6_COMPLETED,
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx b/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx
@@ -2,6 +2,7 @@ import {
AbsoluteTime,
AmlDecisionRequest,
assertUnreachable,
+ Duration,
HttpStatusCode,
TalerError,
} from "@gnu-taler/taler-util";
@@ -67,7 +68,8 @@ export function Summary({
decision.justification === undefined || !decision.justification;
const INVALID_ACCOUNT = !account;
const INVALID_ATTRIBUTES =
- decision.attributes !== undefined && decision.attributes.errors !== undefined;
+ decision.attributes !== undefined &&
+ decision.attributes.errors !== undefined;
const CANT_SUBMIT =
INVALID_ACCOUNT ||
@@ -83,7 +85,7 @@ export function Summary({
custom_events: undefined,
custom_properties: undefined,
deadline: undefined,
- // inhibit_events: undefined,
+ triggering_events: undefined,
attributes: undefined,
justification: undefined,
keep_investigating: false,
@@ -120,6 +122,7 @@ export function Summary({
decision.attributes.expiration,
)
: undefined,
+ events: decision.triggering_events,
attributes: decision.attributes?.data,
properties: decision.properties!, // TODO: compute properites
new_measures: decision.new_measures!.join(" "),
@@ -258,7 +261,8 @@ export function Summary({
onClose={() => onMove("attributes")}
>
<i18n.Translate>
- You should check form errors or submit a decision without attributes.
+ You should check form errors or submit a decision without
+ attributes.
</i18n.Translate>
</Attention>
) : (