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>
Avant : body crème + container blanc avec border-radius + bordure → effet
"card flottante" qui laissait des marges crème en haut/bas et un fond
blanc pour le contenu. Sur iPhone Mail, cette structure créait un gap
mal rendu entre la frame Mail.app et le header rubis-deep.
Maintenant :
- bodyStyle.padding 24px 0 → 0 (rubis-deep colle au haut de la zone mail)
- containerStyle background blanc → crème (toute la zone email est crème,
cohérente avec la palette)
- containerStyle borderRadius + border supprimés (edge-to-edge)
- invoiceCardStyle (checkin) + summaryCardStyle (relance) passent en
blanc pour se détacher du nouveau fond crème
- Dark mode CSS : .rubis-container override aussi mis à crème
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sur iPhone Mail.app en dark mode, iOS inverse automatiquement les
couleurs de l'email : header rubis-deep (#771328) devenait rose
pâle, fond crème devenait noir, texte sombre devenait blanc.
Fix appliqué dans le _layout.tsx (donc impacte checkin + relance) :
- Ajoute meta `color-scheme: light only` + `supported-color-schemes`
→ signal aux clients mail (iOS Mail, Gmail mobile, Yahoo)
qu'on ne souhaite PAS d'auto-dark-mode
- Ajoute style block avec :root color-scheme + overrides
Outlook.com dark mode ([data-ogsc] / [data-ogsb])
- Ajoute className sur Body / Container / header / footer pour
permettre le ciblage CSS dark-mode-resistant
Couvre : iOS Mail, Apple Mail macOS, Gmail mobile dark, Outlook.com.
Aucun impact sur les clients qui ne font pas d'inversion (Outlook
desktop, Thunderbird, etc.).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>