17 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4d0cab8b33 |
feat(ocr): retry exponential backoff sur 429 dans ocr:validate
All checks were successful
Build & Deploy API / build-and-deploy (push) Successful in 1m19s
La free tier Mistral a un rate limit non-linéaire (parfois 4-5 req/min
acceptées, parfois 1 req/2min selon la charge). Un délai fixe entre
calls ne suffit pas — on retry max 3× avec backoff 30s, 60s, 90s.
Combiné avec --delay-ms (espacement nominal entre calls), ça permet
de tenir tout un bench même si le quota se serre en cours de route.
Bench réel observé sur 10 factures variées (templates Boulangerie,
Mercier moderne, Mercier ancien, retards 5j/30j/90j/180j) :
- amountTtcCents : 10/10 (100 %) ← précision financière parfaite
- clientEmail : 10/10 (100 %)
- numero : 9/10 (90 %) ← 1 hallucination "FOUT"
- issueDate : 9/10 (90 %) ← même facture, 1970-01-01 fallback
- dueDate : 9/10 (90 %) ← idem
- clientName : 8/10 (80 %) ← 2 fails : Mistral inclut contact
- Latence moy. : 9.5 s/facture (avec delay 7s)
- 8/10 factures 100 % match (80 %)
- 91.7 % accuracy globale champs
Insights actionnables :
- amountTtcCents et clientEmail sont fiables → ok pour auto-validate
- clientName : ajouter au prompt "ne pas inclure le contact (M./Mme)"
- 1 facture sur 10 fait halluciner Mistral (FOUT + dates 1970) →
afficher "à vérifier" dans la UI quand confidence < 0.5 sur dates
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
2f96238efe |
feat(ocr): throttle --delay-ms + script generate-expected pour ground truth
All checks were successful
Build & Deploy API / build-and-deploy (push) Successful in 1m21s
Améliorations sur la commande de bench OCR validée avec 5 factures
réelles via Mistral (100 % accuracy obtenue sur l'échantillon test) :
- Option `--delay-ms` (default 1500 ms) entre 2 appels provider pour
éviter le rate limit Mistral (1300 free tier ≈ 1 req/s). Permet de
benchmark les 27 factures sans HTTP 429.
- Script `e2e/fixtures/invoices/generate-expected.mjs` qui parse les
PDFs via `pdftotext -layout` (poppler-utils) et génère
automatiquement les <name>.expected.json :
• Numéro F2026-XXXX
• Dates DD/MM/YYYY ou format long ("21 avril 2026")
• Montant TTC en cents (gère séparateur milliers "2 775,02")
• clientName en gérant 3 templates :
- "DOIT : <Nom>"
- "Facturé à :" en colonne droite
- "ADRESSÉE À ... ÉCHÉANCE" côte à côte
Re-générable, idempotent (skip si .expected.json existe déjà).
Le .gitignore du dossier reste sur `*` exclude pour ne pas commit les
PDFs (cohérent avec assets/test-invoices/ déjà ignoré racine), mais
autorise le script `generate-expected.mjs` (reproductible, sans secret).
Workflow utilisateur :
1. Pose tes PDFs dans e2e/fixtures/invoices/
2. `node generate-expected.mjs` génère les ground truth en lot
3. Vérifie/corrige à la main si besoin (parser pas 100 % parfait sur
tous les templates exotiques)
4. `OCR_PROVIDER=mistral pnpm ocr:validate` lance le bench réel
Résultat baseline observé sur 5 factures Mistral en mode réel :
- clientName 5/5 (100 %)
- clientEmail 5/5 (100 %)
- numero 5/5 (100 %)
- amountTtcCents 5/5 (100 %)
- issueDate 5/5 (100 %)
- dueDate 5/5 (100 %)
- Latence moyenne : 3,1 s / facture
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
e38aa224e8 |
feat(api): commande ocr:validate pour bencher l'OCR sur factures réelles
Outil standalone qui mesure la qualité d'extraction du provider OCR
courant (Mock, Mistral, ou autre futur) sur un set de factures avec
ground truth, séparé de la suite Playwright (qui reste sur OCR mock
pour la rapidité CI).
Pourquoi : permet de valider qu'un changement de provider (Mistral
upgrade, ajout Document AI, custom prompt) maintient la précision sur
les factures réelles avant de l'activer en prod.
Architecture :
- Lit `e2e/fixtures/invoices/<name>.pdf` (ou .png/.jpg)
- À côté, `<name>.expected.json` avec la ground truth
- Pour chaque facture : upload temporaire vers le storage courant
(MinIO en dev), appelle provider.extract(), compare field-by-field
- Cleanup du fichier temp après extraction
- Sommaire : accuracy globale, par champ, latence moyenne, exit 1
si une fixture a échoué (utile CI)
Tolérances par champ :
- amountTtcCents : exact (la précision financière compte)
- issueDate / dueDate : jour exact
- numero : exact (trim, case-insensitive)
- clientName : Jaccard similarity ≥ 85 % sur les tokens
(tolère "SARL" final manquant, espaces, etc.)
- clientEmail : exact (lowercased) ou null
Usage :
pnpm ocr:validate # provider courant (.env)
OCR_PROVIDER=mistral MISTRAL_API_KEY=... pnpm ocr:validate
node ace ocr:validate --fixtures-dir=./other --out=report.json
Sécurité :
- `.gitignore` exclut tous les fichiers de e2e/fixtures/invoices/
sauf README + .gitignore eux-mêmes — les vraies factures ne fuitent
pas dans le repo public
À faire par Arthur :
1. Dépose 10-20 vraies factures (anonymisées si possible) dans
e2e/fixtures/invoices/
2. Pour chaque, écrit le .expected.json (5 min par facture)
3. Lance `OCR_PROVIDER=mistral pnpm ocr:validate` → ajuste prompt ou
post-process si l'accuracy descend sous le seuil
Format ground truth + seuils cibles documentés dans le README du dossier.
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> |
||
|
|
51217175ad |
feat(banking): intégration Powens AISP + auto-réconciliation factures
Module banking complet en lecture seule via Powens (ex-Budget Insight)
pour détecter automatiquement les paiements clients et arrêter les
relances dès qu'une facture est payée. Réservé plans Pro / Business,
kill switch global BANKING_ENABLED désactivé en prod tant que le KYC
Powens n'est pas validé (cf. .claude/deploy-memory.md).
Backend (apps/api)
- PowensClient bas niveau : init user, code temporaire 30s, build
webview URL, list/get/delete connections, accounts, transactions,
vérif HMAC SHA-256 timing-safe pour webhook.
- BankingService : ensurePowensUser (chiffrement token via Adonis
encryption / APP_KEY), createWebviewUrl avec state HMAC anti-CSRF
(TTL 10 min), handleCallback (upsert connection + accounts +
fire-and-forget mail + sync 90j + reconcile), disconnect (DELETE
Powens + soft-revoke en DB), setReconciliationMode.
- Réconciliation : match transactions ↔ factures sur montant exact
+ label normalisé (numero ou nom client, NFD strip + alphanum).
Confiance HIGH (label matche) vs LOW (montant seul). Mode auto +
HIGH → invoice.status=paid + bonus rubis + cancel relances +
enqueuePaymentThanks (client) + sendInvoiceAutoPaidNotification
(user). Mode manual ou LOW → match_status='suggested' (UI V2).
- Webhook /webhooks/powens : vérif HMAC, lookup org par
powens_user_id, dispatch CONNECTION_SYNCED / NEW_TRANSACTIONS /
USER_SYNC_ENDED → sync incrémental 7j + reconcile, CONNECTION_ERROR
/ SCA_REQUIRED → update state + last_error. Réponse 200 immédiate
puis processing fire-and-forget pour ne pas timeout côté Powens.
- 4 migrations : bank_connections, bank_accounts, bank_transactions
+ colonnes powens_user_id (chiffré APP_KEY) et reconciliation_mode
sur organizations.
- 2 templates React Email : BankConnectedEmail (post-connection,
récap comptes + lien settings) et InvoiceAutoPaidNotificationEmail
(notif user après match auto, lien direct facture + libellé
bancaire détecté). Toujours en branding Rubis (notif Rubis → user,
jamais marque blanche).
- 2 commandes ace : banking:reconcile (rejoue le reconcile sans
reconnecter la banque) et banking:simulate-payment (injecte une
bank_transaction synthétique qui matche une facture, pour test E2E
sans devoir attendre un vrai virement sandbox).
- Kill switch isBankingEnabled() : flag BANKING_ENABLED + check des
credentials Powens. Endpoint public GET /banking/status renvoie
{ enabled }, /banking/powens/init throw 503 banking_disabled si OFF.
- Fix handler exceptions : UNIQUE violation composite (org, X)
rapporte désormais la vraie colonne en faute (numero/slug/…) avec
message lisible « Le numéro de facture "F2026-0013" existe déjà »,
au lieu d'un message ambigu sur organization_id.
Frontend (apps/web)
- /parametres : nouvelle SettingsSection "Banque" gated par kill
switch + plan Pro/Business. Si Free → upsell card avec CTA vers
/parametres/abonnement. Si Pro/Business sans banque → CTA "Connecter
une banque". Si banque connectée → carte avec accounts (IBAN
masqué FR76 **** **** **** 1234), solde, last sync, bouton
Déconnecter. Toggle Manuel/Auto pour reconciliation_mode.
- /parametres/banque/success : nouvelle route dédiée post-callback
avec badge ✓ animé + halo glow rubis, récap des comptes
synchronisés, 2 CTAs ("Voir mes paramètres" / "Retour dashboard"),
note sécurité "lecture seule, aucun déplacement de fonds".
- Hooks : useBankingStatus, useBankConnections (avec opt-out via
{ enabled }), useInitBanking, useDisconnectBank, useBankingSettings,
useUpdateBankingSettings.
Infrastructure (k3s)
- ConfigMap rubis-api-config : BANKING_ENABLED='false' par défaut,
BANKING_PROVIDER='powens', POWENS_DOMAIN='rubis',
POWENS_API_BASE_URL='https://rubis.biapi.pro/2.0/',
POWENS_REDIRECT_URI='https://app.rubis.pro/api/v1/banking/powens/callback'.
- Secret rubis-app-secrets : 3 nouvelles clés POWENS_CLIENT_ID,
POWENS_CLIENT_SECRET, POWENS_WEBHOOK_SECRET (valeurs sandbox posées
via kubectl patch, à remplacer post-KYC).
Sécurité
- Token Powens chiffré au repos via Adonis encryption (AES-256-GCM,
clé APP_KEY).
- State HMAC SHA-256 signé sur APP_KEY pour le flow webview
(anti-CSRF + porte l'org_id à travers le redirect).
- Webhook HMAC SHA-256 sur header BI-Signature avec
POWENS_WEBHOOK_SECRET, comparaison timing-safe.
- IBAN masqué côté API (transformer).
- Scope par org sur tous les endpoints (anti-IDOR).
- Rate limiting via le middleware Adonis existant.
- Idempotence DB : UNIQUE (org, powens_connection_id), (connection,
powens_account_id), (account, powens_id) → rejouer un event ou un
callback ne pose pas de problème.
Documentation
- /docs/tech/banking-setup.md : procédure complète setup dev avec
Cloudflare Quick Tunnel, compte sandbox Powens, whitelist URLs.
- /.claude/deploy-memory.md : section "Banking (Powens) — activation
prod" avec procédure en 6 étapes (KYC → secrets → ConfigMap →
flip flag → smoke test), snippet kubectl patch pour rotation
ciblée de secrets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
919ebfe755 |
feat(release): v1.11.0 — marque blanche pour le plan Business (backend)
Première moitié de la feature marque blanche : la machinerie complète qui permet à un compte Business d'envoyer ses emails de relance avec son propre logo, ses propres couleurs et son nom comme expéditeur, à la place du branding Rubis. Architecture : - Nouvelle colonne JSONB `organizations.brand_settings` (12 tokens customisables : logo, senderName, et 10 couleurs — primary, banner, body bg, card bg, text, text muted, border, link, button text). Null = palette Rubis intacte. Validation hex stricte (#RRGGBB). - Service `#services/brand` avec `resolveBrandTokens(org)` qui merge defaults + overrides en respectant le plan (couleurs/logo = Business only ; senderName = cascade pour tous les plans). Mergeurs avec sémantique "null = reset au default sur ce champ précis" pour les patches partiels. - Service mutualisé `#services/media_storage` qui remplace l'ancien `blog_uploads.ts`. Scopes `blog` (4 MB, jpg/png/webp) et `brand-logo` (1 MB, + svg accepté). Cleanup automatique du logo précédent lors d'un remplacement (pas de versioning — la conv produit est "on écrase"). - Controller `BrandController` (5 endpoints) + middleware `AssertBusinessPlanMiddleware` qui throw 403 `business_plan_required` (code matché par le SPA pour l'upsell card). - Refactor des 3 templates mail (relance, payment thanks, checkin) + layout commun pour accepter `tokens: BrandTokens` en prop. Le dispatcher résout les tokens per-org pour relance + remerciement (= user → client, branded), et passe `DEFAULT_BRAND` au checkin (= Rubis → user, toujours Rubis-branded). - Routes publiques pour le logo : `/api/v1/uploads/brand-logos/:filename` (sans auth, cache immutable, X-Content-Type-Options: nosniff pour les SVG). UI self-service arrive dans la prochaine version (v1.12.0). En attendant, un compte Business peut être configuré via Bruno / API directe. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
6dcae6956c |
feat(blog): admin CRUD + image upload + sidebar link
L'éditeur du blog (jusqu'ici limité au seeder) a maintenant une vraie
interface au-dessus de l'API.
Backend (apps/api) :
* Migration users.is_admin (boolean default false).
* Middleware admin (404 si user.is_admin=false après auth).
* Commande ace promote:admin --email=… [--revoke].
* AdminPostsController CRUD complet : list/show/store/update/publish/
unpublish/destroy + suggest-slug. Au save, contentHtml + wordCount +
readingTime sont re-calculés via blog_renderer. Au publish, durcit la
validation SEO (titre ≤60, excerpt 120-160, hero+alt requis, ≥600 mots),
flippe status='published' + publishedAt, ping Google+Bing pour le sitemap.
* BlogUploadsController :
- POST /api/v1/admin/uploads (multipart, JPEG/PNG/WebP, max 4MB)
→ MinIO clé uploads/blog/{uuid}.{ext}
→ renvoie URL relative /api/v1/uploads/blog/{filename}
- GET /api/v1/uploads/blog/:filename (public, cache immutable 1 an)
→ stream depuis MinIO, regex anti-traversal sur le nom.
* UserTransformer expose isAdmin (cf. shared/types/user).
* k3s/app/landing.yml : NodePort 30111 explicite (pour Traefik repo proxmox).
Frontend (apps/web) :
* Lib typée admin-blog (calls API, queryKeys, helpers URL).
* Route /admin/blog : liste filtrable avec status badge, ouverture
publique, dépublier, supprimer, "+ Nouveau brouillon".
* Route /admin/blog/:id : éditeur 2-colonnes
- Gauche : @uiw/react-md-editor (lazy import) avec preview live.
- Droite : hero image (drag&drop + alt), excerpt avec compteur
120-160, tags, aperçu Google snippet, validations bloquantes.
- Autosave debounce 2s + bouton Publier qui sauve d'abord.
- Hero image upload via MinIO (HeroImageUpload component).
* Sidebar : lien "Blog (admin)" si user.isAdmin.
* Gate côté client (beforeLoad redirect si non admin) + côté serveur
(middleware admin) — defense in depth.
Note : les requirements de publish miroir backend ↔ frontend (cf.
PUBLISH_REQUIREMENTS dans validators/post.ts et VALIDATION_RULES dans
admin.blog_.\$id.tsx). À synchroniser si un seuil bouge.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
e5530930b3 |
feat: refactor frontend en stack React unifiée (Astro + packages/ui)
Trois surfaces partagent désormais le même design system, Tailwind v4
et React 19 — au lieu d'avoir landing en HTML vanilla, app en React, et
blog en Adonis SSR :
* packages/ui — design system partagé (tokens Tailwind v4 + composants
TSX) extrait depuis apps/web : Brand, Gem, Button, Card, Chip, Eyebrow,
EmptyState. apps/web migre 41 imports vers @rubis/ui.
* apps/landing — nouvelle app Astro 6 SSR (rubis.pro), remplace l'ancienne
landing nginx vanilla. Embarque :
- Landing complète portée en sections React (Hero, Stats, Promise,
HowItWorks, Gamification, Legal, Pricing, FAQ, FinalCTA, Footnotes)
- Pages légales (mentions, confidentialité, CGV) via LegalLayout.astro
- Blog SSR (/blog, /blog/:slug) qui consomme /api/v1/posts
- sitemap.xml, blog/rss.xml, robots.txt en endpoints Astro
- SEO complet (canonical, hreflang, OG, Twitter Card, JSON-LD
Article/BreadcrumbList/Blog/SoftwareApplication)
* apps/api — BlogController réduit à 2 endpoints JSON (GET /api/v1/posts
+ GET /api/v1/posts/:slug). Suppression des templates SSR Adonis
(apps/api/app/blog/), de l'alias #blog/*, des deps react-dom et
@types/react-dom. PostTransformer + PostSummaryTransformer ajoutés.
Le service blog_renderer + le seeder + les 3 articles fondateurs
restent intacts (réutilisés par futurs admin + cron IA).
* Infra :
- Dockerfile.landing (multi-stage Node 22 + tini, Astro standalone)
- k3s/app/landing.yml (Deployment + Service rubis-landing:4321 +
ConfigMap avec API_URL=http://rubis-api.rubis.svc.cluster.local:3333)
- .gitea/workflows/deploy.yml mis à jour pour build rubis-landing
- .gitea/workflows/deploy-web.yml + Dockerfile.web : prennent en
compte packages/ui/ comme dépendance
- Suppression du Dockerfile nginx legacy + k3s/{deployment,service}.yml
- Suppression de landing/ (assets favicons migrés vers
apps/landing/public/)
* Docs : architecture.md (vue d'ensemble + §4bis apps/landing complet,
§3 endpoints JSON blog, layout monorepo), CLAUDE.md (stack technique,
documents associés, déploiement).
Note infra : l'ancien Deployment "rubis" (nginx) et son Service ne sont
PAS supprimés par la CI — à nettoyer manuellement après validation que
Traefik a été repointé sur rubis-landing:4321 dans le repo proxmox.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
1c5a58e09a |
chore(domain): migrate rubis.arthurbarre.fr → rubis.pro
Bascule du domaine principal vers rubis.pro / app.rubis.pro : - K3s ConfigMaps (api.yml, web.yml) : APP_URL, WEB_URL, COOKIE_DOMAIN, OAUTH callbacks pointent vers app.rubis.pro - Dockerfile.web : ARG VITE_API_URL et VITE_PUBLIC_LANDING_URL - Workflows Gitea : commentaires + build args web → rubis.pro - Code API (mail_dispatcher, send_test_email, config/mail) : defaults env LANDING_URL et MAIL_FROM_ADDRESS migrés - Templates env (.env.example) idem - Docs (architecture, backend, frontend, brand-identity) idem - AGENTS.md / CLAUDE.md / deploy-memory : pointeurs domaine MAJ Note : MAIL_FROM_ADDRESS dans le secret K3s reste sur rubis@arthurbarre.fr tant que le domaine rubis.pro n'est pas Verified dans Resend. À switcher manuellement après vérif Resend. Compat : un 301 Traefik redirige rubis.arthurbarre.fr → rubis.pro (et app.X aussi) — config Ansible dans le repo proxmox. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
ff8fe64be2 |
feat(mail): templates HTML React Email + brand "Rubis sur l'ongle"
Templates HTML stylés DA Rubis pour les 2 emails sortants — fini le
plain text moche.
apps/api/app/mails/
├── _brand.ts : tokens couleur + spacing partagés
├── _layout.tsx : squelette commun (header rubis-deep + footer)
├── checkin_email.tsx : email envoyé À L'USER avec 2 boutons CTA
│ Oui (rubis primary) / Non (outlined)
└── relance_email.tsx : email envoyé AU CLIENT, body texte du plan
+ card récap (numéro, montant, échéance,
badge retard rubis-deep)
Stack :
- @react-email/components + @react-email/render
- Tous les styles inline (compatible Gmail / Outlook / Apple Mail)
- HTML + plain text en fallback (anti-spam, accessibility)
mail_dispatcher.ts :
- sendRelanceEmail : .html(rendered) + .text(body)
- sendCheckinEmail : .html(rendered) + .text(body)
- daysLate calculé via clock.now (démo-aware)
send_test_email :
- Nouveau flag --template=checkin (default) | relance | plain pour
tester chaque rendu via Mailpit sans créer de vraie facture.
Brand & landing :
- "Rubis Sur l'Ongle" → "Rubis sur l'ongle" partout (config, mail,
PDF, Stripe appInfo)
- Nouvelle env var LANDING_URL (default https://rubis.arthurbarre.fr)
- Footer email rend "Rubis sur l'ongle" comme <a> rubis cliquable
vers la landing — l'user qui reçoit le mail connaît la marque
derrière l'envoi
- .env.example mis à jour avec LANDING_URL pour les autres devs
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
023f08c261 |
feat(api): commande ace billing:scenario pour tester les états billing
All checks were successful
Build & Deploy API / build-and-deploy (push) Successful in 1m7s
Force un état billing sur l'org d'un user pour tester rapidement chaque comportement UI/enforcement sans passer par Stripe ni attendre 3 mois. Usage : node ace billing:scenario --email <user> --scenario <name> Scénarios : • status : ne touche rien, affiche juste l'état courant • fresh : reset signup neuf (free + grace 3 mois) • grace-expired : free, grace terminée, ≤ 5 actives → OK • limit-reached : free, grace terminée, force 5 actives → bloqué (402) • pro : pro mensuel actif, fake IDs si pas de vrais • pro-cancelling : pro + cancel_at_period_end=true → bandeau ANNULÉ • pro-past-due : pro + status=past_due → warning UI • business : business mensuel actif Sécurité : préserve les VRAIS Stripe IDs s'ils existent (= l'org a déjà payé). Génère des fake `cus_test_FAKE_*` / `sub_test_FAKE_*` seulement si NULL — ne pas écraser une vraie souscription. Le command affiche un récap compact à chaque exécution : - plan / grace / Stripe IDs / status / cancel_at - factures actives vs limite - création autorisée ou non + raison Pour tester un comportement côté UI : 1. Lance le scénario 2. Reload /parametres/abonnement et /factures 3. Vérifie le rendu (bandeau cancel, blocage import, etc.) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
1952265217 |
feat(billing): plans Free/Pro/Business + Stripe Checkout & Customer Portal
Pricing V1 :
- Free : 5 factures actives, 1 user, 3 mois de grâce illimité au signup
- Pro : 19 €/mois ou 190 €/an, factures illimitées, 1 user
- Business : 49 €/mois ou 490 €/an, illimité + 5 sièges (V2 multi-users)
+ reply-from-user-email (V2)
Backend :
- Migration : plan, grace_period_ends_at, stripe_customer_id,
stripe_subscription_id, subscription_status, billing_cycle,
current_period_end sur `organizations`. Backfill grace_period auto.
- `app/services/billing.ts` : PLAN_CAPS, countActiveInvoices,
canCreateInvoices (enforce post-grace), getOrgSubscriptionState.
- `app/services/stripe.ts` : client lazy + lookup_keys stables.
- `app/controllers/billing_controller.ts` :
• GET /billing/subscription → state pour l'UI
• POST /billing/checkout → crée une Checkout Session
• POST /billing/portal → Customer Portal Session
• POST /billing/webhook (public) → handle 4 events Stripe
(checkout.completed, subscription.updated/deleted, invoice.payment_failed)
- `commands/stripe_setup.ts` : `node ace stripe:setup` crée Products +
Prices (idempotent via lookup_key).
- Enforcement 402 `plan_limit_reached` sur :
• POST /invoices (saisie manuelle)
• POST /invoices/import-batch/:id/drafts/:draftId/validate (OCR)
Frontend :
- `lib/billing.ts` : useSubscription, useStartCheckout, useOpenPortal,
useIsAtFreeLimit.
- `routes/_app/parametres_.abonnement.tsx` : page comparaison plans
avec toggle mensuel/annuel, current plan + portail Stripe, CTA upgrade
qui redirige vers Checkout hostée.
- `routes/_app/parametres.tsx` : nouvelle section "Abonnement" qui
affiche le plan courant + lien vers la page abonnement.
- `components/billing/PlanLimitBanner.tsx` : banner sur /factures qui
s'adapte selon période (grâce / approche / atteinte).
- Toast dédié 402 sur la validation OCR avec action "Passer Pro".
Doc :
- flow.md : nouvelle section §11 "Pricing & enforcement" qui couvre
plans, grâce, webhook flow, Customer Portal, env vars.
Setup dev :
1. STRIPE_SECRET_KEY (sk_test_...) dans apps/api/.env
2. `stripe listen --forward-to localhost:3333/api/v1/billing/webhook`
→ copier whsec_... → STRIPE_WEBHOOK_SECRET
3. `node ace stripe:setup` une fois pour créer Products+Prices
4. Tester via /parametres/abonnement → checkout en mode test Stripe
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
b96b62aab6 |
feat(seed): génération PDF cohérente par facture via @react-pdf/renderer
Chaque facture seedée a maintenant son propre PDF dont le contenu
matche exactement les meta DB (vendeur = org du user, client = client
réel, numéro / dates / montant cohérents). Plus de réutilisation
round-robin de PDFs disque non-cohérents.
Stack :
- @react-pdf/renderer : composants React déclaratifs, StyleSheet
inspiré du SPA (mêmes tokens couleur Rubis), même mental model que
le frontend.
- InvoiceDocument décomposé en sous-composants Header / Addresses /
ItemsTable / Totals / Footer pour itération facile.
- Items générés depuis un pool B2B (conseil, dev, audit, formation,
livraison, photo, …) avec quantités/prix unitaires qui s'ajustent
pour que la somme matche le total TTC stocké.
Le command `seed:demo --reset` :
- wipe les invoice-pdfs/{orgId}/* sur MinIO (paginé)
- re-génère 227 PDFs (27 actionnables + 200 historiques)
- CA cumulé paid ≈ 400 K€ pile sur la cible
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
1633fb9bf0 | add factories | ||
|
|
2d3766cc3d |
feat(dashboard): dataviz cohérente DA Rubis (3 charts + page Insights)
Backend - Service dashboard.ts : computeTimeseries + computeClientTimeseries (helper fetchPaidByMonth DRY entre les deux). Buckets pré-créés sur N mois pour pas afficher de "trous" quand un mois n'a aucun paiement. - GET /dashboard/timeseries?range=3|6|12 (paidByMonth + pipelineByStatus) - GET /clients/:id/timeseries?range=3|6|12 (paidByMonth filtré) Frontend — Recharts (43 deps, ~50KB gzip) - components/charts/theme.ts : palette stricte (rubis + neutres chauds, pas de bleu/vert), couleurs statuts cohérentes avec les badges côté liste, format fr-FR pour les axes/tooltips - ChartTooltip themed : carte cream + bordure rubis-glow, font Inter, tabular-nums, série label override - EncaisseChart (area, dégradé rubis-glow → transparent) - DsoTrendChart (line ink + référence pointillée à 30j = norme LME) - PipelineChart (donut avec total au centre + PipelineLegend séparée) - ClientPaidChart (bar chart compact pour fiche client) Wiring - Dashboard / : encaissé + DSO côte à côte, pipeline + top retards en dessous - Fiche client /clients/:id : mini bar chart "encaissés sur 6 mois" entre les stats et la liste factures - Page /insights : version pleine largeur des 3 charts + range selector 3m/6m/12m + 3 cards récap (encaissé total, factures payées, DSO moyen). Lien "Insights" ajouté au sidebar desktop (icône TrendingUp). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
32fcb02108 |
feat(api): factories de démo + commande seed:demo --email pour peupler une org
Factories réutilisables (database/factories.ts) : - makeClient — un client à partir de 8 templates FR (Boulangerie Martin, Maçonnerie Dupont, etc.) avec contact/SIRET/adresse réalistes - makeInvoice — une facture avec status driving les dates et le rubis earned (pending = future, in_relance = échue récente, paid = paidAt cohérent, etc.) - makeActivityForInvoice — events alignés sur le statut (import/relance/paid) - seedDemoOrg — recette V1 : 8 clients + 15 factures réparties sur 5 statuts (5 paid sur 6 mois, 4 in_relance, 2 awaiting_user_confirmation, 3 pending, 1 litigation) → fait vivre dashboard, factures et DSO Commande Ace seed:demo - Args : --email <email> (obligatoire), --reset (wipe avant), --orgName - Flow : trouve user, configure son org (nom + bucket), provision les 4 plans par défaut (idempotent), seed la data, met à jour rubis_count - Pose une signature email par défaut sur le user si vide - Tout en transaction : pas d'état inconsistant si une étape plante Usage : node ace seed:demo --email arthurbarre.js@gmail.com node ace seed:demo --email ... --reset --orgName="Maçonnerie Dupont" Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
a790455ae1 |
feat(api): bascule des envois mail sur Resend (fin de Mailpit en dev)
- MAIL_DRIVER=resend par défaut, from rubis@arthurbarre.fr (domaine vérifié) - replyTo posé sur user.email dans les relances : les réponses des clients reviennent au patron de la TPE, pas dans notre boîte transactionnelle - ajout d'une commande Ace `send:test-email` pour valider la conf (driver, from, SPF/DKIM/clé API) sans passer par tout le flow facture Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |