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 { 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 { const base64 = await storage.getData(createStorageKey(customerName, id)); if (!base64) { throw new Error("QR kód nebyl nalezen"); } return Buffer.from(base64, 'base64'); }