9 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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>
|
||
|
|
87c6f49692 |
fix(mail): from-name = nom de l'org (pas "Rubis Sur l'Ongle")
All checks were successful
Build & Deploy API / build-and-deploy (push) Successful in 1m8s
Le client final qui reçoit la relance voyait "From: Rubis Sur l'Ongle"
alors qu'il connaît "Arthur Barré" (le patron de la TPE qui utilise
Rubis). Confusion garantie côté client → relance perçue comme spam.
Fix : `sendRelanceEmail` utilise maintenant comme display name "From" :
1. `organization.name` (en priorité — c'est le nom commercial connu
du client)
2. `user.fullName` (fallback si l'org n'a pas de nom posé)
3. `MAIL_FROM_NAME` env (dernier recours, "Rubis Sur l'Ongle" en prod)
L'adresse technique reste sur notre domaine vérifié (relances@arthurbarre.fr
→ SPF/DKIM Resend OK), seul le display name change.
Le mail check-in (envoyé À l'user, pas au client) garde "Rubis Sur l'Ongle"
comme display — c'est nous qui le notifions, c'est cohérent.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
68ed8f2ec6 |
feat(api): logs fichier en dev + traces du flow relance/mail
config/logger.ts: en dev, on duplique les logs vers apps/api/storage/logs/app.log via un `multistream` pino (pretty stdout + JSON file en parallèle). Plus fiable que `transport.targets` qui tourne dans un worker thread et fail silencieusement quand le path n'est pas accessible. Logs ciblés sur le pipeline relance pour debug rapide : - relance_scheduler : tâche créée + delaySec + queueJobId - send_relance_job : pick-up / skip / envoi / OK / KO - mail_dispatcher : driver actif (smtp/resend) + send OK / err .gitignore : storage/uploads + storage/logs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
933c6496b1 |
feat(demo): mode démo live — horloge virtuelle + emails capturés
Permet de faire vivre Rubis en accéléré pour démontrer le produit à des
prospects, SANS impacter la prod. Les vrais users ont demoMode=false par
défaut → toute la logique démo est court-circuitée.
Architecture (priorité : zéro impact prod, codebase propre)
Phase 1 — Abstraction Clock
- Migration : organizations.demo_mode + virtual_now + demo_speed_factor
(défaut false/null/1, zéro effet sur les orgs existantes)
- services/clock.ts : now(orgId?) → DateTime.utc() en prod, virtualNow
en démo. Cache mémoire 250ms pour pas spammer la DB. Helpers
setVirtualNow / setDemoMode pour les transitions.
- Refacto 7 fichiers : relance_scheduler, checkin_scheduler, dashboard,
send_relance_job, send_checkin_job, mail_dispatcher (buildRelanceVars
daysLate), activity_recorder, checkin_controller, invoices_controller
(buildTimeline + markPaid). DateTime.now() → clock.now(orgId).
- Tests existants (51) passent identique → preuve que la prod est intacte.
Phase 2 — Capture emails + dispatch
- Migration : demo_captured_emails (kind, to, from, subject, body, sent_at,
meta) — index sur (org, sent_at desc) pour l'inbox.
- services/demo/capture.ts : captureEmailIfDemo() — UNIQUE point de fork
dans la prod (deux lignes dans mail_dispatcher : if captured return).
Hors démo, fonction retourne false → flux Resend inchangé.
- services/demo/dispatch.ts : tickAndDispatch(orgId, target) → bump
virtual_now, trouve les tasks dues (relance + checkin), invoke les
handlers existants synchronement (skip BullMQ, propre). Retourne les
events fired pour l'UI.
- POST /api/v1/demo/{start,end,tick} + GET /demo/{state,inbox}, toutes
protégées par requireDemoOrg() (403 si demoMode=false).
Phase 3 — UI horloge "vivante"
- lib/demo.ts : useDemoState, useDemoTick (boucle rAF locale qui avance
virtualNow à `speed * elapsed` jours/sec, sync backend toutes les
250ms, auto-pause sur fired events). Pas de boutons +1j/+3j —
l'horloge tourne vraiment.
- DemoClock (top-right, fixed) : date pleine en font display, rail
rubis-glow avec pastille ◆ qui glisse vers le prochain event,
play/pause + sélecteur 1x/2x/5x. Auto-cachée si demoMode=false.
- DemoEmailSlide : slide-over droite quand event fires — affiche
l'email capturé (de/à/sujet/body) façon vrai client mail. Pause
forcée tant que tous les events ne sont pas acquittés ("comme si
le temps était vraiment passé").
- DemoToggle dans /parametres : démarrer/quitter le mode démo, avec
copy explicite ("emails capturés, pas envoyés à de vrais clients").
Le code démo vit isolé dans services/demo/, controllers/demo_controller.ts,
components/demo/, lib/demo.ts. La prod ne référence ces fichiers QUE
via captureEmailIfDemo dans mail_dispatcher.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
9e531e32a9 |
feat(plans): wizard de création de plan custom + génération IA Mistral
Backend
- migration : champs contact_first_name / contact_last_name (nullable)
sur clients pour personnaliser les variables de relance
- POST /api/v1/plans : création de plan custom avec slug auto-généré
(suffixé en cas de collision, "nouveau"/"new"/"create" réservés)
- POST /api/v1/ai/generate-relance : génération de subject+body via
mistral-small-latest, avec brief utilisateur et tonalité ciblée
- mail_dispatcher : nouvelles variables {{daysLate}}, {{issueDate}},
{{user.fullName}}, {{user.companyName}}, {{client.contactFirstName}},
{{client.contactLastName}} (helper buildRelanceVars exposé pour preview)
- send_relance_job preload désormais l'organization pour exposer son name
Frontend
- /plans/nouveau : wizard 4 étapes (Identité → Cadence → Messages → Récap)
- Stepper en haut, navigation guidée, validation par étape
- Étape 1 : nom + tonalité globale (4 cards Doux/Standard/Ferme/Strict)
avec aperçu de la cadence par défaut associée
- Étape 2 : timeline horizontale (rail rubis-glow + nœuds ◆ teintés
selon la tonalité), édition décalage/ton de l'étape sélectionnée
- Étape 3 : édition par étape avec preview live à droite, chips de
variables cliquables, bouton "Générer avec l'IA" qui ouvre une modale
Mistral (brief + résultat + régénérer)
- Étape 4 : récap avec preview de chaque email rendu sur un client fictif
- Détection des variables sensibles → warning si X clients existants n'ont
pas le champ contactFirstName/contactLastName rempli (UX informative,
fallback vide à l'envoi)
- "Dupliquer" sur chaque card de plan → /plans/nouveau?from=<slug>
pour pré-remplir le wizard à partir d'un plan existant
- ClientCreateDialog : ajout des champs prénom/nom du contact dédié
- TEMPLATE_VARIABLES étendu, helper renderTemplate côté front en miroir
exact de l'implémentation API
- MSW handlers ai/plans/clients alignés sur le nouveau contrat
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> |
||
|
|
94263c6447 |
feat(api): check-in flow — email à l'user + endpoints publics paid/pending
Le check-in remplace l'intégration banking V1 (cf. CLAUDE.md → Glossaire) :
avant que la 1re relance ne parte, on demande à l'user "as-tu été payé ?"
via email, et il clique sur l'un des 2 liens publics.
Service checkin_token.ts : génération + hash SHA-256. 32 bytes random base64url, plain dans le mail, hash en DB (CheckinTask.token_hash unique).
Service checkin_scheduler.ts :
- scheduleCheckinForInvoice(invoice) : crée 1 CheckinTask à dueDate (now+1min si dueDate dans le passé). Idempotent par invoice — cancel les scheduled précédents avant.
- cancelCheckinForInvoice(invoiceId) : appelé par mark-paid pour stopper.
Job send_checkin_job.ts : worker queue 'checkins', skip si invoice paid/cancelled (no-op), construit l'URL avec le plain token (passé dans le payload du job, pas relu DB), appelle sendCheckinEmail.
mail_dispatcher.ts : sendCheckinEmail() — texte brut, destinataire = user (pas client !), 2 URLs (paid / pending), TTL 24h annoncé.
Controller CheckinController :
- GET /api/v1/checkin/:token/paid : status=answered + answer=paid + mark invoice paid (mêmes effets que POST /invoices/:id/mark-paid : rubis +1, ActivityEvent invoice_paid avec label "via check-in", cancelFutureRelances). Idempotent : 2e click → redirect "already_answered".
- GET /api/v1/checkin/:token/pending : status=answered + answer=still_pending. Les relances suivent leur cours.
- Validation : lookup hash, expiry (sentAt + 24h), redirects propres pour invalid / expired / already_answered.
Routes : nouveau group public `checkin` (PAS de middleware.auth) à côté du group auth, sous /api/v1.
Triggers branchés :
- InvoicesController.store et ImportBatchesController.validateDraft → scheduleCheckinForInvoice après création
- InvoicesController.markPaid → cancelCheckinForInvoice dans la tx
start/queue.ts : registerWorker('checkins', sendCheckinJob).
env : nouveau WEB_URL (URL du SPA pour redirects), default localhost:5173 en dev.
Bruno : nouveau dossier 08-Checkin avec doc complète du flow + 2 requêtes (paid / pending). var d'env `checkinToken` à remplir manuellement après avoir reçu l'email dans Mailpit.
|
||
|
|
a6b35dfe7a |
feat(api): RelanceTask + CheckinTask + worker BullMQ qui envoie les relances
Migrations :
- relance_tasks (uuid id, organization_id FK CASCADE [scope direct sans join], invoice_id FK CASCADE, plan_step_id FK RESTRICT, send_at, status ENUM scheduled/sent/cancelled/failed, sent_at, queue_job_id pour cancel via BullMQ.remove). Indexes (org,status), (invoice_id), (send_at).
- checkin_tasks (uuid id, org_id, invoice_id, send_at, token_hash unique [SHA-256 du HMAC, TTL 24h], status ENUM scheduled/sent/answered/expired, answer 'paid'|'still_pending'). Pas encore branché — flow check-in arrivera dans un commit séparé (cf. backend.md §13.3).
Schema rules : status enums + answer typés.
Models RelanceTask + CheckinTask avec belongsTo Invoice / PlanStep.
Service relance_scheduler.ts :
- scheduleRelancesForInvoice(invoice) : pour chaque step du plan, calcule sendAt = dueDate + offsetDays. Si sendAt < now (facture importée en retard), on programme à `now + 1min` plutôt que skip — l'utilisateur "rattrape" une dette de relance, l'envoi immédiat est cohérent. Crée la RelanceTask + enqueue BullMQ avec delay, retry 5x exponential, jobId = `relance:<taskId>` pour idempotency. Cancelle les tasks scheduled existantes avant de re-programmer (gestion changement de plan).
- cancelFutureRelances(invoiceId, trx) : appelé par mark-paid pour stopper la chaîne.
Service queue.ts :
- getQueue(name) singleton lazy par queue
- registerWorker(name, handler) avec concurrency 5, log failed/completed
- shutdownQueue() pour le terminating hook Adonis
start/queue.ts (preload) : registerWorker('relances', sendRelanceJob) seulement quand `app.getEnvironment() === 'web'` (pas en tests/REPL — pas de connexion Redis pendant Japa).
Job send_relance_job.ts :
- Idempotent : si task.status !== 'scheduled', no-op
- Hook critique : si invoice paid/cancelled entre-temps, task.status = cancelled
- Mise en demeure (step.requiresManualValidation) : on n'envoie PAS, on log un activity_event 'warning_drafted' (cf. CLAUDE.md → Principes : validation manuelle obligatoire)
- Sinon : sendRelanceEmail + task.status=sent + invoice.rubisEarned+1 + organizations.rubis_count+1 + activity_event 'relance_sent'. Si invoice.status='pending', passe en 'in_relance' (sortie de l'état silencieux).
Service mail_dispatcher.ts : sendRelanceEmail interpole step.subject/body via mini moteur Mustache-like (renderTemplate, services/template.ts) avec {{client.name}}/{{numero}}/{{amount}}/{{dueDate}}/{{signature}}, puis @adonisjs/mail.use(MAIL_DRIVER) → Mailpit en dev, Resend en prod. Texte brut V1.
Triggers branchés :
- InvoicesController.store : si planId, scheduleRelancesForInvoice après création
- ImportBatchesController.validateDraft : pareil
- InvoicesController.markPaid : cancelFutureRelances dans la même tx que le paiement
#jobs/* ajouté aux imports package.json. Adonisrc preload start/queue.ts.
Bruno : doc 05-Invoices/04 Create maj avec instructions pour tester l'envoi immédiat (dueDate dans le passé → relance à now+1min → email visible dans Mailpit http://localhost:8025).
|