taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 507410eac6d51a57ccc0535e00b641c9285f1269
parent 255faaa1e685037dc05e9b051b3c36d26976c27b
Author: Sebastian <sebasjm@gmail.com>
Date:   Tue, 24 Jun 2025 11:55:09 -0300

fix #9874

Diffstat:
Mpackages/aml-backoffice-ui/src/components/MeasureList.tsx | 4++--
Mpackages/aml-backoffice-ui/src/components/MeasuresTable.tsx | 78+++++++++++++++++++++++++++++++++---------------------------------------------
Mpackages/aml-backoffice-ui/src/components/ShowLegitimizationInfo.tsx | 6+++---
Mpackages/aml-backoffice-ui/src/pages/AccountDetails.tsx | 2+-
Mpackages/aml-backoffice-ui/src/pages/decision/Measures.tsx | 80++++++-------------------------------------------------------------------------
Mpackages/aml-backoffice-ui/src/pages/decision/Summary.tsx | 39+++++++++++++++++++--------------------
Mpackages/aml-backoffice-ui/src/utils/computeAvailableMesaures.ts | 179+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Mpackages/taler-util/src/codec.ts | 30+++++++++++++++++++++++++-----
Mpackages/taler-util/src/types-taler-exchange.ts | 2+-
Mpackages/taler-util/src/types-taler-merchant.ts | 2+-
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"),