Files
linumiq-invoice/app/services/invoice/girocode.ts
T
Gerhard Scheikl 5b2aa5d62b first version
2026-04-28 21:56:11 +02:00

64 lines
1.9 KiB
TypeScript

/**
* 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<string> {
const payload = buildGiroCodePayload(input);
// EPC requires error correction level M.
return QRCode.toDataURL(payload, {
errorCorrectionLevel: "M",
margin: 1,
width: 256,
});
}