[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-wallet-core] 01/02: ask for justification when making a decision
From: |
gnunet |
Subject: |
[taler-wallet-core] 01/02: ask for justification when making a decision |
Date: |
Wed, 02 Oct 2024 19:16:42 +0200 |
This is an automated email from the git hooks/post-receive script.
sebasjm pushed a commit to branch master
in repository wallet-core.
commit 69e89b4ea0c88ed2f5cb3706d6583b9cf37b24e9
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Wed Oct 2 14:14:52 2024 -0300
ask for justification when making a decision
---
.../aml-backoffice-ui/src/pages/CaseDetails.tsx | 241 ++++++++++++++++-----
packages/aml-backoffice-ui/src/pages/Search.tsx | 6 +-
2 files changed, 185 insertions(+), 62 deletions(-)
diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
index 88366c1d0..52b852e10 100644
--- a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
@@ -15,11 +15,13 @@
*/
import {
AbsoluteTime,
+ AmlDecisionRequest,
AmountJson,
Amounts,
Codec,
CurrencySpecification,
HttpStatusCode,
+ LegitimizationRuleSet,
OperationFail,
OperationOk,
PaytoString,
@@ -35,13 +37,21 @@ import {
} from "@gnu-taler/taler-util";
import {
Attention,
+ Button,
+ convertUiField,
DefaultForm,
FormMetadata,
+ getConverterById,
InternationalizationAPI,
Loading,
+ LocalNotificationBanner,
+ RenderAllFieldsByUiConfig,
ShowInputErrorLabel,
Time,
+ UIFormElementConfig,
+ UIHandlerId,
useExchangeApiContext,
+ useLocalNotificationHandler,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { format, formatDuration, intervalToDuration } from "date-fns";
@@ -54,6 +64,8 @@ import { useAccountInformation } from "../hooks/account.js";
import { useAccountDecisions } from "../hooks/decisions.js";
import { ShowConsolidated } from "./ShowConsolidated.js";
import { useOfficer } from "../hooks/officer.js";
+import { getShapeFromFields, useFormState } from "../hooks/form.js";
+import { privatePages } from "../Routing.js";
export type AmlEvent =
| AmlFormEvent
@@ -174,13 +186,8 @@ export function getEventsFromAmlHistory(
export function CaseDetails({ account, paytoString }: { account: string,
paytoString?: PaytoString }) {
const [selected, setSelected] = useState<AbsoluteTime>(AbsoluteTime.now());
- const [showForm, setShowForm] = useState<{
- justification: Justification;
- metadata: FormMetadata;
- }>();
- const { config, lib } = useExchangeApiContext();
- const officer = useOfficer();
- const session = officer.state === "ready" ? officer.account : undefined;
+ const [request, setDesicionRequest] = useState<Omit<AmlDecisionRequest,
"officer_sig"> | undefined>(undefined)
+ const { config } = useExchangeApiContext();
const { i18n } = useTranslationContext();
const details = useAccountInformation(account);
@@ -228,26 +235,13 @@ export function CaseDetails({ account, paytoString }: {
account: string, paytoSt
const events = getEventsFromAmlHistory(accountDetails, i18n, allForms);
- // if (showForm !== undefined) {
- // return (
- // <DefaultForm
- // readOnly={true}
- // initial={showForm.justification.value}
- // form={showForm.metadata as any} // FIXME: HERE
- // >
- // <div class="mt-6 flex items-center justify-end gap-x-6">
- // <button
- // class="text-sm font-semibold leading-6 text-gray-900"
- // onClick={() => {
- // setShowForm(undefined);
- // }}
- // >
- // <i18n.Translate>Cancel</i18n.Translate>
- // </button>
- // </div>
- // </DefaultForm>
- // );
- // }
+
+
+ if (request) {
+ return <SubmitNewDecision request={request} onComplete={() => {
+ setDesicionRequest(undefined)
+ }} />
+ }
return (
<div class="min-w-60">
@@ -272,8 +266,7 @@ export function CaseDetails({ account, paytoString }: {
account: string, paytoSt
<div>
<button
onClick={async () => {
- if (!session) return;
- lib.exchange.makeAmlDesicion(session, {
+ setDesicionRequest({
payto_uri: paytoString,
decision_time: AbsoluteTime.toProtocolTimestamp(
AbsoluteTime.now(),
@@ -290,7 +283,7 @@ export function CaseDetails({ account, paytoString }: {
account: string, paytoSt
rules: FREEZE_RULES(config.currency),
successor_measure: "verboten",
},
- });
+ })
}}
class="m-4 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"
>
@@ -298,8 +291,7 @@ export function CaseDetails({ account, paytoString }: {
account: string, paytoSt
</button>
<button
onClick={async () => {
- if (!session) return;
- lib.exchange.makeAmlDesicion(session, {
+ setDesicionRequest({
payto_uri: paytoString,
decision_time: AbsoluteTime.toProtocolTimestamp(
AbsoluteTime.now(),
@@ -324,8 +316,7 @@ export function CaseDetails({ account, paytoString }: {
account: string, paytoSt
</button>
<button
onClick={async () => {
- if (!session) return;
- lib.exchange.makeAmlDesicion(session, {
+ setDesicionRequest({
payto_uri: paytoString,
decision_time: AbsoluteTime.toProtocolTimestamp(
AbsoluteTime.now(),
@@ -350,8 +341,7 @@ export function CaseDetails({ account, paytoString }: {
account: string, paytoSt
</button>
<button
onClick={async () => {
- if (!session) return;
- lib.exchange.makeAmlDesicion(session, {
+ setDesicionRequest({
payto_uri: paytoString,
decision_time: AbsoluteTime.toProtocolTimestamp(
AbsoluteTime.now(),
@@ -384,7 +374,17 @@ export function CaseDetails({ account, paytoString }: {
account: string, paytoSt
<h1 class="my-4 text-base font-semibold leading-6 text-black">
<i18n.Translate>Current active rules</i18n.Translate>
</h1>
- <ShowDecisionInfo decision={activeDecision} startOpen />
+ <ShowDecisionLimitInfo
+ since={AbsoluteTime.fromProtocolTimestamp(
+ activeDecision.decision_time,
+ )}
+ until={AbsoluteTime.fromProtocolTimestamp(
+ activeDecision.limits.expiration_time,
+ )}
+ justification={activeDecision.justification}
+ ruleSet={activeDecision.limits}
+
+ startOpen />
</Fragment>
)}
<h1 class="my-4 text-base font-semibold leading-6 text-black">
@@ -420,7 +420,15 @@ export function CaseDetails({ account, paytoString }: {
account: string, paytoSt
<i18n.Translate>Previous AML decisions</i18n.Translate>
</h1>
{restDecisions.map((d) => {
- return <ShowDecisionInfo decision={d} />;
+ return <ShowDecisionLimitInfo
since={AbsoluteTime.fromProtocolTimestamp(
+ d.decision_time,
+ )}
+ until={AbsoluteTime.fromProtocolTimestamp(
+ d.limits.expiration_time,
+ )}
+ justification={d.justification}
+ ruleSet={d.limits}
+ />;
})}
</Fragment>
) : (
@@ -433,17 +441,141 @@ export function CaseDetails({ account, paytoString }: {
account: string, paytoSt
);
}
-function ShowDecisionInfo({
- decision,
+
+function SubmitNewDecision({ request, onComplete }: { onComplete: () => void;
request: Omit<AmlDecisionRequest, "officer_sig"> }): VNode {
+ const { i18n } = useTranslationContext();
+ const { lib } = useExchangeApiContext();
+ const [notification, withErrorHandler] = useLocalNotificationHandler();
+
+ const formDesign: UIFormElementConfig[] = [{
+ id: "justification" as UIHandlerId,
+ type: "textArea",
+ required: true,
+ label: i18n.str`Justification`,
+ }]
+ const officer = useOfficer();
+ const session = officer.state === "ready" ? officer.account : undefined;
+ const decisionForm = useFormState<{ justification: string }>(
+ getShapeFromFields(formDesign),
+ { justification: "" },
+ (d) => {
+ d.justification;
+ return {
+ status: "ok",
+ errors: undefined,
+ result: d as any
+ }
+ },
+ );
+
+ const submitHandler =
+ decisionForm === undefined || !session
+ ? undefined
+ : withErrorHandler(
+ () => {
+ request.justification = decisionForm.status.result.justification ??
"empty"
+ return lib.exchange.makeAmlDesicion(session, request);
+ },
+ onComplete,
+ (fail) => {
+ switch (fail.case) {
+ case HttpStatusCode.Forbidden:
+ if (session) {
+ return i18n.str`Wrong credentials for "${session}"`;
+ } else {
+ return i18n.str`Wrong credentials.`;
+ }
+ case HttpStatusCode.NotFound:
+ return i18n.str`The account was not found`;
+ case HttpStatusCode.Conflict:
+ return i18n.str`Officer disabled or more recent decision was
already submitted.`;
+ default:
+ assertUnreachable(fail);
+ }
+ },
+ );
+
+
+
+ return <div>
+ <LocalNotificationBanner notification={notification} />
+ <h1 class="my-2 text-3xl font-bold tracking-tight text-gray-900 ">
+ <i18n.Translate>Submit decision</i18n.Translate>
+ </h1>
+ <form
+ class="space-y-6"
+ noValidate
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ autoCapitalize="none"
+ autoCorrect="off"
+ >
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3">
+ <RenderAllFieldsByUiConfig
+ fields={convertUiField(
+ i18n,
+ formDesign,
+ decisionForm.handler,
+ getConverterById,
+ )}
+ />
+ </div>
+
+ <div class="mt-6 flex items-center justify-end gap-x-6">
+ <button
+ onClick={onComplete}
+ class="text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+
+ <Button
+ type="submit"
+ handler={submitHandler}
+ disabled={!submitHandler}
+ class="disabled:opacity-50 disabled:cursor-default 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>
+
+ </form>
+
+ <ShowDecisionLimitInfo
+ since={AbsoluteTime.fromProtocolTimestamp(
+ request.decision_time,
+ )}
+ until={AbsoluteTime.fromProtocolTimestamp(
+ request.new_rules.expiration_time,
+ )}
+ ruleSet={request.new_rules}
+
+ startOpen />
+
+ </div>
+
+}
+
+function ShowDecisionLimitInfo({
+ ruleSet,
+ since,
+ until,
startOpen,
+ justification
}: {
- decision: TalerExchangeApi.AmlDecision;
+
+ since: AbsoluteTime;
+ until: AbsoluteTime;
+ justification?: string;
+ ruleSet: LegitimizationRuleSet;
startOpen?: boolean;
}): VNode {
const { i18n } = useTranslationContext();
const { config } = useExchangeApiContext();
const [opened, setOpened] = useState(startOpen ?? false);
+
function Header() {
return (
<ul
@@ -459,9 +591,7 @@ function ShowDecisionInfo({
<div class="p-2 disabled:bg-gray-200 text-right rounded-md
rounded-l-none data-[left=true]:text-left w-full py-1.5 pl-3 text-gray-900
placeholder:text-gray-400 sm:text-sm sm:leading-6">
<Time
format="dd/MM/yyyy HH:mm:ss"
- timestamp={AbsoluteTime.fromProtocolTimestamp(
- decision.decision_time,
- )}
+ timestamp={since}
/>
</div>
</div>
@@ -469,11 +599,7 @@ function ShowDecisionInfo({
<div class="flex shrink-0 items-center gap-x-4">
<div class="flex mt-2 rounded-md shadow-sm border-0 ring-1
ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600">
<div class="pointer-events-none bg-gray-200 inset-y-0 flex
items-center px-3">
- {AbsoluteTime.isExpired(
- AbsoluteTime.fromProtocolTimestamp(
- decision.limits.expiration_time,
- ),
- ) ? (
+ {AbsoluteTime.isExpired(until) ? (
<i18n.Translate>Expired</i18n.Translate>
) : (
<i18n.Translate>Expires</i18n.Translate>
@@ -482,9 +608,7 @@ function ShowDecisionInfo({
<div class="p-2 disabled:bg-gray-200 text-right rounded-md
rounded-l-none data-[left=true]:text-left w-full py-1.5 pl-3 text-gray-900
placeholder:text-gray-400 sm:text-sm sm:leading-6">
<Time
format="dd/MM/yyyy HH:mm:ss"
- timestamp={AbsoluteTime.fromProtocolTimestamp(
- decision.limits.expiration_time,
- )}
+ timestamp={until}
/>
</div>
</div>
@@ -501,7 +625,7 @@ function ShowDecisionInfo({
</div>
);
}
- const balanceLimit = decision.limits.rules.find(
+ const balanceLimit = ruleSet.rules.find(
(r) => r.operation_type === "BALANCE",
);
@@ -511,22 +635,23 @@ function ShowDecisionInfo({
<Header />
</div>
- {!decision.justification ? undefined : (
- <div>
+ {!justification ? undefined : (
+ <div class="mt-4">
<label
for="comment"
class="block text-sm font-medium leading-6 text-gray-900"
>
- Description
+ <i18n.Translate>AML officer justification</i18n.Translate>
</label>
<div class="mt-2">
<textarea
rows={2}
+ readOnly
name="comment"
id="comment"
class="block w-full rounded-md border-0 py-1.5 text-gray-900
shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400
focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
>
- {decision.justification}
+ {justification}
</textarea>
</div>
</div>
@@ -570,7 +695,7 @@ function ShowDecisionInfo({
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
- {decision.limits.rules.map((r) => {
+ {ruleSet.rules.map((r) => {
if (r.operation_type === "BALANCE") return;
return (
<tr>
diff --git a/packages/aml-backoffice-ui/src/pages/Search.tsx
b/packages/aml-backoffice-ui/src/pages/Search.tsx
index 220fc55ee..6996681ed 100644
--- a/packages/aml-backoffice-ui/src/pages/Search.tsx
+++ b/packages/aml-backoffice-ui/src/pages/Search.tsx
@@ -259,8 +259,6 @@ function ShowResult({ payto }: { payto: PaytoUri }): VNode {
</div>
</div>
}
- // const detailsUrl = new URL(, window.location.href)
- // detailsUrl.searchParams.set("payto", encodeCrockForURI(paytoStr))
return <div class="mt-4">
<Attention title={i18n.str`Account not found`} type="warning">
<i18n.Translate>
@@ -303,7 +301,7 @@ function XTalerBankForm({
form.status.result.hostname,
form.status.result.account,
{
- "receiver-name": encodeURIComponent(form.status.result.name),
+ "receiver-name": (form.status.result.name),
},
);
@@ -348,7 +346,7 @@ function IbanForm({
form.status.status === "fail"
? undefined
: buildPayto("iban", form.status.result.account, form.status.result.bic,
{
- "receiver-name": encodeURIComponent(form.status.result.name),
+ "receiver-name": (form.status.result.name),
});
return (
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.