import React from 'react' import { renderToBuffer, Document, Page, Text, View, StyleSheet } from '@react-pdf/renderer' interface InvoiceData { invoiceNumber: string issueDate: string dueDate: string | null company: { name: string email: string phone?: string | null address?: any } subscription?: { plan: string billingPeriod: string currentPeriodStart?: string | null currentPeriodEnd?: string | null currency: string } amount: number currency: string status: string paymentProvider: string transactionId?: string | null paidAt?: string | null lineItems?: Array<{ description: string amount: number currency: string quantity?: number unitAmount?: number periodStart?: string | null periodEnd?: string | null }> totals?: { subtotalAmount: number discountAmount: number creditAmount: number taxAmount: number totalAmount: number amountPaid: number amountDue: number } } const PLAN_LABEL: Record = { STARTER: 'Starter', GROWTH: 'Growth', PRO: 'Pro', } const PERIOD_LABEL: Record = { MONTHLY: 'Monthly', ANNUAL: 'Annual', } const STATUS_COLORS: Record = { PAID: '#10b981', PENDING: '#f59e0b', OPEN: '#f59e0b', PAYMENT_PENDING: '#f59e0b', PARTIALLY_PAID: '#f59e0b', PAST_DUE: '#ef4444', FAILED: '#ef4444', REFUNDED: '#6b7280', PARTIALLY_REFUNDED: '#6b7280', VOID: '#6b7280', UNCOLLECTIBLE: '#6b7280', } const colors = { bg: '#0f0f11', surface: '#18181b', border: '#27272a', textPrimary: '#f4f4f5', textSecondary: '#a1a1aa', textMuted: '#71717a', accent: '#10b981', white: '#ffffff', } const s = StyleSheet.create({ page: { backgroundColor: colors.bg, paddingHorizontal: 48, paddingVertical: 48, fontFamily: 'Helvetica', color: colors.textPrimary, }, header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 40, paddingBottom: 24, borderBottomWidth: 1, borderBottomColor: colors.border, }, brandName: { fontSize: 22, fontFamily: 'Helvetica-Bold', color: colors.accent, letterSpacing: 0.5, }, brandTagline: { fontSize: 9, color: colors.textMuted, marginTop: 3, }, invoiceMeta: { alignItems: 'flex-end', }, invoiceTitle: { fontSize: 26, fontFamily: 'Helvetica-Bold', color: colors.white, letterSpacing: 1, }, invoiceNumber: { fontSize: 11, color: colors.textSecondary, marginTop: 4, }, section: { marginBottom: 24, }, sectionLabel: { fontSize: 8, fontFamily: 'Helvetica-Bold', color: colors.textMuted, letterSpacing: 1.2, textTransform: 'uppercase', marginBottom: 8, }, row: { flexDirection: 'row', gap: 24, marginBottom: 24, }, col: { flex: 1, backgroundColor: colors.surface, borderRadius: 8, padding: 16, borderWidth: 1, borderColor: colors.border, }, colLabel: { fontSize: 8, fontFamily: 'Helvetica-Bold', color: colors.textMuted, letterSpacing: 1, textTransform: 'uppercase', marginBottom: 10, }, companyName: { fontSize: 13, fontFamily: 'Helvetica-Bold', color: colors.textPrimary, marginBottom: 4, }, companyDetail: { fontSize: 10, color: colors.textSecondary, marginBottom: 2, }, metaRow: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 6, }, metaKey: { fontSize: 9, color: colors.textMuted, }, metaValue: { fontSize: 9, color: colors.textSecondary, fontFamily: 'Helvetica-Bold', }, table: { backgroundColor: colors.surface, borderRadius: 8, borderWidth: 1, borderColor: colors.border, overflow: 'hidden', marginBottom: 20, }, tableHeader: { flexDirection: 'row', backgroundColor: '#1c1c1f', paddingHorizontal: 16, paddingVertical: 10, borderBottomWidth: 1, borderBottomColor: colors.border, }, tableHeaderCell: { fontSize: 8, fontFamily: 'Helvetica-Bold', color: colors.textMuted, letterSpacing: 0.8, textTransform: 'uppercase', }, tableRow: { flexDirection: 'row', paddingHorizontal: 16, paddingVertical: 14, borderBottomWidth: 1, borderBottomColor: colors.border, }, tableCell: { fontSize: 10, color: colors.textPrimary, }, tableCellMuted: { fontSize: 10, color: colors.textSecondary, }, col60: { flex: 6 }, col20: { flex: 2, textAlign: 'right' as const }, col20Center: { flex: 2, textAlign: 'center' as const }, totalRow: { flexDirection: 'row', justifyContent: 'flex-end', marginBottom: 6, }, totalLabel: { fontSize: 11, color: colors.textSecondary, marginRight: 32, }, totalValue: { fontSize: 11, fontFamily: 'Helvetica-Bold', color: colors.white, minWidth: 100, textAlign: 'right' as const, }, grandTotalRow: { flexDirection: 'row', justifyContent: 'flex-end', marginTop: 8, paddingTop: 12, borderTopWidth: 1, borderTopColor: colors.border, }, grandTotalLabel: { fontSize: 13, fontFamily: 'Helvetica-Bold', color: colors.textSecondary, marginRight: 32, }, grandTotalValue: { fontSize: 15, fontFamily: 'Helvetica-Bold', color: colors.accent, minWidth: 100, textAlign: 'right' as const, }, statusBadge: { paddingHorizontal: 8, paddingVertical: 4, borderRadius: 4, alignSelf: 'flex-start', marginTop: 6, }, statusText: { fontSize: 9, fontFamily: 'Helvetica-Bold', letterSpacing: 0.5, color: colors.white, }, paymentBox: { backgroundColor: colors.surface, borderRadius: 8, padding: 16, borderWidth: 1, borderColor: colors.border, marginBottom: 24, }, paymentRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8, }, paymentKey: { fontSize: 9, color: colors.textMuted, }, paymentValue: { fontSize: 9, color: colors.textSecondary, fontFamily: 'Helvetica-Bold', maxWidth: 280, textAlign: 'right' as const, }, footer: { position: 'absolute', bottom: 32, left: 48, right: 48, borderTopWidth: 1, borderTopColor: colors.border, paddingTop: 12, flexDirection: 'row', justifyContent: 'space-between', }, footerText: { fontSize: 8, color: colors.textMuted, }, }) function fmt(amount: number, currency: string) { return `${(amount / 100).toFixed(2)} ${currency}` } function fmtDate(iso: string | null | undefined) { if (!iso) return '-' return new Date(iso).toLocaleDateString('en-GB', { day: '2-digit', month: 'long', year: 'numeric' }) } function formatAddress(address: any): string { if (!address) return '' if (typeof address === 'string') return address if (typeof address.formatted === 'string') return address.formatted const parts = [address.street, address.city, address.region, address.country, address.zip] return parts.filter(Boolean).join(', ') } function pdfText(value: unknown) { return String(value ?? '') .normalize('NFKD') .replace(/[\u0300-\u036f]/g, '') .replace(/[\u2013\u2014]/g, '-') .replace(/\u00d7/g, 'x') .replace(/[^\x20-\x7E]/g, '') } function InvoiceDocument({ data }: { data: InvoiceData }) { const statusColor = STATUS_COLORS[data.status] ?? '#6b7280' const addressStr = formatAddress(data.company.address) const planLabel = data.subscription ? (PLAN_LABEL[data.subscription.plan] ?? data.subscription.plan) : 'Manual' const periodLabel = data.subscription ? (PERIOD_LABEL[data.subscription.billingPeriod] ?? data.subscription.billingPeriod) : 'Custom' const lineItems = data.lineItems?.length ? data.lineItems : [{ description: `${planLabel} Plan — ${periodLabel} Subscription`, amount: data.amount, currency: data.currency, periodStart: data.subscription?.currentPeriodStart ?? null, periodEnd: data.subscription?.currentPeriodEnd ?? null, }] const totals = data.totals ?? { subtotalAmount: data.amount, discountAmount: 0, creditAmount: 0, taxAmount: 0, totalAmount: data.amount, amountPaid: data.paidAt ? data.amount : 0, amountDue: data.paidAt ? 0 : data.amount, } return React.createElement( Document, { author: 'RentalDriveGo', title: `Invoice ${data.invoiceNumber}`, subject: 'Subscription Invoice' }, React.createElement( Page, { size: 'A4', style: s.page }, // ── Header ────────────────────────────────────────────────── React.createElement( View, { style: s.header }, React.createElement( View, null, React.createElement(Text, { style: s.brandName }, 'RentalDriveGo'), React.createElement(Text, { style: s.brandTagline }, 'Car Rental Management Platform'), ), React.createElement( View, { style: s.invoiceMeta }, React.createElement(Text, { style: s.invoiceTitle }, 'INVOICE'), React.createElement(Text, { style: s.invoiceNumber }, pdfText(data.invoiceNumber)), React.createElement( View, { style: [s.statusBadge, { backgroundColor: statusColor }] }, React.createElement(Text, { style: s.statusText }, pdfText(data.status)), ), ), ), // ── Bill To / Invoice Dates ────────────────────────────────── React.createElement( View, { style: s.row }, React.createElement( View, { style: s.col }, React.createElement(Text, { style: s.colLabel }, 'Bill To'), React.createElement(Text, { style: s.companyName }, pdfText(data.company.name)), React.createElement(Text, { style: s.companyDetail }, pdfText(data.company.email)), data.company.phone ? React.createElement(Text, { style: s.companyDetail }, pdfText(data.company.phone)) : null, addressStr ? React.createElement(Text, { style: [s.companyDetail, { marginTop: 4 }] }, pdfText(addressStr)) : null, ), React.createElement( View, { style: s.col }, React.createElement(Text, { style: s.colLabel }, 'Invoice Details'), React.createElement( View, { style: s.metaRow }, React.createElement(Text, { style: s.metaKey }, 'Invoice Number'), React.createElement(Text, { style: s.metaValue }, pdfText(data.invoiceNumber)), ), React.createElement( View, { style: s.metaRow }, React.createElement(Text, { style: s.metaKey }, 'Issue Date'), React.createElement(Text, { style: s.metaValue }, fmtDate(data.issueDate)), ), data.dueDate ? React.createElement( View, { style: s.metaRow }, React.createElement(Text, { style: s.metaKey }, 'Due Date'), React.createElement(Text, { style: s.metaValue }, fmtDate(data.dueDate)), ) : null, React.createElement( View, { style: s.metaRow }, React.createElement(Text, { style: s.metaKey }, 'Currency'), React.createElement(Text, { style: s.metaValue }, pdfText(data.currency)), ), React.createElement( View, { style: s.metaRow }, React.createElement(Text, { style: s.metaKey }, 'Billing Period'), React.createElement(Text, { style: s.metaValue }, pdfText(periodLabel)), ), data.subscription ? React.createElement( View, { style: s.metaRow }, React.createElement(Text, { style: s.metaKey }, 'Plan'), React.createElement(Text, { style: s.metaValue }, pdfText(planLabel)), ) : null, ), ), // ── Line Items ─────────────────────────────────────────────── React.createElement( View, { style: s.table }, React.createElement( View, { style: s.tableHeader }, React.createElement(Text, { style: [s.tableHeaderCell, s.col60] }, 'Description'), React.createElement(Text, { style: [s.tableHeaderCell, s.col20Center] }, 'Period'), React.createElement(Text, { style: [s.tableHeaderCell, s.col20] }, 'Amount'), ), ...lineItems.map((item) => { const periodStr = item.periodStart && item.periodEnd ? `${fmtDate(item.periodStart)} - ${fmtDate(item.periodEnd)}` : '-' const description = item.quantity && item.quantity > 1 && item.unitAmount !== undefined ? `${item.description} (${item.quantity} x ${fmt(item.unitAmount, item.currency)})` : item.description return React.createElement( View, { key: `${item.description}-${item.amount}-${item.periodStart ?? ''}`, style: s.tableRow }, React.createElement(Text, { style: [s.tableCell, s.col60] }, pdfText(description)), React.createElement(Text, { style: [s.tableCellMuted, s.col20Center, { textAlign: 'center' }] }, pdfText(periodStr)), React.createElement(Text, { style: [s.tableCell, s.col20, { textAlign: 'right' }] }, pdfText(fmt(item.amount, item.currency))), ) }), ), // ── Totals ─────────────────────────────────────────────────── React.createElement( View, { style: s.totalRow }, React.createElement(Text, { style: s.totalLabel }, 'Subtotal'), React.createElement(Text, { style: s.totalValue }, fmt(totals.subtotalAmount, data.currency)), ), totals.discountAmount > 0 ? React.createElement( View, { style: s.totalRow }, React.createElement(Text, { style: s.totalLabel }, 'Discounts'), React.createElement(Text, { style: s.totalValue }, `- ${fmt(totals.discountAmount, data.currency)}`), ) : null, totals.creditAmount > 0 ? React.createElement( View, { style: s.totalRow }, React.createElement(Text, { style: s.totalLabel }, 'Credits'), React.createElement(Text, { style: s.totalValue }, `- ${fmt(totals.creditAmount, data.currency)}`), ) : null, totals.taxAmount > 0 ? React.createElement( View, { style: s.totalRow }, React.createElement(Text, { style: s.totalLabel }, 'Tax'), React.createElement(Text, { style: s.totalValue }, fmt(totals.taxAmount, data.currency)), ) : null, React.createElement( View, { style: s.grandTotalRow }, React.createElement(Text, { style: s.grandTotalLabel }, 'Total Due'), React.createElement(Text, { style: s.grandTotalValue }, fmt(totals.totalAmount, data.currency)), ), // ── Payment Info ───────────────────────────────────────────── React.createElement( View, { style: [s.paymentBox, { marginTop: 24 }] }, React.createElement(Text, { style: [s.colLabel, { marginBottom: 10 }] }, 'Payment Information'), React.createElement( View, { style: s.paymentRow }, React.createElement(Text, { style: s.paymentKey }, 'Payment Provider'), React.createElement(Text, { style: s.paymentValue }, pdfText(data.paymentProvider)), ), data.transactionId ? React.createElement( View, { style: s.paymentRow }, React.createElement(Text, { style: s.paymentKey }, 'Transaction ID'), React.createElement(Text, { style: s.paymentValue }, pdfText(data.transactionId)), ) : null, React.createElement( View, { style: s.paymentRow }, React.createElement(Text, { style: s.paymentKey }, 'Payment Date'), React.createElement(Text, { style: s.paymentValue }, fmtDate(data.paidAt)), ), React.createElement( View, { style: s.paymentRow }, React.createElement(Text, { style: s.paymentKey }, 'Status'), React.createElement(Text, { style: [s.paymentValue, { color: statusColor }] }, pdfText(data.status)), ), ), // ── Footer ─────────────────────────────────────────────────── React.createElement( View, { style: s.footer }, React.createElement(Text, { style: s.footerText }, 'RentalDriveGo - Car Rental Management Platform'), React.createElement(Text, { style: s.footerText }, `Generated on ${fmtDate(new Date().toISOString())}`), ), ), ) } export async function generateInvoicePdf(data: InvoiceData): Promise { const doc = React.createElement(InvoiceDocument, { data }) try { const buffer = await renderToBuffer(doc as any) return buffer as unknown as Buffer } catch { return buildSimpleInvoicePdf(data) } } export function buildInvoiceNumber(invoiceId: string, createdAt: Date): string { const year = createdAt.getFullYear() const short = invoiceId.slice(-6).toUpperCase() return `INV-${year}-${short}` } function pdfEscape(value: unknown) { return pdfText(value).replace(/\\/g, '\\\\').replace(/\(/g, '\\(').replace(/\)/g, '\\)') } function buildSimpleInvoicePdf(data: InvoiceData) { const lineItems = data.lineItems?.length ? data.lineItems : [{ description: 'Invoice charge', amount: data.amount, currency: data.currency }] const totals = data.totals ?? { subtotalAmount: data.amount, discountAmount: 0, creditAmount: 0, taxAmount: 0, totalAmount: data.amount, amountPaid: data.paidAt ? data.amount : 0, amountDue: data.paidAt ? 0 : data.amount, } const address = formatAddress(data.company.address) const lines = [ 'RentalDriveGo - Invoice', `Invoice Number: ${data.invoiceNumber}`, `Status: ${data.status}`, `Issue Date: ${fmtDate(data.issueDate)}`, data.dueDate ? `Due Date: ${fmtDate(data.dueDate)}` : null, `Bill To: ${data.company.name}`, `Email: ${data.company.email}`, data.company.phone ? `Phone: ${data.company.phone}` : null, address ? `Address: ${address}` : null, '', 'Line Items', ...lineItems.map((item) => `${item.description} - ${fmt(item.amount, item.currency)}`), '', `Subtotal: ${fmt(totals.subtotalAmount, data.currency)}`, totals.discountAmount ? `Discounts: -${fmt(totals.discountAmount, data.currency)}` : null, totals.creditAmount ? `Credits: -${fmt(totals.creditAmount, data.currency)}` : null, totals.taxAmount ? `Tax: ${fmt(totals.taxAmount, data.currency)}` : null, `Total: ${fmt(totals.totalAmount, data.currency)}`, `Amount Paid: ${fmt(totals.amountPaid, data.currency)}`, `Amount Due: ${fmt(totals.amountDue, data.currency)}`, '', `Payment Provider: ${data.paymentProvider}`, data.transactionId ? `Transaction ID: ${data.transactionId}` : null, ].filter((line): line is string => line !== null) const content = [ 'BT', '/F1 18 Tf', '50 790 Td', `(${pdfEscape(lines[0])}) Tj`, '/F1 10 Tf', ...lines.slice(1).flatMap((line) => ['0 -18 Td', `(${pdfEscape(line)}) Tj`]), 'ET', ].join('\n') const objects = [ '<< /Type /Catalog /Pages 2 0 R >>', '<< /Type /Pages /Kids [3 0 R] /Count 1 >>', '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>', '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>', `<< /Length ${Buffer.byteLength(content, 'utf8')} >>\nstream\n${content}\nendstream`, ] let pdf = '%PDF-1.4\n' const offsets = [0] objects.forEach((object, index) => { offsets.push(Buffer.byteLength(pdf, 'utf8')) pdf += `${index + 1} 0 obj\n${object}\nendobj\n` }) const xrefOffset = Buffer.byteLength(pdf, 'utf8') pdf += `xref\n0 ${objects.length + 1}\n` pdf += '0000000000 65535 f \n' for (let i = 1; i < offsets.length; i += 1) { pdf += `${String(offsets[i]).padStart(10, '0')} 00000 n \n` } pdf += `trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xrefOffset}\n%%EOF\n` return Buffer.from(pdf, 'utf8') }