diff options
author | Sebastian <sebasjm@gmail.com> | 2024-08-27 19:01:57 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2024-08-27 19:01:57 -0300 |
commit | ca4bccc5de2d92333267ced69535d4400c394fb9 (patch) | |
tree | 4c53d12734aabc4863f57d9820b482052d63c127 /packages | |
parent | 94cb658024a17084da5de310bc5104cff6fd8337 (diff) |
refresh decisions history when the auditor makes decision
Diffstat (limited to 'packages')
-rw-r--r-- | packages/aml-backoffice-ui/src/App.tsx | 31 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/hooks/account.ts | 32 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/hooks/decisions.ts | 17 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/CaseDetails.tsx | 702 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx | 36 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx | 22 | ||||
-rw-r--r-- | packages/bank-ui/src/pages/PaytoWireTransferForm.tsx | 5 | ||||
-rw-r--r-- | packages/kyc-ui/src/app.tsx | 12 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/exchange.ts | 16 |
9 files changed, 493 insertions, 380 deletions
diff --git a/packages/aml-backoffice-ui/src/App.tsx b/packages/aml-backoffice-ui/src/App.tsx index 0b66e0d26..d2cb2fd62 100644 --- a/packages/aml-backoffice-ui/src/App.tsx +++ b/packages/aml-backoffice-ui/src/App.tsx @@ -13,7 +13,12 @@ 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/> */ -import { canonicalizeBaseUrl } from "@gnu-taler/taler-util"; +import { + CacheEvictor, + TalerExchangeCacheEviction, + assertUnreachable, + canonicalizeBaseUrl, +} from "@gnu-taler/taler-util"; import { BrowserHashNavigationProvider, ExchangeApiProvider, @@ -31,6 +36,8 @@ import { strings } from "./i18n/strings.js"; import "./scss/main.css"; import { UiSettings, fetchUiSettings } from "./context/ui-settings.js"; import { UiFormsProvider, fetchUiForms } from "./context/ui-forms.js"; +import { revalidateAccountDecisions } from "./hooks/decisions.js"; +import { revalidateAccountInformation } from "./hooks/account.js"; const WITH_LOCAL_STORAGE_CACHE = false; @@ -56,6 +63,9 @@ export function App(): VNode { <ExchangeApiProvider baseUrl={new URL("/", baseUrl)} frameOnError={ExchangeAmlFrame} + evictors={{ + exchange: evictExchangeSwrCache, + }} > <SWRConfig value={{ @@ -136,3 +146,22 @@ function getInitialBackendBaseURL( return canonicalizeBaseUrl(window.origin); } } + +const evictExchangeSwrCache: CacheEvictor<TalerExchangeCacheEviction> = { + async notifySuccess(op) { + switch (op) { + case TalerExchangeCacheEviction.MAKE_AML_DECISION: { + await revalidateAccountDecisions(); + await revalidateAccountInformation(); + return; + } + case TalerExchangeCacheEviction.CREATE_DESCISION: + case TalerExchangeCacheEviction.UPLOAD_KYC_FORM: { + return; + } + default: { + assertUnreachable(op); + } + } + }, +}; diff --git a/packages/aml-backoffice-ui/src/hooks/account.ts b/packages/aml-backoffice-ui/src/hooks/account.ts index 1c5f013a7..9889c906d 100644 --- a/packages/aml-backoffice-ui/src/hooks/account.ts +++ b/packages/aml-backoffice-ui/src/hooks/account.ts @@ -13,25 +13,43 @@ 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/> */ -import { OfficerAccount, PaytoString, TalerExchangeResultByMethod, TalerHttpError } from "@gnu-taler/taler-util"; +import { + OfficerAccount, + PaytoString, + TalerExchangeResultByMethod, + TalerHttpError, +} from "@gnu-taler/taler-util"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import _useSWR, { SWRHook } from "swr"; +import _useSWR, { SWRHook, mutate } from "swr"; import { useOfficer } from "./officer.js"; import { useExchangeApiContext } from "@gnu-taler/web-util/browser"; const useSWR = _useSWR as unknown as SWRHook; +export function revalidateAccountInformation() { + return mutate( + (key) => + Array.isArray(key) && + key[key.length - 1] === "getAmlAttributesForAccount", + undefined, + { revalidate: true }, + ); +} export function useAccountInformation(paytoHash: string) { const officer = useOfficer(); const session = officer.state === "ready" ? officer.account : undefined; - const { lib: {exchange: api} } = useExchangeApiContext(); + const { + lib: { exchange: api }, + } = useExchangeApiContext(); async function fetcher([officer, account]: [OfficerAccount, PaytoString]) { - return await api.getAmlAttributesForAccount(officer, account) + return await api.getAmlAttributesForAccount(officer, account); } - const { data, error } = useSWR<TalerExchangeResultByMethod<"getAmlAttributesForAccount">, TalerHttpError>( - !session ? undefined : [session, paytoHash], fetcher, { + const { data, error } = useSWR< + TalerExchangeResultByMethod<"getAmlAttributesForAccount">, + TalerHttpError + >(!session ? undefined : [session, paytoHash], fetcher, { refreshInterval: 0, refreshWhenHidden: false, revalidateOnFocus: false, @@ -47,5 +65,3 @@ export function useAccountInformation(paytoHash: string) { if (error) return error; return undefined; } - - diff --git a/packages/aml-backoffice-ui/src/hooks/decisions.ts b/packages/aml-backoffice-ui/src/hooks/decisions.ts index de8f1637e..eb861ae14 100644 --- a/packages/aml-backoffice-ui/src/hooks/decisions.ts +++ b/packages/aml-backoffice-ui/src/hooks/decisions.ts @@ -20,10 +20,10 @@ import { OfficerAccount, OperationOk, TalerExchangeResultByMethod, - TalerHttpError + TalerHttpError, } from "@gnu-taler/taler-util"; import { useExchangeApiContext } from "@gnu-taler/web-util/browser"; -import _useSWR, { SWRHook } from "swr"; +import _useSWR, { SWRHook, mutate } from "swr"; import { useOfficer } from "./officer.js"; const useSWR = _useSWR as unknown as SWRHook; @@ -49,7 +49,7 @@ export function useCurrentDecisions(filtered?: boolean) { async function fetcher([officer, offset, investigation]: [ OfficerAccount, string | undefined, - boolean | undefined + boolean | undefined, ]) { return await api.getAmlDecisions(officer, { order: "dec", @@ -64,7 +64,9 @@ export function useCurrentDecisions(filtered?: boolean) { TalerExchangeResultByMethod<"getAmlDecisions">, TalerHttpError >( - !session ? undefined : [session, offset, filtered ? true : filtered, "getAmlDecisions"], + !session + ? undefined + : [session, offset, filtered ? true : filtered, "getAmlDecisions"], fetcher, ); @@ -77,6 +79,13 @@ export function useCurrentDecisions(filtered?: boolean) { ); } +export function revalidateAccountDecisions() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "getAmlDecisions", + undefined, + { revalidate: true }, + ); +} /** * @param account * @param args diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx index b421bd8c5..a1c09025b 100644 --- a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx +++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx @@ -18,6 +18,7 @@ import { AmountJson, Amounts, Codec, + CurrencySpecification, HttpStatusCode, OperationFail, OperationOk, @@ -40,7 +41,7 @@ import { ShowInputErrorLabel, Time, useExchangeApiContext, - useTranslationContext + useTranslationContext, } from "@gnu-taler/web-util/browser"; import { format, formatDuration, intervalToDuration } from "date-fns"; import { Fragment, Ref, VNode, h } from "preact"; @@ -165,7 +166,7 @@ export function getEventsFromAmlHistory( when: AbsoluteTime.fromProtocolTimestamp(event.collection_time), values: !event.attributes ? {} : event.attributes, provider: event.provider_name, - } as AmlEvent + } as AmlEvent; }); return ke.sort(selectSooner); } @@ -176,11 +177,10 @@ export function CaseDetails({ account }: { account: string }) { justification: Justification; metadata: FormMetadata; }>(); - const { config, lib } = useExchangeApiContext() + const { config, lib } = useExchangeApiContext(); const officer = useOfficer(); const session = officer.state === "ready" ? officer.account : undefined; - const { i18n } = useTranslationContext(); const details = useAccountInformation(account); const history = useAccountDecisions(account); @@ -220,14 +220,12 @@ export function CaseDetails({ account }: { account: string }) { } } const { details: accountDetails } = details.body; - const activeDecision = history.body.find(d => d.is_active) - const restDecisions = !activeDecision ? history.body : history.body.filter(d => d.rowid !== activeDecision.rowid) + const activeDecision = history.body.find((d) => d.is_active); + const restDecisions = !activeDecision + ? history.body + : history.body.filter((d) => d.rowid !== activeDecision.rowid); - const events = getEventsFromAmlHistory( - accountDetails, - i18n, - allForms, - ); + const events = getEventsFromAmlHistory(accountDetails, i18n, allForms); if (showForm !== undefined) { return ( @@ -261,27 +259,36 @@ export function CaseDetails({ account }: { account: string }) { </h1> </header> - {!activeDecision || !activeDecision.to_investigate ? undefined : <Attention title={i18n.str`Under investigation`} type="warning" > - <i18n.Translate>This account requires a manual review and is waiting for a decision to be made.</i18n.Translate> - </Attention>} + {!activeDecision || !activeDecision.to_investigate ? undefined : ( + <Attention title={i18n.str`Under investigation`} type="warning"> + <i18n.Translate> + This account requires a manual review and is waiting for a decision + to be made. + </i18n.Translate> + </Attention> + )} <div> <button onClick={async () => { if (!session) return; lib.exchange.makeAmlDesicion(session, { - decision_time: AbsoluteTime.toProtocolTimestamp(AbsoluteTime.now()), + decision_time: AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.now(), + ), h_payto: account, justification: "", keep_investigating: false, properties: {}, new_rules: { custom_measures: {}, - expiration_time: AbsoluteTime.toProtocolTimestamp(AbsoluteTime.never()), + expiration_time: AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.never(), + ), 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" > @@ -291,20 +298,23 @@ export function CaseDetails({ account }: { account: string }) { onClick={async () => { if (!session) return; lib.exchange.makeAmlDesicion(session, { - decision_time: AbsoluteTime.toProtocolTimestamp(AbsoluteTime.now()), + decision_time: AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.now(), + ), h_payto: account, justification: "", keep_investigating: false, properties: {}, new_rules: { custom_measures: {}, - expiration_time: AbsoluteTime.toProtocolTimestamp(AbsoluteTime.never()), + expiration_time: AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.never(), + ), rules: THRESHOLD_100_HOUR(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" > <i18n.Translate>Set threshold to 100 / hour</i18n.Translate> @@ -313,20 +323,23 @@ export function CaseDetails({ account }: { account: string }) { onClick={async () => { if (!session) return; lib.exchange.makeAmlDesicion(session, { - decision_time: AbsoluteTime.toProtocolTimestamp(AbsoluteTime.now()), + decision_time: AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.now(), + ), h_payto: account, justification: "", keep_investigating: false, properties: {}, new_rules: { custom_measures: {}, - expiration_time: AbsoluteTime.toProtocolTimestamp(AbsoluteTime.never()), + expiration_time: AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.never(), + ), rules: THRESHOLD_2000_WEEK(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" > <i18n.Translate>Set threshold to 2000 / week</i18n.Translate> @@ -335,7 +348,9 @@ export function CaseDetails({ account }: { account: string }) { onClick={async () => { if (!session) return; lib.exchange.makeAmlDesicion(session, { - decision_time: AbsoluteTime.toProtocolTimestamp(AbsoluteTime.now()), + decision_time: AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.now(), + ), h_payto: account, justification: "", keep_investigating: false, @@ -343,26 +358,30 @@ export function CaseDetails({ account }: { account: string }) { new_measure: "onlyTalers", new_rules: { custom_measures: {}, - expiration_time: AbsoluteTime.toProtocolTimestamp(AbsoluteTime.never()), + expiration_time: AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.never(), + ), 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" > <i18n.Translate>Ask for more information</i18n.Translate> </button> </div> - {!activeDecision ? <Attention title={i18n.str`No active decision found`} type="warning" /> : <Fragment> - <h1 class="my-4 text-base font-semibold leading-6 text-black"> - <i18n.Translate>Current active rules</i18n.Translate> - </h1> - - <ShowDecisionInfo decision={activeDecision} /> - </Fragment>} + {!activeDecision ? ( + <Attention title={i18n.str`No active rules found`} type="warning" /> + ) : ( + <Fragment> + <h1 class="my-4 text-base font-semibold leading-6 text-black"> + <i18n.Translate>Current active rules</i18n.Translate> + </h1> + <ShowDecisionInfo decision={activeDecision} /> + </Fragment> + )} <h1 class="my-4 text-base font-semibold leading-6 text-black"> <i18n.Translate>KYC collection events</i18n.Translate> </h1> @@ -386,91 +405,143 @@ export function CaseDetails({ account }: { account: string }) { /> {/* {selected && <ShowEventDetails event={selected} />} */} {selected && <ShowConsolidated history={events} until={selected} />} - {restDecisions.length > 0 ? + {restDecisions.length > 0 ? ( <Fragment> <h1 class="my-4 text-base font-semibold leading-6 text-black"> <i18n.Translate>Previous AML decisions</i18n.Translate> </h1> - {restDecisions.map(d => { - return <ShowDecisionInfo decision={d} /> + {restDecisions.map((d) => { + return <ShowDecisionInfo decision={d} />; })} - - </Fragment> : <div />} + </Fragment> + ) : ( + <div /> + )} </div> ); } -function ShowDecisionInfo({ decision }: { decision: TalerExchangeApi.AmlDecision }): VNode { +function ShowDecisionInfo({ + decision, +}: { + decision: TalerExchangeApi.AmlDecision; +}): VNode { const { i18n } = useTranslationContext(); - const { config } = useExchangeApiContext() - - return <Fragment> - - <div class="sm:col-span-5"> - <label - for="amount" - class="block text-sm font-medium leading-6 text-gray-900" - > - <b><i18n.Translate>Since</i18n.Translate></b> - <div class="flex 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="p-4 disabled:bg-gray-200 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" - > + const { config } = useExchangeApiContext(); - <Time format="dd/MM/yyyy HH:mm:ss" timestamp={AbsoluteTime.fromProtocolTimestamp(decision.decision_time)} /> + return ( + <Fragment> + <textarea>asdasdlaksdj</textarea> + <div class="flex col justify-between"> + <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"> + <i18n.Translate>Since</i18n.Translate> + </div> + <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, + )} + /> </div> </div> - - </label> - </div> - - <div class="sm:col-span-5"> - <label - for="amount" - class="block text-sm font-medium leading-6 text-gray-900" - > - {AbsoluteTime.isExpired(AbsoluteTime.fromProtocolTimestamp(decision.limits.expiration_time)) ? - <b><i18n.Translate>Expired at</i18n.Translate></b> : - <b><i18n.Translate>Expires at</i18n.Translate></b> - } - <div class="flex 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="p-4 disabled:bg-gray-200 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)} /> + <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, + ), + ) ? ( + <i18n.Translate>Expired</i18n.Translate> + ) : ( + <i18n.Translate>Expires</i18n.Translate> + )} + </div> + <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, + )} + /> </div> </div> + </div> + <table class="w-full text-center m-4"> + <thead> + <tr> + <th>operation</th> + <th>timeframe</th> + <th>amount</th> + </tr> + </thead> + <tbody> + {decision.limits.rules.map((r) => { + const bySpec = Amounts.stringifyValueWithSpec( + Amounts.parseOrThrow(r.threshold), + config.currency_specification, + ); + return ( + <tr> + <td class="text-left">{r.operation_type}</td> + <td> + {r.timeframe.d_us === "forever" + ? "" + : formatDuration( + intervalToDuration({ + start: 0, + end: r.timeframe.d_us / 1000, + }), + )} + </td> + <td class="text-right"> + <RenderAmount + value={Amounts.parseOrThrow(r.threshold)} + spec={config.currency_specification} + /> + </td> + </tr> + ); + })} + </tbody> + </table> + </Fragment> + ); +} - </label> - </div> - {decision.limits.rules.map(r => { - const bySpec = Amounts.stringifyValueWithSpec(Amounts.parseOrThrow(r.threshold), config.currency_specification) - return <div class="sm:col-span-5"> - <label - for="amount" - class="block text-sm font-medium leading-6 text-gray-900" - > - {r.operation_type} - <InputAmount - name="minCashout" - left - currency={bySpec.currency} - value={bySpec.normal} - onChange={undefined} - /> - </label> - <p class="mt-2 text-sm text-gray-500"> - over {r.timeframe.d_us === "forever" ? "" : formatDuration(intervalToDuration({ start: 0, end: r.timeframe.d_us / 1000 }))} - </p> - </div> - })} +export function RenderAmount({ + value, + spec, + negative, + withColor, + hideSmall, +}: { + spec: CurrencySpecification; + value: AmountJson; + hideSmall?: boolean; + negative?: boolean; + withColor?: boolean; +}): VNode { + const neg = !!negative; // convert to true or false - </Fragment> + const { currency, normal, small } = Amounts.stringifyValueWithSpec( + value, + spec, + ); + + return ( + <span + data-negative={withColor ? neg : undefined} + class="whitespace-nowrap data-[negative=false]:text-green-600 data-[negative=true]:text-red-600" + > + {negative ? "- " : undefined} + {currency} {normal}{" "} + {!hideSmall && small && <sup class="-ml-1">{small}</sup>} + </span> + ); } function AmlStateBadge({ state }: { state: TalerExchangeApi.AmlState }): VNode { @@ -631,7 +702,7 @@ function InputAmount( }, ref: Ref<HTMLInputElement>, ): VNode { - const FRAC_SEPARATOR = "," + const FRAC_SEPARATOR = ","; const { config } = useExchangeApiContext(); return ( <div class="mt-2"> @@ -658,13 +729,13 @@ function InputAmount( if ( sep_pos !== -1 && l - sep_pos - 1 > - config.currency_specification.num_fractional_input_digits + config.currency_specification.num_fractional_input_digits ) { e.currentTarget.value = e.currentTarget.value.substring( 0, sep_pos + - config.currency_specification.num_fractional_input_digits + - 1, + config.currency_specification.num_fractional_input_digits + + 1, ); } onChange(e.currentTarget.value); @@ -702,9 +773,9 @@ function parseJustification( listOfAllKnownForms: FormMetadata[], ): | OperationOk<{ - justification: Justification; - metadata: FormMetadata; - }> + justification: Justification; + metadata: FormMetadata; + }> | OperationFail<ParseJustificationFail> { try { const justification = JSON.parse(s); @@ -749,242 +820,215 @@ function parseJustification( } } -const THRESHOLD_2000_WEEK: (currency: string) => TalerExchangeApi.KycRule[] = (currency) => [{ - "operation_type": "WITHDRAW", - "threshold": `${currency}:0`, - "timeframe": { - "d_us": "forever" +const THRESHOLD_2000_WEEK: (currency: string) => TalerExchangeApi.KycRule[] = ( + currency, +) => [ + { + operation_type: "WITHDRAW", + threshold: `${currency}:2000`, + timeframe: { + d_us: 7 * 24 * 60 * 60 * 1000 * 1000, + }, + measures: ["verboten"], + display_priority: 1, + exposed: true, + is_and_combinator: true, }, - "measures": [ - "verboten" - ], - "display_priority": 1, - "exposed": true, - "is_and_combinator": true -}, -{ - "operation_type": "DEPOSIT", - "threshold": `${currency}:0`, - "timeframe": { - "d_us": "forever" + { + operation_type: "DEPOSIT", + threshold: `${currency}:2000`, + timeframe: { + d_us: 7 * 24 * 60 * 60 * 1000 * 1000, + }, + measures: ["verboten"], + display_priority: 1, + exposed: true, + is_and_combinator: true, }, - "measures": [ - "verboten" - ], - "display_priority": 1, - "exposed": true, - "is_and_combinator": true -}, -{ - "operation_type": "AGGREGATE", - "threshold": `${currency}:0`, - "timeframe": { - "d_us": "forever" + { + operation_type: "AGGREGATE", + threshold: `${currency}:2000`, + timeframe: { + d_us: 7 * 24 * 60 * 60 * 1000 * 1000, + }, + measures: ["verboten"], + display_priority: 1, + exposed: true, + is_and_combinator: true, }, - "measures": [ - "verboten" - ], - "display_priority": 1, - "exposed": true, - "is_and_combinator": true -}, -{ - "operation_type": "MERGE", - "threshold": `${currency}:0`, - "timeframe": { - "d_us": "forever" + { + operation_type: "MERGE", + threshold: `${currency}:2000`, + timeframe: { + d_us: 7 * 24 * 60 * 60 * 1000 * 1000, + }, + measures: ["verboten"], + display_priority: 1, + exposed: true, + is_and_combinator: true, }, - "measures": [ - "verboten" - ], - "display_priority": 1, - "exposed": true, - "is_and_combinator": true -}, -{ - "operation_type": "BALANCE", - "threshold": `${currency}:0`, - "timeframe": { - "d_us": "forever" + { + operation_type: "BALANCE", + threshold: `${currency}:2000`, + timeframe: { + d_us: 7 * 24 * 60 * 60 * 1000 * 1000, + }, + measures: ["verboten"], + display_priority: 1, + exposed: true, + is_and_combinator: true, }, - "measures": [ - "verboten" - ], - "display_priority": 1, - "exposed": true, - "is_and_combinator": true -}, -{ - "operation_type": "CLOSE", - "threshold": `${currency}:0`, - "timeframe": { - "d_us": "forever" + { + operation_type: "CLOSE", + threshold: `${currency}:2000`, + timeframe: { + d_us: 7 * 24 * 60 * 60 * 1000 * 1000, + }, + measures: ["verboten"], + display_priority: 1, + exposed: true, + is_and_combinator: true, }, - "measures": [ - "verboten" - ], - "display_priority": 1, - "exposed": true, - "is_and_combinator": true -}] - -const THRESHOLD_100_HOUR: (currency: string) => TalerExchangeApi.KycRule[] = (currency) => [{ - "operation_type": "WITHDRAW", - "threshold": `${currency}:0`, - "timeframe": { - "d_us": "forever" +]; + +const THRESHOLD_100_HOUR: (currency: string) => TalerExchangeApi.KycRule[] = ( + currency, +) => [ + { + operation_type: "WITHDRAW", + threshold: `${currency}:100`, + timeframe: { + d_us: 1 * 60 * 60 * 1000 * 1000, + }, + measures: ["verboten"], + display_priority: 1, + exposed: true, + is_and_combinator: true, }, - "measures": [ - "verboten" - ], - "display_priority": 1, - "exposed": true, - "is_and_combinator": true -}, -{ - "operation_type": "DEPOSIT", - "threshold": `${currency}:0`, - "timeframe": { - "d_us": "forever" + { + operation_type: "DEPOSIT", + threshold: `${currency}:100`, + timeframe: { + d_us: 1 * 60 * 60 * 1000 * 1000, + }, + measures: ["verboten"], + display_priority: 1, + exposed: true, + is_and_combinator: true, }, - "measures": [ - "verboten" - ], - "display_priority": 1, - "exposed": true, - "is_and_combinator": true -}, -{ - "operation_type": "AGGREGATE", - "threshold": `${currency}:0`, - "timeframe": { - "d_us": "forever" + { + operation_type: "AGGREGATE", + threshold: `${currency}:100`, + timeframe: { + d_us: 1 * 60 * 60 * 1000 * 1000, + }, + measures: ["verboten"], + display_priority: 1, + exposed: true, + is_and_combinator: true, }, - "measures": [ - "verboten" - ], - "display_priority": 1, - "exposed": true, - "is_and_combinator": true -}, -{ - "operation_type": "MERGE", - "threshold": `${currency}:0`, - "timeframe": { - "d_us": "forever" + { + operation_type: "MERGE", + threshold: `${currency}:100`, + timeframe: { + d_us: 1 * 60 * 60 * 1000 * 1000, + }, + measures: ["verboten"], + display_priority: 1, + exposed: true, + is_and_combinator: true, }, - "measures": [ - "verboten" - ], - "display_priority": 1, - "exposed": true, - "is_and_combinator": true -}, -{ - "operation_type": "BALANCE", - "threshold": `${currency}:0`, - "timeframe": { - "d_us": "forever" + { + operation_type: "BALANCE", + threshold: `${currency}:100`, + timeframe: { + d_us: 1 * 60 * 60 * 1000 * 1000, + }, + measures: ["verboten"], + display_priority: 1, + exposed: true, + is_and_combinator: true, }, - "measures": [ - "verboten" - ], - "display_priority": 1, - "exposed": true, - "is_and_combinator": true -}, -{ - "operation_type": "CLOSE", - "threshold": `${currency}:0`, - "timeframe": { - "d_us": "forever" + { + operation_type: "CLOSE", + threshold: `${currency}:100`, + timeframe: { + d_us: 1 * 60 * 60 * 1000 * 1000, + }, + measures: ["verboten"], + display_priority: 1, + exposed: true, + is_and_combinator: true, }, - "measures": [ - "verboten" - ], - "display_priority": 1, - "exposed": true, - "is_and_combinator": true -}] - +]; - -const FREEZE_RULES: (currency: string) => TalerExchangeApi.KycRule[] = (currency) => [{ - "operation_type": "WITHDRAW", - "threshold": `${currency}:0`, - "timeframe": { - "d_us": "forever" +const FREEZE_RULES: (currency: string) => TalerExchangeApi.KycRule[] = ( + currency, +) => [ + { + operation_type: "WITHDRAW", + threshold: `${currency}:0`, + timeframe: { + d_us: "forever", + }, + measures: ["verboten"], + display_priority: 1, + exposed: true, + is_and_combinator: true, }, - "measures": [ - "verboten" - ], - "display_priority": 1, - "exposed": true, - "is_and_combinator": true -}, -{ - "operation_type": "DEPOSIT", - "threshold": `${currency}:0`, - "timeframe": { - "d_us": "forever" + { + operation_type: "DEPOSIT", + threshold: `${currency}:0`, + timeframe: { + d_us: "forever", + }, + measures: ["verboten"], + display_priority: 1, + exposed: true, + is_and_combinator: true, }, - "measures": [ - "verboten" - ], - "display_priority": 1, - "exposed": true, - "is_and_combinator": true -}, -{ - "operation_type": "AGGREGATE", - "threshold": `${currency}:0`, - "timeframe": { - "d_us": "forever" + { + operation_type: "AGGREGATE", + threshold: `${currency}:0`, + timeframe: { + d_us: "forever", + }, + measures: ["verboten"], + display_priority: 1, + exposed: true, + is_and_combinator: true, }, - "measures": [ - "verboten" - ], - "display_priority": 1, - "exposed": true, - "is_and_combinator": true -}, -{ - "operation_type": "MERGE", - "threshold": `${currency}:0`, - "timeframe": { - "d_us": "forever" + { + operation_type: "MERGE", + threshold: `${currency}:0`, + timeframe: { + d_us: "forever", + }, + measures: ["verboten"], + display_priority: 1, + exposed: true, + is_and_combinator: true, }, - "measures": [ - "verboten" - ], - "display_priority": 1, - "exposed": true, - "is_and_combinator": true -}, -{ - "operation_type": "BALANCE", - "threshold": `${currency}:0`, - "timeframe": { - "d_us": "forever" + { + operation_type: "BALANCE", + threshold: `${currency}:0`, + timeframe: { + d_us: "forever", + }, + measures: ["verboten"], + display_priority: 1, + exposed: true, + is_and_combinator: true, }, - "measures": [ - "verboten" - ], - "display_priority": 1, - "exposed": true, - "is_and_combinator": true -}, -{ - "operation_type": "CLOSE", - "threshold": `${currency}:0`, - "timeframe": { - "d_us": "forever" + { + operation_type: "CLOSE", + threshold: `${currency}:0`, + timeframe: { + d_us: "forever", + }, + measures: ["verboten"], + display_priority: 1, + exposed: true, + is_and_combinator: true, }, - "measures": [ - "verboten" - ], - "display_priority": 1, - "exposed": true, - "is_and_combinator": true -}] - +]; diff --git a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx index d1257c8fa..b79746107 100644 --- a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx +++ b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx @@ -157,28 +157,30 @@ export function CaseUpdate({ value: validatedForm, }; - const decision: Omit<TalerExchangeApi.AmlDecisionRequest, "officer_sig"> = - { - justification: JSON.stringify(justification), - decision_time: TalerProtocolTimestamp.now(), - h_payto: account, - keep_investigating: false, - new_rules: { - custom_measures: {}, - expiration_time: { - t_s: "never" - }, - rules: [], - successor_measure: undefined + const decision: Omit< + TalerExchangeApi.AmlDecisionRequest, + "officer_sig" + > = { + justification: JSON.stringify(justification), + decision_time: TalerProtocolTimestamp.now(), + h_payto: account, + keep_investigating: false, + new_rules: { + custom_measures: {}, + expiration_time: { + t_s: "never", }, - properties: {}, - new_measure: undefined, - }; + rules: [], + successor_measure: undefined, + }, + properties: {}, + new_measure: undefined, + }; return api.makeAmlDesicion(officer.account, decision); }, () => { - window.location.href = privatePages.cases.url({}); + window.location.href = privatePages.profile.url({}); }, (fail) => { switch (fail.case) { diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx index cea8157b0..e53c74aa5 100644 --- a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx +++ b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx @@ -24,7 +24,7 @@ import { FormConfiguration, UIFormElementConfig, UIHandlerId, - useTranslationContext + useTranslationContext, } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; import { Fragment, VNode, h } from "preact"; @@ -33,21 +33,21 @@ import { AmlEvent } from "./CaseDetails.js"; /** * the exchange doesn't hava a consistent api * https://bugs.gnunet.org/view.php?id=9142 - * - * @param data - * @returns + * + * @param data + * @returns */ function fixProvidedInfo(data: object): object { - return Object.entries(data).reduce((prev, [key,value]) => { - prev[key] = value + return Object.entries(data).reduce((prev, [key, value]) => { + prev[key] = value; if (typeof value === "object" && value["value"]) { - const v = value["value"] + const v = value["value"]; if (typeof v === "object" && v["text"]) { - prev[key].value = v["text"] + prev[key].value = v["text"]; } } - return prev - }, {} as any) + return prev; + }, {} as any); } export function ShowConsolidated({ @@ -62,7 +62,7 @@ export function ShowConsolidated({ const cons = getConsolidated(history, until); const fixed = fixProvidedInfo(cons.kyc); - console.log("fixed", fixed) + const form: FormConfiguration = { type: "double-column", design: [ diff --git a/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx index 852cfe816..743a1092b 100644 --- a/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx +++ b/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx @@ -779,6 +779,11 @@ export function InputAmount( ); } +/** + * send to web-utils + * @param param0 + * @returns + */ export function RenderAmount({ value, spec, diff --git a/packages/kyc-ui/src/app.tsx b/packages/kyc-ui/src/app.tsx index fb63771d7..0c7cd71bf 100644 --- a/packages/kyc-ui/src/app.tsx +++ b/packages/kyc-ui/src/app.tsx @@ -20,14 +20,14 @@ import { assertUnreachable, canonicalizeBaseUrl, getGlobalLogLevel, - setGlobalLogLevelFromString + setGlobalLogLevelFromString, } from "@gnu-taler/taler-util"; import { BrowserHashNavigationProvider, ExchangeApiProvider, Loading, TalerWalletIntegrationBrowserProvider, - TranslationProvider + TranslationProvider, } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { useEffect, useState } from "preact/hooks"; @@ -63,7 +63,7 @@ export function App(): VNode { baseUrl={new URL("/", baseUrl)} frameOnError={Frame} evictors={{ - exchange: evictExchangeSwrCache + exchange: evictExchangeSwrCache, }} > <SWRConfig @@ -154,10 +154,11 @@ function getInitialBackendBaseURL( const evictExchangeSwrCache: CacheEvictor<TalerExchangeCacheEviction> = { async notifySuccess(op) { switch (op) { - case TalerExchangeCacheEviction.CREATE_DESCISION:{ - await revalidateKycInfo() + case TalerExchangeCacheEviction.CREATE_DESCISION: { + await revalidateKycInfo(); return; } + case TalerExchangeCacheEviction.MAKE_AML_DECISION: case TalerExchangeCacheEviction.UPLOAD_KYC_FORM: { return; } @@ -167,4 +168,3 @@ const evictExchangeSwrCache: CacheEvictor<TalerExchangeCacheEviction> = { } }, }; - diff --git a/packages/taler-util/src/http-client/exchange.ts b/packages/taler-util/src/http-client/exchange.ts index 6ab7706b5..4bc888cc4 100644 --- a/packages/taler-util/src/http-client/exchange.ts +++ b/packages/taler-util/src/http-client/exchange.ts @@ -79,6 +79,7 @@ export type TalerExchangeErrorsByMethod< export enum TalerExchangeCacheEviction { CREATE_DESCISION, UPLOAD_KYC_FORM, + MAKE_AML_DECISION, } declare const __pubId: unique symbol; @@ -779,7 +780,7 @@ export class TalerExchangeHttpClient { * https://docs.taler.net/core/api-exchange.html#get--kyc-proof-$PROVIDER_NAME?state=$H_PAYTO * */ - async completeExternalKycProcess(provider: string, state: string) { } + async completeExternalKycProcess(provider: string, state: string) {} // // AML operations @@ -948,8 +949,12 @@ export class TalerExchangeHttpClient { }); switch (resp.status) { - case HttpStatusCode.NoContent: + case HttpStatusCode.NoContent: { + this.cacheEvictor.notifySuccess( + TalerExchangeCacheEviction.MAKE_AML_DECISION, + ); return opEmptySuccess(resp); + } case HttpStatusCode.Forbidden: return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotFound: @@ -1028,11 +1033,14 @@ function buildAMLDecisionSignature( .put(hash(stringToBytes(decision.justification))) .put(hash(stringToBytes(canonicalJson(decision.properties) + "\0"))) .put(hash(stringToBytes(canonicalJson(decision.new_rules) + "\0"))) - .put(decision.new_measure != null ? hash(stringToBytes(decision.new_measure)) : zero) + .put( + decision.new_measure != null + ? hash(stringToBytes(decision.new_measure)) + : zero, + ) .put(bufferForUint64(decision.keep_investigating ? 1 : 0)) .build(); - const officer_sig = encodeCrock(eddsaSign(sigBlob, key)); return { |