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>
64 lines
1.8 KiB
TypeScript
64 lines
1.8 KiB
TypeScript
import Stripe from 'stripe'
|
|
import env from '#start/env'
|
|
|
|
/**
|
|
* Singleton client Stripe — lazy init pour ne pas crasher en dev/test
|
|
* quand la clé n'est pas définie. Toute fonction qui nécessite Stripe
|
|
* appelle `getStripe()` qui throw si la clé manque.
|
|
*/
|
|
let _stripe: Stripe | null = null
|
|
|
|
export function getStripe(): Stripe {
|
|
if (_stripe) return _stripe
|
|
const key = env.get('STRIPE_SECRET_KEY')
|
|
if (!key) {
|
|
throw new Error(
|
|
'STRIPE_SECRET_KEY manquante. Configurer la clé dans .env avant d\'utiliser le billing.'
|
|
)
|
|
}
|
|
_stripe = new Stripe(key, {
|
|
apiVersion: '2026-04-22.dahlia',
|
|
typescript: true,
|
|
appInfo: {
|
|
name: 'Rubis sur l\'ongle',
|
|
version: '1.0.0',
|
|
},
|
|
})
|
|
return _stripe
|
|
}
|
|
|
|
/**
|
|
* Lookup keys utilisés pour identifier les Prices Stripe sans hardcoder
|
|
* d'IDs en env. Les Prices sont créées par `node ace stripe:setup` avec
|
|
* ces lookup_keys, et le code les retrouve via `prices.list({lookup_keys})`.
|
|
*/
|
|
export const STRIPE_LOOKUP_KEYS = {
|
|
pro_monthly: 'rubis_pro_monthly',
|
|
pro_yearly: 'rubis_pro_yearly',
|
|
business_monthly: 'rubis_business_monthly',
|
|
business_yearly: 'rubis_business_yearly',
|
|
} as const
|
|
|
|
export type StripeLookupKey = (typeof STRIPE_LOOKUP_KEYS)[keyof typeof STRIPE_LOOKUP_KEYS]
|
|
|
|
/**
|
|
* Récupère un Price Stripe via son lookup_key. Throw si introuvable
|
|
* (signal que `stripe:setup` n'a pas été lancé ou que les lookup_keys
|
|
* ont changé).
|
|
*/
|
|
export async function getPriceByLookup(key: StripeLookupKey): Promise<Stripe.Price> {
|
|
const stripe = getStripe()
|
|
const result = await stripe.prices.list({
|
|
lookup_keys: [key],
|
|
limit: 1,
|
|
expand: ['data.product'],
|
|
})
|
|
const price = result.data[0]
|
|
if (!price) {
|
|
throw new Error(
|
|
`Stripe Price introuvable pour lookup_key="${key}". Lancer \`node ace stripe:setup\` ?`
|
|
)
|
|
}
|
|
return price
|
|
}
|