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>
124 lines
4.4 KiB
TypeScript
124 lines
4.4 KiB
TypeScript
import { BaseCommand, args, flags } from '@adonisjs/core/ace'
|
||
import type { CommandOptions } from '@adonisjs/core/types/ace'
|
||
import mail from '@adonisjs/mail/services/main'
|
||
import { render } from '@react-email/components'
|
||
import { DateTime } from 'luxon'
|
||
import env from '#start/env'
|
||
|
||
import { CheckinEmail } from '#mails/checkin_email'
|
||
import { RelanceEmail } from '#mails/relance_email'
|
||
|
||
/**
|
||
* Envoie un email de test via le mailer courant (typiquement Resend)
|
||
* pour valider la conf SPF/DKIM/clé API ET le rendu visuel des templates
|
||
* HTML React Email — sans passer par toute la chaîne facture → BullMQ.
|
||
*
|
||
* Usage :
|
||
* node ace send:test-email arthur@example.com # checkin
|
||
* node ace send:test-email arthur@example.com --template=checkin
|
||
* node ace send:test-email arthur@example.com --template=relance
|
||
* node ace send:test-email arthur@example.com --template=plain # ancien
|
||
* node ace send:test-email arthur@example.com --reply-to=patron@tpe.fr
|
||
*
|
||
* Default `checkin` car c'est le template avec les boutons CTA — le plus
|
||
* intéressant à valider visuellement au premier coup d'œil.
|
||
*/
|
||
export default class SendTestEmail extends BaseCommand {
|
||
static commandName = 'send:test-email'
|
||
static description = 'Envoie un email de test (HTML React Email) pour valider rendu + driver'
|
||
|
||
static options: CommandOptions = {
|
||
startApp: true,
|
||
}
|
||
|
||
@args.string({ description: 'Adresse destinataire' })
|
||
declare to: string
|
||
|
||
@flags.string({
|
||
description: 'Template à envoyer : checkin (default) | relance | plain',
|
||
default: 'checkin',
|
||
})
|
||
declare template: string
|
||
|
||
@flags.string({ description: 'Adresse de reply-to (optionnelle)' })
|
||
declare replyTo?: string
|
||
|
||
async run() {
|
||
const driver = env.get('MAIL_DRIVER', 'smtp')
|
||
const fromAddress = env.get('MAIL_FROM_ADDRESS', 'relances@rubis-sur-l-ongle.fr')
|
||
const fromName = env.get('MAIL_FROM_NAME', "Rubis sur l'ongle")
|
||
const landingUrl = env.get('LANDING_URL', 'https://rubis.arthurbarre.fr')
|
||
|
||
this.logger.info(`Driver: ${driver}`)
|
||
this.logger.info(`From: ${fromName} <${fromAddress}>`)
|
||
this.logger.info(`To: ${this.to}`)
|
||
this.logger.info(`Template: ${this.template}`)
|
||
if (this.replyTo) this.logger.info(`ReplyTo: ${this.replyTo}`)
|
||
|
||
// Données factices pour le rendu — évite d'avoir à créer une vraie
|
||
// facture en DB juste pour tester l'email.
|
||
const fakeInvoice = {
|
||
numero: 'F2026-TEST',
|
||
amountFormatted: '1 234,56 €',
|
||
dueDateFormatted: DateTime.utc().toFormat('dd/LL/yyyy'),
|
||
}
|
||
|
||
let subject: string
|
||
let text: string
|
||
let html: string | undefined
|
||
|
||
if (this.template === 'plain') {
|
||
subject = "[Rubis] Test d'envoi (plain text)"
|
||
text =
|
||
`Bonjour,\n\n` +
|
||
`Ceci est un email de test envoyé depuis Rubis sur l'ongle.\n` +
|
||
`Driver : ${driver}\n` +
|
||
`Date : ${new Date().toISOString()}\n\n` +
|
||
`— L'équipe Rubis`
|
||
html = undefined
|
||
} else if (this.template === 'relance') {
|
||
subject = `Facture ${fakeInvoice.numero} échue (test)`
|
||
text =
|
||
`Bonjour,\n\nPetit rappel pour la facture ${fakeInvoice.numero}.\n\nCordialement,\nArthur Barré`
|
||
html = await render(
|
||
RelanceEmail({
|
||
brandName: 'Arthur Barré (test)',
|
||
invoice: { ...fakeInvoice, daysLate: 3 },
|
||
bodyText: text,
|
||
landingUrl,
|
||
})
|
||
)
|
||
} else {
|
||
// checkin par défaut
|
||
subject = `[Rubis] Test — Facture ${fakeInvoice.numero}, payée ?`
|
||
text =
|
||
`Test d'envoi du template check-in.\n\n` +
|
||
`Lien "payée" : https://example.com/paid\n` +
|
||
`Lien "pending" : https://example.com/pending\n\n` +
|
||
`— L'équipe Rubis`
|
||
html = await render(
|
||
CheckinEmail({
|
||
invoice: fakeInvoice,
|
||
client: { name: 'Boulangerie Test' },
|
||
user: { fullName: 'Arthur Barré' },
|
||
paidUrl: 'https://example.com/paid',
|
||
pendingUrl: 'https://example.com/pending',
|
||
landingUrl,
|
||
})
|
||
)
|
||
}
|
||
|
||
const mailer = mail.use(driver)
|
||
const response = await mailer.send((m) => {
|
||
m.from(fromAddress, fromName).to(this.to).subject(subject).text(text)
|
||
if (html) m.html(html)
|
||
if (this.replyTo) m.replyTo(this.replyTo)
|
||
})
|
||
|
||
this.logger.success('Email envoyé')
|
||
if (response?.messageId) {
|
||
this.logger.info(`messageId: ${response.messageId}`)
|
||
}
|
||
}
|
||
}
|