/*
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 (