2 Commits

Author SHA1 Message Date
ordinarthur
692b514fe9 feat(api): domaine Plan + PlanStep + provisioning des 4 plans pré-fournis
Migrations :
- plans (uuid id, organization_id FK CASCADE, slug nullable, name, description, is_default). Unique (organization_id, slug) — un slug max par org.
- plan_steps (uuid id, plan_id FK CASCADE, order, offset_days, tone ENUM PG natif, subject, body, requires_manual_validation).

Schema rules : override du tone (introspection PG → 'any', on précise l'union).

Modèles Plan (belongsTo Organization, hasMany PlanStep) et PlanStep (belongsTo Plan).

Décision : plans dupliqués par organisation au signup (pas de table globale partagée). Permet l'édition isolée par org sans toucher aux templates des autres tenants. Le service `provisionDefaultPlans(orgId, trx)` est idempotent et appelé depuis NewAccountController dans la transaction de création.

Source de vérité des 4 plans (Standard B2B, Rapide, Patient, Ferme) dans app/services/default_plans.ts — alignée sur apps/web/src/mocks/seed.ts.

Endpoints :
- GET /plans : liste enrichie avec usageCount (à 0 tant qu'Invoice n'est pas câblé).
- GET /plans/:slug : détail (lookup par slug pour URL stable côté SPA).
- PATCH /plans/:slug : édition partielle. Les steps sont remplacés en bloc dans une transaction (pas de diff fin id-par-id, plus simple et prévisible).

POST plan custom = V2 (cf. backend.md §5.5).
2026-05-06 14:25:06 +02:00
ordinarthur
b6006ad1f7 feat(api): domaine Client + CRUD /api/v1/clients
Migration clients (uuid id, organization_id FK uuid CASCADE, name, email REQUIS, phone, address, siret, notes). Index sur organization_id.

Modèle Client avec belongsTo Organization. La relation hasMany Invoice est volontairement omise tant que le domaine Invoice n'est pas câblé.

Validators Vine alignés sur le contrat MSW :
- create : name 2-120, email requis avec format, siret 14 chiffres si fourni
- update : tout optionnel
- email REQUIS au create — pivot produit, pas de relance possible sans

Endpoints (auth requise, scopés par organizationId du user courant) :
- GET /clients?withStats=1&q= : liste filtrée + recherche, enrichissement stats optionnel, tri par actionnabilité (retards d'abord) quand withStats
- GET /clients/:id : détail (id en UUID via router.matchers.uuid())
- POST /clients : 201 + détection doublon par nom case-insensitive → 409 avec payload `existing` (le SPA peut proposer "voir le client existant")
- PATCH /clients/:id : merge partiel

Service ClientStats avec interface bulkComputeClientStats() qui retourne EMPTY pour l'instant — sera vraiment branché quand Invoice arrive. Le contrat reste stable côté SPA, juste les compteurs à 0.

Sérialisation : pour les listes avec stats per-item, on instancie le transformer manuellement (`new ClientTransformer(c).toObject()`) plutôt que de passer par BaseTransformer.transform() qui retourne un Item nested non-unwrappable hors clé directe de serialize().
2026-05-06 14:13:13 +02:00