/* 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 */ import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util"; import { ErrorNotification, ErrorType, HttpError, useTranslationContext, } from "@gnu-taler/web-util/browser"; /** * Validate (the number part of) an amount. If needed, * replace comma with a dot. Returns 'false' whenever * the input is invalid, the valid amount otherwise. */ const amountRegex = /^[0-9]+(.[0-9]+)?$/; export function validateAmount( maybeAmount: string | undefined, ): string | undefined { if (!maybeAmount || !amountRegex.test(maybeAmount)) { return; } return maybeAmount; } /** * Extract IBAN from a Payto URI. */ export function getIbanFromPayto(url: string): string { const pathSplit = new URL(url).pathname.split("/"); let lastIndex = pathSplit.length - 1; // Happens if the path ends with "/". if (pathSplit[lastIndex] === "") lastIndex--; const iban = pathSplit[lastIndex]; return iban; } export function undefinedIfEmpty(obj: T): T | undefined { return Object.keys(obj).some((k) => (obj as any)[k] !== undefined) ? obj : undefined; } export type PartialButDefined = { [P in keyof T]: T[P] | undefined; }; export type WithIntermediate = { [prop in keyof Type]: Type[prop] extends object ? WithIntermediate : Type[prop] | undefined; }; export type RecursivePartial = { [P in keyof T]?: T[P] extends (infer U)[] ? RecursivePartial[] : T[P] extends object ? RecursivePartial : T[P]; }; export enum TanChannel { SMS = "sms", EMAIL = "email", FILE = "file", } export enum CashoutStatus { // The payment was initiated after a valid // TAN was received by the bank. CONFIRMED = "confirmed", // The cashout was created and now waits // for the TAN by the author. PENDING = "pending", } export const PAGE_SIZE = 20; export const MAX_RESULT_SIZE = PAGE_SIZE * 2 - 1; export function buildRequestErrorMessage( i18n: ReturnType["i18n"], cause: HttpError, specialCases: { onClientError?: (status: HttpStatusCode) => TranslatedString | undefined; onServerError?: (status: HttpStatusCode) => TranslatedString | undefined; } = {}, ): ErrorNotification { let result: ErrorNotification; switch (cause.type) { case ErrorType.TIMEOUT: { result = { type: "error", title: i18n.str`Request timeout`, }; break; } case ErrorType.CLIENT: { const title = specialCases.onClientError && specialCases.onClientError(cause.status); result = { type: "error", title: title ? title : i18n.str`The server didn't accept the request`, description: cause?.payload?.error?.description as TranslatedString, debug: JSON.stringify(cause), }; break; } case ErrorType.SERVER: { const title = specialCases.onServerError && specialCases.onServerError(cause.status); result = { type: "error", title: title ? title : i18n.str`The server had problems processing the request`, description: cause?.payload?.error?.description as TranslatedString, debug: JSON.stringify(cause), }; break; } case ErrorType.UNREADABLE: { result = { type: "error", title: i18n.str`Unexpected error`, description: `Response from ${cause?.info?.url} is unreadable, status: ${cause?.status}` as TranslatedString, debug: JSON.stringify(cause), }; break; } case ErrorType.UNEXPECTED: { result = { type: "error", title: i18n.str`Unexpected error`, debug: JSON.stringify(cause), }; break; } } return result; } export const COUNTRY_TABLE = { AE: "U.A.E.", AF: "Afghanistan", AL: "Albania", AM: "Armenia", AN: "Netherlands Antilles", AR: "Argentina", AT: "Austria", AU: "Australia", AZ: "Azerbaijan", BA: "Bosnia and Herzegovina", BD: "Bangladesh", BE: "Belgium", BG: "Bulgaria", BH: "Bahrain", BN: "Brunei Darussalam", BO: "Bolivia", BR: "Brazil", BT: "Bhutan", BY: "Belarus", BZ: "Belize", CA: "Canada", CG: "Congo", CH: "Switzerland", CI: "Cote d'Ivoire", CL: "Chile", CM: "Cameroon", CN: "People's Republic of China", CO: "Colombia", CR: "Costa Rica", CS: "Serbia and Montenegro", CZ: "Czech Republic", DE: "Germany", DK: "Denmark", DO: "Dominican Republic", DZ: "Algeria", EC: "Ecuador", EE: "Estonia", EG: "Egypt", ER: "Eritrea", ES: "Spain", ET: "Ethiopia", FI: "Finland", FO: "Faroe Islands", FR: "France", GB: "United Kingdom", GD: "Caribbean", GE: "Georgia", GL: "Greenland", GR: "Greece", GT: "Guatemala", HK: "Hong Kong", // HK: "Hong Kong S.A.R.", HN: "Honduras", HR: "Croatia", HT: "Haiti", HU: "Hungary", ID: "Indonesia", IE: "Ireland", IL: "Israel", IN: "India", IQ: "Iraq", IR: "Iran", IS: "Iceland", IT: "Italy", JM: "Jamaica", JO: "Jordan", JP: "Japan", KE: "Kenya", KG: "Kyrgyzstan", KH: "Cambodia", KR: "South Korea", KW: "Kuwait", KZ: "Kazakhstan", LA: "Laos", LB: "Lebanon", LI: "Liechtenstein", LK: "Sri Lanka", LT: "Lithuania", LU: "Luxembourg", LV: "Latvia", LY: "Libya", MA: "Morocco", MC: "Principality of Monaco", MD: "Moldava", // MD: "Moldova", ME: "Montenegro", MK: "Former Yugoslav Republic of Macedonia", ML: "Mali", MM: "Myanmar", MN: "Mongolia", MO: "Macau S.A.R.", MT: "Malta", MV: "Maldives", MX: "Mexico", MY: "Malaysia", NG: "Nigeria", NI: "Nicaragua", NL: "Netherlands", NO: "Norway", NP: "Nepal", NZ: "New Zealand", OM: "Oman", PA: "Panama", PE: "Peru", PH: "Philippines", PK: "Islamic Republic of Pakistan", PL: "Poland", PR: "Puerto Rico", PT: "Portugal", PY: "Paraguay", QA: "Qatar", RE: "Reunion", RO: "Romania", RS: "Serbia", RU: "Russia", RW: "Rwanda", SA: "Saudi Arabia", SE: "Sweden", SG: "Singapore", SI: "Slovenia", SK: "Slovak", SN: "Senegal", SO: "Somalia", SR: "Suriname", SV: "El Salvador", SY: "Syria", TH: "Thailand", TJ: "Tajikistan", TM: "Turkmenistan", TN: "Tunisia", TR: "Turkey", TT: "Trinidad and Tobago", TW: "Taiwan", TZ: "Tanzania", UA: "Ukraine", US: "United States", UY: "Uruguay", VA: "Vatican", VE: "Venezuela", VN: "Viet Nam", YE: "Yemen", ZA: "South Africa", ZW: "Zimbabwe", }; /** * An IBAN is validated by converting it into an integer and performing a * basic mod-97 operation (as described in ISO 7064) on it. * If the IBAN is valid, the remainder equals 1. * * The algorithm of IBAN validation is as follows: * 1.- Check that the total IBAN length is correct as per the country. If not, the IBAN is invalid * 2.- Move the four initial characters to the end of the string * 3.- Replace each letter in the string with two digits, thereby expanding the string, where A = 10, B = 11, ..., Z = 35 * 4.- Interpret the string as a decimal integer and compute the remainder of that number on division by 97 * * If the remainder is 1, the check digit test is passed and the IBAN might be valid. * */ export function validateIBAN( iban: string, i18n: ReturnType["i18n"], ): string | undefined { // 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.substring(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.substring(4) + iban.substring(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 not valid, checksum is wrong`; return undefined; } function calculate_iban_checksum(str: string): number { const numberStr = str.substring(0, 5); const rest = str.substring(5); const number = parseInt(numberStr, 10); const result = number % 97; if (rest.length > 0) { return calculate_iban_checksum(`${result}${rest}`); } return result; }