/** * EPC (SEPA Credit Transfer) QR code payload, a.k.a. GiroCode. * Specification: EPC069-12 v3.0. * * The payload is a fixed-order line-delimited block of fields that any SEPA * banking app can scan to pre-fill a transfer. */ import QRCode from "qrcode"; export interface GiroCodeInput { beneficiaryName: string; iban: string; bic?: string; amount: number; currency?: string; /** Free-form remittance information (e.g. invoice number). Max 140 chars. */ remittance: string; } export function buildGiroCodePayload(input: GiroCodeInput): string { const currency = input.currency || "EUR"; if (currency !== "EUR") { // EPC069-12 requires EUR; keep going but warn (most invoices are EUR). console.warn(`GiroCode: non-EUR currency ${currency} is non-standard.`); } // Beneficiary name max 70 chars per spec. const name = input.beneficiaryName.slice(0, 70); const iban = input.iban.replace(/\s+/g, "").toUpperCase(); const bic = (input.bic || "").replace(/\s+/g, "").toUpperCase(); const amount = input.amount.toFixed(2); const remittance = input.remittance.slice(0, 140); // Field order is fixed; trailing fields can be empty. // Service tag SCT = SEPA Credit Transfer. const lines = [ "BCD", "002", // version "1", // character set 1 = UTF-8 "SCT", // SEPA Credit Transfer bic, // BIC (optional in v002) name, iban, `EUR${amount}`, "", // purpose (optional) "", // structured remittance remittance, // unstructured remittance ]; return lines.join("\n"); } export async function buildGiroCodeDataUrl( input: GiroCodeInput, ): Promise { const payload = buildGiroCodePayload(input); // EPC requires error correction level M. return QRCode.toDataURL(payload, { errorCorrectionLevel: "M", margin: 1, width: 256, }); }