gnunet-svn
[Top][All Lists]
Advanced

[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.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]