commit 4f719b2da928f910b3ad6d94ce521762e90586ad
parent a4dccd0ee51a09e4217401ce80ea4f82d37064b1
Author: Sebastian <sebasjm@gmail.com>
Date: Sun, 27 Apr 2025 04:17:06 -0300
fixing custom measure
Diffstat:
9 files changed, 373 insertions(+), 384 deletions(-)
diff --git a/packages/aml-backoffice-ui/src/hooks/custom-measures.ts b/packages/aml-backoffice-ui/src/hooks/custom-measures.ts
@@ -1,94 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022-2024 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import {
- Codec,
- buildCodecForObject,
- codecForAny,
- codecForConstString,
- codecForEither,
- codecForList,
- codecForString,
- codecOptionalDefault,
-} from "@gnu-taler/taler-util";
-import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
-
-export interface CustomMeasures {
- measures: CustomMeasure[];
-}
-
-type CustomMeasure = {
- type: "procedure" | "form";
- program: string;
- name: string;
- check_name: string;
- context: Object;
-};
-
-export const codecForCustomMeasures = (): Codec<CustomMeasures> =>
- buildCodecForObject<CustomMeasures>()
- .property(
- "measures",
- codecOptionalDefault(codecForList(codecForCustomMeasure()), []),
- )
- .build("CustomMeasures");
-export const codecForCustomMeasure = (): Codec<CustomMeasure> =>
- buildCodecForObject<CustomMeasure>()
- .property("check_name", codecForString())
- .property("program", codecForString())
- .property("name", codecForString())
- .property("context", codecForAny())
- .property(
- "type",
- codecForEither(
- codecForConstString("procedure"),
- codecForConstString("form"),
- ),
- )
- .build("CustomMeasure");
-
-const defaultCustomMeasures: CustomMeasures = {
- measures: [],
-};
-
-const CUSTOM_MEASURE_KEY = buildStorageKey(
- "aml-custom-measures",
- codecForCustomMeasures(),
-);
-/**
- * User preferences.
- *
- * @returns tuple of [state, update()]
- */
-export function useCustomMeasures(): [
- Readonly<CustomMeasures>,
- <T extends keyof CustomMeasures>(key: T, value: CustomMeasures[T]) => void,
- (s: CustomMeasures) => void,
-] {
- const { value, update } = useLocalStorage(
- CUSTOM_MEASURE_KEY,
- defaultCustomMeasures,
- );
-
- function updateField<T extends keyof CustomMeasures>(
- k: T,
- v: CustomMeasures[T],
- ) {
- const newValue = { ...value, [k]: v };
- update(newValue);
- }
- return [value, updateField, update];
-}
diff --git a/packages/aml-backoffice-ui/src/hooks/decision-request.ts b/packages/aml-backoffice-ui/src/hooks/decision-request.ts
@@ -16,22 +16,19 @@
import {
AbsoluteTime,
- Codec,
- Duration,
- KycRule,
buildCodecForObject,
+ Codec,
codecForAbsoluteTime,
codecForAny,
codecForBoolean,
- codecForDuration,
- codecForDurationMs,
codecForKycRules,
codecForList,
codecForMap,
codecForNumber,
codecForString,
codecOptional,
- codecOptionalDefault,
+ KycRule,
+ MeasureInformation,
} from "@gnu-taler/taler-util";
import {
buildStorageKey,
@@ -101,8 +98,20 @@ export interface DecisionRequest {
* Custom unsupported events to be triggered
*/
custom_events: string[] | undefined;
+
+ /**
+ * Custom measures defined by the officer
+ */
+ custom_measures: Record<string, MeasureInformation> | undefined;
}
+export const codecForMeasure = (): Codec<MeasureInformation> =>
+ buildCodecForObject<MeasureInformation>()
+ .property("check_name", codecForString())
+ .property("prog_name", codecForString())
+ .property("context", codecOptional(codecForMap(codecForString())))
+ .build("MeasureInformation");
+
export const codecForAccountAttributes = (): Codec<AccountAttributes> =>
buildCodecForObject<AccountAttributes>()
.property("expiration", codecOptional(codecForAbsoluteTime))
@@ -122,6 +131,7 @@ export const codecForDecisionRequest = (): Codec<DecisionRequest> =>
.property("justification", codecOptional(codecForString()))
.property("accountName", codecOptional(codecForString()))
.property("custom_events", codecOptional(codecForList(codecForString())))
+ .property("custom_measures", codecOptional(codecForMap(codecForMeasure())))
.property(
"triggering_events",
codecOptional(codecForList(codecForString())),
@@ -142,6 +152,7 @@ export const DECISION_REQUEST_EMPTY: DecisionRequest = {
justification: undefined,
keep_investigating: undefined,
new_measures: undefined,
+ custom_measures: undefined,
properties: undefined,
rules: undefined,
};
diff --git a/packages/aml-backoffice-ui/src/hooks/preferences.ts b/packages/aml-backoffice-ui/src/hooks/preferences.ts
@@ -19,6 +19,7 @@ import {
TranslatedString,
buildCodecForObject,
codecForBoolean,
+ codecOptionalDefault,
} from "@gnu-taler/taler-util";
import {
buildStorageKey,
@@ -36,11 +37,20 @@ interface Preferences {
export const codecForPreferences = (): Codec<Preferences> =>
buildCodecForObject<Preferences>()
- .property("allowInsecurePassword", codecForBoolean())
- .property("showDebugInfo", codecForBoolean())
- .property("testingDialect", codecForBoolean())
- .property("keepSessionAfterReload", codecForBoolean())
- .property("preventCompression", codecForBoolean())
+ .property(
+ "allowInsecurePassword",
+ codecOptionalDefault(codecForBoolean(), false),
+ )
+ .property("showDebugInfo", codecOptionalDefault(codecForBoolean(), false))
+ .property("testingDialect", codecOptionalDefault(codecForBoolean(), false))
+ .property(
+ "keepSessionAfterReload",
+ codecOptionalDefault(codecForBoolean(), false),
+ )
+ .property(
+ "preventCompression",
+ codecOptionalDefault(codecForBoolean(), false),
+ )
.build("Preferences");
const defaultPreferences: Preferences = {
@@ -82,7 +92,7 @@ export function getAllBooleanPreferences(): Array<keyof Preferences> {
"allowInsecurePassword",
"keepSessionAfterReload",
"testingDialect",
- "preventCompression"
+ "preventCompression",
];
}
diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
@@ -54,7 +54,6 @@ import { Fragment, h, Ref, VNode } from "preact";
import { useState } from "preact/hooks";
import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
import { useAccountInformation } from "../hooks/account.js";
-import { CustomMeasures, useCustomMeasures } from "../hooks/custom-measures.js";
import {
DECISION_REQUEST_EMPTY,
DecisionRequest,
@@ -181,7 +180,15 @@ export function CaseDetails({
<div>
<button
onClick={async () => {
- onNewDecision(DECISION_REQUEST_EMPTY);
+ // the wizard should not require checking the account state
+ // instead here all the values from the current decision should be
+ // loaded into the new decision request, like we are doing with e
+ // custom measures
+ // FIXME add properties, limits, investigation state
+ onNewDecision({
+ ...DECISION_REQUEST_EMPTY,
+ custom_measures: activeDecision?.limits.custom_measures,
+ });
}}
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"
>
@@ -493,7 +500,6 @@ function SubmitNewDecision({
function ShowMesaureInfo({ nextMeasures }: { nextMeasures: string[] }): VNode {
const { i18n } = useTranslationContext();
const measures = useServerMeasures();
- const [cm] = useCustomMeasures();
if (!measures) {
return <Loading />;
}
@@ -1204,7 +1210,6 @@ export function ShowMeasuresToSelect({
}): VNode {
const measures = useServerMeasures();
const { i18n } = useTranslationContext();
- const [cm] = useCustomMeasures();
if (!measures) {
return <Loading />;
}
@@ -1341,46 +1346,3 @@ export function computeAvailableMesaures(
// return serverAndCustom;
}
-
-export function computeAvailableMesauresCustom(
- customMeasures: Readonly<CustomMeasures>,
- serverMeasures: TalerExchangeApi.AvailableMeasureSummary | undefined,
- skpiFilter?: (m: MeasureInfo) => boolean,
-): Mesaures {
- const init: Mesaures = { forms: [], procedures: [] };
-
- if (!customMeasures || !serverMeasures) {
- return init;
- }
-
- const custom = customMeasures.measures.reduce((prev, value) => {
- if (value.check_name !== "SKIP") {
- const r: MeasureInfo = {
- type: "form",
- name: value.name,
- context: value.context,
- programName: value.program,
- program: serverMeasures.programs[value.program],
- checkName: value.check_name,
- check: serverMeasures.checks[value.check_name],
- custom: true,
- };
- if (skpiFilter && skpiFilter(r)) return prev; // skip
- prev.forms.push(r);
- } else {
- const r: MeasureInfo = {
- type: "procedure",
- name: value.name,
- context: value.context,
- programName: value.program,
- program: serverMeasures.programs[value.program],
- custom: true,
- };
- if (skpiFilter && skpiFilter(r)) return prev; // skip
- prev.procedures.push(r);
- }
- return prev;
- }, init);
-
- return custom;
-}
diff --git a/packages/aml-backoffice-ui/src/pages/MeasuresTable.tsx b/packages/aml-backoffice-ui/src/pages/MeasuresTable.tsx
@@ -59,11 +59,11 @@ export function CurrentMeasureTable({
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-base font-semibold text-gray-900">
- <i18n.Translate>Gather information</i18n.Translate>
+ <i18n.Translate>Forms</i18n.Translate>
</h1>
<p class="mt-2 text-sm text-gray-700">
<i18n.Translate>
- These measures will ask the customer for information.
+ Measures used to get gather information about the customer.
</i18n.Translate>
</p>
</div>
@@ -109,28 +109,19 @@ export function CurrentMeasureTable({
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
{measures.forms.map((m) => {
- // if (
- // m.context &&
- // "internal" in m.context &&
- // m.context.internal
- // ) {
- // return <Fragment />;
- // }
return (
<tr>
- {onSelect ? (
+ {!onSelect ? undefined : (
<td class="relative whitespace-nowrap p-2 text-right text-sm font-medium ">
<button
onClick={() => onSelect(m)}
- class="rounded-md w-fit border-0 p-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
+ class="rounded-md w-fit border-0 p-1 mr-1 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
>
- <i18n.Translate>Select</i18n.Translate>
+ <i18n.Translate>Modify</i18n.Translate>
</button>
</td>
- ) : (
- <Fragment />
)}
- <td class="whitespace-nowrap p-2 text-sm font-medium text-gray-900 sm:pl-6">
+ <td class="whitespace-nowrap p-2 text-sm font-medium text-gray-900 ">
{m.name}
</td>
<td class="whitespace-nowrap p-2 text-sm text-gray-500">
@@ -161,8 +152,7 @@ export function CurrentMeasureTable({
</h1>
<p class="mt-2 text-sm text-gray-700">
<i18n.Translate>
- These measures will be triggered immediately without customer
- interaction.
+ Triggered immediately without customer interaction.
</i18n.Translate>
</p>
</div>
@@ -223,7 +213,7 @@ export function CurrentMeasureTable({
onClick={() => onSelect(m)}
class="rounded-md w-fit border-0 p-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
>
- <i18n.Translate>Select</i18n.Translate>
+ <i18n.Translate>Modify</i18n.Translate>
</button>
</td>
) : (
diff --git a/packages/aml-backoffice-ui/src/pages/NewMeasure.tsx b/packages/aml-backoffice-ui/src/pages/NewMeasure.tsx
@@ -10,13 +10,13 @@ import {
FormDesign,
FormUI,
InternationalizationAPI,
+ undefinedIfEmpty,
useForm,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
-import { h, VNode } from "preact";
+import { Fragment, h, VNode } from "preact";
import { useServerMeasures } from "../hooks/server-info.js";
-import { computeAvailableMesaures } from "./CaseDetails.js";
-import { CurrentMeasureTable } from "./MeasuresTable.js";
+import { useCurrentDecisionRequest } from "../hooks/decision-request.js";
export type MeasureDefinition = {
name: string;
@@ -33,95 +33,84 @@ export type MeasureDefinition = {
export function NewMeasure({
initial,
onCancel,
- onNewMeasure,
}: {
- initial?: MeasureDefinition;
+ initial?: Partial<MeasureDefinition>;
onCancel: () => void;
- onNewMeasure: (m: MeasureDefinition) => void;
}): VNode {
- const { i18n } = useTranslationContext();
const measures = useServerMeasures();
- // const [rules, setRules] = useState<KycRule[]>([]);
+ const { i18n } = useTranslationContext();
const summary =
!measures || measures instanceof TalerError || measures.type === "fail"
? undefined
: measures.body;
- const names = !summary
- ? { measures: [], programs: [], checks: [] }
- : {
- measures: Object.entries(summary.roots).map(([key, value]) => ({
- key,
- value,
- })),
- programs: Object.entries(summary.programs).map(([key, value]) => ({
- key,
- value,
- })),
- checks: Object.entries(summary.checks).map(([key, value]) => ({
- key,
- value,
- })),
- };
+ if (!summary) {
+ return (
+ <div>
+ <i18n.Translate>loading...</i18n.Translate>
+ </div>
+ );
+ }
- const design = formDesign(i18n, names.programs, names.checks, summary);
+ const addingNew = !undefinedIfEmpty(initial);
- const form = useForm<MeasureDefinition>(
- design,
- initial ?? {
- program: "check-tos",
- // check: "form-accept-tos",
- check: "askEmail", // testing invalid
- context: [
- {
- key: "domain",
- value: "taler.net",
- },
- ],
- },
- // (f) => {
- // if (!summary) return undefined;
- // return undefinedIfEmpty<FormErrors<FormType>>({
- // // name: !f.name
- // // ? i18n.str`required`
- // // : summary.roots[f.name]
- // // ? i18n.str`already exist`
- // // : undefined,
- // // program: !f.program
- // // ? i18n.str`required`
- // // : programAndCheckMatch(i18n, summary, f.program, f.check) ??
- // // undefined,
- // // check: checkAndcontextMatch(
- // // i18n,
- // // summary,
- // // f.check,
- // // (f.context ?? []) as {
- // // key: string;
- // // value: string;
- // // }[],
- // // ),
- // // context: checkAndcontextMatch(
- // // i18n,
- // // summary,
- // // f.check,
- // // (f.context ?? []) as {
- // // key: string;
- // // value: string;
- // // }[],
- // // ) as any,
- // });
- // },
+ return (
+ <MeasureForm
+ summary={summary}
+ initial={initial}
+ onCancel={onCancel}
+ addingNew={addingNew}
+ />
);
+}
- if (!summary) {
- return <div>loading...</div>;
- }
+export function MeasureForm({
+ summary,
+ onCancel,
+ initial,
+ addingNew,
+}: {
+ initial?: Partial<MeasureDefinition>;
+ addingNew?: boolean;
+ summary: AvailableMeasureSummary;
+ onCancel: () => void;
+}) {
+ const { i18n } = useTranslationContext();
+ const [request, _, update] = useCurrentDecisionRequest();
+
+ const names = {
+ measures: Object.entries(summary.roots).map(([key, value]) => ({
+ key,
+ value,
+ })),
+ programs: Object.entries(summary.programs).map(([key, value]) => ({
+ key,
+ value,
+ })),
+ checks: Object.entries(summary.checks).map(([key, value]) => ({
+ key,
+ value,
+ })),
+ };
+
+ const design = formDesign(
+ i18n,
+ names.programs,
+ names.checks,
+ summary,
+ !addingNew,
+ );
+
+ initial?.context;
+ const form = useForm<MeasureDefinition>(design, initial ?? {});
const name = !form.status.result ? undefined : form.status.result.name;
const program =
- !form.status.result || !form.status.result.program
+ !form.status.result ||
+ !form.status.result.program ||
+ !summary.programs[form.status.result.program]
? undefined
: {
...summary.programs[form.status.result.program],
@@ -129,7 +118,9 @@ export function NewMeasure({
};
const check =
- !form.status.result || !form.status.result.check
+ !form.status.result ||
+ !form.status.result.check ||
+ !summary.checks[form.status.result.check]
? undefined
: {
...summary.checks[form.status.result.check],
@@ -141,25 +132,6 @@ export function NewMeasure({
? []
: (form.status.result.context as { key: string; value: string }[]);
- // const related = computeAvailableMesaures(
- // summary,
- // // custom,
- // (m) => {
- // if (name && m.name === name) {
- // return false;
- // }
- // if (program && m.programName === program.name) {
- // return false;
- // }
- // if (m.type === "form" && check && m.checkName === check.name) {
- // return false;
- // }
- // return true;
- // },
- // );
-
- // const haveRelated = related.forms.length > 0 || related.procedures.length > 0;
-
return (
<div>
<h2 class="mt-4 mb-2">
@@ -177,15 +149,77 @@ export function NewMeasure({
<i18n.Translate>Cancel</i18n.Translate>
</button>
- <button
- disabled={form.status.status === "fail"}
- onClick={() => {
- onNewMeasure(form.status.result as MeasureDefinition);
- }}
- 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>
+ {addingNew ? (
+ <button
+ disabled={form.status.status === "fail"}
+ onClick={() => {
+ const newMeasure = form.status.result as MeasureDefinition;
+ const currentMeasures = { ...request.custom_measures };
+ currentMeasures[name!] = {
+ check_name: newMeasure.check,
+ prog_name: newMeasure.program,
+ context: (newMeasure.context ?? []).reduce(
+ (prev, cur) => {
+ prev[cur.key] = cur.value;
+ return prev;
+ },
+ {} as Record<string, string>,
+ ),
+ };
+ update({
+ ...request,
+ custom_measures: currentMeasures,
+ });
+ onCancel();
+ }}
+ 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>
+ ) : (
+ <Fragment>
+ <button
+ disabled={form.status.status === "fail"}
+ onClick={() => {
+ const newMeasure = form.status.result as MeasureDefinition;
+ const currentMeasures = { ...request.custom_measures };
+ currentMeasures[name!] = {
+ check_name: newMeasure.check,
+ prog_name: newMeasure.program,
+ context: (newMeasure.context ?? []).reduce(
+ (prev, cur) => {
+ prev[cur.key] = cur.value;
+ return prev;
+ },
+ {} as Record<string, string>,
+ ),
+ };
+ update({
+ ...request,
+ custom_measures: currentMeasures,
+ });
+ onCancel();
+ }}
+ 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>Update</i18n.Translate>
+ </button>
+ <button
+ onClick={() => {
+ const currentMeasures = { ...request.custom_measures };
+ delete currentMeasures[name!];
+ update({
+ ...request,
+ custom_measures: currentMeasures,
+ });
+ onCancel();
+ }}
+ 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>Remove</i18n.Translate>
+ </button>
+ </Fragment>
+ )}
<h2 class="mt-4 mb-2">
<i18n.Translate>Description</i18n.Translate>
@@ -302,25 +336,6 @@ export function NewMeasure({
<div class="px-4 pb-2"></div>
</div>
)}
-
- {/* {!haveRelated ? undefined : (
- <div class="px-4 mt-4">
- <div class="sm:flex sm:items-center">
- <div class="sm:flex-auto">
- <h1 class="text-base font-semibold text-gray-900">
- <i18n.Translate>Related measures</i18n.Translate>
- </h1>
- <p class="mt-2 text-sm text-gray-700">
- <i18n.Translate>
- This measures share checks or programs
- </i18n.Translate>
- </p>
- </div>
- </div>
-
- <CurrentMeasureTable measures={related} />
- </div>
- )} */}
</div>
);
}
@@ -329,7 +344,8 @@ const formDesign = (
i18n: InternationalizationAPI,
programs: { key: string; value: AmlProgramRequirement }[],
checks: { key: string; value: KycCheckInformation }[],
- summary: AvailableMeasureSummary | undefined,
+ summary: AvailableMeasureSummary,
+ cantChangeName: boolean,
): FormDesign<KycRule> => ({
type: "single-column",
fields: [
@@ -337,11 +353,12 @@ const formDesign = (
id: "name",
type: "text",
required: true,
+ disabled: cantChangeName,
label: i18n.str`Name`,
validator(value) {
return !value
? i18n.str`required`
- : summary && summary.roots[value]
+ : summary.roots[value]
? i18n.str`There is already a measure with that name`
: undefined;
},
@@ -360,9 +377,7 @@ const formDesign = (
validator(value, form) {
return !value
? i18n.str`required`
- : !summary
- ? undefined
- : programAndCheckMatch(i18n, summary, value, form.check) ??
+ : programAndCheckMatch(i18n, summary, value, form.check) ??
programAndContextMatch(i18n, summary, value, form.context);
},
},
@@ -380,7 +395,7 @@ const formDesign = (
validator(value, form) {
return checkAndcontextMatch(
i18n,
- summary!,
+ summary,
value,
(form.context ?? []) as {
key: string;
@@ -398,7 +413,7 @@ const formDesign = (
{
type: "text",
id: "key",
- label: i18n.str`Key`,
+ label: i18n.str`Field name`,
},
{
type: "text",
@@ -439,13 +454,13 @@ function checkAndcontextMatch(
i18n: InternationalizationAPI,
summary: AvailableMeasureSummary,
checkName: string | undefined,
- context: { key: string; value: string }[],
+ context: { key: string; value: string }[] | undefined,
): TranslatedString | undefined {
if (checkName === undefined) {
return undefined;
}
const check = summary.checks[checkName];
- const output = context.map((d) => d.key);
+ const output = !context ? [] : context.map((d) => d.key);
const missing = check.requires.filter((d) => {
return output.indexOf(d) === -1;
});
@@ -459,10 +474,10 @@ function programAndContextMatch(
i18n: InternationalizationAPI,
summary: AvailableMeasureSummary,
program: string,
- context: { key: string; value: string }[],
+ context: { key: string; value: string }[] | undefined,
): TranslatedString | undefined {
const check = summary.programs[program];
- const output = context.map((d) => d.key);
+ const output = !context ? [] : context.map((d) => d.key);
const missing = check.context.filter((d) => {
return output.indexOf(d) === -1;
});
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx b/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx
@@ -1,4 +1,8 @@
-import { MeasureInformation, TalerError } from "@gnu-taler/taler-util";
+import {
+ MeasureInformation,
+ TalerError,
+ TalerExchangeApi,
+} from "@gnu-taler/taler-util";
import {
FormDesign,
FormUI,
@@ -9,18 +13,15 @@ import {
} from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
import { useMemo, useState } from "preact/hooks";
-import {
- CustomMeasures,
- useCustomMeasures,
-} from "../../hooks/custom-measures.js";
import { useCurrentDecisionRequest } from "../../hooks/decision-request.js";
import { useServerMeasures } from "../../hooks/server-info.js";
+import { computeAvailableMesaures } from "../CaseDetails.js";
import {
- computeAvailableMesaures,
- computeAvailableMesauresCustom,
-} from "../CaseDetails.js";
-import { CurrentMeasureTable } from "../MeasuresTable.js";
-import { NewMeasure } from "../NewMeasure.js";
+ CurrentMeasureTable,
+ MeasureInfo,
+ Mesaures,
+} from "../MeasuresTable.js";
+import { MeasureDefinition, NewMeasure } from "../NewMeasure.js";
/**
* Ask for more information, define new paths to proceed
@@ -28,19 +29,45 @@ import { NewMeasure } from "../NewMeasure.js";
* @returns
*/
export function Measures({}: {}): VNode {
+ const [initMeasure, setAddMeasure] = useState<Partial<MeasureDefinition>>(); //test;
+ if (initMeasure) {
+ return (
+ <NewMeasure
+ onCancel={() => {
+ setAddMeasure(undefined);
+ }}
+ initial={initMeasure}
+ />
+ );
+ }
+
+ return (
+ <Fragment>
+ <ActiveMeasureForm />
+ <ShowAllMeasures
+ addNewMeasure={(m) => {
+ setAddMeasure(m);
+ }}
+ />
+ </Fragment>
+ );
+}
+
+function ActiveMeasureForm(): VNode {
const { i18n } = useTranslationContext();
const [request, _, updateRequest] = useCurrentDecisionRequest();
const measures = useServerMeasures();
- const [custom] = useCustomMeasures();
const measureBody =
!measures || measures instanceof TalerError || measures.type === "fail"
? undefined
: measures.body;
- const measureList = !measureBody
- ? []
- : Object.entries(measureBody.roots).map(([id, mi]) => ({ id, ...mi }));
+ const measureList = !measureBody ? [] : Object.keys(measureBody.roots);
+ const design = formDesign(i18n, [
+ ...measureList,
+ ...Object.keys(request?.custom_measures ?? {}),
+ ]);
const nm = !request.new_measures ? [] : request.new_measures;
@@ -49,9 +76,7 @@ export function Measures({}: {}): VNode {
[request.new_measures],
);
- const design = formDesign(i18n, measureList, custom);
const form = useForm<FormType>(design, initValue);
-
onComponentUnload(() => {
updateRequest({
...request,
@@ -59,42 +84,78 @@ export function Measures({}: {}): VNode {
});
});
- const haveCustomMeasures = Object.keys(custom.measures).length > 0;
+ return <FormUI design={design} model={form.model} />;
+}
+
+function ShowAllMeasures({
+ addNewMeasure,
+}: {
+ addNewMeasure: (m: Partial<MeasureDefinition>) => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const measures = useServerMeasures();
+
+ const measureBody =
+ !measures || measures instanceof TalerError || measures.type === "fail"
+ ? undefined
+ : measures.body;
+
+ const [request] = useCurrentDecisionRequest();
+ const haveCustomMeasures =
+ Object.keys(request?.custom_measures ?? {}).length > 0;
- const [addMesaure, setAddMeasure] = useState<boolean>(true)//test;
- if (addMesaure) {
- return (
- <NewMeasure
- onCancel={() => {
- setAddMeasure(false);
- }}
- onNewMeasure={() => {
-
- }}
- />
- );
- }
return (
<div>
- <FormUI design={design} model={form.model} />
<button
onClick={() => {
- setAddMeasure(true);
+ addNewMeasure({});
}}
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 custom measure</i18n.Translate>
</button>
{!haveCustomMeasures ? undefined : (
- <Fragment>
- <h1>Custom measures</h1>
+ <div class="divide-y divide-gray-200 overflow-x-scroll rounded-lg bg-white shadow-sm">
+ <div class="p-2">
+ <h1>
+ <i18n.Translate>Custom measures</i18n.Translate>
+ </h1>
+ </div>
+ <div class="p-2">
+ <CurrentMeasureTable
+ measures={computeAvailableMesauresCustom(
+ request.custom_measures,
+ measureBody,
+ )}
+ onSelect={(m) => {
+ addNewMeasure({
+ check: m.type === "form" ? m.checkName : undefined,
+ context: !m.context
+ ? []
+ : Object.entries(m.context).map(([key, value]) => ({
+ key,
+ value,
+ })),
+ name: m.name,
+ program: m.programName,
+ });
+ }}
+ />
+ </div>
+ </div>
+ )}
+ <div class="divide-y divide-gray-200 overflow-x-scroll rounded-lg bg-white shadow-sm">
+ <div class="p-2">
+ <h1>
+ <i18n.Translate>Server measures</i18n.Translate>
+ </h1>
+ </div>
+ <div class="p-2">
<CurrentMeasureTable
- measures={computeAvailableMesauresCustom(custom, measureBody)}
+ measures={computeAvailableMesaures(measureBody)}
/>
- </Fragment>
- )}
- <h1>Server measures</h1>
- <CurrentMeasureTable measures={computeAvailableMesaures(measureBody)} />
+ </div>
+ </div>
</div>
);
}
@@ -105,8 +166,7 @@ type FormType = {
function formDesign(
i18n: InternationalizationAPI,
- mi: (MeasureInformation & { id: string })[],
- cm: CustomMeasures,
+ measureNames: string[],
): FormDesign<FormType> {
return {
type: "single-column",
@@ -114,20 +174,12 @@ function formDesign(
{
type: "selectMultiple",
unique: true,
- choices: [
- ...mi.map((m) => {
- return {
- value: m.id,
- label: m.id,
- };
- }),
- ...cm.measures.map((m) => {
- return {
- value: m.name,
- label: m.name,
- };
- }),
- ],
+ choices: measureNames.map((name) => {
+ return {
+ value: name,
+ label: name,
+ };
+ }),
id: "measures",
label: i18n.str`Active measures`,
help: i18n.str`Measures that the customer will need to satisfy while the rules are active.`,
@@ -135,3 +187,46 @@ function formDesign(
],
};
}
+
+function computeAvailableMesauresCustom(
+ customMeasures: Record<string, MeasureInformation> | undefined,
+ serverMeasures: TalerExchangeApi.AvailableMeasureSummary | undefined,
+ skpiFilter?: (m: MeasureInfo) => boolean,
+): Mesaures {
+ const init: Mesaures = { forms: [], procedures: [] };
+
+ if (!customMeasures || !serverMeasures) {
+ return init;
+ }
+
+ const custom = Object.entries(customMeasures).reduce((prev, [key, value]) => {
+ if (value.check_name !== "SKIP") {
+ const r: MeasureInfo = {
+ type: "form",
+ name: key,
+ context: value.context,
+ programName: value.prog_name,
+ program: serverMeasures.programs[value.prog_name],
+ checkName: value.check_name,
+ check: serverMeasures.checks[value.check_name],
+ custom: true,
+ };
+ if (skpiFilter && skpiFilter(r)) return prev; // skip
+ prev.forms.push(r);
+ } else {
+ const r: MeasureInfo = {
+ type: "procedure",
+ name: key,
+ context: value.context,
+ programName: value.prog_name,
+ program: serverMeasures.programs[value.prog_name],
+ custom: true,
+ };
+ if (skpiFilter && skpiFilter(r)) return prev; // skip
+ prev.procedures.push(r);
+ }
+ return prev;
+ }, init);
+
+ return custom;
+}
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx b/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx
@@ -212,7 +212,7 @@ function AddNewRuleForm({
}: {
onAdd: (nr: RuleFormType) => void;
config: ExchangeVersionResponse;
- measureList: MeasureListWithId;
+ measureList: string[];
onClose: () => void;
isWallet: boolean;
}): VNode {
@@ -271,12 +271,12 @@ function UpdateRulesForm({
const [request, updateRequestField, updateRequest] =
useCurrentDecisionRequest();
const [showAddRuleForm, setShowAddRuleForm] = useState(false);
- const measureList: MeasureListWithId = !rootMeasures
- ? []
- : Object.entries(rootMeasures).map(([id, mi]) => ({ id, ...mi }));
-
- const expirationFormDesign = expirationFormDesignTemplate(i18n, measureList);
-
+ const measureList = !rootMeasures ? [] : Object.keys(rootMeasures);
+ const customMeasures = Object.keys(request.custom_measures ?? {});
+ const expirationFormDesign = expirationFormDesignTemplate(i18n, [
+ ...measureList,
+ ...customMeasures,
+ ]);
const expirationForm = useForm<ExpirationFormType>(expirationFormDesign, {
expiration:
request.deadline ??
@@ -520,7 +520,7 @@ function labelForOperationType(
const ruleFormDesignTemplate = (
i18n: InternationalizationAPI,
currency: string,
- mi: (MeasureInformation & { id: string })[],
+ measureNames: string[],
isWallet: boolean,
): FormDesign<KycRule> => ({
type: "single-column",
@@ -561,10 +561,10 @@ const ruleFormDesignTemplate = (
{
type: "selectMultiple",
unique: true,
- choices: mi.map((m) => {
+ choices: measureNames.map((name) => {
return {
- value: m.id,
- label: m.id,
+ value: name,
+ label: name,
};
}),
id: "measures",
@@ -581,7 +581,7 @@ const ruleFormDesignTemplate = (
});
const expirationFormDesignTemplate = (
i18n: InternationalizationAPI,
- mi: (MeasureInformation & { id: string })[],
+ measureNames: string[],
): FormDesign<KycRule> => ({
type: "single-column",
fields: [
@@ -628,10 +628,10 @@ const expirationFormDesignTemplate = (
},
{
type: "selectOne",
- choices: mi.map((m) => {
+ choices: measureNames.map((name) => {
return {
- value: m.id,
- label: m.id,
+ value: name,
+ label: name,
};
}),
id: "measure",
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx b/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx
@@ -2,8 +2,6 @@ import {
AbsoluteTime,
AmlDecisionRequest,
assertUnreachable,
- buildPayto,
- Duration,
HttpStatusCode,
parsePaytoUri,
PaytoString,
@@ -19,14 +17,17 @@ import {
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
-import { DECISION_REQUEST_EMPTY, useCurrentDecisionRequest } from "../../hooks/decision-request.js";
+import {
+ DECISION_REQUEST_EMPTY,
+ useCurrentDecisionRequest,
+} from "../../hooks/decision-request.js";
+import { useOfficer } from "../../hooks/officer.js";
import { useServerMeasures } from "../../hooks/server-info.js";
import {
computeAvailableMesaures,
ShowDecisionLimitInfo,
} from "../CaseDetails.js";
import { CurrentMeasureTable, Mesaures } from "../MeasuresTable.js";
-import { useOfficer } from "../../hooks/officer.js";
import {
isAttributesCompleted,
isEventsCompleted,
@@ -37,7 +38,6 @@ import {
isRulesCompleted,
WizardSteps,
} from "./AmlDecisionRequestWizard.js";
-import { useCustomMeasures } from "../../hooks/custom-measures.js";
/**
* Mark for further investigation and explain decision
@@ -99,7 +99,7 @@ export function Summary({
onMove(undefined);
}
- const fullPayto = !newPayto? undefined: parsePaytoUri(newPayto)
+ const fullPayto = !newPayto ? undefined : parsePaytoUri(newPayto);
if (fullPayto && decision.accountName) {
fullPayto.params["receiver-name"] = decision.accountName;
}
@@ -124,15 +124,15 @@ export function Summary({
rules: decision.rules!,
successor_measure: decision.onExpire_measure,
custom_measures: {
- "asd": {
+ asd: {
check_name: "form-accept-tos",
prog_name: "check-tos",
context: {
tos_url: "taler.net",
provider_name: "asd",
- }
- }
- },
+ },
+ },
+ },
},
attributes_expiration: decision.attributes?.expiration
? AbsoluteTime.toProtocolTimestamp(