import { AbsoluteTime, AmountJson, Amounts, PaytoString, TalerError, TranslatedString, assertUnreachable, } from "@gnu-taler/taler-util"; import { ErrorLoading, Loading, useTranslationContext } from "@gnu-taler/web-util/browser"; import { ArrowDownCircleIcon, ClockIcon } from "@heroicons/react/20/solid"; import { format } from "date-fns"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { NiceForm } from "../NiceForm.js"; import { FlexibleForm } from "../forms/index.js"; import { UIFormField } from "../handlers/forms.js"; import { useCaseDetails } from "../hooks/useCaseDetails.js"; import { Pages } from "../pages.js"; import { AmlExchangeBackend } from "../types.js"; type AmlEvent = AmlFormEvent | KycCollectionEvent | KycExpirationEvent; type AmlFormEvent = { type: "aml-form"; when: AbsoluteTime; title: TranslatedString; 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 getEventsFromAmlHistory( aml: AmlExchangeBackend.AmlDecisionDetail[], kyc: AmlExchangeBackend.KycDetail[], ): AmlEvent[] { const ae: AmlEvent[] = aml.map((a) => { return { type: "aml-form", state: a.new_state, threshold: Amounts.parseOrThrow(a.new_threshold), title: a.justification as TranslatedString, 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(undefined); 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); return (
New AML form

Case history

    {events.map((e, idx) => { const isLast = events.length - 1 === idx; return (
  • { setSelected(e); }} >
    {!isLast ? ( ) : undefined}
    {(() => { switch (e.type) { case "aml-form": { switch (e.state) { case AmlExchangeBackend.AmlState.normal: { return (
    Normal {e.threshold.currency}{" "} {Amounts.stringifyValue(e.threshold)}
    ); } case AmlExchangeBackend.AmlState.pending: { return (
    Pending {e.threshold.currency}{" "} {Amounts.stringifyValue(e.threshold)}
    ); } case AmlExchangeBackend.AmlState.frozen: { return (
    Frozen {e.threshold.currency}{" "} {Amounts.stringifyValue(e.threshold)}
    ); } } } case "kyc-collection": { return ( ); } case "kyc-expiration": { return ; } } })()}

    {e.title}

    {e.when.t_ms === "never" ? ( "never" ) : ( )}
  • ); })}
{selected && } {selected && }
); } function ShowEventDetails({ event }: { event: AmlEvent }): VNode { return
type {event.type}
; } function ShowConsolidated({ history, until, }: { history: AmlEvent[]; until: AmlEvent; }): VNode { const cons = getConsolidated(history, until.when); const form: FlexibleForm = { versionId: "1", behavior: (form) => { return { aml: { threshold: { hidden: !form.aml }, since: { hidden: !form.aml }, state: { hidden: !form.aml } } }; }, design: [ { title: "AML" as TranslatedString, fields: [ { type: "amount", props: { label: "Threshold" as TranslatedString, name: "aml.threshold", }, }, { type: "choiceHorizontal", props: { label: "State" as TranslatedString, name: "aml.state", converter: amlStateConverter, choices: [ { label: "Frozen" as TranslatedString, value: AmlExchangeBackend.AmlState.frozen, }, { label: "Pending" as TranslatedString, value: AmlExchangeBackend.AmlState.pending, }, { label: "Normal" as TranslatedString, value: AmlExchangeBackend.AmlState.normal, }, ], }, }, ], }, Object.entries(cons.kyc).length > 0 ? { title: "KYC" as TranslatedString, fields: Object.entries(cons.kyc).map(([key, field]) => { const result: UIFormField = { type: "text", props: { label: key as TranslatedString, name: `kyc.${key}.value`, help: `${field.provider} since ${field.since.t_ms === "never" ? "never" : format(field.since.t_ms, "dd/MM/yyyy") }` as TranslatedString, }, }; return result; }), } : undefined, ], }; return (

Consolidated information after{" "} {until.when.t_ms === "never" ? "never" : format(until.when.t_ms, "dd MMMM yyyy")}

{ }} />
); } interface Consolidated { aml: { state: AmlExchangeBackend.AmlState; threshold: AmountJson; since: AbsoluteTime; }; kyc: { [field: string]: { value: any; provider: string; since: AbsoluteTime; }; }; } function getConsolidated( history: AmlEvent[], when: AbsoluteTime, ): Consolidated { const initial: Consolidated = { aml: { state: AmlExchangeBackend.AmlState.normal, threshold: { currency: "ARS", value: 1000, fraction: 0, }, since: AbsoluteTime.never() }, kyc: {}, }; return history.reduce((prev, cur) => { if (AbsoluteTime.cmp(when, cur.when) < 0) { return prev; } switch (cur.type) { case "kyc-expiration": { cur.fields.forEach((field) => { delete prev.kyc[field]; }); break; } case "aml-form": { prev.aml = { since: cur.when, state: cur.state, threshold: cur.threshold } break; } case "kyc-collection": { Object.keys(cur.values).forEach((field) => { prev.kyc[field] = { value: (cur.values as any)[field], provider: cur.provider, since: cur.when, }; }); break; } } return prev; }, initial); } export const amlStateConverter = { toStringUI: stringifyAmlState, fromStringUI: parseAmlState, }; function stringifyAmlState(s: AmlExchangeBackend.AmlState | undefined): string { if (s === undefined) return ""; switch (s) { case AmlExchangeBackend.AmlState.normal: return "normal"; case AmlExchangeBackend.AmlState.pending: return "pending"; case AmlExchangeBackend.AmlState.frozen: return "frozen"; } } function parseAmlState(s: string | undefined): AmlExchangeBackend.AmlState { switch (s) { case "normal": return AmlExchangeBackend.AmlState.normal; case "pending": return AmlExchangeBackend.AmlState.pending; case "frozen": return AmlExchangeBackend.AmlState.frozen; default: throw Error(`unknown AML state: ${s}`); } }