commit 507410eac6d51a57ccc0535e00b641c9285f1269
parent 255faaa1e685037dc05e9b051b3c36d26976c27b
Author: Sebastian <sebasjm@gmail.com>
Date: Tue, 24 Jun 2025 11:55:09 -0300
fix #9874
Diffstat:
10 files changed, 216 insertions(+), 206 deletions(-)
diff --git a/packages/aml-backoffice-ui/src/components/MeasureList.tsx b/packages/aml-backoffice-ui/src/components/MeasureList.tsx
@@ -27,7 +27,7 @@ import {
import { Fragment, h } from "preact";
import { ErrorLoadingWithDebug } from "./ErrorLoadingWithDebug.js";
import { useServerMeasures } from "../hooks/server-info.js";
-import { computeAvailableMesaures } from "../utils/computeAvailableMesaures.js";
+import { computeMeasureInformation } from "../utils/computeAvailableMesaures.js";
import { CurrentMeasureTable } from "./MeasuresTable.js";
import { Profile } from "../pages/Profile.js";
@@ -89,7 +89,7 @@ export function MeasureList({ routeToNew }: { routeToNew: RouteDefinition }) {
}
}
- const ms = computeAvailableMesaures(
+ const ms = computeMeasureInformation(
measures.body,
);
diff --git a/packages/aml-backoffice-ui/src/components/MeasuresTable.tsx b/packages/aml-backoffice-ui/src/components/MeasuresTable.tsx
@@ -19,42 +19,13 @@ import {
} from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
-
-export type MeasureInfo = ProcedureMeasure | FormMeasure | InfoMeasure;
+import {
+ MeasureInfo,
+ UiMeasureInformation,
+} from "../utils/computeAvailableMesaures.js";
const TALER_SCREEN_ID = 123;
-export type ProcedureMeasure = {
- type: "procedure";
- name: string;
- programName: string;
- program: AmlProgramRequirement;
- context?: object;
- custom: boolean;
-};
-export type FormMeasure = {
- type: "form";
- name: string;
- programName?: string;
- program?: AmlProgramRequirement;
- checkName: string;
- check: KycCheckInformation;
- context?: object;
- custom: boolean;
-};
-export type InfoMeasure = {
- type: "info";
- name: string;
- checkName: string;
- check: KycCheckInformation;
- context?: object;
- custom: boolean;
-};
-export type UiMeasureInformation = {
- procedures: ProcedureMeasure[];
- forms: FormMeasure[];
- info: InfoMeasure[];
-};
export function CurrentMeasureTable({
measures,
onSelect,
@@ -188,11 +159,19 @@ export function CurrentMeasureTable({
) : (
<Fragment />
)}
+ {hideMeasureNames ? undefined : (
+ <th
+ scope="col"
+ class="p-2 text-left text-sm font-semibold text-gray-900 sm:pl-6"
+ >
+ <i18n.Translate>Name</i18n.Translate>
+ </th>
+ )}
<th
scope="col"
class="p-2 text-left text-sm font-semibold text-gray-900 sm:pl-6"
>
- <i18n.Translate>Name</i18n.Translate>
+ <i18n.Translate>Check</i18n.Translate>
</th>
<th
scope="col"
@@ -218,8 +197,13 @@ export function CurrentMeasureTable({
) : (
<Fragment />
)}
- <td class="whitespace-nowrap p-2 text-sm font-medium text-gray-900 sm:pl-6">
- {m.name}
+ {hideMeasureNames ? undefined : (
+ <td class="whitespace-nowrap p-2 text-sm font-medium text-gray-900 sm:pl-6">
+ {m.name}
+ </td>
+ )}
+ <td class="whitespace-nowrap p-2 text-sm text-gray-500">
+ {m.checkName}
</td>
<td class="whitespace-nowrap p-2 text-sm text-gray-500">
{Object.keys(m.context ?? {}).join(", ")}
@@ -261,12 +245,14 @@ export function CurrentMeasureTable({
) : (
<Fragment />
)}
- <th
- scope="col"
- class="p-2 text-left text-sm font-semibold text-gray-900 sm:pl-6"
- >
- <i18n.Translate>Name</i18n.Translate>
- </th>
+ {hideMeasureNames ? undefined : (
+ <th
+ scope="col"
+ class="p-2 text-left text-sm font-semibold text-gray-900 sm:pl-6"
+ >
+ <i18n.Translate>Name</i18n.Translate>
+ </th>
+ )}
<th
scope="col"
class="p-2 text-left text-sm font-semibold text-gray-900"
@@ -303,9 +289,11 @@ export function CurrentMeasureTable({
) : (
<Fragment />
)}
- <td class="whitespace-nowrap p-2 text-sm font-medium text-gray-900 sm:pl-6">
- {m.name}
- </td>
+ {hideMeasureNames ? undefined : (
+ <td class="whitespace-nowrap p-2 text-sm font-medium text-gray-900 sm:pl-6">
+ {m.name}
+ </td>
+ )}
<td class="whitespace-nowrap p-2 text-sm text-gray-500">
{m.program.description}
</td>
diff --git a/packages/aml-backoffice-ui/src/components/ShowLegitimizationInfo.tsx b/packages/aml-backoffice-ui/src/components/ShowLegitimizationInfo.tsx
@@ -25,7 +25,7 @@ import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { RulesInfo } from "./RulesInfo.js";
import { CurrentMeasureTable } from "./MeasuresTable.js";
-import { computeAvailableMesaures } from "../utils/computeAvailableMesaures.js";
+import { computeMeasureInformation } from "../utils/computeAvailableMesaures.js";
import { CheckCircleIcon } from "@heroicons/react/20/solid";
const TALER_SCREEN_ID = 120;
@@ -120,8 +120,8 @@ export function ShowLegistimizationInfo({
);
}
- const info = computeAvailableMesaures(serverMeasures, {
- measures: legitimization.measures,
+ const info = computeMeasureInformation(serverMeasures, {
+ measureList: legitimization.measures,
});
return (
diff --git a/packages/aml-backoffice-ui/src/pages/AccountDetails.tsx b/packages/aml-backoffice-ui/src/pages/AccountDetails.tsx
@@ -41,7 +41,7 @@ import { Fragment } from "preact/jsx-runtime";
import { useServerMeasures } from "../hooks/server-info.js";
import { BANK_RULES, WALLET_RULES } from "./decision/Rules.js";
import { useCurrentLegitimizations } from "../hooks/legitimizations.js";
-import { computeAvailableMesaures } from "../utils/computeAvailableMesaures.js";
+import { computeMeasureInformation } from "../utils/computeAvailableMesaures.js";
import { CurrentMeasureTable } from "../components/MeasuresTable.js";
import { ShowLegistimizationInfo } from "../components/ShowLegitimizationInfo.js";
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx b/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx
@@ -17,8 +17,7 @@ import {
assertUnreachable,
KycCheckInformation,
MeasureInformation,
- TalerError,
- TalerExchangeApi,
+ TalerError
} from "@gnu-taler/taler-util";
import {
FormDesign,
@@ -32,13 +31,11 @@ import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import {
CurrentMeasureTable,
- MeasureInfo,
- UiMeasureInformation,
} from "../../components/MeasuresTable.js";
import { MeasureDefinition, NewMeasure } from "../../components/NewMeasure.js";
import { useCurrentDecisionRequest } from "../../hooks/decision-request.js";
import { useServerMeasures } from "../../hooks/server-info.js";
-import { computeAvailableMesaures } from "../../utils/computeAvailableMesaures.js";
+import { computeMeasureInformation } from "../../utils/computeAvailableMesaures.js";
const TALER_SCREEN_ID = 105;
/**
@@ -309,10 +306,9 @@ function ShowAllMeasures({
</div>
<div class="p-2">
<CurrentMeasureTable
- measures={computeAvailableMesauresCustom(
- request.custom_measures,
- measureBody,
- )}
+ measures={computeMeasureInformation(measureBody, {
+ measureMap: request.custom_measures,
+ })}
onSelect={(m) => {
editMeasure({
check: m.type === "form" ? m.checkName : undefined,
@@ -339,7 +335,7 @@ function ShowAllMeasures({
</div>
<div class="p-2">
<CurrentMeasureTable
- measures={computeAvailableMesaures(measureBody)}
+ measures={computeMeasureInformation(measureBody)}
onSelect={(m) => {
addNewMeasure({
check: m.type === "form" ? m.checkName : undefined,
@@ -437,67 +433,3 @@ function formDesign(
],
};
}
-
-function computeAvailableMesauresCustom(
- customMeasures: Record<string, MeasureInformation> | undefined,
- serverMeasures: TalerExchangeApi.AvailableMeasureSummary | undefined,
- skpiFilter?: (m: MeasureInfo) => boolean,
-): UiMeasureInformation {
- const init: UiMeasureInformation = { forms: [], procedures: [], info: [] };
-
- if (!customMeasures || !serverMeasures) {
- return init;
- }
-
- const custom = Object.entries(customMeasures).reduce((prev, [key, value]) => {
- if (value.check_name !== "SKIP") {
- if (!value.prog_name) {
- const r: MeasureInfo = {
- type: "info",
- name: key,
- context: value.context,
- checkName: value.check_name,
- check: serverMeasures.checks[value.check_name],
- custom: true,
- };
- if (skpiFilter && skpiFilter(r)) return prev; // skip
- prev.info.push(r);
- } else {
- const r: MeasureInfo = {
- type: "form",
- name: key,
- context: value.context,
- programName: value.prog_name,
- program: value.prog_name
- ? serverMeasures.programs[value.prog_name]
- : undefined,
- checkName: value.check_name,
- check: serverMeasures.checks[value.check_name],
- custom: true,
- };
- if (skpiFilter && skpiFilter(r)) return prev; // skip
- prev.forms.push(r);
- }
- } else {
- if (!value.prog_name) {
- console.error(
- `ERROR: program name can't be empty for measure "${key}"`,
- );
- return prev;
- }
- 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/Summary.tsx b/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx
@@ -29,6 +29,7 @@ import {
import {
Attention,
Button,
+ LocalNotificationBanner,
useExchangeApiContext,
useLocalNotificationHandler,
useTranslationContext,
@@ -37,13 +38,12 @@ import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import {
CurrentMeasureTable,
- UiMeasureInformation,
} from "../../components/MeasuresTable.js";
import { ShowDecisionLimitInfo } from "../../components/ShowDecisionLimitInfo.js";
import { useCurrentDecisionRequest } from "../../hooks/decision-request.js";
import { useOfficer } from "../../hooks/officer.js";
import { useServerMeasures } from "../../hooks/server-info.js";
-import { computeAvailableMesaures } from "../../utils/computeAvailableMesaures.js";
+import { computeMeasureInformation, UiMeasureInformation } from "../../utils/computeAvailableMesaures.js";
import {
isAttributesCompleted,
isEventsCompleted,
@@ -73,10 +73,10 @@ export function Summary({
const { i18n } = useTranslationContext();
const [decision, , cleanUpDecision] = useCurrentDecisionRequest();
const measures = useServerMeasures();
- const [, withErrorHandler] = useLocalNotificationHandler();
+ const [notification, withErrorHandler] = useLocalNotificationHandler();
const officer = useOfficer();
const session = officer.state === "ready" ? officer.account : undefined;
- const allMeasures = computeAvailableMesaures(
+ const allMeasures = computeMeasureInformation(
!measures || measures instanceof TalerError || measures.type === "fail"
? undefined
: measures.body,
@@ -150,9 +150,7 @@ export function Summary({
),
rules: decision.rules!,
successor_measure: decision.onExpire_measure,
- custom_measures: workaround_defaultProgramName(
- decision.custom_measures ?? {},
- ),
+ custom_measures: decision.custom_measures ?? {},
},
attributes_expiration: decision.attributes?.expiration
? AbsoluteTime.toProtocolTimestamp(
@@ -244,6 +242,7 @@ export function Summary({
return (
<Fragment>
+ <LocalNotificationBanner notification={notification} showDebug />
{INVALID_RULES ? (
<Fragment>
{!decision.deadline && (
@@ -388,16 +387,16 @@ export function Summary({
*
* @param measures
* @returns
- */
-function workaround_defaultProgramName(
- measures: Record<string, MeasureInformation>,
-) {
- const ms = Object.keys(measures);
- for (const name of ms) {
- const m = measures[name];
- if (!m.prog_name) {
- measures[name].prog_name = "preserve-investigate";
- }
- }
- return measures;
-}
+// */
+// function workaround_defaultProgramName(
+// measures: Record<string, MeasureInformation>,
+// ) {
+// const ms = Object.keys(measures);
+// for (const name of ms) {
+// const m = measures[name];
+// if (!m.prog_name) {
+// measures[name].prog_name = "preserve-investigate";
+// }
+// }
+// return measures;
+// }
diff --git a/packages/aml-backoffice-ui/src/utils/computeAvailableMesaures.ts b/packages/aml-backoffice-ui/src/utils/computeAvailableMesaures.ts
@@ -13,14 +13,59 @@
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 type { TalerExchangeApi } from "@gnu-taler/taler-util";
-import { MeasureInfo, UiMeasureInformation } from "../components/MeasuresTable.js";
+import type {
+ AmlProgramRequirement,
+ AvailableMeasureSummary,
+ KycCheckInformation,
+ MeasureInformation,
+} from "@gnu-taler/taler-util";
-export function computeAvailableMesaures(
- serverMeasures: TalerExchangeApi.AvailableMeasureSummary | undefined,
+export type MeasureInfo = ProcedureMeasure | FormMeasure | InfoMeasure;
+
+export type ProcedureMeasure = {
+ type: "procedure";
+ name: string;
+ programName: string;
+ program: AmlProgramRequirement;
+ context?: object;
+};
+export type FormMeasure = {
+ type: "form";
+ name: string;
+ programName?: string;
+ program?: AmlProgramRequirement;
+ checkName: string;
+ check: KycCheckInformation;
+ context?: object;
+};
+export type InfoMeasure = {
+ type: "info";
+ name: string;
+ checkName: string;
+ check: KycCheckInformation;
+ context?: object;
+};
+export type UiMeasureInformation = {
+ procedures: ProcedureMeasure[];
+ forms: FormMeasure[];
+ info: InfoMeasure[];
+};
+/**
+ * Take a list of measures and fills it with information from server for the UI
+ *
+ * If measureList is not present then measureMap is going to be used
+ * If measureMap is not present then serverMeasures.roots is going to be used
+ *
+ * @param serverMeasures reference from the server, where the real info is
+ * @param opts.measureList a list of measures from which the information is needed
+ * @param opts.measureMap a map of measures from which the information is needed
+ * @returns
+ */
+export function computeMeasureInformation(
+ serverMeasures: AvailableMeasureSummary | undefined,
opts: {
- measures?: TalerExchangeApi.MeasureInformation[];
- skpiFilter?: (m: MeasureInfo) => boolean;
+ measureList?: MeasureInformation[];
+ measureMap?: Record<string, MeasureInformation> | undefined;
} = {},
): UiMeasureInformation {
const init: UiMeasureInformation = { forms: [], procedures: [], info: [] };
@@ -28,58 +73,84 @@ export function computeAvailableMesaures(
return init;
}
- const defaultServerMeasures = Object.entries(serverMeasures.roots);
- const measures: typeof defaultServerMeasures = !opts.measures
- ? defaultServerMeasures
- : opts.measures.map((m) => ["", m]); // we don't have the names in this case
+ type MeasuerList = [string | undefined, MeasureInformation][];
+ const measures: MeasuerList = opts.measureList
+ ? opts.measureList.map((m) => [undefined, m]) // we don't have the names in this case
+ : opts.measureMap
+ ? Object.entries(opts.measureMap)
+ : Object.entries(serverMeasures.roots);
- const server = measures.reduce((prev, [key, value]) => {
- if (value.check_name !== "SKIP") {
- if (!value.prog_name) {
- const r: MeasureInfo = {
- type: "info",
- name: key,
- context: value.context,
- checkName: value.check_name,
- check: serverMeasures.checks[value.check_name],
- custom: true,
- };
- if (opts.skpiFilter && opts.skpiFilter(r)) return prev; // skip
- prev.info.push(r);
- } else {
- 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: false,
- };
- if (opts.skpiFilter && opts.skpiFilter(r)) return prev; // skip
- prev.forms.push(r);
- }
- } else {
- if (!value.prog_name) {
- console.error(
- `ERROR: program name can't be empty for measure "${key}"`,
- );
- return prev;
+ return measures.reduce((prev, [key, value]) => {
+ const measure = buildMeasureInformation(serverMeasures, key, value);
+ if (measure) {
+ switch (measure.type) {
+ case "procedure": {
+ prev.procedures.push(measure);
+ break;
+ }
+ case "form": {
+ prev.forms.push(measure);
+ break;
+ }
+ case "info": {
+ prev.info.push(measure);
+ break;
+ }
}
- const r: MeasureInfo = {
- type: "procedure",
- name: key,
- context: value.context,
- programName: value.prog_name,
- program: serverMeasures.programs[value.prog_name],
- custom: false,
- };
- if (opts.skpiFilter && opts.skpiFilter(r)) return prev; // skip
- prev.procedures.push(r);
}
return prev;
}, init);
+}
- return server;
+/**
+ *
+ * @param serverMeasures server information about measures, checks and programs
+ * @param name the name of the measure
+ * @param measure the incomplete measure
+ * @returns
+ */
+function buildMeasureInformation(
+ serverMeasures: AvailableMeasureSummary,
+ name: string | undefined,
+ measure: MeasureInformation,
+): MeasureInfo | undefined {
+ if (measure.check_name !== "SKIP") {
+ if (!measure.prog_name) {
+ const r: MeasureInfo = {
+ type: "info",
+ name: name ?? "",
+ context: measure.context,
+ checkName: measure.check_name,
+ check: serverMeasures.checks[measure.check_name],
+ // custom: true,
+ };
+ return r;
+ } else {
+ const r: MeasureInfo = {
+ type: "form",
+ name: name ?? "",
+ context: measure.context,
+ programName: measure.prog_name,
+ program: serverMeasures.programs[measure.prog_name],
+ checkName: measure.check_name,
+ check: serverMeasures.checks[measure.check_name],
+ // custom: false,
+ };
+ return r;
+ }
+ } else {
+ if (!measure.prog_name) {
+ console.error(`ERROR: program name can't be empty for measure "${name}"`);
+ return undefined;
+ }
+ const r: MeasureInfo = {
+ type: "procedure",
+ name: name ?? "",
+ context: measure.context,
+ programName: measure.prog_name,
+ program: serverMeasures.programs[measure.prog_name],
+ // custom: false,
+ };
+ return r;
+ }
}
diff --git a/packages/taler-util/src/codec.ts b/packages/taler-util/src/codec.ts
@@ -101,10 +101,30 @@ class ObjectCodecBuilder<OutputType, PartialOutputType> {
/**
* Define a property for the object.
*/
- property<K extends keyof OutputType & string, V extends OutputType[K]>(
+ property<K extends keyof OutputType & string>(
x: K,
- codec: Codec<V>,
- ): ObjectCodecBuilder<OutputType, PartialOutputType & SingletonRecord<K, V>> {
+ codec: Codec<OutputType[K]>,
+ ): ObjectCodecBuilder<
+ OutputType,
+ PartialOutputType & SingletonRecord<K, OutputType[K]>
+ > {
+ if (!codec) {
+ throw Error("inner codec must be defined");
+ }
+ this.propList.push({ name: x, codec: codec });
+ return this as any;
+ }
+
+ /**
+ * Define a property for the object.
+ */
+ propertyStrict<K extends keyof OutputType & string>(
+ x: K,
+ codec: Codec<OutputType[K]>,
+ ): ObjectCodecBuilder<
+ OutputType,
+ PartialOutputType & SingletonRecord<K, OutputType[K]>
+ > {
if (!codec) {
throw Error("inner codec must be defined");
}
@@ -120,7 +140,7 @@ class ObjectCodecBuilder<OutputType, PartialOutputType> {
* FIXME: do proper union of all `other' props.
*/
mixin<K extends keyof OutputType & string, V extends OutputType[K]>(
- other: ObjectCodec<V>
+ other: ObjectCodec<V>,
): ObjectCodecBuilder<OutputType, PartialOutputType & V> {
this.propList.push(...other.getProps());
return this as any;
@@ -205,7 +225,7 @@ class ObjectCodecBuilder<OutputType, PartialOutputType> {
getProps(): Prop[] {
return propList;
- }
+ },
};
}
}
diff --git a/packages/taler-util/src/types-taler-exchange.ts b/packages/taler-util/src/types-taler-exchange.ts
@@ -2669,7 +2669,7 @@ export const codecForKycCheckInformation = (): Codec<KycCheckInformation> =>
export const codecForMeasureInformation = (): Codec<MeasureInformation> =>
buildCodecForObject<MeasureInformation>()
- .property("prog_name", codecForString())
+ .property("prog_name", codecOptional(codecForString()))
.property("check_name", codecForString())
.property("context", codecForAny())
.property("operation_type", codecOptional(codecForOperationType))
diff --git a/packages/taler-util/src/types-taler-merchant.ts b/packages/taler-util/src/types-taler-merchant.ts
@@ -3608,7 +3608,7 @@ export const codecForQueryInstancesResponse =
.property(
"auth",
buildCodecForObject<{
- method: "external" | "token";
+ method: MerchantAuthMethod.EXTERNAL | MerchantAuthMethod.TOKEN;
}>()
.property("method", codecForMerchantAuthMethod)
.build("TalerMerchantApi.QueryInstancesResponse.auth"),