diff options
Diffstat (limited to 'packages/aml-backoffice-ui/src/pages')
10 files changed, 476 insertions, 276 deletions
diff --git a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx b/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx new file mode 100644 index 000000000..a14966cc0 --- /dev/null +++ b/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx @@ -0,0 +1,94 @@ +/* + This file is part of GNU Taler + (C) 2022 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 <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import * as tests from "@gnu-taler/web-util/testing"; +import { + AntiMoneyLaunderingForm as TestedComponent, +} from "./AntiMoneyLaunderingForm.js"; + +export default { + title: "aml form", +}; + +export const SimpleComment = tests.createExample(TestedComponent, { + account: "the_account", + selectedForm: 0, + onSubmit: async (justification, newState, newThreshold) => { + alert(JSON.stringify({justification, newState, newThreshold}, undefined, 2)) + } +}); +export const Identification = tests.createExample(TestedComponent, { + account: "the_account", + selectedForm: 1, + onSubmit: async (justification, newState, newThreshold) => { + alert(JSON.stringify({justification, newState, newThreshold}, undefined, 2)) + } +}); +export const OperationalLegalEntity = tests.createExample(TestedComponent, { + account: "the_account", + selectedForm: 2, + onSubmit: async (justification, newState, newThreshold) => { + alert(JSON.stringify({justification, newState, newThreshold}, undefined, 2)) + } +}); +export const Foundations = tests.createExample(TestedComponent, { + account: "the_account", + selectedForm: 3, + onSubmit: async (justification, newState, newThreshold) => { + alert(JSON.stringify({justification, newState, newThreshold}, undefined, 2)) + } +}); +export const DelcarationOfTrusts = tests.createExample(TestedComponent, { + account: "the_account", + selectedForm: 4, + onSubmit: async (justification, newState, newThreshold) => { + alert(JSON.stringify({justification, newState, newThreshold}, undefined, 2)) + } +}); +export const InformationOnLifeInsurance = tests.createExample(TestedComponent, { + account: "the_account", + selectedForm: 5, + onSubmit: async (justification, newState, newThreshold) => { + alert(JSON.stringify({justification, newState, newThreshold}, undefined, 2)) + } +}); +export const DeclarationOfBeneficialOwner = tests.createExample(TestedComponent, { + account: "the_account", + selectedForm: 6, + onSubmit: async (justification, newState, newThreshold) => { + alert(JSON.stringify({justification, newState, newThreshold}, undefined, 2)) + } +}); +export const CustomerProfile = tests.createExample(TestedComponent, { + account: "the_account", + selectedForm: 7, + onSubmit: async (justification, newState, newThreshold) => { + alert(JSON.stringify({justification, newState, newThreshold}, undefined, 2)) + } +}); +export const RiskProfile = tests.createExample(TestedComponent, { + account: "the_account", + selectedForm: 8, + onSubmit: async (justification, newState, newThreshold) => { + alert(JSON.stringify({justification, newState, newThreshold}, undefined, 2)) + } +}); + diff --git a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx b/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx index c3fb7dafe..5d2a3dffe 100644 --- a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx +++ b/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx @@ -1,3 +1,5 @@ +import { AbsoluteTime, AmountJson, Amounts } from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h } from "preact"; import { NiceForm } from "../NiceForm.js"; import { v1 as form_902_11e_v1 } from "../forms/902_11e.js"; @@ -9,30 +11,63 @@ import { v1 as form_902_4e_v1 } from "../forms/902_4e.js"; import { v1 as form_902_5e_v1 } from "../forms/902_5e.js"; import { v1 as form_902_9e_v1 } from "../forms/902_9e.js"; import { v1 as simplest } from "../forms/simplest.js"; -import { DocumentDuplicateIcon } from "@heroicons/react/24/solid"; -import { AbsoluteTime } from "@gnu-taler/taler-util"; -import { AmountJson, Amounts } from "@gnu-taler/taler-util"; +import { Pages } from "../pages.js"; import { AmlExchangeBackend } from "../types.js"; -export function AntiMoneyLaunderingForm({ number }: { number?: string }) { - const selectedForm = Number.parseInt(number ?? "0", 10); - if (Number.isNaN(selectedForm)) { - return <div>WHAT! {number}</div>; - } +export type Justification = { + // form index in the list of forms + index: number; + // form name + name: string; + // form values + value: any; +} + +export function AntiMoneyLaunderingForm({ account, selectedForm, onSubmit }: { account: string, selectedForm: number, onSubmit: (justification: Justification, state: AmlExchangeBackend.AmlState, threshold: AmountJson) => Promise<void>; }) { + const { i18n } = useTranslationContext() const showingFrom = allForms[selectedForm].impl; - const storedValue = { + const formName = allForms[selectedForm].name + const initial = { fullName: "loggedIn_user_fullname", when: AbsoluteTime.now(), + state: AmlExchangeBackend.AmlState.pending, + threshold: Amounts.parseOrThrow("KUDOS:1000"), }; return ( <NiceForm - initial={storedValue} - form={showingFrom({ - state: AmlExchangeBackend.AmlState.pending, - threshold: Amounts.parseOrThrow("USD:10"), - })} - onUpdate={() => {}} - /> + initial={initial} + form={showingFrom(initial)} + onUpdate={() => { }} + onSubmit={(formValue) => { + if (formValue.state === undefined || formValue.threshold === undefined) return; + const st = formValue.state; + const amount = formValue.threshold; + + const justification = { + index: selectedForm, + name: formName, + value: formValue + } + + onSubmit(justification, st, amount); + }} + > + <div class="mt-6 flex items-center justify-end gap-x-6"> + <a + // type="button" + href={Pages.account.url({ account })} + class="text-sm font-semibold leading-6 text-gray-900" + > + <i18n.Translate>Cancel</i18n.Translate> + </a> + <button + type="submit" + class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + > + <i18n.Translate>Confirm</i18n.Translate> + </button> + </div> + </NiceForm> ); } @@ -41,6 +76,11 @@ export interface State { threshold: AmountJson; } +const DocumentDuplicateIcon = <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> + <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" /> +</svg> + + export const allForms = [ { name: "Simple comment", diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx index f618a3592..1f8d6ac5e 100644 --- a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx +++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx @@ -2,24 +2,20 @@ import { AbsoluteTime, AmountJson, Amounts, - PaytoString, TalerError, TranslatedString, - assertUnreachable, + 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"; +import { ShowConsolidated } from "./ShowConsolidated.js"; -type AmlEvent = AmlFormEvent | KycCollectionEvent | KycExpirationEvent; +export type AmlEvent = AmlFormEvent | KycCollectionEvent | KycExpirationEvent; type AmlFormEvent = { type: "aml-form"; when: AbsoluteTime; @@ -47,7 +43,7 @@ function selectSooner(a: WithTime, b: WithTime) { return AbsoluteTime.cmp(a.when, b.when); } -function getEventsFromAmlHistory( +export function getEventsFromAmlHistory( aml: AmlExchangeBackend.AmlDecisionDetail[], kyc: AmlExchangeBackend.KycDetail[], ): AmlEvent[] { @@ -113,12 +109,16 @@ export function CaseDetails({ account }: { account: string }) { 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 + <i18n.Translate> + New AML form + </i18n.Translate> </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 + <i18n.Translate> + Case history + </i18n.Translate> </h1> </header> <div class="flow-root"> @@ -187,11 +187,18 @@ export function CaseDetails({ account }: { account: string }) { } case "kyc-collection": { return ( - <ArrowDownCircleIcon class="h-8 w-8 text-green-700" /> + // <ArrowDownCircleIcon class="h-8 w-8 text-green-700" /> + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> + <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75l3 3m0 0l3-3m-3 3v-7.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> + </svg> ); } case "kyc-expiration": { - return <ClockIcon class="h-8 w-8 text-gray-700" />; + // return <ClockIcon class="h-8 w-8 text-gray-700" />; + return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> + <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" /> + </svg> + } } })()} @@ -217,7 +224,7 @@ export function CaseDetails({ account }: { account: string }) { </ul> </div> {selected && <ShowEventDetails event={selected} />} - {selected && <ShowConsolidated history={events} until={selected} />} + {selected && <ShowConsolidated history={events} until={selected.when} />} </div> ); } @@ -226,197 +233,4 @@ function ShowEventDetails({ event }: { event: AmlEvent }): VNode { return <div>type {event.type}</div>; } -function ShowConsolidated({ - history, - until, -}: { - history: AmlEvent[]; - until: AmlEvent; -}): VNode { - const cons = getConsolidated(history, until.when); - - const form: FlexibleForm<Consolidated> = { - 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 ( - <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: 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}`); - } -} diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx index 624f2c985..64cacf68c 100644 --- a/packages/aml-backoffice-ui/src/pages/Cases.tsx +++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx @@ -6,8 +6,9 @@ import { createNewForm } from "../handlers/forms.js"; import { useCases } from "../hooks/useCases.js"; import { Pages } from "../pages.js"; import { AmlExchangeBackend } from "../types.js"; -import { amlStateConverter } from "./CaseDetails.js"; + import { Officer } from "./Officer.js"; +import { amlStateConverter } from "./ShowConsolidated.js"; export function Cases() { const { i18n } = useTranslationContext(); @@ -43,10 +44,14 @@ export function Cases() { <div class="sm:flex sm:items-center"> <div class="sm:flex-auto"> <h1 class="text-base font-semibold leading-6 text-gray-900"> + <i18n.Translate> Cases + </i18n.Translate> </h1> <p class="mt-2 text-sm text-gray-700"> + <i18n.Translate> A list of all the account with the status + </i18n.Translate> </p> </div> <form.Provider @@ -166,6 +171,25 @@ export function Cases() { ); } +export const PeopleIcon = () => <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> +<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" /> +</svg> + +export const HomeIcon = () => <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> +<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" /> +</svg> + + +export const ChevronRightIcon = () => <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> +<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" /> +</svg> + + +export const ArrowRightIcon = () => <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> + <path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" /> +</svg> + + function Pagination() { return ( <nav class="flex items-center justify-between px-4 sm:px-0"> diff --git a/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx b/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx index b3d04d97e..ff800ebdc 100644 --- a/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx +++ b/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx @@ -2,6 +2,7 @@ import { VNode, h } from "preact"; import { OfficerNotReady } from "../hooks/useOfficer.js"; import { CreateAccount } from "./CreateAccount.js"; import { UnlockAccount } from "./UnlockAccount.js"; +import { assertUnreachable } from "@gnu-taler/taler-util"; export function HandleAccountNotReady({ officer, @@ -24,14 +25,11 @@ export function HandleAccountNotReady({ onRemoveAccount={() => { officer.forget(); }} - onAccountUnlocked={(pwd) => { - officer.tryUnlock(pwd); + onAccountUnlocked={async (pwd) => { + await officer.tryUnlock(pwd); }} /> ); } - return <div> - some - </div> - throw Error(`unexpected account state ${(officer as any).state}`); + assertUnreachable(officer) } diff --git a/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx b/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx index b291ffbee..e70536cb2 100644 --- a/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx +++ b/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx @@ -1,5 +1,5 @@ import { VNode, h } from "preact"; -import { allForms } from "./AntiMoneyLaunderingForm.js"; +import { AntiMoneyLaunderingForm, allForms } from "./AntiMoneyLaunderingForm.js"; import { Pages } from "../pages.js"; import { NiceForm } from "../NiceForm.js"; import { AbsoluteTime, Amounts, TalerExchangeApi, TalerProtocolTimestamp } from "@gnu-taler/taler-util"; @@ -31,60 +31,27 @@ export function NewFormEntry({ if (Number.isNaN(selectedForm)) { return <div>WHAT! {type}</div>; } - const showingFrom = allForms[selectedForm].impl; - const formName = allForms[selectedForm].name - const initial = { - fullName: "loggedIn_user_fullname", - when: AbsoluteTime.now(), - state: AmlExchangeBackend.AmlState.pending, - threshold: Amounts.parseOrThrow("KUDOS:1000"), - }; + const { api } = useExchangeApiContext() return ( - <NiceForm - initial={initial} - form={showingFrom(initial)} - onSubmit={(formValue) => { - if (formValue.state === undefined || formValue.threshold === undefined) return; - - const justification = { - index: selectedForm, - name: formName, - value: formValue - } + <AntiMoneyLaunderingForm + account={account} + selectedForm={selectedForm} + onSubmit={async (justification, new_state, new_threshold) => { const decision: TalerExchangeApi.AmlDecision = { justification: JSON.stringify(justification), decision_time: TalerProtocolTimestamp.now(), h_payto: account, - new_state: formValue.state, - new_threshold: Amounts.stringify(formValue.threshold), + new_state, + new_threshold: Amounts.stringify(new_threshold), officer_sig: "", kyc_requirements: undefined } - // const signature = buildDecisionSignature(officer.account.signingKey, decision); - // decision.officer_sig = signature api.addDecisionDetails(officer.account, decision); - // alert(JSON.stringify(formValue)); }} - > - <div class="mt-6 flex items-center justify-end gap-x-6"> - <a - // type="button" - href={Pages.account.url({ account })} - class="text-sm font-semibold leading-6 text-gray-900" - > - Cancel - </a> - <button - type="submit" - class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" - > - Confirm - </button> - </div> - </NiceForm> + /> ); } diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx new file mode 100644 index 000000000..1a86e8e98 --- /dev/null +++ b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx @@ -0,0 +1,60 @@ +/* + This file is part of GNU Taler + (C) 2022 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 <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { addDays } from "date-fns"; +import { + ShowConsolidated as TestedComponent, +} from "./ShowConsolidated.js"; +import * as tests from "@gnu-taler/web-util/testing"; +import { getEventsFromAmlHistory } from "./CaseDetails.js"; +import { AbsoluteTime, Duration } from "@gnu-taler/taler-util"; + +export default { + title: "show consolidated", +}; + +export const WithEmptyHistory = tests.createExample(TestedComponent, { + history: getEventsFromAmlHistory([],[]), + until: AbsoluteTime.now() +}); + +export const WithSomeEvents = tests.createExample(TestedComponent, { + history: getEventsFromAmlHistory([{ + decider_pub: "123", + decision_time: { t_s: 1 }, + justification: "yes", + new_state: 1, + new_threshold: "USD:10", + }],[{ + collection_time: AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.subtractDuraction(AbsoluteTime.now(), Duration.fromPrettyString("1d")) + ), + expiration_time: { t_s: "never"}, + provider_section: "asd", + attributes: { + email: "sebasjm@qwe.com" + } + }]), + until: AbsoluteTime.now() +}); + + + diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx new file mode 100644 index 000000000..0efc68632 --- /dev/null +++ b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx @@ -0,0 +1,204 @@ +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 { AmlEvent } from "./CaseDetails.js"; +import { AmlExchangeBackend } from "../types.js"; +import { AbsoluteTime, AmountJson, TranslatedString } from "@gnu-taler/taler-util"; +import { format } from "date-fns"; + +export function ShowConsolidated({ + history, + until, +}: { + history: AmlEvent[]; + until: AbsoluteTime; +}): VNode { + const cons = getConsolidated(history, until); + + const form: FlexibleForm<Consolidated> = { + 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 ( + <Fragment> + <h1 class="text-base font-semibold leading-7 text-black"> + Consolidated information + {until.t_ms === "never" + ? "" + : `after ${format(until.t_ms, "dd MMMM yyyy")}` } + </h1> + <NiceForm + key={`${String(Date.now())}`} + form={form} + initial={cons} + onUpdate={() => { }} + /> + </Fragment> + ); +} + +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}`); + } +} diff --git a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx index a6570ffcc..ba5aa7b1f 100644 --- a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx +++ b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx @@ -7,7 +7,7 @@ export function UnlockAccount({ onAccountUnlocked, onRemoveAccount, }: { - onAccountUnlocked: (password: string) => void; + onAccountUnlocked: (password: string) => Promise<void>; onRemoveAccount: () => void; }): VNode { const { i18n } = useTranslationContext() @@ -30,13 +30,10 @@ export function UnlockAccount({ <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] "> <div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12"> <Form.Provider - initialValue={{ - password: "qwe", - }} + initialValue={{}} onSubmit={async (v) => { try { await onAccountUnlocked(v.password!); - notifyInfo("Account unlocked" as TranslatedString); } catch (e) { if (e instanceof UnwrapKeyError) { diff --git a/packages/aml-backoffice-ui/src/pages/index.stories.ts b/packages/aml-backoffice-ui/src/pages/index.stories.ts new file mode 100644 index 000000000..e31e13a28 --- /dev/null +++ b/packages/aml-backoffice-ui/src/pages/index.stories.ts @@ -0,0 +1,2 @@ +export * as a1 from "./ShowConsolidated.stories.js"; +export * as a2 from "./AntiMoneyLaunderingForm.stories.js"; |