/** * pain.001 (ISO 20022) payment file generator for salary batch payments. * * Swedish banks (SEB, Handelsbanken, Swedbank, Nordea) accept * pain.001.001.03 for credit transfer initiation. * * The generated file is uploaded to the bank's corporate portal. */ export interface Pain001CompanyData { name: string orgNumber: string // NNNNNN-NNNN iban: string // SE + 22 digits bic: string // SWIFT/BIC code } export interface Pain001Employee { name: string clearingNumber: string bankAccountNumber: string netSalary: number // Amount to pay } export interface Pain001Options { messageId: string // Unique message ID paymentDate: string // YYYY-MM-DD requested execution date periodLabel: string // e.g. "2026-04" for remittance info } /** * Generate pain.001.001.03 XML for salary batch payment. * * Structure: * Document > CstmrCdtTrfInitn > GrpHdr + PmtInf (one per batch) * PmtInf contains CdtTrfTxInf per employee * * Per BFL: The generated file is räkenskapsinformation (underlag) * linked to the salary journal entry. Subject to 7-year retention. */ export function generatePain001( company: Pain001CompanyData, employees: Pain001Employee[], options: Pain001Options ): string { const now = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z') const totalAmount = employees.reduce((sum, e) => sum + e.netSalary, 0) const formattedTotal = formatDecimal(totalAmount) const lines: string[] = [] lines.push('') lines.push('') lines.push(' ') // ─── Group Header ─── lines.push(' ') lines.push(` ${escapeXml(options.messageId)}`) lines.push(` ${now}`) lines.push(` ${employees.length}`) lines.push(` ${formattedTotal}`) lines.push(' ') lines.push(` ${escapeXml(company.name)}`) lines.push(' ') lines.push(' ') lines.push(` ${escapeXml(company.orgNumber.replace('-', ''))}`) lines.push(' ') lines.push(' ') lines.push(' ') lines.push(' ') // ─── Payment Information ─── lines.push(' ') lines.push(` ${escapeXml(options.messageId)}-PMT`) lines.push(' TRF') // Transfer lines.push(' true') // Batch booking lines.push(` ${employees.length}`) lines.push(` ${formattedTotal}`) lines.push(' ') lines.push(' SEPA') lines.push(' SALA') // Salary payment lines.push(' ') lines.push(` ${options.paymentDate}`) // Debtor (company) lines.push(' ') lines.push(` ${escapeXml(company.name)}`) lines.push(' ') lines.push(' ') lines.push(' ') lines.push(` ${escapeXml(company.iban)}`) lines.push(' ') lines.push(' SEK') lines.push(' ') lines.push(' ') lines.push(' ') lines.push(` ${escapeXml(company.bic)}`) lines.push(' ') lines.push(' ') // ─── Per-employee credit transfers ─── for (let i = 0; i < employees.length; i++) { const emp = employees[i] const txId = `${options.messageId}-TX${String(i + 1).padStart(4, '0')}` lines.push(' ') lines.push(' ') lines.push(` ${escapeXml(txId)}`) lines.push(` ${escapeXml(txId)}`) lines.push(' ') lines.push(' ') lines.push(` ${formatDecimal(emp.netSalary)}`) lines.push(' ') lines.push(' ') lines.push(` ${escapeXml(emp.name)}`) lines.push(' ') lines.push(' ') lines.push(' ') // Swedish domestic: clearing + account number (not IBAN for domestic) lines.push(` ${escapeXml(emp.clearingNumber)}${escapeXml(emp.bankAccountNumber)}`) lines.push(' ') lines.push(' ') lines.push(' ') lines.push(` Lon ${escapeXml(options.periodLabel)}`) lines.push(' ') lines.push(' ') } lines.push(' ') lines.push(' ') lines.push('') return lines.join('\n') } // ============================================================ // Helpers // ============================================================ function escapeXml(str: string): string { return str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') } /** Format number as decimal with 2 decimal places (ISO 20022 requires dot separator) */ function formatDecimal(amount: number): string { return (Math.round(amount * 100) / 100).toFixed(2) }