c2bbf7ea60
CI / Generate TypeScript types (push) Successful in 14s
CI / Server unit tests (push) Successful in 21s
CI / Build server (push) Successful in 25s
CI / Build client (push) Successful in 34s
CI / Playwright E2E tests (push) Successful in 1m18s
CI / Build and push Docker image (push) Successful in 49s
CI / Notify (push) Successful in 3s
107 lines
4.3 KiB
TypeScript
107 lines
4.3 KiB
TypeScript
import axios from "axios";
|
|
import crypto from "crypto";
|
|
import getStorage from "./storage";
|
|
|
|
const QR_GENERATOR_URL = 'https://api.paylibo.com/paylibo/generator/image';
|
|
const COUNTRY_CODE = 'CZ';
|
|
const CURRENCY_CODE = 'CZK';
|
|
const QR_PIXEL_SIZE = 256;
|
|
|
|
const storage = getStorage();
|
|
|
|
/**
|
|
* Převede číslo účtu z BBAN do IBAN. Automaticky dopočítá kontrolní číslice.
|
|
*
|
|
* @param bankAccountNumber číslo účtu ve formátu BBAN (123456-0123456789/0100)
|
|
*/
|
|
export function convertBbanToIban(bankAccountNumber: string): string {
|
|
// TODO validovat číslo účtu stejně jako na klientovi, pro případ že sem někdo pošle nesmysl
|
|
let prefix: string = '';
|
|
let accountNumber: string = bankAccountNumber;
|
|
if (bankAccountNumber.indexOf('-') >= 0) {
|
|
const split = bankAccountNumber.split('-');
|
|
prefix = split[0];
|
|
accountNumber = split[1];
|
|
}
|
|
prefix = prefix.padStart(6, '0');
|
|
const split = accountNumber.split('/');
|
|
accountNumber = split[0].padStart(10, '0');
|
|
const bankCode = split[1].padStart(4, '0');
|
|
let iban = `${bankCode}${prefix}${accountNumber}${COUNTRY_CODE}00`;
|
|
// Zatím napevno, nemá smysl řešit nic jiného než CZ
|
|
iban = iban.replace('C', '12').replace('Z', '35');
|
|
const remainder = BigInt(iban) % BigInt(97);
|
|
const checkDigits = (BigInt(98) - remainder).toString().padStart(2, '0');
|
|
iban = `${COUNTRY_CODE}${checkDigits}${bankCode}${prefix}${accountNumber}`;
|
|
if (iban.length !== 24) {
|
|
throw new Error("Neplatná délka sestaveného IBAN: " + iban.length + ", očekáváno 24");
|
|
}
|
|
return iban;
|
|
}
|
|
|
|
function createStorageKey(customerName: string, id: string): string {
|
|
const nameHash = crypto.createHash('md5').update(customerName).digest('hex');
|
|
return `qr_${nameHash}_${id}`;
|
|
}
|
|
|
|
/**
|
|
* Očistí zprávu (účel platby) pro QR platbu:
|
|
* - transliteruje diakritiku na základní písmena (š→s, č→c, ř→r, ...)
|
|
* - odstraní zbylé znaky mimo ISO 8859-1
|
|
* - odstraní '*', který v QR platbě slouží jako oddělovač polí
|
|
* - ořízne na max. 60 znaků
|
|
*
|
|
* @param message původní zpráva
|
|
* @returns očištěná zpráva vhodná pro QR platbu
|
|
*/
|
|
export function sanitizeQrMessage(message: string): string {
|
|
const sanitized = message
|
|
.normalize('NFD').replace(/[\u0300-\u036f]/g, '') // diakritika → základní písmeno
|
|
.replace(/[^\x00-\xff]/g, '') // znaky mimo ISO 8859-1
|
|
.replace(/\*/g, ''); // '*' je v QR platbě oddělovač
|
|
return sanitized.length > 60 ? sanitized.substring(0, 60) : sanitized;
|
|
}
|
|
|
|
/**
|
|
* Vygeneruje a uloží obrázek platebního QR kódu do storage (Redis/JSON).
|
|
* Data přežijí redeploy — není třeba persistentní filesystém.
|
|
*
|
|
* @param customerName jméno uživatele, pro kterého je QR kód generován
|
|
* @param bankAccountNumber číslo cílového bankovního účtu ve formátu BBAN
|
|
* @param bankAccountHolder jméno držitele cílového bankovního účtu
|
|
* @param amount částka v Kč
|
|
* @param message zpráva pro příjemce
|
|
* @param id unikátní identifikátor (UUID) tohoto QR kódu
|
|
*/
|
|
export async function generateQr(customerName: string, bankAccountNumber: string, bankAccountHolder: string, amount: number, message: string, id: string): Promise<void> {
|
|
message = sanitizeQrMessage(message);
|
|
const payload = {
|
|
iban: convertBbanToIban(bankAccountNumber),
|
|
amount,
|
|
currency: CURRENCY_CODE,
|
|
message,
|
|
recipientName: bankAccountHolder,
|
|
branding: false,
|
|
compress: false,
|
|
size: QR_PIXEL_SIZE,
|
|
};
|
|
const response = await axios.get(QR_GENERATOR_URL, { responseType: 'arraybuffer', params: { ...payload } });
|
|
const base64 = Buffer.from(response.data).toString('base64');
|
|
await storage.setData(createStorageKey(customerName, id), base64);
|
|
}
|
|
|
|
/**
|
|
* Vrátí obrázek s QR kódem ze storage.
|
|
*
|
|
* @param customerName jméno uživatele
|
|
* @param id unikátní identifikátor QR kódu
|
|
* @returns data obrázku
|
|
*/
|
|
export async function getQr(customerName: string, id: string): Promise<Buffer> {
|
|
const base64 = await storage.getData<string>(createStorageKey(customerName, id));
|
|
if (!base64) {
|
|
throw new Error("QR kód nebyl nalezen");
|
|
}
|
|
return Buffer.from(base64, 'base64');
|
|
}
|