rubis/apps/api/commands/send_test_email.ts
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

124 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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}`)
}
}
}