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>
68 lines
2.9 KiB
TypeScript
68 lines
2.9 KiB
TypeScript
import vine from '@vinejs/vine'
|
|
|
|
const name = () => vine.string().minLength(2).maxLength(120)
|
|
const email = () => vine.string().email().maxLength(254)
|
|
// SIRET = 14 chiffres, SIREN = 9 chiffres (cf. INSEE).
|
|
const siret = () => vine.string().regex(/^\d{14}$/)
|
|
const siren = () => vine.string().regex(/^\d{9}$/)
|
|
// TVA intracom UE — FR + 11 chiffres ; les autres pays ont des formats variés
|
|
// (DE9, BE10…). On accepte du 4 à 20 chars alphanum après le préfixe pays.
|
|
const tvaIntra = () => vine.string().regex(/^[A-Z]{2}[A-Z0-9]{2,18}$/u)
|
|
const phone = () => vine.string().maxLength(40)
|
|
const address = () => vine.string().maxLength(500)
|
|
const addressLine = () => vine.string().maxLength(200)
|
|
const addressZip = () => vine.string().maxLength(20)
|
|
const addressCity = () => vine.string().maxLength(100)
|
|
const addressCountry = () => vine.string().regex(/^[A-Z]{2}$/u)
|
|
const notes = () => vine.string().maxLength(2000)
|
|
// Prénom/nom du contact dédié — utilisés comme variables dans les templates
|
|
// custom ({{client.contactFirstName}}). Optionnels.
|
|
const contactName = () => vine.string().minLength(1).maxLength(80)
|
|
|
|
/**
|
|
* Validator pour POST /clients. Email **requis** : sans email, Rubis ne
|
|
* peut pas relancer (pivot produit, cf. CLAUDE.md → Principes).
|
|
*
|
|
* Adresse structurée (lines/zip/city/country) ajoutée avec l'éditeur de
|
|
* factures natif. `address` (string legacy) reste accepté pour compat —
|
|
* le nouveau code lit en priorité les champs structurés.
|
|
*/
|
|
export const createClientValidator = vine.create({
|
|
name: name(),
|
|
email: email(),
|
|
contactFirstName: contactName().nullable().optional(),
|
|
contactLastName: contactName().nullable().optional(),
|
|
phone: phone().nullable().optional(),
|
|
address: address().nullable().optional(),
|
|
siret: siret().nullable().optional(),
|
|
siren: siren().nullable().optional(),
|
|
tvaIntra: tvaIntra().nullable().optional(),
|
|
addressLine1: addressLine().nullable().optional(),
|
|
addressLine2: addressLine().nullable().optional(),
|
|
addressZip: addressZip().nullable().optional(),
|
|
addressCity: addressCity().nullable().optional(),
|
|
addressCountry: addressCountry().nullable().optional(),
|
|
notes: notes().nullable().optional(),
|
|
})
|
|
|
|
/**
|
|
* Validator pour PATCH /clients/:id. Tous les champs optionnels.
|
|
*/
|
|
export const updateClientValidator = vine.create({
|
|
name: name().optional(),
|
|
email: email().optional(),
|
|
contactFirstName: contactName().nullable().optional(),
|
|
contactLastName: contactName().nullable().optional(),
|
|
phone: phone().nullable().optional(),
|
|
address: address().nullable().optional(),
|
|
siret: siret().nullable().optional(),
|
|
siren: siren().nullable().optional(),
|
|
tvaIntra: tvaIntra().nullable().optional(),
|
|
addressLine1: addressLine().nullable().optional(),
|
|
addressLine2: addressLine().nullable().optional(),
|
|
addressZip: addressZip().nullable().optional(),
|
|
addressCity: addressCity().nullable().optional(),
|
|
addressCountry: addressCountry().nullable().optional(),
|
|
notes: notes().nullable().optional(),
|
|
})
|