import { AbsoluteTime, AmountJson, Amounts, OperationResult, TalerError, TranslatedString, assertUnreachable } from "@gnu-taler/taler-util"; import { ErrorLoading, Loading, useTranslationContext } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { useCaseDetails } from "../hooks/useCaseDetails.js"; import { Pages } from "../pages.js"; import { AmlExchangeBackend } from "../types.js"; import { ShowConsolidated } from "./ShowConsolidated.js"; import { FormMetadata, Justification, allForms, parseJustification } from "./AntiMoneyLaunderingForm.js"; import { NiceForm } from "../NiceForm.js"; export type AmlEvent = AmlFormEvent | AmlFormEventError | KycCollectionEvent | KycExpirationEvent; type AmlFormEvent = { type: "aml-form"; when: AbsoluteTime; title: TranslatedString; justification: Justification; metadata: FormMetadata; state: AmlExchangeBackend.AmlState; threshold: AmountJson; }; type AmlFormEventError = { type: "aml-form-error"; when: AbsoluteTime; title: TranslatedString; justification: undefined, metadata: undefined, state: AmlExchangeBackend.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): TranslatedString { if (op.type === "ok") { return op.body.justification.label as TranslatedString; } switch (op.case) { case "not-json": return "error: the justification is not a form" as TranslatedString case "id-not-found": return "error: justification form's id not found" as TranslatedString case "version-not-found": return "error: justification form's version not found" as TranslatedString case "form-not-found": return `error: justification form not found` as TranslatedString } assertUnreachable(op.case) } export function getEventsFromAmlHistory( aml: AmlExchangeBackend.AmlDecisionDetail[], kyc: AmlExchangeBackend.KycDetail[], ): AmlEvent[] { const ae: AmlEvent[] = aml.map((a) => { const just = parseJustification(a.justification, allForms) return { type: just.type === "ok" ? "aml-form" : "aml-form-error", state: a.new_state, threshold: Amounts.parseOrThrow(a.new_threshold), title: titleForJustification(just), 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: "collection" as TranslatedString, when: AbsoluteTime.fromProtocolTimestamp(k.collection_time), values: !k.attributes ? {} : k.attributes, provider: k.provider_section, }); prev.push({ type: "kyc-expiration", title: "expiration" as TranslatedString, 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) if (!details) { return } if (details instanceof TalerError) { return } if (details.type === "fail") { switch (details.case) { case "unauthorized": case "officer-not-found": case "officer-disabled": return
default: assertUnreachable(details) } } const { aml_history, kyc_attributes } = details.body const events = getEventsFromAmlHistory(aml_history, kyc_attributes); 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: AmlExchangeBackend.AmlState }): VNode { switch (state) { case AmlExchangeBackend.AmlState.normal: { return ( Normal ); } case AmlExchangeBackend.AmlState.pending: { return ( Pending ); } case AmlExchangeBackend.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" ) : ( )}
  • ); })}
} function ShowEventDetails({ event }: { event: AmlEvent }): VNode { return
type {event.type}
; }