/* 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 */ import { AbsoluteTime, AmountJson, Amounts, Codec, HttpStatusCode, OperationFail, OperationOk, TalerError, TalerErrorDetail, TalerExchangeApi, TranslatedString, assertUnreachable, buildCodecForObject, codecForNumber, codecForString, codecOptional, } from "@gnu-taler/taler-util"; import { DefaultForm, ErrorLoading, FormMetadata, InternationalizationAPI, Loading, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; import { VNode, h } from "preact"; import { useState } from "preact/hooks"; import { privatePages } from "../Routing.js"; import { useUiFormsContext } from "../context/ui-forms.js"; import { preloadedForms } from "../forms/index.js"; import { useCaseDetails } from "../hooks/useCaseDetails.js"; import { ShowConsolidated } from "./ShowConsolidated.js"; export type AmlEvent = | AmlFormEvent | AmlFormEventError | KycCollectionEvent | KycExpirationEvent; type AmlFormEvent = { type: "aml-form"; when: AbsoluteTime; title: TranslatedString; justification: Justification; metadata: FormMetadata; state: TalerExchangeApi.AmlState; threshold: AmountJson; }; type AmlFormEventError = { type: "aml-form-error"; when: AbsoluteTime; title: TranslatedString; justification: undefined; metadata: undefined; state: TalerExchangeApi.AmlState; threshold: AmountJson; }; type KycCollectionEvent = { type: "kyc-collection"; when: AbsoluteTime; title: TranslatedString; values: object; provider: string; }; type KycExpirationEvent = { type: "kyc-expiration"; when: AbsoluteTime; title: TranslatedString; fields: string[]; }; type WithTime = { when: AbsoluteTime }; function selectSooner(a: WithTime, b: WithTime) { return AbsoluteTime.cmp(a.when, b.when); } function titleForJustification( op: ReturnType, i18n: InternationalizationAPI, ): TranslatedString { if (op.type === "ok") { return op.body.justification.label as TranslatedString; } switch (op.case) { case "not-json": return i18n.str`error: the justification is not a form`; case "id-not-found": return i18n.str`error: justification form's id not found`; case "version-not-found": return i18n.str`error: justification form's version not found`; case "form-not-found": return i18n.str`error: justification form not found`; default: { assertUnreachable(op.case); } } } export function getEventsFromAmlHistory( aml: TalerExchangeApi.AmlDecisionDetail[], kyc: TalerExchangeApi.KycDetail[], i18n: InternationalizationAPI, forms: FormMetadata[], ): AmlEvent[] { const ae: AmlEvent[] = aml.map((a) => { const just = parseJustification(a.justification, forms); return { type: just.type === "ok" ? "aml-form" : "aml-form-error", state: a.new_state, threshold: Amounts.parseOrThrow(a.new_threshold), title: titleForJustification(just, i18n), metadata: just.type === "ok" ? just.body.metadata : undefined, justification: just.type === "ok" ? just.body.justification : undefined, when: { t_ms: a.decision_time.t_s === "never" ? "never" : a.decision_time.t_s * 1000, }, } as AmlEvent; }); const ke = kyc.reduce((prev, k) => { prev.push({ type: "kyc-collection", title: i18n.str`collection`, when: AbsoluteTime.fromProtocolTimestamp(k.collection_time), values: !k.attributes ? {} : k.attributes, provider: k.provider_section, }); prev.push({ type: "kyc-expiration", title: i18n.str`expiration`, when: AbsoluteTime.fromProtocolTimestamp(k.expiration_time), fields: !k.attributes ? [] : Object.keys(k.attributes), }); return prev; }, [] as AmlEvent[]); return ae.concat(ke).sort(selectSooner); } export function CaseDetails({ account }: { account: string }) { const [selected, setSelected] = useState(AbsoluteTime.now()); const [showForm, setShowForm] = useState<{ justification: Justification; metadata: FormMetadata; }>(); const { i18n } = useTranslationContext(); const details = useCaseDetails(account); const { forms } = useUiFormsContext(); const allForms = [...forms, ...preloadedForms(i18n)]; if (!details) { return ; } if (details instanceof TalerError) { return ; } if (details.type === "fail") { switch (details.case) { case HttpStatusCode.Unauthorized: case HttpStatusCode.Forbidden: case HttpStatusCode.NotFound: case HttpStatusCode.Conflict: return
; default: assertUnreachable(details); } } const { aml_history, kyc_attributes } = details.body; const events = getEventsFromAmlHistory( aml_history, kyc_attributes, i18n, allForms, ); if (showForm !== undefined) { return (
); } return (
New AML form

Case history for account{" "} {account.substring(0, 16)}...

{ switch (e.type) { case "aml-form": { const { justification, metadata } = e; setShowForm({ justification, metadata }); break; } case "kyc-collection": case "kyc-expiration": { setSelected(e.when); break; } case "aml-form-error": } }} /> {/* {selected && } */} {selected && }
); } function AmlStateBadge({ state }: { state: TalerExchangeApi.AmlState }): VNode { switch (state) { case TalerExchangeApi.AmlState.normal: { return ( Normal ); } case TalerExchangeApi.AmlState.pending: { return ( Pending ); } case TalerExchangeApi.AmlState.frozen: { return ( Frozen ); } } assertUnreachable(state); } function ShowTimeline({ history, onSelect, }: { onSelect: (e: AmlEvent) => void; history: AmlEvent[]; }): VNode { return (
    {history.map((e, idx) => { const isLast = history.length - 1 === idx; return (
  • { onSelect(e); }} >
    {!isLast ? ( ) : undefined}
    {(() => { switch (e.type) { case "aml-form-error": case "aml-form": { return (
    {e.threshold.currency}{" "} {Amounts.stringifyValue(e.threshold)}
    ); } case "kyc-collection": { return ( // ); } case "kyc-expiration": { // return ; return ( ); } } assertUnreachable(e); })()}
    {e.type === "aml-form" ? ( {e.title} ) : (

    {e.title}

    )}
    {e.when.t_ms === "never" ? ( "never" ) : ( )}
  • ); })}
); } export type Justification> = { // form values value: T; } & Omit, "config">; type SimpleFormMetadata = { version?: number; id?: string; }; export const codecForSimpleFormMetadata = (): Codec => buildCodecForObject() .property("id", codecOptional(codecForString())) .property("version", codecOptional(codecForNumber())) .build("SimpleFormMetadata"); type ParseJustificationFail = | "not-json" | "id-not-found" | "form-not-found" | "version-not-found"; function parseJustification( s: string, listOfAllKnownForms: FormMetadata[], ): | OperationOk<{ justification: Justification; metadata: FormMetadata; }> | OperationFail { try { const justification = JSON.parse(s); const info = codecForSimpleFormMetadata().decode(justification); if (!info.id) { return { type: "fail", case: "id-not-found", detail: {} as TalerErrorDetail, }; } if (!info.version) { return { type: "fail", case: "version-not-found", detail: {} as TalerErrorDetail, }; } const found = listOfAllKnownForms.find((f) => { return f.id === info.id && f.version === info.version; }); if (!found) { return { type: "fail", case: "form-not-found", detail: {} as TalerErrorDetail, }; } return { type: "ok", body: { justification, metadata: found, }, }; } catch (e) { return { type: "fail", case: "not-json", detail: {} as TalerErrorDetail, }; } }