rubis/apps/api/app/models/invoice.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

91 lines
2.2 KiB
TypeScript

import { InvoiceSchema } from '#database/schema'
import { belongsTo, column } from '@adonisjs/lucid/orm'
import type { BelongsTo } from '@adonisjs/lucid/types/relations'
import type { DateTime } from 'luxon'
import Organization from '#models/organization'
import Client from '#models/client'
import Plan from '#models/plan'
import type {
InvoiceIssuer,
InvoiceThemeSlug,
} from '#services/invoice_settings'
import type {
ComputedInvoiceLine,
TvaBreakdownItem,
} from '#services/invoice_totals'
/**
* Snapshot du client figé au moment de l'émission. Permet à la facture
* de rester intacte si le client change d'adresse ou de raison sociale.
*/
export interface ClientSnapshot {
name: string
email: string
contactFirstName: string | null
contactLastName: string | null
phone: string | null
siret: string | null
siren: string | null
tvaIntra: string | null
addressLine1: string | null
addressLine2: string | null
addressZip: string | null
addressCity: string | null
addressCountry: string | null
}
export default class Invoice extends InvoiceSchema {
/**
* Champs ajoutés par la migration `1778800000200_enrich_invoices_for_native_editor`.
* Déclarations manuelles en attendant que `schema.ts` soit régénéré par
* `node ace migration:run`.
*/
@column()
declare lines: ComputedInvoiceLine[] | null
@column()
declare clientSnapshot: ClientSnapshot | null
@column()
declare issuerSnapshot: InvoiceIssuer | null
@column()
declare amountHtCents: number | null
@column()
declare amountTvaCents: number | null
@column()
declare tvaBreakdown: TvaBreakdownItem[] | null
@column()
declare paymentTermsDays: number | null
@column()
declare footerNotes: string | null
@column()
declare themeSlug: InvoiceThemeSlug | null
@column()
declare themeAccentColor: string | null
@column()
declare isNative: boolean
@column()
declare sequenceNumber: number | null
@column.dateTime()
declare pdfGeneratedAt: DateTime | null
@belongsTo(() => Organization)
declare organization: BelongsTo<typeof Organization>
@belongsTo(() => Client)
declare client: BelongsTo<typeof Client>
@belongsTo(() => Plan)
declare plan: BelongsTo<typeof Plan>
}