/** * invoice_pdf — génération de PDF pour les factures natives. * * Phase 2 : implémentation réelle via @react-pdf/renderer + upload MinIO. * * Pipeline : * 1. Construit les `InvoiceTemplateProps` depuis l'invoice + settings résolus * 2. Appelle `renderInvoiceToBuffer(themeSlug, props)` (dispatcher de thèmes) * 3. `generateInvoicePdf` upload sur MinIO et retourne la storageKey. * `previewInvoicePdf` retourne le buffer brut pour stream HTTP. * * Note Factur-X (V1.5) : le buffer généré est un PDF/A-3 compatible. Pour * passer en Factur-X, il faudra injecter un XML CII en pièce jointe dans * le PDF (cf. roadmap dans CLAUDE.md / decisions.md ADR-022). Ce point * d'extension est laissé en post-traitement Buffer → Buffer. */ import type Invoice from '#models/invoice' import type { ResolvedInvoiceSettings } from '#services/invoice_settings' import { resolveBrandTokens } from '#services/brand' import { renderInvoiceToBuffer } from '#pdf-templates/index' import { uploadBuffer } from '#services/media_storage' import type { InvoiceTemplateProps } from '#pdf-templates/common' import type Organization from '#models/organization' export interface InvoiceRenderContext { invoice: Invoice resolvedSettings: ResolvedInvoiceSettings /** Org de l'invoice — utilisée pour le logo (brand_settings.logoUrl). */ organization: Organization } export interface GeneratedPdf { /** Clé MinIO sous laquelle le PDF est stocké. */ storageKey: string /** Taille du PDF en bytes. */ bytes: number } /** * Construit les props passées au template à partir d'une facture (potentiellement * non-persistée, pour la preview) et des settings résolus. * * On lit en priorité les snapshots de la facture (`issuerSnapshot`, `clientSnapshot`) * — c'est l'état figé à l'émission. Si les snapshots sont absents (preview avant * persistance), on retombe sur les settings résolus pour issuer et sur les * données fournies à la preview pour client (cf. previewPdf controller qui * construit un Invoice virtuel avec un clientSnapshot synthétisé). */ function buildProps(ctx: InvoiceRenderContext): InvoiceTemplateProps { const { invoice, resolvedSettings, organization } = ctx const issuer = invoice.issuerSnapshot ?? resolvedSettings.issuer if (!invoice.clientSnapshot) { throw new Error( 'invoice_pdf: clientSnapshot manquant — impossible de rendre la facture sans destinataire' ) } const brand = resolveBrandTokens(organization) const logoUrl = brand.logoUrl ?? null return { numero: invoice.numero, issueDate: invoice.issueDate, dueDate: invoice.dueDate, paymentTermsDays: invoice.paymentTermsDays ?? resolvedSettings.paymentTermsDays, issuer, client: invoice.clientSnapshot, lines: invoice.lines ?? [], tvaBreakdown: invoice.tvaBreakdown ?? [], amountHtCents: invoice.amountHtCents ?? 0, amountTvaCents: invoice.amountTvaCents ?? 0, amountTtcCents: invoice.amountTtcCents, penaltyRateText: resolvedSettings.penaltyRateText, escompteText: resolvedSettings.escompteText, footerLegalText: resolvedSettings.footerLegalText, footerNotes: invoice.footerNotes, rib: { iban: resolvedSettings.rib.iban, bic: resolvedSettings.rib.bic, bankName: resolvedSettings.rib.bankName, }, accentColor: invoice.themeAccentColor ?? resolvedSettings.accentColor, logoUrl, } } /** * Génère le PDF et l'upload sur MinIO. Retourne la storageKey à persister * dans `invoices.pdf_storage_key`. */ export async function generateInvoicePdf( ctx: InvoiceRenderContext ): Promise { const themeSlug = ctx.invoice.themeSlug ?? ctx.resolvedSettings.themeSlug const props = buildProps(ctx) const buffer = await renderInvoiceToBuffer(themeSlug, props) const uploaded = await uploadBuffer(buffer, 'invoice-pdf', ctx.invoice.organizationId) return { storageKey: uploaded.storageKey, bytes: uploaded.sizeBytes } } /** * Génère le PDF sans l'uploader — utilisé pour la preview (stream HTTP direct). */ export async function previewInvoicePdf(ctx: InvoiceRenderContext): Promise { const themeSlug = ctx.invoice.themeSlug ?? ctx.resolvedSettings.themeSlug const props = buildProps(ctx) return await renderInvoiceToBuffer(themeSlug, props) }