rubis/apps/api/app/mails/relance_email.tsx
ordinarthur ff8fe64be2
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 1m1s
Build & Deploy API / build-and-deploy (push) Successful in 2m3s
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>
2026-05-07 18:10:27 +02:00

136 lines
4.1 KiB
TypeScript

/**
* Template email de relance — envoyé AU CLIENT FINAL pour réclamer le
* paiement d'une facture impayée. Le subject + le body sont définis par
* le user dans son plan de relance (avec variables {{numero}} etc.) ; on
* injecte ce body brut dans une mise en page Rubis :
*
* - Header rubis-deep avec le NOM DE L'ORG (ce que connaît le client)
* et le numéro de facture en sous-titre
* - Body rendu (le texte que le user a rédigé, déjà interpolé)
* - Card "récap facture" en pied de body : numéro, montant, échéance
*
* Pas de boutons CTA dans la relance — le client est censé payer hors
* mail (virement, espèces, ...). Le mail rappelle juste le contexte.
*/
import * as React from 'react'
import { Section, Text } from '@react-email/components'
import { BRAND, sp } from './_brand.js'
import { EmailLayout } from './_layout.js'
export type RelanceEmailProps = {
/** Nom commercial visible côté client (l'org du user). */
brandName: string
invoice: {
numero: string
amountFormatted: string
dueDateFormatted: string
daysLate: number
}
/** Texte de la relance (déjà interpolé) que le user a posé dans son plan. */
bodyText: string
/** URL landing publique (footer cliquable "Rubis sur l'ongle"). */
landingUrl?: string
}
export function RelanceEmail({
brandName,
invoice,
bodyText,
landingUrl,
}: RelanceEmailProps) {
const isLate = invoice.daysLate > 0
return (
<EmailLayout
preview={`${invoice.numero} (${invoice.amountFormatted}) — ${
isLate ? `${invoice.daysLate}j de retard` : 'à régler'
}`}
brandName={brandName}
brandSubtitle={`Facture ${invoice.numero}`}
landingUrl={landingUrl}
>
{/* Body texte en pre-line pour conserver les sauts de ligne tels que
le user les a écrits. Le client lit le mot du patron, pas un
template impersonnel. */}
<Text style={bodyTextStyle}>{bodyText}</Text>
{/* Card récap en pied : tableau visuel court qui rappelle les chiffres. */}
<Section style={summaryCardStyle}>
<Text style={summaryRowStyle}>
<span style={summaryLabelStyle}>Facture</span>
<span style={summaryValueStyle}>{invoice.numero}</span>
</Text>
<Text style={summaryRowStyle}>
<span style={summaryLabelStyle}>Montant TTC</span>
<span
style={{
...summaryValueStyle,
fontSize: '18px',
fontWeight: 800,
fontVariantNumeric: 'tabular-nums',
}}
>
{invoice.amountFormatted}
</span>
</Text>
<Text style={summaryRowStyle}>
<span style={summaryLabelStyle}>Échéance</span>
<span
style={{
...summaryValueStyle,
color: isLate ? BRAND.rubisDeep : BRAND.ink,
}}
>
{invoice.dueDateFormatted}
{isLate ? (
<span style={{ color: BRAND.rubisDeep, fontSize: '12px', marginLeft: sp.sm }}>
({invoice.daysLate}j de retard)
</span>
) : null}
</span>
</Text>
</Section>
</EmailLayout>
)
}
// ---------------------------------------------------------------------------
// Styles inline
// ---------------------------------------------------------------------------
const bodyTextStyle: React.CSSProperties = {
color: BRAND.ink,
fontSize: '15px',
lineHeight: '1.6',
margin: `0 0 ${sp.xl} 0`,
whiteSpace: 'pre-line', // préserve les \n du body sans nécessiter <br/>
}
const summaryCardStyle: React.CSSProperties = {
backgroundColor: BRAND.cream,
border: `1px solid ${BRAND.line}`,
borderRadius: BRAND.radiusCard,
padding: `${sp.md} ${sp.lg}`,
margin: `${sp.lg} 0 0 0`,
}
const summaryRowStyle: React.CSSProperties = {
display: 'block',
margin: `${sp.sm} 0`,
fontSize: '13px',
lineHeight: '1.4',
}
const summaryLabelStyle: React.CSSProperties = {
display: 'inline-block',
width: '110px',
color: BRAND.ink3,
fontWeight: 500,
}
const summaryValueStyle: React.CSSProperties = {
color: BRAND.ink,
fontWeight: 600,
}