commit dd9dc8c523334278a532f8eb1b3645eeb6799a6a
parent 60c162a57d77ad2b79430caeba4d14b015a6547d
Author: Sebastian <sebasjm@gmail.com>
Date: Wed, 2 Apr 2025 16:48:02 -0300
fix #9684
Diffstat:
9 files changed, 188 insertions(+), 121 deletions(-)
diff --git a/packages/aml-backoffice-ui/src/hooks/decision-request.ts b/packages/aml-backoffice-ui/src/hooks/decision-request.ts
@@ -86,6 +86,10 @@ export interface DecisionRequest {
*/
justification: string | undefined;
/**
+ * Name of the account holder if this is an unknown account to the exchange
+ */
+ accountName: string | undefined;
+ /**
* Custom properties not listed on GANA
*/
custom_properties: Record<string, any> | undefined;
@@ -116,6 +120,7 @@ export const codecForDecisionRequest = (): Codec<DecisionRequest> =>
.property("attributes", codecOptional(codecForAccountAttributes()))
.property("custom_properties", codecForAny())
.property("justification", codecOptional(codecForString()))
+ .property("accountName", codecOptional(codecForString()))
.property("custom_events", codecOptional(codecForList(codecForString())))
.property("triggering_events", codecOptional(codecForList(codecForString())))
.property(
@@ -133,6 +138,7 @@ const defaultDecisionRequest: DecisionRequest = {
deadline: undefined,
onExpire_measures: undefined,
custom_events: undefined,
+ accountName: undefined,
justification: undefined,
keep_investigating: false,
new_measures: undefined,
diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
@@ -203,6 +203,7 @@ export function CaseDetails({
onExpire_measures: undefined,
custom_events: undefined,
attributes: undefined,
+ accountName: undefined,
triggering_events: undefined,
justification: undefined,
keep_investigating: false,
diff --git a/packages/aml-backoffice-ui/src/pages/Search.tsx b/packages/aml-backoffice-ui/src/pages/Search.tsx
@@ -21,11 +21,13 @@ import {
getURLHostnamePortPath,
hashNormalizedPaytoUri,
HttpStatusCode,
+ IbanError,
parsePaytoUri,
PaytoUri,
stringifyPaytoUri,
TalerError,
TranslatedString,
+ validateIban,
} from "@gnu-taler/taler-util";
import {
Attention,
@@ -614,45 +616,25 @@ function validateIBAN(
if (!iban) {
return i18n.str`required`;
}
- // Check total length
- if (iban.length < 4)
- return i18n.str`IBAN numbers usually have more that 4 digits`;
- if (iban.length > 34)
- return i18n.str`IBAN numbers usually have less that 34 digits`;
-
- const A_code = "A".charCodeAt(0);
- const Z_code = "Z".charCodeAt(0);
- const IBAN = iban.toUpperCase();
-
- // check supported country
- // const code = IBAN.substr(0, 2);
- // const found = code in COUNTRY_TABLE;
- // if (!found) return i18n.str`IBAN country code not found`;
-
- // 2.- Move the four initial characters to the end of the string
- const step2 = IBAN.substr(4) + iban.substr(0, 4);
- const step3 = Array.from(step2)
- .map((letter) => {
- const code = letter.charCodeAt(0);
- if (code < A_code || code > Z_code) return letter;
- return `${letter.charCodeAt(0) - "A".charCodeAt(0) + 10}`;
- })
- .join("");
-
- const checksum = calculate_iban_checksum(step3);
- if (checksum !== 1)
- return i18n.str`IBAN number is invalid, checksum is wrong`;
- return undefined;
-}
-function calculate_iban_checksum(str: string): number {
- const numberStr = str.substr(0, 5);
- const rest = str.substr(5);
- const number = parseInt(numberStr, 10);
- const result = number % 97;
- if (rest.length > 0) {
- return calculate_iban_checksum(`${result}${rest}`);
+ const result = validateIban(iban);
+ if (result.type === "valid") {
+ return undefined;
+ }
+ switch (result.code) {
+ case IbanError.TOO_LONG:
+ return i18n.str`IBAN numbers usually have less that 34 digits`;
+ case IbanError.TOO_SHORT:
+ return i18n.str`IBAN numbers usually have more that 4 digits`;
+ case IbanError.INVALID_CHARSET:
+ return i18n.str`IBAN number is invalid, should only contain numbers and letters`;
+ case IbanError.INVALID_COUNTRY:
+ return i18n.str`Unsupported country`;
+ case IbanError.INVALID_CHECKSUM:
+ return i18n.str`IBAN number is invalid, checksum is wrong`;
+ default: {
+ assertUnreachable(result.code);
+ }
}
- return result;
}
const DOMAIN_REGEX =
diff --git a/packages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx b/packages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx
@@ -69,24 +69,27 @@ const STEPS_ORDER_MAP = STEPS_ORDER.reduce(
},
);
-function isAttributesCompleted(request: DecisionRequest): boolean {
- return request.attributes === undefined || request.attributes.errors === undefined;
+export function isRulesCompleted(request: DecisionRequest): boolean {
+ return request.rules !== undefined && request.deadline !== undefined;
}
-function isRulesCompleted(request: DecisionRequest): boolean {
- return request.rules !== undefined;
+export function isAttributesCompleted(request: DecisionRequest): boolean {
+ return request.attributes === undefined || request.attributes.errors === undefined;
}
-function isPropertiesCompleted(request: DecisionRequest): boolean {
+export function isPropertiesCompleted(request: DecisionRequest): boolean {
return request.properties !== undefined;
}
-function isEventsCompleted(request: DecisionRequest): boolean {
+export function isEventsCompleted(request: DecisionRequest): boolean {
return request.custom_events !== undefined;
}
-function isMeasuresCompleted(request: DecisionRequest): boolean {
+export function isMeasuresCompleted(request: DecisionRequest): boolean {
return request.new_measures !== undefined;
}
-function isJustificationCompleted(request: DecisionRequest): boolean {
+export function isJustificationCompleted(request: DecisionRequest): boolean {
return request.keep_investigating !== undefined && !!request.justification;
}
+export function isJustificationCompletedForNewACcount(request: DecisionRequest): boolean {
+ return request.keep_investigating !== undefined && !!request.justification && !!request.accountName;
+}
export function AmlDecisionRequestWizard({
account,
@@ -114,7 +117,7 @@ export function AmlDecisionRequestWizard({
case "measures":
return <Measures />;
case "justification":
- return <Justification />;
+ return <Justification newPayto={newPayto} />;
case "attributes":
return <Attributes formId={formId}/>;
case "summary":
@@ -125,7 +128,7 @@ export function AmlDecisionRequestWizard({
return (
<div>
- <WizardSteps step={stepOrDefault} onMove={onMove} />
+ <WizardSteps step={stepOrDefault} onMove={onMove} newAccount={!!newPayto} />
<button
disabled={!STEPS_ORDER_MAP[stepOrDefault].prev}
onClick={() => {
@@ -151,9 +154,11 @@ export function AmlDecisionRequestWizard({
function WizardSteps({
step: currentStep,
onMove,
+ newAccount,
}: {
step: WizardSteps;
onMove: (n: WizardSteps | undefined) => void;
+ newAccount: boolean;
}): VNode {
const [request] = useCurrentDecisionRequest();
const { i18n } = useTranslationContext();
@@ -187,7 +192,7 @@ function WizardSteps({
justification: {
label: i18n.str`Justification`,
description: i18n.str`Describe the decision.`,
- isCompleted: isJustificationCompleted,
+ isCompleted: newAccount ? isJustificationCompletedForNewACcount : isJustificationCompleted,
},
properties: {
label: i18n.str`Properties`,
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Justification.tsx b/packages/aml-backoffice-ui/src/pages/decision/Justification.tsx
@@ -2,6 +2,7 @@ import {
AbsoluteTime,
Duration,
MeasureInformation,
+ PaytoString,
TalerError,
} from "@gnu-taler/taler-util";
import {
@@ -22,7 +23,7 @@ import { useServerMeasures } from "../../hooks/server-info.js";
* @param param0
* @returns
*/
-export function Justification({}: {}): VNode {
+export function Justification({ newPayto }: { newPayto?: PaytoString }): VNode {
const { i18n } = useTranslationContext();
const [request, _, updateRequest] = useCurrentDecisionRequest();
const measures = useServerMeasures();
@@ -30,7 +31,9 @@ export function Justification({}: {}): VNode {
!measures || measures instanceof TalerError || measures.type === "fail"
? []
: Object.entries(measures.body.roots).map(([id, mi]) => ({ id, ...mi }));
- const design = formDesign(i18n, measureList);
+
+ const unknownAccount = !!newPayto
+ const design = formDesign(i18n, measureList, unknownAccount);
const expMeasres: string[] = !request.onExpire_measures
? []
@@ -39,8 +42,8 @@ export function Justification({}: {}): VNode {
const form = useForm<FormType>(design, {
investigate: request.keep_investigating,
justification: request.justification,
- expiration: request.deadline,
measures: expMeasres,
+ accountName: request.accountName,
});
onComponentUnload(() => {
@@ -49,10 +52,7 @@ export function Justification({}: {}): VNode {
keep_investigating: !!form.status.result.investigate,
justification: form.status.result.justification ?? "",
onExpire_measures: (form.status.result.measures ?? []) as string[],
-
- deadline:
- (form.status.result.expiration as AbsoluteTime) ?? AbsoluteTime.never(),
- // onExpire_measures,
+ accountName: form.status.result.justification ?? "",
});
});
@@ -65,14 +65,15 @@ export function Justification({}: {}): VNode {
type FormType = {
justification: string;
+ accountName: string;
investigate: boolean;
- expiration: AbsoluteTime;
measures: string[];
};
const formDesign = (
i18n: InternationalizationAPI,
mi: (MeasureInformation & { id: string })[],
+ unknownAccount: boolean
): FormDesign<FormType> => ({
type: "single-column",
fields: [
@@ -88,32 +89,12 @@ const formDesign = (
label: i18n.str`Keep investigation?`,
},
{
- type: "choiceHorizontal",
- label: i18n.str`Expiration`,
- id: "expiration",
- choices: [
- {
- label: i18n.str`In a week`,
- value: AbsoluteTime.addDuration(
- AbsoluteTime.now(),
- Duration.fromSpec({ days: 7 }),
- ) as any,
- },
- {
- label: i18n.str`In a month`,
- value: AbsoluteTime.addDuration(
- AbsoluteTime.now(),
- Duration.fromSpec({ months: 1 }),
- ) as any,
- },
- ],
- },
- {
- id: "expiration",
- type: "absoluteTimeText",
- placeholder: "dd/MM/yyyy",
- pattern: "dd/MM/yyyy",
- label: i18n.str`Expiration`,
+ id: "accountName",
+ type: "text",
+ label: i18n.str`Account holder`,
+ required: true,
+ help:i18n.str`Full name of the account holder`,
+ hidden: !unknownAccount,
},
{
type: "selectMultiple",
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx b/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx
@@ -38,7 +38,7 @@ export function Rules({ account }: { account: string }): VNode {
const { i18n } = useTranslationContext();
const { config } = useExchangeApiContext();
- const [request, updateRequest] = useCurrentDecisionRequest();
+ const [request, updateRequestField, updateRequest] = useCurrentDecisionRequest();
const measures = useServerMeasures();
const measureList =
@@ -47,7 +47,9 @@ export function Rules({ account }: { account: string }): VNode {
: Object.entries(measures.body.roots).map(([id, mi]) => ({ id, ...mi }));
const design = formDesign(i18n, config.config.currency, measureList);
- const form = useForm<FormType>(design, {});
+ const form = useForm<FormType>(design, {
+ expiration: request.deadline,
+ });
const info =
!activeDecision ||
@@ -57,8 +59,15 @@ export function Rules({ account }: { account: string }): VNode {
: activeDecision.body;
onComponentUnload(() => {
- if (!request.rules) {
- updateRequest("rules", []);
+ if (!request.rules) {
+ updateRequestField("rules", []);
+ } else {
+ updateRequest({
+ ...request,
+ deadline:
+ (form.status.result.expiration as AbsoluteTime) ?? AbsoluteTime.never(),
+ });
+
}
});
@@ -77,7 +86,7 @@ export function Rules({ account }: { account: string }): VNode {
is_and_combinator: nr.all,
measures,
});
- updateRequest("rules", result);
+ updateRequestField("rules", result);
}
return (
@@ -104,7 +113,7 @@ export function Rules({ account }: { account: string }): VNode {
<button
onClick={() => {
- updateRequest(
+ updateRequestField(
"rules",
Object.values(LimitOperationType).map((operation_type) => ({
display_priority: 1,
@@ -125,7 +134,7 @@ export function Rules({ account }: { account: string }): VNode {
</button>
<button
onClick={() => {
- updateRequest(
+ updateRequestField(
"rules",
Object.values(LimitOperationType).map((operation_type) => ({
display_priority: 1,
@@ -153,7 +162,7 @@ export function Rules({ account }: { account: string }): VNode {
</button>
<button
onClick={() => {
- updateRequest(
+ updateRequestField(
"rules",
Object.values(LimitOperationType).map((operation_type) => ({
display_priority: 1,
@@ -184,7 +193,7 @@ export function Rules({ account }: { account: string }): VNode {
onRemove={(r, idx) => {
const nr = !request.rules ? [] : [...request.rules];
nr.splice(idx, 1);
- updateRequest("rules", nr);
+ updateRequestField("rules", nr);
}}
/>
@@ -213,6 +222,7 @@ type FormType = {
threshold: AmountJson;
timeframe: Duration;
exposed: boolean;
+ expiration: AbsoluteTime;
measures: string[];
all: boolean;
};
@@ -300,5 +310,42 @@ const formDesign = (
label: i18n.str`All measures`,
help: i18n.str`Hint the customer that all measure should be completed`,
},
+ {
+ type: "choiceHorizontal",
+ label: i18n.str`Expiration`,
+ help: i18n.str`Predefined shortcuts`,
+ id: "expiration",
+ choices: [
+ {
+ label: i18n.str`In a week`,
+ value: AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ days: 7 }),
+ ) as any,
+ },
+ {
+ label: i18n.str`In a month`,
+ value: AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ months: 1 }),
+ ) as any,
+ },
+ {
+ label: i18n.str`In a year`,
+ value: AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ years: 1 }),
+ ) as any,
+ },
+ ],
+ },
+ {
+ id: "expiration",
+ type: "absoluteTimeText",
+ placeholder: "dd/MM/yyyy",
+ pattern: "dd/MM/yyyy",
+ label: i18n.str`Expiration`,
+ help: i18n.str`For how long this rules will last`,
+ },
],
});
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx b/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx
@@ -2,9 +2,12 @@ import {
AbsoluteTime,
AmlDecisionRequest,
assertUnreachable,
+ buildPayto,
Duration,
HttpStatusCode,
+ parsePaytoUri,
PaytoString,
+ stringifyPaytoUri,
TalerError,
} from "@gnu-taler/taler-util";
import {
@@ -24,7 +27,16 @@ import {
} from "../CaseDetails.js";
import { CurrentMeasureTable, Mesaures } from "../MeasuresTable.js";
import { useOfficer } from "../../hooks/officer.js";
-import { WizardSteps } from "./AmlDecisionRequestWizard.js";
+import {
+ isAttributesCompleted,
+ isEventsCompleted,
+ isJustificationCompleted,
+ isJustificationCompletedForNewACcount,
+ isMeasuresCompleted,
+ isPropertiesCompleted,
+ isRulesCompleted,
+ WizardSteps,
+} from "./AmlDecisionRequestWizard.js";
import { useCustomMeasures } from "../../hooks/custom-measures.js";
/**
@@ -63,24 +75,26 @@ export function Summary({
const { lib } = useExchangeApiContext();
- const INVALID_RULES = !decision.deadline || !decision.rules;
- const INVALID_MEASURES = decision.new_measures === undefined;
- const INVALID_PROPERTIES = decision.properties === undefined;
- const INVALID_EVENTS = false; //decision.inhibit_events === undefined;
- const INVALID_JUSTIFICATION =
- decision.justification === undefined || !decision.justification;
+ const isNewAccount = !!newPayto;
const INVALID_ACCOUNT = !account;
- const INVALID_ATTRIBUTES =
- decision.attributes !== undefined &&
- decision.attributes.errors !== undefined;
+ const INVALID_RULES = !isRulesCompleted(decision); //!decision.deadline || !decision.rules;
+ const INVALID_MEASURES = !isMeasuresCompleted(decision); //.new_measures === undefined;
+ const INVALID_PROPERTIES = !isPropertiesCompleted(decision); //.properties === undefined;
+ const INVALID_EVENTS = !isEventsCompleted(decision); //false; //decision.inhibit_events === undefined;
+ const INVALID_JUSTIFICATION = isNewAccount
+ ? !isJustificationCompletedForNewACcount(decision)
+ : !isJustificationCompleted(decision);
+ const INVALID_ATTRIBUTES = !isAttributesCompleted(decision);
+ // decision.attributes !== undefined &&
+ // decision.attributes.errors !== undefined;
const CANT_SUBMIT =
INVALID_ACCOUNT ||
- INVALID_EVENTS ||
- INVALID_JUSTIFICATION ||
+ INVALID_RULES ||
INVALID_MEASURES ||
INVALID_PROPERTIES ||
- INVALID_RULES ||
+ INVALID_EVENTS ||
+ INVALID_JUSTIFICATION ||
INVALID_ATTRIBUTES;
function clearUp() {
@@ -90,6 +104,7 @@ export function Summary({
deadline: undefined,
triggering_events: undefined,
attributes: undefined,
+ accountName: undefined,
justification: undefined,
keep_investigating: false,
new_measures: undefined,
@@ -100,6 +115,11 @@ export function Summary({
onMove(undefined);
}
+ const fullPayto = !newPayto? undefined: parsePaytoUri(newPayto)
+ if (fullPayto && decision.accountName) {
+ fullPayto.params["receiver-name"] = decision.accountName;
+ }
+
const submitHandler =
CANT_SUBMIT || !session
? undefined
@@ -111,7 +131,7 @@ export function Summary({
AbsoluteTime.now(),
),
justification: decision.justification!,
- payto_uri: newPayto,
+ payto_uri: !fullPayto ? undefined : stringifyPaytoUri(fullPayto),
keep_investigating: decision.keep_investigating,
new_rules: {
expiration_time: AbsoluteTime.toProtocolTimestamp(
@@ -192,8 +212,8 @@ export function Summary({
<ShowDecisionLimitInfo
fixed
since={AbsoluteTime.now()}
- until={decision.deadline}
- rules={decision.rules}
+ until={decision.deadline!}
+ rules={decision.rules!}
startOpen
/>
</div>
@@ -208,7 +228,7 @@ export function Summary({
You should specify in the measure section.
</i18n.Translate>
</Attention>
- ) : decision.new_measures.length === 0 ? (
+ ) : decision.new_measures!.length === 0 ? (
<Attention
type="info"
title={i18n.str`No customer action required.`}
@@ -252,7 +272,7 @@ export function Summary({
onClose={() => onMove("justification")}
>
<i18n.Translate>
- You must specify in the justification section.
+ You must complete the justification section.
</i18n.Translate>
</Attention>
) : (
diff --git a/packages/taler-util/src/iban.ts b/packages/taler-util/src/iban.ts
@@ -28,8 +28,16 @@
* @author Florian Dold <dold@taler.net>
*/
+export enum IbanError {
+ INVALID_COUNTRY,
+ TOO_LONG,
+ TOO_SHORT,
+ INVALID_CHARSET,
+ INVALID_CHECKSUM,
+}
+
export type IbanValidationResult =
- | { type: "invalid" }
+ | { type: "invalid"; code: IbanError }
| {
type: "valid";
normalizedIban: string;
@@ -228,13 +236,27 @@ function mod97(digits: number[]): number {
}
export function validateIban(ibanString: string): IbanValidationResult {
- let myIban = ibanString.toLocaleUpperCase().replace(" ", "");
- let countryCode = myIban.substring(0, 2);
- let countryInfo = ibanCountryInfoTable[countryCode];
+ if (ibanString.length < 4) {
+ return {
+ type: "invalid",
+ code: IbanError.TOO_SHORT,
+ };
+ }
+ if (ibanString.length > 34) {
+ return {
+ type: "invalid",
+ code: IbanError.TOO_LONG,
+ };
+ }
+
+ const myIban = ibanString.toLocaleUpperCase().replace(" ", "");
+ const countryCode = myIban.substring(0, 2);
+ const countryInfo = ibanCountryInfoTable[countryCode];
if (!countryInfo) {
return {
type: "invalid",
+ code: IbanError.INVALID_COUNTRY,
};
}
@@ -245,6 +267,7 @@ export function validateIban(ibanString: string): IbanValidationResult {
if (!appendDigit(digits, cc)) {
return {
type: "invalid",
+ code: IbanError.INVALID_CHARSET,
};
}
}
@@ -253,6 +276,7 @@ export function validateIban(ibanString: string): IbanValidationResult {
if (!appendDigit(digits, ibanString.charCodeAt(i))) {
return {
type: "invalid",
+ code: IbanError.INVALID_CHARSET,
};
}
}
@@ -266,6 +290,7 @@ export function validateIban(ibanString: string): IbanValidationResult {
} else {
return {
type: "invalid",
+ code: IbanError.INVALID_CHECKSUM,
};
}
}
diff --git a/packages/taler-wallet-webextension/src/cta/DevExperiment/test.ts b/packages/taler-wallet-webextension/src/cta/DevExperiment/test.ts
@@ -46,14 +46,14 @@ describe("DevExperiment CTA states", () => {
({ status }) => {
expect(status).equals("error");
},
- ({ status, error }) => {
- expect(status).equals("error");
+ // ({ status, error }) => {
+ // expect(status).equals("error");
- if (!error) expect.fail();
- // if (!error.hasError) expect.fail();
- // if (error.operational) expect.fail();
- // expect(error.description).eq("ERROR_NO-URI-FOR-DEPOSIT");
- },
+ // if (!error) expect.fail();
+ // // if (!error.hasError) expect.fail();
+ // // if (error.operational) expect.fail();
+ // // expect(error.description).eq("ERROR_NO-URI-FOR-DEPOSIT");
+ // },
],
TestingContext,
);