12 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5b8898ba9c |
test(auth): enrichit l'assertion signup duplicate email (format de réponse)
All checks were successful
Build & Deploy API / build-and-deploy (push) Successful in 1m18s
Le test "refuse un email déjà pris" vérifiait seulement `assertStatus(422)`.
On enrichit pour vérifier la shape complète :
- body.errors est un array non-vide
- au moins une entrée a `field === 'email'`
- cette entrée a un code et un message strings
Pourquoi : un mutation test du 2026-05-18 a révélé qu'un simple
`assertStatus(422)` laissait passer le retrait du `.unique()` côté
validator Vine — la contrainte DB UNIQUE(email) prenait le relais
silencieusement via le handler PG 23505, qui renvoie le même 422.
Apprentissage : .unique() Vine et DB unique sont équivalents côté
contrat API (le handler `23505` met `field='email'` identique). Le
.unique() Vine est donc une optimisation (skip round-trip DB) plutôt
qu'une garantie de sécurité. On garde la double protection comme
best practice (defense-in-depth).
Le test enrichi ne catche pas le retrait du .unique() spécifiquement
(les 2 paths sont indistinguables côté client) MAIS catche des
régressions plus graves :
- Si le handler 23505 casse → 500 au lieu de 422
- Si `field` n'est plus posé → SPA ne peut plus highlight l'input
- Si la shape {errors:[...]} change → contrat API cassé
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
59f81879d8 |
test(e2e): tests Playwright multi-stack — vrai navigateur, DB isolée, Stripe mocké
Ajoute une couche end-to-end où un Chromium drive la SPA + API ensemble
contre une DB Postgres séparée, avec Stripe entièrement mocké au niveau
API. 6 scénarios couverts (signup + onboarding + 4 sur le billing trial).
Architecture :
- DB `rubis_test_e2e` séparée, TRUNCATE entre tests (~50 ms reset)
- Routes test-only `/__test__/*` gated par NODE_ENV=test_e2e
(reset, install Stripe mock, fire webhook, lire org state, last-org)
- Stripe mocké via __setStripeForTests — pas d'appel réseau
- Playwright spawn API + SPA automatiquement (webServer config)
- CORS étendu à test_e2e pour le cross-origin localhost:5173 → :3333
Scénarios :
- signup.spec.ts : signup → onboarding 3 étapes → dashboard (assert rubis hero)
- billing-trial.spec.ts :
• démarrer essai 14j → redirect Stripe Checkout (mock)
• fallback Free 2 factures continue l'onboarding
• webhook checkout.completed → org en trialing + trial_ends_at
• retour ?trial=cancel après abandon
• inspection DB : stripeCustomerId posé après start-trial
Scripts :
- pnpm e2e (headless)
- pnpm e2e:headed (Chromium visible)
- pnpm e2e:ui (mode interactif Playwright)
- pnpm e2e:setup (crée + migre rubis_test_e2e via docker exec)
Documentation : docs/tech/e2e-tests.md — architecture, scénarios,
extensions, CI, troubleshooting.
Limites assumées :
- L'UI Stripe Checkout (3DS, formulaire CB) n'est pas testée — externe.
Pour ça : playbook manuel docs/tech/stripe-trial-e2e-playbook.md.
- Le rendu du banner "Essai Pro" n'est pas asserté en E2E à cause de
TanStack Query staleTime — couvert par les tests vitest à la place.
État global du chantier billing : 127 tests japa + 6 Playwright + 11
vitest = couverture multi-niveaux.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
0db7ff877c |
fix(clients): aligne adresse structurée sur convention Lucid snakeCase
All checks were successful
Build & Deploy API / build-and-deploy (push) Successful in 1m17s
La migration 1778800000100_enrich_clients_for_invoicing avait créé les colonnes `address_line1` et `address_line2` (sans underscore avant le chiffre), mais Lucid v22 auto-snake-case `addressLine1` → `address_line_1` côté écriture. Résultat : tous les INSERT dans `clients` cassaient avec `column "address_line_1" of relation "clients" does not exist`. Bug latent — surfacé en lançant la suite functional complète après `node ace migration:run`. Affectait 20 tests Clients/Dashboard/Invoices. Fix : migration de rename qui aligne `address_line1` → `address_line_1` et `address_line2` → `address_line_2`. Le `RENAME COLUMN` préserve les données existantes en prod. Modèle Client simplifié (les déclarations manuelles ne sont plus nécessaires depuis que `schema.ts` les a régen). Bonus : fix du test `invoices.spec.ts` "liste paginée + meta total/page" qui créait 3 factures, ce qui dépasse la nouvelle limite Free 2 (ADR-023). Posé un `gracePeriodEndsAt` futur sur l'org du test pour bypass le quota. État après ce commit : 127 tests verts (60 unit + 67 functional). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
094c26059f |
test(billing): tests E2E HTTP du tunnel essai 14 j + playbook Stripe test mode
All checks were successful
Build & Deploy API / build-and-deploy (push) Successful in 1m18s
Ajoute 16 tests E2E qui hit les vraies routes `/api/v1/billing/*` à
travers le middleware auth, les validators et la persistance DB.
Complémentaire des 60 tests unitaires sur les services.
Suites couvertes :
- POST /start-trial : 200 happy path, customer Stripe réutilisé,
409 trial déjà consommé (2 garde-fous), 401 sans Bearer
- GET /subscription : expose inTrial + trialEndsAt, garde-fou
trial_ends_at passé
- POST /webhook : checkout.completed, subscription.updated trialing→active,
trial_will_end → enqueue recap (avec spy), payment_failed → past_due,
subscription.deleted → free + trial_ends_at conservé
- Idempotence : 2× le même event = même état final
- Event type inconnu → 200 silencieux (pas de DB write)
- 400 si stripe-signature absent / signature invalide
Helpers de test :
- installFullStripeMock(opts) → mock complet : customers, prices,
checkout, billingPortal, subscriptions, webhooks. Avec
passThroughWebhook qui bypass la vérif signature pour tester
le routing applicatif sans signer manuellement chaque payload.
env.test : STRIPE_SECRET_KEY + STRIPE_WEBHOOK_SECRET dummy +
WEB_URL/LANDING_URL.
Documentation : docs/tech/stripe-trial-e2e-playbook.md — playbook
manuel pour valider en mode Stripe test (5 scénarios : happy path
3DS, carte refusée au prélèvement, annulation Customer Portal,
re-trial bloqué, fallback Free). Utilise Stripe Test Clocks pour
fast-forward sans attendre 14 jours réels.
Total après ce commit : 76 tests sur la chaîne billing (60 unit + 16 E2E).
Les cas Stripe-side (3DS UI réel, prélèvement effectif J+14) restent
à valider manuellement via le playbook avant le go-live.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
b0e6f83655 |
feat(billing): essai 14 j Pro avec CB à l'inscription (Stripe trial_period_days)
Implémente le chantier #6 de docs/tech/landing-optimisations.md. Le funnel signup propose maintenant un essai 14 j Pro avec carte demandée mais non prélevée — prélèvement automatique à J+14 avec rappel à J+11 (webhook customer.subscription.trial_will_end de Stripe). Couverture tests : 60 tests unitaires sur la couche billing - billing.spec.ts (25) — quota Free, bypass trial, inTrial state - stripe_billing.spec.ts (24) — handlers webhook, idempotence, dispatcher - trial_recap_job.spec.ts (11) — stats aggregation, formatRubisToHoursFr + 3 nouveaux tests vitest côté SPA (useTrialDaysRemaining, useIsAtFreeLimit bypass trial). Backend : - Migration 1779000000000_add_trial_ends_at_to_organizations - PLAN_CAPS bypass quand status=trialing AND trial_ends_at futur - getOrgSubscriptionState expose inTrial + trialEndsAt - Refactor handlers webhook en service stripe_billing.ts (pures, testables) — extraction depuis le controller. dispatchWebhookEvent routeur typé également extrait pour les tests. - createTrialCheckoutSession avec subscription_data.trial_period_days=14, garde-fou TrialAlreadyConsumedError contre re-trial. - handleTrialWillEnd → enqueue job recap (BullMQ jobId déterministe basé sur subscriptionId, idempotent contre re-delivery Stripe). - Endpoint POST /api/v1/billing/start-trial. - Email template trial_recap (React Email, branding Rubis figé) avec stats: factures importées, relances envoyées, € récupérés, rubis + heures libérées. Infra de test : - tests/helpers/stripe_mock.ts : __setStripeForTests injection + factories fakeSubscription / fakeCheckoutSession / fakeInvoice. - __setTrialRecapEnqueueForTests : permet de spy l'enqueue sans Redis. Frontend : - /onboarding/billing.tsx (opt-in, pas encore forcé dans le flow) : bouton primaire essai 14j + fallback "Free 2 factures". - PlanLimitBanner : nouveau état "Essai Pro · X jours restants" qui prime sur les autres bandeaux. Discret rubis-glow, non blocant. - useStartTrial hook + useTrialDaysRemaining (arrondi sup). - SubscriptionState typé avec inTrial + trialEndsAt. Landing : - Sous-texte CTA réactivé : « CB demandée, non prélevée avant J+14 » (Hero + FinalCTA), maintenant promesse véridique. Notes ouvertes (à décider ultérieurement) : - Tunnel /onboarding/billing FORCÉ entre signup et /onboarding/compte : guard reste à activer (risque cassage du signup actuel sinon). Pour l'instant l'écran est accessible mais opt-in. - Cron de redondance trial-recap : pas encore implémenté (le jobId déterministe BullMQ couvre déjà la double-livraison Stripe). À ajouter si on observe des trial sans recap en prod. - Tests E2E avec Stripe test mode à faire avant le go-live (cartes 3DS 4000 0027 6000 3184, declined 4000 0000 0000 0341). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
f9cba50b5e |
feat(billing,landing): plan Free 2 factures + scaffold preuves sociales/SEO
Suite des chantiers structurants de landing-optimisations.md. #5 — Plan Free : 5 → 2 factures actives (cf. ADR-023) - PLAN_CAPS.free.activeInvoicesLimit dans apps/api/app/services/billing.ts - Tests unitaires alignés (4 → 1, 5 → 2 cap, delta 3 → delta 2) - billing:scenario command : commentaires + valeur par défaut - PlanLimitBanner : copy dynamique via {limit} au lieu de "5" hardcodé - /parametres/abonnement : H1 + tile Free (3 mois → 14 jours, 5 → 2) - billing.test.tsx (fixtures + cas test) - landing copy : hero feature pill, Pricing tile, FinalCTA, CGV §5 - CLAUDE.md pricing table #7 — Scaffold <TrustedBy /> (preuve sociale) - Composant qui render null tant que copy.trustedBy.{logos,testimonials} sont vides — pas de placeholder bidon. - Structure data dans copy.ts avec commentaires sur les prérequis avant d'ajouter une entrée (accord signé, photo, citation chiffrée). - Section insérée juste avant <Pricing /> (cf. doc §4). #8 — Plan articles SEO + brouillon article 1 - docs/marketing/seo-articles.md : 5 articles ciblés, mots-clés, structure type, lead magnet, calendrier 5 semaines. - Article 1 ("Modèle d'email de relance facture impayée") en brouillon complet, prêt à valider via l'admin blog (apps/api). #6 — Plan détaillé migration Stripe trial 14 j (code reporté) - docs/tech/stripe-trial-with-card.md : état actuel vs cible, architecture (Stripe Checkout + trial_period_days), modifs DB (trial_ends_at), API (start-trial + webhook trial_will_end), SPA (onboarding/billing), 3 emails transactionnels avec contenu intégral, risques + mitigations, plan d'exécution 2,5 j. - Implémentation reportée à une session focus avec accès Stripe test mode (cartes 3DS, webhook signing secret). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
4dcd85f912 |
test(billing): unit tests backend (17) + frontend (7)
Backend (`apps/api/tests/unit/billing.spec.ts`) — 17 tests :
- PLAN_CAPS sanity : Free 5 invoices/1 user, Pro illimité, Business 5 sièges
- countActiveInvoices :
• compte les 4 statuts actifs (pending, awaiting, in_relance, litigation)
• exclut paid + cancelled
• isolation par org (ne fuit pas entre orgs)
- canCreateInvoices :
• Free + grace period → autorisé même à 50+ actives
• Free post-grace + 4 actives + delta=1 → autorisé (≤ limite)
• Free post-grace + 5 actives + delta=1 → BLOQUÉ + bonne raison/limit/current
• Free post-grace + 3 actives + delta=3 → BLOQUÉ (over par batch)
• Pro + Business → toujours autorisé
• paid n'occupe pas de slot (5 paid + delta=5 → autorisé)
- getOrgSubscriptionState :
• inGracePeriod=true quand date future
• inGracePeriod=false quand date passée
• Pro reflète subscription_status / billing_cycle / current_period_end
• activeInvoicesCount inclut bien les 4 statuts
Frontend (`apps/web/src/lib/billing.test.tsx`) — 7 tests :
- useSubscription : appelle /billing/subscription, retourne le state
- useIsAtFreeLimit :
• false en loading
• false sur Pro avec 200 factures
• false en grace period même si activeCount > limit (12)
• true sur Free post-grace + activeCount = limit (5/5)
• true sur Free post-grace + activeCount > limit (8/5)
• false sur Free post-grace + activeCount < limit (4/5)
Setup :
- vitest.config.ts : ajout de `env: {VITE_API_URL, ...}` pour stub
les variables exigées par src/lib/env.ts au chargement (sinon plante
au boot des tests).
- Mock vi.spyOn(api, "get") pour éviter les vraies requêtes HTTP.
- QueryClient avec retry:false pour fail-fast.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
5e41e2a9fa | add ocr + add factures | ||
|
|
554ae4ba4a |
test(api): tests fonctionnels Invoices + Imports + Dashboard
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) |
||
|
|
691b5fd09f |
test(api): tests fonctionnels Clients + Plans (CRUD + cross-org + validation)
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) |
||
|
|
fc66d80f56 |
test(api): setup Japa + tests fonctionnels auth (signup/login/logout/onboarding)
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` |
||
|
|
8d3bab6a89 |
feat: scaffold frontend monorepo + first /login screen
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>
|