commit 2ba33b6e71d21f523f1730f2ae2cf7803a8f67c0
parent b90c8b01c375c656426d13d241e80263d2fe03ca
Author: Florian Dold <dold@taler.net>
Date: Mon, 31 Aug 2026 14:23:33 +0200
exchange AML web UI: share detailed measure cards
Diffstat:
11 files changed, 650 insertions(+), 904 deletions(-)
diff --git a/packages/taler-exchange-aml-webui/src/components/MeasureDetails.stories.tsx b/packages/taler-exchange-aml-webui/src/components/MeasureDetails.stories.tsx
@@ -0,0 +1,114 @@
+/*
+ This file is part of GNU Taler
+ (C) 2026 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.
+*/
+
+import {
+ AvailableMeasureSummary,
+ LimitOperationType,
+} from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import { h, VNode } from "preact";
+import { MeasureCard } from "./MeasureDetails.js";
+
+export default { title: "AML configuration/measure details" };
+
+const summary: AvailableMeasureSummary = {
+ roots: {
+ "collect-address": {
+ check_name: "address-form",
+ prog_name: "review-address",
+ operation_type: LimitOperationType.withdraw,
+ context: { document_type: "utility-bill" },
+ },
+ "show-information": {
+ check_name: "information-notice",
+ voluntary: true,
+ },
+ "freeze-account": {
+ check_name: "SKIP",
+ prog_name: "freeze-account",
+ },
+ "broken-reference": {
+ check_name: "missing-check",
+ prog_name: "missing-program",
+ context: { reason: "configuration example" },
+ },
+ },
+ checks: {
+ "address-form": {
+ description: "Collect a recent proof of address",
+ description_i18n: { de: "Aktuellen Adressnachweis erfassen" },
+ requires: ["document_type"],
+ outputs: ["street", "city", "country"],
+ fallback: "show-information",
+ },
+ "information-notice": {
+ description: "Show information without starting an AML program",
+ requires: [],
+ outputs: [],
+ fallback: "collect-address",
+ },
+ },
+ programs: {
+ "review-address": {
+ description: "Review the submitted address information",
+ context: ["case_id"],
+ inputs: ["street", "city", "country"],
+ },
+ "freeze-account": {
+ description: "Apply an immediate account freeze",
+ context: [],
+ inputs: [],
+ },
+ },
+ default_rules: [],
+};
+
+function AvailableCatalog(): VNode {
+ return (
+ <div class="grid gap-4 lg:grid-cols-2">
+ {Object.entries(summary.roots).map(([name, measure]) => (
+ <MeasureCard
+ key={name}
+ name={name}
+ measure={measure}
+ summary={summary}
+ language="en"
+ action={
+ <button
+ type="button"
+ class="rounded-md border border-primary px-3 py-2 text-sm font-semibold text-primary"
+ >
+ Customize a copy
+ </button>
+ }
+ />
+ ))}
+ </div>
+ );
+}
+
+export const AvailableExchangeMeasures = tests.createExample(
+ AvailableCatalog,
+ {},
+);
+
+export const LocalizedCustomerForm = tests.createExample(MeasureCard, {
+ name: "collect-address",
+ measure: summary.roots["collect-address"],
+ summary,
+ language: "de-CH",
+});
+
+export const StandaloneCheck = tests.createExample(MeasureCard, {
+ name: "check-information-notice",
+ measure: { check_name: "information-notice" },
+ summary,
+ language: "en",
+ category: "standalone-check",
+});
diff --git a/packages/taler-exchange-aml-webui/src/components/MeasureDetails.tsx b/packages/taler-exchange-aml-webui/src/components/MeasureDetails.tsx
@@ -0,0 +1,339 @@
+/*
+ This file is part of GNU Taler
+ (C) 2026 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.
+*/
+
+import {
+ AvailableMeasureSummary,
+ KycCheckInformation,
+ MeasureInformation,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { ComponentChildren, Fragment, h, VNode } from "preact";
+import { labelForOperationType } from "./RulesInfo.js";
+import { isBuiltInSkipCheck } from "../utils/measure-check.js";
+
+export type MeasureCategory =
+ | "customer-form"
+ | "information"
+ | "instant"
+ | "standalone-check";
+
+export interface MeasureCardProps {
+ name?: string;
+ measure: MeasureInformation;
+ summary: AvailableMeasureSummary | undefined;
+ language: string;
+ category?: MeasureCategory;
+ action?: ComponentChildren;
+}
+
+export function MeasureCard({
+ name,
+ measure,
+ summary,
+ language,
+ category,
+ action,
+}: MeasureCardProps): VNode {
+ const { i18n } = useTranslationContext();
+ const builtInSkip = isBuiltInSkipCheck(measure.check_name);
+ const check = builtInSkip ? undefined : summary?.checks[measure.check_name];
+ const program = measure.prog_name
+ ? summary?.programs[measure.prog_name]
+ : undefined;
+ const effectiveCategory =
+ category ??
+ (builtInSkip
+ ? "instant"
+ : measure.prog_name
+ ? "customer-form"
+ : "information");
+
+ return (
+ <article class="flex h-full flex-col rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-950">
+ <div class="flex flex-wrap items-start justify-between gap-3">
+ <div class="min-w-0">
+ <span class="inline-flex rounded-full bg-gray-100 px-2 py-0.5 text-xs font-semibold text-gray-700 dark:bg-gray-800 dark:text-gray-200">
+ {labelForMeasureCategory(effectiveCategory, i18n)}
+ </span>
+ {name ? (
+ <div class="mt-2">
+ <Code>{name}</Code>
+ </div>
+ ) : undefined}
+ </div>
+ {action}
+ </div>
+
+ <dl class="mt-4 space-y-3">
+ <ReferenceField
+ label={i18n.str`KYC check`}
+ name={measure.check_name}
+ description={
+ builtInSkip
+ ? i18n.str`Built-in check that proceeds without collecting customer data.`
+ : check && localizedCheckDescription(check, language)
+ }
+ missing={!builtInSkip && !!summary && !check}
+ />
+ {measure.prog_name ? (
+ <ReferenceField
+ label={i18n.str`AML program`}
+ name={measure.prog_name}
+ description={program?.description}
+ missing={!!summary && !program}
+ />
+ ) : (
+ <SimpleField label={i18n.str`AML program`} value={i18n.str`None`} />
+ )}
+ <div class="grid gap-3 sm:grid-cols-2">
+ <SimpleField
+ label={i18n.str`Operation`}
+ value={
+ measure.operation_type
+ ? labelForOperationType(measure.operation_type, i18n)
+ : i18n.str`Not specified`
+ }
+ />
+ <SimpleField
+ label={i18n.str`Voluntary`}
+ value={measure.voluntary ? i18n.str`Yes` : i18n.str`No`}
+ />
+ </div>
+ </dl>
+
+ <details class="mt-4 border-t border-gray-200 pt-3 dark:border-gray-700">
+ <summary class="min-h-9 cursor-pointer py-1 text-sm font-semibold text-gray-700 dark:text-gray-200">
+ <i18n.Translate>Technical details</i18n.Translate>
+ </summary>
+ <dl class="mt-3 space-y-3">
+ {check ? (
+ <>
+ <TokenField
+ label={i18n.str`Required check context`}
+ values={check.requires}
+ />
+ <TokenField
+ label={i18n.str`Check outputs`}
+ values={check.outputs}
+ />
+ <ReferenceField
+ label={i18n.str`Fallback measure`}
+ name={check.fallback}
+ missing={!!summary && !summary.roots[check.fallback]}
+ />
+ </>
+ ) : undefined}
+ {program ? (
+ <>
+ <TokenField
+ label={i18n.str`Required program context`}
+ values={program.context}
+ />
+ <TokenField
+ label={i18n.str`Required program inputs`}
+ values={program.inputs}
+ />
+ </>
+ ) : undefined}
+ <ContextField context={measure.context} />
+ </dl>
+ </details>
+ </article>
+ );
+}
+
+export function CheckCard({
+ name,
+ check,
+ language,
+ fallbackAvailable,
+}: {
+ name: string;
+ check: KycCheckInformation;
+ language: string;
+ fallbackAvailable: boolean;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <DataCard
+ title={name}
+ description={localizedCheckDescription(check, language)}
+ >
+ <TokenField label={i18n.str`Required context`} values={check.requires} />
+ <TokenField label={i18n.str`Outputs`} values={check.outputs} />
+ <ReferenceField
+ label={i18n.str`Fallback measure`}
+ name={check.fallback}
+ missing={!fallbackAvailable}
+ />
+ </DataCard>
+ );
+}
+
+export function DataCard({
+ title,
+ description,
+ children,
+}: {
+ title: string;
+ description?: string;
+ children: ComponentChildren;
+}): VNode {
+ return (
+ <article class="rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-950">
+ <Code>{title}</Code>
+ {description ? (
+ <p class="mt-3 text-sm leading-6 text-gray-700 dark:text-gray-300">
+ {description}
+ </p>
+ ) : undefined}
+ <dl class="mt-4 space-y-3">{children}</dl>
+ </article>
+ );
+}
+
+export function SimpleField({
+ label,
+ value,
+}: {
+ label: TranslatedString;
+ value: string | TranslatedString;
+}): VNode {
+ return (
+ <div>
+ <dt class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
+ {label}
+ </dt>
+ <dd class="mt-1 text-sm text-gray-800 dark:text-gray-200">{value}</dd>
+ </div>
+ );
+}
+
+export function ReferenceField({
+ label,
+ name,
+ description,
+ missing = false,
+}: {
+ label: TranslatedString;
+ name: string;
+ description?: string;
+ missing?: boolean;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div>
+ <dt class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
+ {label}
+ </dt>
+ <dd class="mt-1 text-sm text-gray-800 dark:text-gray-200">
+ <Code>{name}</Code>{" "}
+ {missing ? (
+ <span class="font-semibold text-red-700 dark:text-red-300">
+ <i18n.Translate>Unavailable reference</i18n.Translate>
+ </span>
+ ) : description ? (
+ <span class="ml-1 text-gray-600 dark:text-gray-300">
+ {description}
+ </span>
+ ) : undefined}
+ </dd>
+ </div>
+ );
+}
+
+export function TokenField({
+ label,
+ values,
+}: {
+ label: TranslatedString;
+ values: string[];
+}): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div>
+ <dt class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
+ {label}
+ </dt>
+ <dd class="mt-1 flex flex-wrap gap-1.5">
+ {values.length ? (
+ values.map((value) => <Code key={value}>{value}</Code>)
+ ) : (
+ <span class="text-sm text-gray-500 dark:text-gray-400">
+ <i18n.Translate>None</i18n.Translate>
+ </span>
+ )}
+ </dd>
+ </div>
+ );
+}
+
+export function ContextField({
+ context,
+}: {
+ context: object | undefined;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const hasContext = context && Object.keys(context).length > 0;
+ return (
+ <div>
+ <dt class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
+ <i18n.Translate>Configured context</i18n.Translate>
+ </dt>
+ <dd class="mt-1">
+ {hasContext ? (
+ <pre class="overflow-x-auto rounded bg-gray-50 p-3 text-xs text-gray-800 dark:bg-gray-900 dark:text-gray-200">
+ {JSON.stringify(context, undefined, 2)}
+ </pre>
+ ) : (
+ <span class="text-sm text-gray-500 dark:text-gray-400">
+ <i18n.Translate>None</i18n.Translate>
+ </span>
+ )}
+ </dd>
+ </div>
+ );
+}
+
+export function Code({ children }: { children: string }): VNode {
+ return (
+ <code class="break-all rounded bg-gray-100 px-1.5 py-0.5 text-xs font-medium text-gray-800 dark:bg-gray-800 dark:text-gray-200">
+ {children}
+ </code>
+ );
+}
+
+export function localizedCheckDescription(
+ check: KycCheckInformation,
+ language: string,
+): string {
+ const baseLanguage = language.split("-")[0];
+ return (
+ check.description_i18n?.[language] ??
+ check.description_i18n?.[baseLanguage] ??
+ check.description
+ );
+}
+
+function labelForMeasureCategory(
+ category: MeasureCategory,
+ i18n: ReturnType<typeof useTranslationContext>["i18n"],
+): TranslatedString {
+ switch (category) {
+ case "customer-form":
+ return i18n.str`Customer form`;
+ case "information":
+ return i18n.str`Information notice`;
+ case "instant":
+ return i18n.str`Instant AML program`;
+ case "standalone-check":
+ return i18n.str`Standalone KYC check`;
+ }
+}
diff --git a/packages/taler-exchange-aml-webui/src/components/MeasureList.tsx b/packages/taler-exchange-aml-webui/src/components/MeasureList.tsx
@@ -1,124 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022-2025 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 {
- assertUnreachable,
- HttpStatusCode,
- TalerError,
-} from "@gnu-taler/taler-util";
-import {
- Attention,
- ErrorLoading,
- FailLoading,
- Loading,
- RouteDefinition,
- useTranslationContext,
-} from "@gnu-taler/web-util/browser";
-import { Fragment, h } from "preact";
-import { useServerMeasures } from "../hooks/server-info.js";
-import { Profile } from "../pages/Profile.js";
-import { computeMeasureInformation } from "../utils/computeAvailableMesaures.js";
-import { CurrentMeasureTable } from "./MeasuresTable.js";
-
-const TALER_SCREEN_ID = 124;
-
-export function MeasureList({ routeToNew }: { routeToNew: RouteDefinition }) {
- const { i18n } = useTranslationContext();
-
- const measures = useServerMeasures();
- // const [custom] = useCustomMeasures();
-
- if (!measures) {
- return <Loading />;
- }
- if (measures instanceof TalerError) {
- return (
- <ErrorLoading
- title={i18n.str`Failed to load server measures.`}
- error={measures}
- />
- );
- }
-
- if (measures.type === "fail") {
- return (
- <Fragment>
- <Profile />
- <FailLoading
- operation={measures}
- title={i18n.str`Failed to load the measures`}
- translate={(d) => {
- switch (d.case) {
- case HttpStatusCode.Forbidden:
- return (
- <i18n.Translate>
- This session signature is invalid, contact administrator or
- create a new one.
- </i18n.Translate>
- );
- case HttpStatusCode.NotFound:
- return (
- <i18n.Translate>
- The designated AML session is not known, contact
- administrator or create a new one.
- </i18n.Translate>
- );
- case HttpStatusCode.Conflict:
- return (
- <i18n.Translate>
- The designated AML session is not enabled, contact
- administrator or create a new one.
- </i18n.Translate>
- );
- default:
- assertUnreachable(d.case);
- }
- }}
- />
- </Fragment>
- );
- }
-
- const ms = computeMeasureInformation(measures.body);
-
- return (
- <div>
- <div class="px-4 sm:px-6 lg:px-8">
- <div class="sm:flex sm:items-center">
- <div class="sm:flex-auto">
- <h1 class="text-base font-semibold text-gray-900">
- <i18n.Translate>Measures</i18n.Translate>
- </h1>
- <p class="mt-2 text-sm text-gray-700">
- <i18n.Translate>
- A list of all the predefined measures you can use.
- </i18n.Translate>
- </p>
- </div>
- <div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
- <a
- href={routeToNew.url({})}
- class="block rounded-md bg-indigo-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
- >
- <i18n.Translate>Add custom measure</i18n.Translate>
- </a>
- </div>
- </div>
-
- <CurrentMeasureTable measures={ms} />
- </div>
- </div>
- );
-}
diff --git a/packages/taler-exchange-aml-webui/src/components/MeasureSelection.test.ts b/packages/taler-exchange-aml-webui/src/components/MeasureSelection.test.ts
@@ -0,0 +1,56 @@
+/*
+ This file is part of GNU Taler
+ (C) 2026 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.
+*/
+
+import {
+ AvailableMeasureSummary,
+ i18n,
+ setupI18n,
+} from "@gnu-taler/taler-util";
+import assert from "node:assert/strict";
+import test from "node:test";
+import {
+ categoryForMeasureDefinition,
+ descriptionForMeasureDefinition,
+} from "./MeasureSelection.js";
+
+setupI18n("de-CH", {});
+
+const summary: AvailableMeasureSummary = {
+ roots: {},
+ checks: {
+ identity: {
+ description: "Collect identity",
+ description_i18n: { de: "Identität erfassen" },
+ requires: [],
+ outputs: [],
+ fallback: "manual-review",
+ },
+ },
+ programs: {
+ review: {
+ description: "Review the submission",
+ context: [],
+ inputs: [],
+ },
+ },
+ default_rules: [],
+};
+
+test("measure choices use the same category and localized detail summary", () => {
+ const measure = { check_name: "identity", prog_name: "review" };
+ assert.equal(categoryForMeasureDefinition(measure), "customer-form");
+ assert.equal(
+ descriptionForMeasureDefinition(measure, summary, "de-CH", i18n),
+ "Customer form · Identität erfassen · Review the submission",
+ );
+ assert.equal(
+ categoryForMeasureDefinition({ check_name: "SKIP", prog_name: "review" }),
+ "instant",
+ );
+});
diff --git a/packages/taler-exchange-aml-webui/src/components/MeasureSelection.ts b/packages/taler-exchange-aml-webui/src/components/MeasureSelection.ts
@@ -0,0 +1,55 @@
+/*
+ This file is part of GNU Taler
+ (C) 2026 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.
+*/
+
+import {
+ AvailableMeasureSummary,
+ InternationalizationAPI,
+ MeasureInformation,
+} from "@gnu-taler/taler-util";
+import {
+ MeasureCategory,
+ localizedCheckDescription,
+} from "./MeasureDetails.js";
+import { isBuiltInSkipCheck } from "../utils/measure-check.js";
+
+export function categoryForMeasureDefinition(
+ measure: MeasureInformation,
+): MeasureCategory {
+ if (isBuiltInSkipCheck(measure.check_name)) return "instant";
+ return measure.prog_name ? "customer-form" : "information";
+}
+
+export function descriptionForMeasureDefinition(
+ measure: MeasureInformation,
+ summary: AvailableMeasureSummary,
+ language: string,
+ i18n: InternationalizationAPI,
+ category = categoryForMeasureDefinition(measure),
+): string {
+ const check = summary.checks[measure.check_name];
+ const program = measure.prog_name
+ ? summary.programs[measure.prog_name]
+ : undefined;
+ const categoryLabel =
+ category === "instant"
+ ? i18n.str`Instant AML program`
+ : category === "customer-form"
+ ? i18n.str`Customer form`
+ : category === "standalone-check"
+ ? i18n.str`Standalone KYC check`
+ : i18n.str`Information notice`;
+ const checkDescription = check
+ ? localizedCheckDescription(check, language)
+ : isBuiltInSkipCheck(measure.check_name)
+ ? i18n.str`Runs without customer interaction`
+ : undefined;
+ return [categoryLabel, checkDescription, program?.description]
+ .filter((part): part is string => !!part)
+ .join(" · ");
+}
diff --git a/packages/taler-exchange-aml-webui/src/components/MeasuresTable.tsx b/packages/taler-exchange-aml-webui/src/components/MeasuresTable.tsx
@@ -1,327 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022-2025 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 { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { ComponentChildren, Fragment, h, VNode } from "preact";
-import {
- MeasureInfo,
- UiMeasureInformation,
-} from "../utils/computeAvailableMesaures.js";
-
-const TALER_SCREEN_ID = 123;
-
-export function CurrentMeasureTable({
- measures,
- onSelect,
- hideMeasureNames,
- actionLabel,
-}: {
- measures: UiMeasureInformation;
- hideMeasureNames?: boolean;
- onSelect?: (m: MeasureInfo) => void;
- actionLabel?: ComponentChildren;
-}): VNode {
- const { i18n } = useTranslationContext();
- return (
- <Fragment>
- {!measures.forms.length ? undefined : (
- <div class="mt-4 flow-root">
- <div class="sm:flex sm:items-center">
- <div class="sm:flex-auto">
- <h4 class="text-base font-semibold text-gray-900 dark:text-gray-100">
- <i18n.Translate>Forms</i18n.Translate>
- </h4>
- <p class="mt-2 text-sm text-gray-700 dark:text-gray-300">
- <i18n.Translate>
- Measures used to gather information about the customer.
- </i18n.Translate>
- </p>
- </div>
- </div>
-
- <div class="min-w-full py-2 align-middle">
- <div class="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700">
- <table class="min-w-[42rem] divide-y divide-gray-300 dark:divide-gray-700">
- <thead class="bg-gray-50 dark:bg-gray-900">
- <tr>
- {onSelect ? (
- <th scope="col" class="relative p-2 ">
- <span class="sr-only">
- <i18n.Translate>Select</i18n.Translate>
- </span>
- </th>
- ) : (
- <Fragment />
- )}
- {hideMeasureNames ? undefined : (
- <th
- scope="col"
- class="p-2 text-left text-sm font-semibold text-gray-900 sm:pl-6 dark:text-gray-100"
- >
- <i18n.Translate>Name</i18n.Translate>
- </th>
- )}
- <th
- scope="col"
- class="p-2 text-left text-sm font-semibold text-gray-900 dark:text-gray-100"
- >
- <i18n.Translate>Check</i18n.Translate>
- </th>
- <th
- scope="col"
- class="p-2 text-left text-sm font-semibold text-gray-900 dark:text-gray-100"
- >
- <i18n.Translate>Program</i18n.Translate>
- </th>
- <th
- scope="col"
- class="p-2 text-left text-sm font-semibold text-gray-900 dark:text-gray-100"
- >
- <i18n.Translate>Context</i18n.Translate>
- </th>
- </tr>
- </thead>
- <tbody class="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-950">
- {measures.forms.map((m, k) => {
- return (
- <tr class="even:bg-gray-50 dark:even:bg-gray-900" key={k}>
- {!onSelect ? undefined : (
- <td class="relative whitespace-nowrap p-2 text-right text-sm font-medium ">
- <button
- onClick={() => onSelect(m)}
- class="min-h-10 rounded-md bg-primary px-3 py-2 text-sm font-semibold text-onPrimary hover:bg-primary/90 dark:bg-darkPrimary dark:text-darkOnPrimary"
- >
- {actionLabel ?? (
- <i18n.Translate>Modify</i18n.Translate>
- )}
- </button>
- </td>
- )}
- {hideMeasureNames ? undefined : (
- <td class="whitespace-nowrap p-2 text-sm font-medium text-gray-900 dark:text-gray-100">
- {m.name}
- </td>
- )}
- <td class="p-2 text-sm text-gray-500 dark:text-gray-300">
- {m.check?.description ?? ""}
- </td>
- <td class="p-2 text-sm text-gray-500 dark:text-gray-300">
- {m.program?.description}
- </td>
- <td class="whitespace-nowrap p-2 text-sm text-gray-500 dark:text-gray-300">
- {Object.keys(m.context ?? {}).join(", ")}
- </td>
- </tr>
- );
- })}
- </tbody>
- </table>
- </div>
- </div>
- </div>
- )}
-
- {!measures.info.length ? undefined : (
- <div class="mt-4 flow-root">
- <div class="sm:flex sm:items-center">
- <div class="sm:flex-auto">
- <h4 class="text-base font-semibold text-gray-900 dark:text-gray-100">
- <i18n.Translate>Information</i18n.Translate>
- </h4>
- <p class="mt-2 text-sm text-gray-700 dark:text-gray-300">
- <i18n.Translate>
- Shows information to the customer without running an AML
- program.
- </i18n.Translate>
- </p>
- </div>
- </div>
-
- <div class="min-w-full py-2 align-middle">
- <div class="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700">
- <table class="min-w-[36rem] divide-y divide-gray-300 dark:divide-gray-700">
- <thead class="bg-gray-50 dark:bg-gray-900">
- <tr>
- {onSelect ? (
- <th scope="col" class="relative p-2 ">
- <span class="sr-only">
- <i18n.Translate>Select</i18n.Translate>
- </span>
- </th>
- ) : (
- <Fragment />
- )}
- {hideMeasureNames ? undefined : (
- <th
- scope="col"
- class="p-2 text-left text-sm font-semibold text-gray-900 sm:pl-6 dark:text-gray-100"
- >
- <i18n.Translate>Name</i18n.Translate>
- </th>
- )}
- <th
- scope="col"
- class="p-2 text-left text-sm font-semibold text-gray-900 sm:pl-6 dark:text-gray-100"
- >
- <i18n.Translate>Check</i18n.Translate>
- </th>
- <th
- scope="col"
- class="p-2 text-left text-sm font-semibold text-gray-900 dark:text-gray-100"
- >
- <i18n.Translate>Context</i18n.Translate>
- </th>
- </tr>
- </thead>
- <tbody class="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-950">
- {measures.info.map((m, k) => {
- return (
- <tr class="even:bg-gray-50 dark:even:bg-gray-900" key={k}>
- {onSelect ? (
- <td class="relative whitespace-nowrap p-2 text-right text-sm font-medium ">
- <button
- onClick={() => onSelect(m)}
- class="min-h-10 rounded-md bg-primary px-3 py-2 text-sm font-semibold text-onPrimary hover:bg-primary/90 dark:bg-darkPrimary dark:text-darkOnPrimary"
- >
- {actionLabel ?? (
- <i18n.Translate>Modify</i18n.Translate>
- )}
- </button>
- </td>
- ) : (
- <Fragment />
- )}
- {hideMeasureNames ? undefined : (
- <td class="whitespace-nowrap p-2 text-sm font-medium text-gray-900 sm:pl-6 dark:text-gray-100">
- {m.name}
- </td>
- )}
- <td class="whitespace-nowrap p-2 text-sm text-gray-500 dark:text-gray-300">
- {m.checkName}
- </td>
- <td class="whitespace-nowrap p-2 text-sm text-gray-500 dark:text-gray-300">
- {Object.keys(m.context ?? {}).join(", ")}
- </td>
- </tr>
- );
- })}
- </tbody>
- </table>
- </div>
- </div>
- </div>
- )}
-
- {!measures.procedures.length ? undefined : (
- <div class="mt-4 flow-root">
- <div class="sm:flex sm:items-center">
- <div class="sm:flex-auto">
- <h4 class="text-base font-semibold text-gray-900 dark:text-gray-100">
- <i18n.Translate>Instant measures</i18n.Translate>
- </h4>
- <p class="mt-2 text-sm text-gray-700 dark:text-gray-300">
- <i18n.Translate>
- Triggered immediately without customer interaction.
- </i18n.Translate>
- </p>
- </div>
- </div>
-
- <div class="min-w-full py-2 align-middle">
- <div class="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700">
- <table class="min-w-[42rem] divide-y divide-gray-300 dark:divide-gray-700">
- <thead class="bg-gray-50 dark:bg-gray-900">
- <tr>
- {onSelect ? (
- <th scope="col" class="relative p-2 ">
- <span class="sr-only">
- <i18n.Translate>Select</i18n.Translate>
- </span>
- </th>
- ) : (
- <Fragment />
- )}
- {hideMeasureNames ? undefined : (
- <th
- scope="col"
- class="p-2 text-left text-sm font-semibold text-gray-900 sm:pl-6 dark:text-gray-100"
- >
- <i18n.Translate>Name</i18n.Translate>
- </th>
- )}
- <th
- scope="col"
- class="p-2 text-left text-sm font-semibold text-gray-900 dark:text-gray-100"
- >
- <i18n.Translate>Program</i18n.Translate>
- </th>
- <th
- scope="col"
- class="p-2 text-left text-sm font-semibold text-gray-900 dark:text-gray-100"
- >
- <i18n.Translate>Input requirement</i18n.Translate>
- </th>
- <th
- scope="col"
- class="p-2 text-left text-sm font-semibold text-gray-900 dark:text-gray-100"
- >
- <i18n.Translate>Context</i18n.Translate>
- </th>
- </tr>
- </thead>
- <tbody class="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-950">
- {measures.procedures.map((m, k) => {
- return (
- <tr class="even:bg-gray-50 dark:even:bg-gray-900" key={k}>
- {onSelect ? (
- <td class="relative whitespace-nowrap p-2 text-right text-sm font-medium ">
- <button
- onClick={() => onSelect(m)}
- class="min-h-10 rounded-md bg-primary px-3 py-2 text-sm font-semibold text-onPrimary hover:bg-primary/90 dark:bg-darkPrimary dark:text-darkOnPrimary"
- >
- {actionLabel ?? (
- <i18n.Translate>Modify</i18n.Translate>
- )}
- </button>
- </td>
- ) : (
- <Fragment />
- )}
- {hideMeasureNames ? undefined : (
- <td class="whitespace-nowrap p-2 text-sm font-medium text-gray-900 sm:pl-6 dark:text-gray-100">
- {m.name}
- </td>
- )}
- <td class="whitespace-nowrap p-2 text-sm text-gray-500 dark:text-gray-300">
- {m.program.description}
- </td>
- <td class="whitespace-nowrap p-2 text-sm text-gray-500 dark:text-gray-300">
- {m.program.inputs.join(",")}
- </td>
- <td class="whitespace-nowrap p-2 text-sm text-gray-500 dark:text-gray-300">
- {Object.keys(m.context ?? {}).join(", ")}
- </td>
- </tr>
- );
- })}
- </tbody>
- </table>
- </div>
- </div>
- </div>
- )}
- </Fragment>
- );
-}
diff --git a/packages/taler-exchange-aml-webui/src/components/ShowLegitimizationInfo.tsx b/packages/taler-exchange-aml-webui/src/components/ShowLegitimizationInfo.tsx
@@ -14,8 +14,7 @@ import {
import { Time, useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
-import { computeMeasureInformation } from "../utils/computeAvailableMesaures.js";
-import { CurrentMeasureTable } from "./MeasuresTable.js";
+import { MeasureCard } from "./MeasureDetails.js";
export function ShowLegistimizationInfo({
since,
@@ -32,12 +31,9 @@ export function ShowLegistimizationInfo({
completed: boolean;
serverMeasures: AvailableMeasureSummary | undefined;
}): VNode {
- const { i18n } = useTranslationContext();
+ const { i18n, lang } = useTranslationContext();
const [opened, setOpened] = useState(startOpen ?? false);
const expandable = !fixed;
- const info = computeMeasureInformation(serverMeasures, {
- measureList: legitimization.measures,
- });
return (
<div class="overflow-hidden rounded-lg border border-gray-300 dark:border-gray-700">
@@ -106,7 +102,16 @@ export function ShowLegistimizationInfo({
</i18n.Translate>
)}
</p>
- <CurrentMeasureTable measures={info} hideMeasureNames />
+ <div class="grid gap-4 lg:grid-cols-2">
+ {legitimization.measures.map((measure, index) => (
+ <MeasureCard
+ key={index}
+ measure={measure}
+ summary={serverMeasures}
+ language={lang}
+ />
+ ))}
+ </div>
</>
)}
</div>
diff --git a/packages/taler-exchange-aml-webui/src/pages/Info.tsx b/packages/taler-exchange-aml-webui/src/pages/Info.tsx
@@ -11,8 +11,6 @@ import {
assertUnreachable,
AvailableMeasureSummary,
HttpStatusCode,
- KycCheckInformation,
- MeasureInformation,
TalerError,
TranslatedString,
} from "@gnu-taler/taler-util";
@@ -25,7 +23,14 @@ import {
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
-import { RulesInfo, labelForOperationType } from "../components/RulesInfo.js";
+import {
+ CheckCard,
+ Code,
+ DataCard,
+ MeasureCard,
+ TokenField,
+} from "../components/MeasureDetails.js";
+import { RulesInfo } from "../components/RulesInfo.js";
import { useAmlSpaDialect, usePreferences } from "../hooks/preferences.js";
import {
revalidateServerMeasures,
@@ -36,7 +41,6 @@ import {
DialectPropertyInfo,
getDialectInfo,
} from "../utils/dialect-info.js";
-import { isBuiltInSkipCheck } from "../utils/measure-check.js";
export function Info(): VNode {
const { config, url } = useExchangeApiContext();
@@ -352,7 +356,7 @@ export function MeasuresContent({
{roots.length ? (
<div class="grid gap-4 lg:grid-cols-2">
{roots.map(([name, measure]) => (
- <RootMeasureCard
+ <MeasureCard
key={name}
name={name}
measure={measure}
@@ -457,207 +461,6 @@ function MeasureGroup({
);
}
-function RootMeasureCard({
- name,
- measure,
- summary,
- language,
-}: {
- name: string;
- measure: MeasureInformation;
- summary: AvailableMeasureSummary;
- language: string;
-}): VNode {
- const { i18n } = useTranslationContext();
- const check = summary.checks[measure.check_name];
- const isBuiltInSkip = isBuiltInSkipCheck(measure.check_name);
- const program = measure.prog_name
- ? summary.programs[measure.prog_name]
- : undefined;
- return (
- <DataCard title={name}>
- <ReferenceField
- label={i18n.str`KYC check`}
- name={measure.check_name}
- description={
- isBuiltInSkip
- ? i18n.str`Built-in check that proceeds without collecting customer data.`
- : check && localizedCheckDescription(check, language)
- }
- missing={!isBuiltInSkip && !check}
- />
- {measure.prog_name ? (
- <ReferenceField
- label={i18n.str`AML program`}
- name={measure.prog_name}
- description={program?.description}
- missing={!program}
- />
- ) : (
- <SimpleField label={i18n.str`AML program`} value={i18n.str`None`} />
- )}
- <SimpleField
- label={i18n.str`Operation`}
- value={
- measure.operation_type
- ? labelForOperationType(measure.operation_type, i18n)
- : i18n.str`Not specified`
- }
- />
- <SimpleField
- label={i18n.str`Voluntary`}
- value={measure.voluntary ? i18n.str`Yes` : i18n.str`No`}
- />
- <ContextField context={measure.context} />
- </DataCard>
- );
-}
-
-function CheckCard({
- name,
- check,
- language,
- fallbackAvailable,
-}: {
- name: string;
- check: KycCheckInformation;
- language: string;
- fallbackAvailable: boolean;
-}): VNode {
- const { i18n } = useTranslationContext();
- return (
- <DataCard
- title={name}
- description={localizedCheckDescription(check, language)}
- >
- <TokenField label={i18n.str`Required context`} values={check.requires} />
- <TokenField label={i18n.str`Outputs`} values={check.outputs} />
- <ReferenceField
- label={i18n.str`Fallback measure`}
- name={check.fallback}
- missing={!fallbackAvailable}
- />
- </DataCard>
- );
-}
-
-function DataCard({
- title,
- description,
- children,
-}: {
- title: string;
- description?: string;
- children: VNode | VNode[];
-}): VNode {
- return (
- <article class="rounded-lg border border-gray-200 bg-white p-5">
- <Code>{title}</Code>
- {description ? (
- <p class="mt-3 text-sm leading-6 text-gray-700">{description}</p>
- ) : undefined}
- <dl class="mt-4 space-y-3">{children}</dl>
- </article>
- );
-}
-
-function SimpleField({
- label,
- value,
-}: {
- label: TranslatedString;
- value: string | TranslatedString;
-}): VNode {
- return (
- <div>
- <dt class="text-xs font-semibold uppercase tracking-wide text-gray-500">
- {label}
- </dt>
- <dd class="mt-1 text-sm text-gray-800">{value}</dd>
- </div>
- );
-}
-
-function ReferenceField({
- label,
- name,
- description,
- missing = false,
-}: {
- label: TranslatedString;
- name: string;
- description?: string;
- missing?: boolean;
-}): VNode {
- const { i18n } = useTranslationContext();
- return (
- <div>
- <dt class="text-xs font-semibold uppercase tracking-wide text-gray-500">
- {label}
- </dt>
- <dd class="mt-1 text-sm text-gray-800">
- <Code>{name}</Code>{" "}
- {missing ? (
- <span class="font-semibold text-red-700">
- <i18n.Translate>Unavailable reference</i18n.Translate>
- </span>
- ) : description ? (
- <span class="ml-1 text-gray-600">{description}</span>
- ) : undefined}
- </dd>
- </div>
- );
-}
-
-function TokenField({
- label,
- values,
-}: {
- label: TranslatedString;
- values: string[];
-}): VNode {
- const { i18n } = useTranslationContext();
- return (
- <div>
- <dt class="text-xs font-semibold uppercase tracking-wide text-gray-500">
- {label}
- </dt>
- <dd class="mt-1 flex flex-wrap gap-1.5">
- {values.length ? (
- values.map((value) => <Code key={value}>{value}</Code>)
- ) : (
- <span class="text-sm text-gray-500">
- <i18n.Translate>None</i18n.Translate>
- </span>
- )}
- </dd>
- </div>
- );
-}
-
-function ContextField({ context }: { context: object | undefined }): VNode {
- const { i18n } = useTranslationContext();
- const hasContext = context && Object.keys(context).length > 0;
- return (
- <div>
- <dt class="text-xs font-semibold uppercase tracking-wide text-gray-500">
- <i18n.Translate>Context</i18n.Translate>
- </dt>
- <dd class="mt-1">
- {hasContext ? (
- <pre class="overflow-x-auto rounded bg-gray-50 p-3 text-xs text-gray-800">
- {JSON.stringify(context, undefined, 2)}
- </pre>
- ) : (
- <span class="text-sm text-gray-500">
- <i18n.Translate>None</i18n.Translate>
- </span>
- )}
- </dd>
- </div>
- );
-}
-
function EmptyData({ children }: { children: VNode }): VNode {
return (
<p class="rounded-lg border border-dashed border-gray-300 bg-gray-50 p-5 text-sm text-gray-600">
@@ -666,22 +469,4 @@ function EmptyData({ children }: { children: VNode }): VNode {
);
}
-function Code({ children }: { children: string }): VNode {
- return (
- <code class="break-all rounded bg-gray-100 px-1.5 py-0.5 text-xs font-medium text-gray-800">
- {children}
- </code>
- );
-}
-
-export function localizedCheckDescription(
- check: KycCheckInformation,
- language: string,
-): string {
- const baseLanguage = language.split("-")[0];
- return (
- check.description_i18n?.[language] ??
- check.description_i18n?.[baseLanguage] ??
- check.description
- );
-}
+export { localizedCheckDescription } from "../components/MeasureDetails.js";
diff --git a/packages/taler-exchange-aml-webui/src/pages/decision/Summary.tsx b/packages/taler-exchange-aml-webui/src/pages/decision/Summary.tsx
@@ -39,15 +39,11 @@ import {
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { ConfirmationModal } from "../../components/ConfirmationModal.js";
-import { CurrentMeasureTable } from "../../components/MeasuresTable.js";
+import { Code, MeasureCard } from "../../components/MeasureDetails.js";
import { RulesInfo } from "../../components/RulesInfo.js";
import { useCurrentDecisionRequest } from "../../hooks/decision-request.js";
import { OfficerReady } from "../../hooks/officer.js";
import { useServerMeasures } from "../../hooks/server-info.js";
-import {
- computeMeasureInformation,
- UiMeasureInformation,
-} from "../../utils/computeAvailableMesaures.js";
import { buildAmlDecisionRequest } from "../../utils/decision-validation.js";
import { deadlineIsValid, rulesAreValid } from "../../utils/rule-validation.js";
import {
@@ -80,7 +76,7 @@ export function Summary({
officer: OfficerReady;
onMove: (n: WizardSteps | undefined) => void;
}): VNode {
- const { i18n } = useTranslationContext();
+ const { i18n, lang } = useTranslationContext();
const [decision, , cleanUpDecision] = useCurrentDecisionRequest();
const measures = useServerMeasures();
@@ -91,25 +87,18 @@ export function Summary({
measures && !(measures instanceof TalerError) && measures.type === "ok"
? measures.body
: undefined;
- const configuredMeasures = computeMeasureInformation(measureSummary);
- const customMeasures = computeMeasureInformation(measureSummary, {
- measureMap: decision.custom_measures,
- });
- const allMeasures: UiMeasureInformation = {
- forms: [...configuredMeasures.forms, ...customMeasures.forms],
- procedures: [
- ...configuredMeasures.procedures,
- ...customMeasures.procedures,
- ],
- info: [...configuredMeasures.info, ...customMeasures.info],
- };
-
const d = decision.new_measures === undefined ? [] : decision.new_measures;
- const activeMeasureInfo: UiMeasureInformation = {
- forms: allMeasures.forms.filter((m) => d.indexOf(m.name) !== -1),
- procedures: allMeasures.procedures.filter((m) => d.indexOf(m.name) !== -1),
- info: allMeasures.info.filter((m) => d.indexOf(m.name) !== -1),
+ const allMeasureDefinitions = {
+ ...(measureSummary?.roots ?? {}),
+ ...(decision.custom_measures ?? {}),
};
+ const activeMeasureInfo = d.flatMap((name) => {
+ const measure = allMeasureDefinitions[name];
+ return measure ? [{ name, measure }] : [];
+ });
+ const unresolvedMeasureNames = d.filter(
+ (name) => allMeasureDefinitions[name] === undefined,
+ );
// preserve-investigate
const { lib } = useExchangeApiContext();
@@ -304,9 +293,12 @@ export function Summary({
</Attention>
) : undefined}
{built.errors.includes("multiple-skip-measures") ? (
- <Attention type="danger" title={i18n.str`Too many SKIP measures`}>
+ <Attention
+ type="danger"
+ title={i18n.str`Only one instant measure is allowed`}
+ >
<i18n.Translate>
- At most one immediate measure may use the SKIP check.
+ Remove all but one instant measure before submitting this decision.
</i18n.Translate>
</Attention>
) : undefined}
@@ -402,7 +394,53 @@ export function Summary({
<ReviewStepButton onClick={() => onMove("measures")} />
</Attention>
) : (
- <CurrentMeasureTable measures={activeMeasureInfo} />
+ <section class="space-y-3">
+ <div class="flex flex-wrap items-baseline justify-between gap-2">
+ <h2 class="text-lg font-semibold text-gray-950 dark:text-gray-50">
+ <i18n.Translate>Immediate measures</i18n.Translate>
+ </h2>
+ {decision.new_measures!.length > 1 ? (
+ <p class="text-sm font-medium text-gray-600 dark:text-gray-300">
+ {decision.measures_and ? (
+ <i18n.Translate>
+ Every selected measure is required.
+ </i18n.Translate>
+ ) : (
+ <i18n.Translate>
+ Any one selected measure is sufficient.
+ </i18n.Translate>
+ )}
+ </p>
+ ) : undefined}
+ </div>
+ <div class="grid gap-4 lg:grid-cols-2">
+ {activeMeasureInfo.map(({ name, measure }) => (
+ <MeasureCard
+ key={name}
+ name={name}
+ measure={measure}
+ summary={measureSummary}
+ language={lang}
+ />
+ ))}
+ </div>
+ {unresolvedMeasureNames.length ? (
+ <Attention type="warning" title={i18n.str`Unavailable measures`}>
+ <p>
+ <i18n.Translate>
+ The exchange did not return definitions for these selected
+ measures:
+ </i18n.Translate>{" "}
+ {unresolvedMeasureNames.map((name) => (
+ <span class="mr-2" key={name}>
+ <Code>{name}</Code>
+ </span>
+ ))}
+ </p>
+ </Attention>
+ ) : undefined}
+ <ReviewStepButton onClick={() => onMove("measures")} />
+ </section>
)}
{INVALID_PROPERTIES ? (
<Attention
diff --git a/packages/taler-exchange-aml-webui/src/utils/computeAvailableMesaures.test.ts b/packages/taler-exchange-aml-webui/src/utils/computeAvailableMesaures.test.ts
@@ -1,32 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2026 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.
-*/
-import { AvailableMeasureSummary } from "@gnu-taler/taler-util";
-import assert from "node:assert/strict";
-import test from "node:test";
-import { computeMeasureInformation } from "./computeAvailableMesaures.js";
-
-test("lowercase skip measures remain available as procedures", () => {
- const summary: AvailableMeasureSummary = {
- roots: {
- automatic: { check_name: "skip", prog_name: "review" },
- },
- checks: {},
- programs: {
- review: { description: "Review account", context: [], inputs: [] },
- },
- default_rules: [],
- };
- const measures = computeMeasureInformation(summary);
- assert.deepEqual(
- measures.procedures.map(({ name, programName }) => ({ name, programName })),
- [{ name: "automatic", programName: "review" }],
- );
- assert.deepEqual(measures.forms, []);
- assert.deepEqual(measures.info, []);
-});
diff --git a/packages/taler-exchange-aml-webui/src/utils/computeAvailableMesaures.ts b/packages/taler-exchange-aml-webui/src/utils/computeAvailableMesaures.ts
@@ -1,163 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022-2025 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 type {
- AmlProgramRequirement,
- AvailableMeasureSummary,
- KycCheckInformation,
- MeasureInformation,
-} from "@gnu-taler/taler-util";
-import { isBuiltInSkipCheck } from "./measure-check.js";
-
-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: {
- measureList?: MeasureInformation[];
- measureMap?: Record<string, MeasureInformation> | undefined;
- } = {},
-): UiMeasureInformation {
- const init: UiMeasureInformation = { forms: [], procedures: [], info: [] };
- if (!serverMeasures) {
- return init;
- }
-
- 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);
-
- 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;
- }
- }
- }
- return prev;
- }, init);
-}
-
-/**
- *
- * @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 (!isBuiltInSkipCheck(measure.check_name)) {
- const check = serverMeasures.checks[measure.check_name];
- if (!check) return undefined;
- if (!measure.prog_name) {
- const r: MeasureInfo = {
- type: "info",
- name: name ?? "",
- context: measure.context,
- checkName: measure.check_name,
- check,
- // custom: true,
- };
- return r;
- } else {
- const program = serverMeasures.programs[measure.prog_name];
- if (!program) return undefined;
- const r: MeasureInfo = {
- type: "form",
- name: name ?? "",
- context: measure.context,
- programName: measure.prog_name,
- program,
- 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 program = serverMeasures.programs[measure.prog_name];
- if (!program) return undefined;
- const r: MeasureInfo = {
- type: "procedure",
- name: name ?? "",
- context: measure.context,
- programName: measure.prog_name,
- program,
- // custom: false,
- };
- return r;
- }
-}