wallet-core/packages/exchange-backoffice-ui/src/pages/AccountDetails.tsx
2023-05-26 09:26:09 -03:00

458 lines
13 KiB
TypeScript

import { Fragment, VNode, h } from "preact";
import {
AmlDecisionDetail,
AmlDecisionDetails,
AmlState,
KycDetail,
} from "../types.js";
import {
AbsoluteTime,
AmountJson,
Amounts,
TranslatedString,
} from "@gnu-taler/taler-util";
import { format } from "date-fns";
import { ArrowDownCircleIcon, ClockIcon } from "@heroicons/react/20/solid";
import { useState } from "preact/hooks";
import { NiceForm } from "../NiceForm.js";
import { FlexibleForm } from "../forms/index.js";
import { UIFormField } from "../handlers/forms.js";
import { Pages } from "../pages.js";
const response: AmlDecisionDetails = {
aml_history: [
{
justification: "Lack of documentation",
decider_pub: "ASDASDASD",
decision_time: {
t_s: Date.now() / 1000,
},
new_state: 2,
new_threshold: "USD:0",
},
{
justification: "Doing a transfer of high amount",
decider_pub: "ASDASDASD",
decision_time: {
t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 6,
},
new_state: 1,
new_threshold: "USD:2000",
},
{
justification: "Account is known to the system",
decider_pub: "ASDASDASD",
decision_time: {
t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 9,
},
new_state: 0,
new_threshold: "USD:100",
},
],
kyc_attributes: [
{
collection_time: {
t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 8,
},
expiration_time: {
t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 4,
},
provider_section: "asdasd",
attributes: {
name: "Sebastian",
},
},
{
collection_time: {
t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 5,
},
expiration_time: {
t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 2,
},
provider_section: "asdasd",
attributes: {
creditCard: "12312312312",
},
},
],
};
type AmlEvent = AmlFormEvent | KycCollectionEvent | KycExpirationEvent;
type AmlFormEvent = {
type: "aml-form";
when: AbsoluteTime;
title: TranslatedString;
state: 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: AmlDecisionDetail[],
kyc: 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: {
t_ms:
k.collection_time.t_s === "never"
? "never"
: k.collection_time.t_s * 1000,
},
values: !k.attributes ? {} : k.attributes,
provider: k.provider_section,
});
prev.push({
type: "kyc-expiration",
title: "expired" as TranslatedString,
when: {
t_ms:
k.expiration_time.t_s === "never"
? "never"
: k.expiration_time.t_s * 1000,
},
fields: !k.attributes ? [] : Object.keys(k.attributes),
});
return prev;
}, [] as AmlEvent[]);
return ae.concat(ke).sort(selectSooner);
}
export function AccountDetails({ account }: { account?: string }) {
const events = getEventsFromAmlHistory(
response.aml_history,
response.kyc_attributes,
);
console.log("DETAILS", events, events[events.length - 1 - 2]);
const [selected, setSelected] = useState<AmlEvent>(
events[events.length - 1 - 2],
);
return (
<div>
<a
href={Pages.newFormEntry.url({ account })}
class="m-4 block rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
>
New AML form
</a>
<header class="flex items-center justify-between border-b border-white/5 px-4 py-4 sm:px-6 sm:py-6 lg:px-8">
<h1 class="text-base font-semibold leading-7 text-black">
Case history
</h1>
</header>
<div class="flow-root">
<ul role="list">
{events.map((e, idx) => {
const isLast = events.length - 1 === idx;
return (
<li
class="hover:bg-gray-200 p-2 rounded cursor-pointer"
onClick={() => {
setSelected(e);
}}
>
<div class="relative pb-6">
{!isLast ? (
<span
class="absolute left-4 top-4 -ml-px h-full w-1 bg-gray-200"
aria-hidden="true"
></span>
) : undefined}
<div class="relative flex space-x-3">
{(() => {
switch (e.type) {
case "aml-form": {
switch (e.state) {
case AmlState.normal: {
return (
<div>
<span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20">
Normal
</span>
<span class="inline-flex items-center px-2 py-1 text-xs font-medium text-gray-700 ">
{e.threshold.currency}{" "}
{Amounts.stringifyValue(e.threshold)}
</span>
</div>
);
}
case AmlState.pending: {
return (
<div>
<span class="inline-flex items-center rounded-md bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-700 ring-1 ring-inset ring-green-600/20">
Pending
</span>
<span class="inline-flex items-center px-2 py-1 text-xs font-medium text-gray-700 ">
{e.threshold.currency}{" "}
{Amounts.stringifyValue(e.threshold)}
</span>
</div>
);
}
case AmlState.frozen: {
return (
<div>
<span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-green-600/20">
Frozen
</span>
<span class="inline-flex items-center px-2 py-1 text-xs font-medium text-gray-700 ">
{e.threshold.currency}{" "}
{Amounts.stringifyValue(e.threshold)}
</span>
</div>
);
}
}
}
case "kyc-collection": {
return (
<ArrowDownCircleIcon class="h-8 w-8 text-green-700" />
);
}
case "kyc-expiration": {
return <ClockIcon class="h-8 w-8 text-gray-700" />;
}
}
})()}
<div class="flex min-w-0 flex-1 justify-between space-x-4 pt-1.5">
<div>
<p class="text-sm text-gray-900">{e.title}</p>
</div>
<div class="whitespace-nowrap text-right text-sm text-gray-500">
{e.when.t_ms === "never" ? (
"never"
) : (
<time dateTime={format(e.when.t_ms, "dd MMM yyyy")}>
{format(e.when.t_ms, "dd MMM yyyy")}
</time>
)}
</div>
</div>
</div>
</div>
</li>
);
})}
</ul>
</div>
{selected && <ShowEventDetails event={selected} />}
{selected && <ShowConsolidated history={events} until={selected} />}
</div>
);
}
function ShowEventDetails({ event }: { event: AmlEvent }): VNode {
return <div>type {event.type}</div>;
}
function ShowConsolidated({
history,
until,
}: {
history: AmlEvent[];
until: AmlEvent;
}): VNode {
console.log("UNTIL", until);
const cons = getConsolidated(history, until.when);
const form: FlexibleForm<Consolidated> = {
versionId: "1",
behavior: (form) => {
return {};
},
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: AmlState.frozen,
},
{
label: "Pending" as TranslatedString,
value: AmlState.pending,
},
{
label: "Normal" as TranslatedString,
value: 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 (
<Fragment>
<h1 class="text-base font-semibold leading-7 text-black">
Consolidated information after{" "}
{until.when.t_ms === "never"
? "never"
: format(until.when.t_ms, "dd MMMM yyyy")}
</h1>
<NiceForm
key={`${String(Date.now())}`}
form={form}
initial={cons}
onUpdate={() => {}}
/>
</Fragment>
);
}
interface Consolidated {
aml: {
state?: 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: {
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.threshold = cur.threshold;
prev.aml.state = cur.state;
prev.aml.since = cur.when;
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: AmlState | undefined): string {
if (s === undefined) return "";
switch (s) {
case AmlState.normal:
return "normal";
case AmlState.pending:
return "pending";
case AmlState.frozen:
return "frozen";
}
}
function parseAmlState(s: string | undefined): AmlState {
switch (s) {
case "normal":
return AmlState.normal;
case "pending":
return AmlState.pending;
case "frozen":
return AmlState.frozen;
default:
throw Error(`unknown AML state: ${s}`);
}
}