rubis/apps/api/app/services/invoice_pdf.ts
ordinarthur e0b47ddfdc feat(invoices): éditeur de factures natif — data model + API (Phase 1)
Pose les fondations pour permettre aux utilisateurs de créer leurs
factures directement dans Rubis (en complément de l'upload OCR existant),
avec snapshots immuables, numérotation strict séquentielle (art. 242
nonies A CGI) et 4 thèmes pré-faits paramétrables.

Data model
- organizations.invoice_settings (JSONB) : thème par défaut, accent color,
  préfixe et compteur de numérotation, mentions légales (pénalités,
  escompte), identité émetteur (SIREN/SIRET/TVA intra/RCS/capital), RIB.
- clients enrichi : SIREN, TVA intra, adresse structurée (lines/zip/city
  /country). Le champ address legacy reste pour les clients pré-feature.
- invoices enrichi : lines (JSONB), client_snapshot + issuer_snapshot
  figés à l'émission, amount_ht/tva, tva_breakdown, payment_terms_days,
  theme_slug + theme_accent_color, is_native, sequence_number (unique
  per org), pdf_generated_at.

API
- GET/PATCH /organizations/me/invoice-settings (resolveInvoiceSettings)
- GET /invoice-themes (4 thèmes : classique, moderne, minimal, élégant)
- POST /invoices/native (séquence strict allouée en transaction,
  totaux recalculés serveur, snapshots immuables)
- POST /invoices/preview-pdf (stream PDF sans persister, stub Phase 1)

Le rendu PDF lui-même (@react-pdf/renderer + templates) arrive en
Phase 2 ; le storeNative crée bien la facture mais pdf_storage_key
reste null jusqu'à Phase 2. Conformité Factur-X visée pour V1.5
(Q3-Q4 2026, avant l'échéance d'émission TPE-PME au 1er sept 2027).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 02:07:45 +02:00

59 lines
2.2 KiB
TypeScript

/**
* invoice_pdf — génération de PDF pour les factures natives.
*
* **Phase 1 stub.** L'implémentation réelle (templates @react-pdf/renderer
* + upload MinIO) arrive en Phase 2 avec packages/ui/invoice-templates/.
* Pour l'instant, `generateInvoicePdf` renvoie `null` (= pas de PDF stocké)
* et `previewInvoicePdf` throw `not_implemented` (501).
*
* Le contrat de l'interface est figé pour que la Phase 2 soit un drop-in
* remplacement sans toucher au controller : on remplace le corps de ces
* fonctions par l'appel à `@react-pdf/renderer.renderToBuffer(...)` puis
* `media_storage.uploadBuffer(...)`.
*/
import type Invoice from '#models/invoice'
import { Exception } from '@adonisjs/core/exceptions'
export interface InvoiceRenderContext {
/** L'invoice complet, snapshots compris. */
invoice: Invoice
/** Settings résolus (themeSlug, accentColor, issuer, rib…). */
// Le type complet est dans #services/invoice_settings → ResolvedInvoiceSettings,
// mais comme c'est un stub on garde unknown pour ne pas créer de couplage
// que Phase 2 devra de toute façon retravailler.
resolvedSettings: unknown
}
export interface GeneratedPdf {
/** Clé MinIO sous laquelle le PDF est stocké. */
storageKey: string
/** Taille du PDF en bytes. */
bytes: number
}
/**
* Génère le PDF de la facture et l'upload sur MinIO. Stub Phase 1.
*
* Phase 2 : renderToBuffer(<Theme {...props} />) → uploadBuffer → storageKey.
*/
export async function generateInvoicePdf(_ctx: InvoiceRenderContext): Promise<GeneratedPdf | null> {
// Phase 1 : pas de génération. Le controller persiste l'invoice avec
// pdfStorageKey=null et l'UI affichera "PDF en cours de génération"
// (ou un placeholder). La Phase 2 active la vraie génération.
return null
}
/**
* Renvoie un buffer PDF pour preview (sans persister). Stub Phase 1.
*
* Phase 2 : même rendu que generateInvoicePdf, mais retourne le buffer
* directement au lieu d'uploader.
*/
export async function previewInvoicePdf(_ctx: InvoiceRenderContext): Promise<Buffer> {
throw new Exception('PDF preview not yet implemented (Phase 2)', {
status: 501,
code: 'not_implemented',
})
}