invoices.spec.ts (10 cas) :
- Création : 201 + rubisEarned=1 (bonus saisie) + status=pending
- Création client à la volée : si nom non matché + email fourni → client créé
- 422 client_email_required si pas d'email pour création à la volée
- Si planId fourni : RelanceTasks scheduled créées (assertion sur DB count)
- Numéro unique par org : 422 sur duplicate (testant l'exception handler)
- mark-paid idempotent : 2e appel ne re-bumpe pas rubisEarned ni org.rubis_count
- mark-paid annule les RelanceTasks scheduled (passe à cancelled)
- Cross-org : user B → 404 sur mark-paid d'une facture de A
- GET /invoices : pagination + meta total/page
- GET /invoices/counts : agrège par status
imports.spec.ts (6 cas) :
- POST /upload mock JSON : 1 batch + N drafts en pending, mock OCR rempli les fields
- Refus > 20 fichiers
- Validate transforme draft en Invoice + status=validated + invoiceId set
- Validate sur draft déjà processed → 409 draft_already_processed
- Skip → status=skipped, idempotent
- DELETE batch → CASCADE supprime les drafts
dashboard.spec.ts (6 cas) :
- KPIs zéros sur org vierge
- factureToRelance compte les invoices pending
- Après mark-paid : encaisseCents et rubisCount bumpent (org.rubisCount agrégé)
- Activity vide sur org sans actions
- Activity loggue invoice_paid après mark-paid (label dans le feed)
- Top-late liste les clients avec invoices actives en retard (dueDate < today)
Helper response.ts : `body<T>()` pour caster Tuyau strict response shapes (Tuyau type chaque code de statut comme une union, assertStatus ne narrow pas → on cast explicitement vers ApiOk<T>/ApiError/ApiConflict<T>).
clients.spec.ts (16 cas) :
- POST /clients : refus sans email (422 + field=email), refus SIRET ≠ 14 chiffres, création OK avec UUID + association org, doublon nom case-insensitive (409 + payload existing)
- GET /clients : isolation cross-org (user A ne voit pas les clients de B), withStats=1 enrichit (zéros sans factures), recherche q ILIKE
- Perms cross-org : user B → 404 sur GET/PATCH d'un client de A, l'objet ne bouge pas
plans.spec.ts (7 cas) :
- GET /plans : 4 plans pré-fournis avec steps préchargés, isolation cross-org (UUIDs disjoints entre A et B)
- GET /plans/:slug : steps ordonnés, 404 si inconnu
- PATCH /plans/:slug : remplace les steps en bloc dans une tx, rejette tone invalide, cross-org (B édite SA copie sans toucher celle de A)
Setup :
- .env.test étoffé : DRIVE_DISK=fs, MAIL_DRIVER=smtp local, OCR_PROVIDER=mock. Réutilise la DB rubis_dev avec global transactions par test (rollback auto, isolation parfaite).
- Schedulers (relance + checkin) détectent NODE_ENV=test et skippent BullMQ.add. Les tasks DB sont quand même créées (utiles pour assertions) mais aucun job orphelin n'arrive en Redis après rollback de tx.
- helpers/auth.ts : factory createTestUser() qui crée org + user + 4 plans pré-fournis dans une tx, retourne user/org/accessToken/bearer header. createTwoOrgs() pour les tests cross-org à venir.
Tests fonctionnels auth (tests/functional/auth.spec.ts) :
- Signup : crée user + org + 4 plans pré-fournis (vérifie les slugs), refuse email mal formé / password court / email déjà pris
- Login : émet AuthSession avec credentials valides, rejette mauvais password / email inconnu
- Bearer auth : 401 sans token, 401 avec token bidon, 200 avec token valide
- Logout : révoque le token courant, requêtes suivantes en 401
- Onboarding : PATCH /organizations/me pose onboardingCompletedAt à la 1re mise du nom, idempotent ensuite
Pour lancer : `pnpm -F api test`
Monorepo Turborepo (pnpm workspaces) avec 3 packages :
- apps/web : SPA React 19 + Vite 8 + Tailwind v4 (CSS-first)
• TanStack Router (file-based, auto code-splitting), Query, Form
• Radix primitives bruts + CVA + clsx + tailwind-merge
• MSW pour mocker l'API tant qu'Adonis n'est pas branché
• Polices Bricolage Grotesque + Inter self-hostées via fontsource
• Tokens marque (rubis, cream, ink) exposés via @theme
• Primitives maison : Gem, Brand, Eyebrow, Button, Input, Field
• Route /login full flow : TanStack Form + Zod + mutation Query
- apps/api : Adonis 7 (kit api, scaffold via create-adonisjs)
• Auth access tokens (Bearer) — cf. ADR-017
• Tuyau core déjà câblé pour la génération de types
• Routes /api/v1/auth/{signup,login} + /api/v1/account/{profile,logout}
• Minimal — uniquement le pont front ↔ back
- packages/shared : types TS + schemas Zod + constantes
• Source unique de vérité partagée api ↔ web
• Domaines : User, Org, Auth, Client, Invoice, Plan
Tooling racine : Turbo, ESLint v9 flat, Prettier, husky, lint-staged.
CLAUDE.md et docs/decisions.md mis à jour avec ADR-014 à ADR-018
(stack, monorepo, PG existant, Bearer tokens, MinIO existant)
et le pointeur vers docs/tech/architecture.md.
Logo Rubis déplacé de landing/assets/ vers /assets/ (source unique
réutilisée par la landing et l'app).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>