import PDFDocument from 'pdfkit'; import type { InteracTransaction } from '@icc/shared'; const ICC_BLUE = '#1773cf'; const GRAY_TEXT = '#4B5563'; const LIGHT_GRAY = '#F3F4F6'; function formatCurrencyPdf(amount: number): string { return new Intl.NumberFormat('fr-CA', { style: 'currency', currency: 'CAD', minimumFractionDigits: 2, }).format(amount); } function formatDatePdf(dateStr: string): string { try { return new Intl.DateTimeFormat('fr-CA', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }).format(new Date(dateStr)); } catch { return dateStr; } } const STATUS_LABELS: Record = { deposited: 'Déposé ✓', pending: 'En attente', expired: 'Expiré', cancelled: 'Annulé', }; export function generateReceipt(transaction: InteracTransaction): Promise { return new Promise((resolve, reject) => { const doc = new PDFDocument({ size: 'A4', margin: 50, info: { Title: `Reçu - ${transaction.reference || 'N/A'}`, Author: 'ICC Interac Manager', }, }); const chunks: Buffer[] = []; doc.on('data', (chunk: Buffer) => chunks.push(chunk)); doc.on('end', () => resolve(Buffer.concat(chunks))); doc.on('error', reject); const pageWidth = doc.page.width - 100; const startX = 50; let y = 50; // Header background doc.rect(startX, y, pageWidth, 80).fill(ICC_BLUE); // Header text doc.fillColor('#FFFFFF') .font('Helvetica-Bold') .fontSize(22) .text('ICC AMÉRIQUES', startX, y + 15, { width: pageWidth, align: 'center' }); doc.fontSize(12) .font('Helvetica') .text('Reçu de virement Interac', startX, y + 45, { width: pageWidth, align: 'center' }); y += 100; // Separator line doc.moveTo(startX, y).lineTo(startX + pageWidth, y).strokeColor(ICC_BLUE).lineWidth(2).stroke(); y += 20; // Transaction details const fields: [string, string][] = [ ['Date', formatDatePdf(transaction.date)], ['Expéditeur', transaction.sender], ['Montant', formatCurrencyPdf(transaction.amount)], ['Devise', transaction.currency || 'CAD'], ['Référence', transaction.reference || 'N/A'], ['Message', transaction.message || '—'], ['Succursale', (transaction as any).branch || 'Montreal'], ['Statut', STATUS_LABELS[transaction.status] || transaction.status], ]; for (const [label, value] of fields) { // Alternating row background if (fields.indexOf([label, value] as any) % 2 === 0) { doc.rect(startX, y - 5, pageWidth, 30).fill(LIGHT_GRAY); } doc.fillColor(GRAY_TEXT) .font('Helvetica-Bold') .fontSize(11) .text(`${label}:`, startX + 15, y, { width: 150 }); doc.fillColor('#111827') .font('Helvetica') .fontSize(11) .text(value, startX + 170, y, { width: pageWidth - 185 }); y += 30; } y += 10; // Footer separator doc.moveTo(startX, y).lineTo(startX + pageWidth, y).strokeColor('#E5E7EB').lineWidth(1).stroke(); y += 15; // Footer doc.fillColor(GRAY_TEXT) .font('Helvetica') .fontSize(9) .text('Reçu généré automatiquement par ICC Interac Manager', startX, y, { width: pageWidth, align: 'center' }); y += 15; doc.text(`Date de génération: ${new Date().toISOString().split('T')[0]}`, startX, y, { width: pageWidth, align: 'center' }); doc.end(); }); } export async function generateBatchReceipts(txns: InteracTransaction[]): Promise { const doc = new PDFDocument({ size: 'A4', margin: 50 }); const chunks: Buffer[] = []; return new Promise((resolve, reject) => { doc.on('data', (chunk: Buffer) => chunks.push(chunk)); doc.on('end', () => resolve(Buffer.concat(chunks))); doc.on('error', reject); for (let i = 0; i < txns.length; i++) { if (i > 0) doc.addPage(); const tx = txns[i]; const pageWidth = doc.page.width - 100; const startX = 50; let y = 50; // Header doc.rect(startX, y, pageWidth, 60).fill(ICC_BLUE); doc.fillColor('#FFFFFF').font('Helvetica-Bold').fontSize(18) .text('ICC AMÉRIQUES', startX, y + 10, { width: pageWidth, align: 'center' }); doc.fontSize(10).font('Helvetica') .text('Reçu de virement Interac', startX, y + 35, { width: pageWidth, align: 'center' }); y += 75; const fields: [string, string][] = [ ['Date', formatDatePdf(tx.date)], ['Expéditeur', tx.sender], ['Montant', formatCurrencyPdf(tx.amount)], ['Référence', tx.reference || 'N/A'], ['Message', tx.message || '—'], ['Succursale', (tx as any).branch || 'Montreal'], ['Statut', STATUS_LABELS[tx.status] || tx.status], ]; for (const [label, value] of fields) { doc.fillColor(GRAY_TEXT).font('Helvetica-Bold').fontSize(10) .text(`${label}:`, startX + 15, y, { width: 130 }); doc.fillColor('#111827').font('Helvetica').fontSize(10) .text(value, startX + 150, y, { width: pageWidth - 165 }); y += 25; } y += 10; doc.fillColor(GRAY_TEXT).font('Helvetica').fontSize(8) .text(`Reçu ${i + 1}/${txns.length} — Généré le ${new Date().toISOString().split('T')[0]}`, startX, y, { width: pageWidth, align: 'center' }); } doc.end(); }); }