Files
Luncher/server/src/qr.ts
T
mates 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
fix: opravy generování QR kódů, zobrazení také na stránce objednání
2026-06-05 10:37:58 +02:00

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');
}