All checks were successful
CI / test (pull_request) Successful in 2m40s
Marketplace encaisse centralisé sur System D → besoin de tracer les
virements mensuels aux prestataires tiers. Migration appliquée prod.
Schema :
- Modèle RentalPayoutMark { id, providerId, periodMonth, amount,
reference, paidAt, paidByEmail }. Unique (providerId, periodMonth)
→ 1 mark = 1 mois = 1 virement par provider.
Lib src/lib/payouts.ts :
- monthKey(d) → 1er du mois minuit UTC (clé de période).
- listProviderPayouts({monthsBack=6}) → grid provider × mois avec
bookingsCount + grossAmount (itemsTotal) + commission + netAmount
(gross-commission) + statut paid via RentalPayoutMark. Exclut
System D (commission 0%, géré par l'asso). Statut « payé » lu
depuis les marks. Tri : mois desc puis providerName.
- createPayoutMark (idempotent via findUnique avant insert) +
deletePayoutMark.
Politique : net dû = itemsTotal - commissionAmount (depositTotal
hors flux, collecté par le provider auprès du client). Politique
documentée dans le commentaire en tête de payouts.ts.
/admin/payouts/page.tsx :
- 3 KPIs (À payer / Déjà payé / Mois affichés).
- Une section par mois (6 derniers), tableau provider × CA brut +
commission + net dû + statut.
- MarkPaidForm : bouton « Marquer payé » → form inline (amount
pré-rempli avec net dû, reference optionnelle) → action
markPayoutPaidAction. Statut payé montre amount + ref + bouton
« Annuler marquage ».
Server actions :
- markPayoutPaidAction (admin only, idempotent, audit
admin.payouts/payout.mark + payout.already_marked) → envoie
sendPayoutSent au contactEmail du provider (best-effort).
- unmarkPayoutPaidAction → delete + audit payout.unmark.
Email sendPayoutSent : notification au provider quand un virement est
marqué payé. Inclut amount + reference + lien dashboard.
Sidebar admin gagne entrée « Reversements » sous Activité.
Tests vitest tests/lib/payouts.test.ts (4 cas) : monthKey
normalisation UTC + idempotence + janvier sans bug, formatMonth fr-FR.
Total : 74/74 ✓.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
461 lines
18 KiB
TypeScript
461 lines
18 KiB
TypeScript
/**
|
||
* Service email — Resend si `RESEND_API_KEY` est configuré, sinon log console.
|
||
*
|
||
* Le code consommateur ne doit jamais bloquer ni jeter d'erreur sur un échec
|
||
* d'envoi (best-effort, le booking est l'action principale).
|
||
*/
|
||
|
||
import "server-only";
|
||
|
||
let resendClient: import("resend").Resend | null | undefined;
|
||
|
||
async function getResend(): Promise<import("resend").Resend | null> {
|
||
if (resendClient !== undefined) return resendClient;
|
||
const key = process.env.RESEND_API_KEY?.trim();
|
||
if (!key) {
|
||
resendClient = null;
|
||
return null;
|
||
}
|
||
try {
|
||
const { Resend } = await import("resend");
|
||
resendClient = new Resend(key);
|
||
return resendClient;
|
||
} catch (e) {
|
||
console.error("[email] resend init failed:", e instanceof Error ? e.message : e);
|
||
resendClient = null;
|
||
return null;
|
||
}
|
||
}
|
||
|
||
export type EmailOpts = {
|
||
to: string | string[];
|
||
subject: string;
|
||
html: string;
|
||
text?: string;
|
||
replyTo?: string;
|
||
};
|
||
|
||
const DEFAULT_FROM = process.env.RESEND_FROM ?? "Karbé <no-reply@karbe.cosmolan.fr>";
|
||
|
||
export async function sendEmail(opts: EmailOpts): Promise<{ ok: boolean; id?: string; reason?: string }> {
|
||
const client = await getResend();
|
||
if (!client) {
|
||
console.log(
|
||
"[email] dry-run (no RESEND_API_KEY):",
|
||
JSON.stringify({ to: opts.to, subject: opts.subject }),
|
||
);
|
||
return { ok: true, reason: "dry-run" };
|
||
}
|
||
try {
|
||
const { data, error } = await client.emails.send({
|
||
from: DEFAULT_FROM,
|
||
to: Array.isArray(opts.to) ? opts.to : [opts.to],
|
||
subject: opts.subject,
|
||
html: opts.html,
|
||
text: opts.text,
|
||
replyTo: opts.replyTo,
|
||
});
|
||
if (error) {
|
||
console.error("[email] resend error:", error);
|
||
return { ok: false, reason: error.message };
|
||
}
|
||
return { ok: true, id: data?.id };
|
||
} catch (e) {
|
||
const msg = e instanceof Error ? e.message : String(e);
|
||
console.error("[email] send failed:", msg);
|
||
return { ok: false, reason: msg };
|
||
}
|
||
}
|
||
|
||
// ---------- Templates ----------
|
||
|
||
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://karbe.cosmolan.fr";
|
||
|
||
const baseStyle = `
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||
color: #18181b;
|
||
max-width: 580px;
|
||
margin: 0 auto;
|
||
padding: 24px;
|
||
line-height: 1.5;
|
||
`;
|
||
|
||
function wrap(title: string, content: string): string {
|
||
return `<!doctype html><html><body style="background:#fafafa;margin:0;padding:24px 12px;">
|
||
<div style="${baseStyle}background:white;border-radius:12px;box-shadow:0 1px 3px rgba(0,0,0,0.05);">
|
||
<h1 style="margin:0 0 16px;font-size:22px;font-weight:600;color:#18181b;">${title}</h1>
|
||
${content}
|
||
<hr style="margin:24px 0;border:0;border-top:1px solid #e4e4e7;" />
|
||
<p style="font-size:11px;color:#71717a;margin:0;">
|
||
Karbé · <a href="${SITE_URL}" style="color:#71717a;">${SITE_URL}</a><br/>
|
||
Cet email a été envoyé suite à une action sur votre compte. Si ce n'est pas vous, ignorez-le.
|
||
</p>
|
||
</div>
|
||
</body></html>`;
|
||
}
|
||
|
||
export async function sendSignupWelcome(to: string, firstName: string): Promise<void> {
|
||
await sendEmail({
|
||
to,
|
||
subject: "Bienvenue sur Karbé",
|
||
html: wrap(
|
||
`Bienvenue ${firstName} !`,
|
||
`<p>Votre compte Karbé est créé. Vous pouvez désormais réserver un séjour ou, si vous êtes hôte, publier votre carbet.</p>
|
||
<p><a href="${SITE_URL}/carbets" style="display:inline-block;background:#18181b;color:white;padding:10px 20px;border-radius:6px;text-decoration:none;font-weight:600;">Découvrir les carbets</a></p>`,
|
||
),
|
||
text: `Bienvenue ${firstName} ! Votre compte Karbé est créé. ${SITE_URL}/carbets`,
|
||
});
|
||
}
|
||
|
||
export async function sendBookingRequestToTenant(
|
||
to: string,
|
||
firstName: string,
|
||
bookingId: string,
|
||
carbetTitle: string,
|
||
startDate: Date,
|
||
endDate: Date,
|
||
amount: string,
|
||
currency: string,
|
||
): Promise<void> {
|
||
const fmt = (d: Date) =>
|
||
new Intl.DateTimeFormat("fr-FR", { day: "2-digit", month: "long", year: "numeric" }).format(d);
|
||
await sendEmail({
|
||
to,
|
||
subject: `Demande de réservation enregistrée — ${carbetTitle}`,
|
||
html: wrap(
|
||
"Demande de réservation envoyée",
|
||
`<p>Bonjour ${firstName},</p>
|
||
<p>Votre demande de réservation pour <strong>${carbetTitle}</strong> a bien été enregistrée :</p>
|
||
<ul>
|
||
<li>Arrivée : ${fmt(startDate)}</li>
|
||
<li>Départ : ${fmt(endDate)}</li>
|
||
<li>Montant : ${Number(amount).toFixed(2)} ${currency}</li>
|
||
</ul>
|
||
<p>Vous recevrez un nouvel email dès que l'hôte ou l'équipe Karbé confirmera votre séjour.</p>
|
||
<p><a href="${SITE_URL}/reservations/${bookingId}" style="display:inline-block;background:#18181b;color:white;padding:10px 20px;border-radius:6px;text-decoration:none;font-weight:600;">Voir ma réservation</a></p>`,
|
||
),
|
||
});
|
||
}
|
||
|
||
export async function sendBookingRequestToOwner(
|
||
to: string,
|
||
ownerFirstName: string,
|
||
bookingId: string,
|
||
carbetTitle: string,
|
||
tenantName: string,
|
||
startDate: Date,
|
||
endDate: Date,
|
||
): Promise<void> {
|
||
const fmt = (d: Date) =>
|
||
new Intl.DateTimeFormat("fr-FR", { day: "2-digit", month: "long", year: "numeric" }).format(d);
|
||
await sendEmail({
|
||
to,
|
||
subject: `Nouvelle demande de réservation — ${carbetTitle}`,
|
||
html: wrap(
|
||
"Nouvelle demande à confirmer",
|
||
`<p>Bonjour ${ownerFirstName},</p>
|
||
<p><strong>${tenantName}</strong> souhaite réserver <strong>${carbetTitle}</strong> :</p>
|
||
<ul>
|
||
<li>Du ${fmt(startDate)} au ${fmt(endDate)}</li>
|
||
</ul>
|
||
<p>Connectez-vous à votre espace hôte pour confirmer ou refuser.</p>
|
||
<p><a href="${SITE_URL}/espace-hote" style="display:inline-block;background:#18181b;color:white;padding:10px 20px;border-radius:6px;text-decoration:none;font-weight:600;">Mon espace hôte</a></p>`,
|
||
),
|
||
});
|
||
}
|
||
|
||
export async function sendBookingConfirmed(
|
||
to: string,
|
||
firstName: string,
|
||
bookingId: string,
|
||
carbetTitle: string,
|
||
startDate: Date,
|
||
endDate: Date,
|
||
): Promise<void> {
|
||
const fmt = (d: Date) =>
|
||
new Intl.DateTimeFormat("fr-FR", { day: "2-digit", month: "long", year: "numeric" }).format(d);
|
||
await sendEmail({
|
||
to,
|
||
subject: `Réservation confirmée — ${carbetTitle}`,
|
||
html: wrap(
|
||
"Votre séjour est confirmé",
|
||
`<p>Bonjour ${firstName},</p>
|
||
<p>Votre réservation pour <strong>${carbetTitle}</strong> du ${fmt(startDate)} au ${fmt(endDate)} est confirmée.</p>
|
||
<p><a href="${SITE_URL}/reservations/${bookingId}" style="display:inline-block;background:#16a34a;color:white;padding:10px 20px;border-radius:6px;text-decoration:none;font-weight:600;">Voir ma réservation</a></p>`,
|
||
),
|
||
});
|
||
}
|
||
|
||
export async function sendNewCeRequest(
|
||
orgName: string,
|
||
managerEmail: string,
|
||
): Promise<void> {
|
||
const adminEmail = process.env.ADMIN_NOTIFICATION_EMAIL ?? "contact@karbe.cosmolan.fr";
|
||
await sendEmail({
|
||
to: adminEmail,
|
||
subject: `Nouvelle demande CE — ${orgName}`,
|
||
html: wrap(
|
||
"Demande de Comité d'Entreprise à valider",
|
||
`<p>Une organisation vient de s'inscrire en tant que Comité d'Entreprise.</p>
|
||
<ul>
|
||
<li>Nom : <strong>${orgName}</strong></li>
|
||
<li>Email du manager : ${managerEmail}</li>
|
||
</ul>
|
||
<p><a href="${SITE_URL}/admin/organizations?status=pending" style="display:inline-block;background:#18181b;color:white;padding:10px 20px;border-radius:6px;text-decoration:none;font-weight:600;">Valider sur l'admin Karbé</a></p>
|
||
<p style="font-size:12px;color:#71717a;">Le CE_MANAGER peut accéder à son dashboard mais ne peut rien publier tant que <code>Organization.approved=false</code>.</p>`,
|
||
),
|
||
});
|
||
}
|
||
|
||
export async function sendCeInviteEmail(
|
||
to: string,
|
||
orgName: string,
|
||
inviteUrl: string,
|
||
inviterName?: string | null,
|
||
): Promise<void> {
|
||
const intro = inviterName
|
||
? `<strong>${inviterName}</strong> vous invite à rejoindre`
|
||
: "Vous êtes invité à rejoindre";
|
||
await sendEmail({
|
||
to,
|
||
subject: `Invitation à rejoindre « ${orgName} » sur Karbé`,
|
||
html: wrap(
|
||
`Invitation Karbé — ${orgName}`,
|
||
`<p>${intro} le Comité d'Entreprise <strong>${orgName}</strong> sur Karbé.</p>
|
||
<p>Cliquez sur le bouton ci-dessous pour créer votre compte CE_MEMBER et accéder aux carbets et matériel de votre CE :</p>
|
||
<p><a href="${inviteUrl}" style="display:inline-block;background:#16a34a;color:white;padding:10px 20px;border-radius:6px;text-decoration:none;font-weight:600;">Rejoindre ${orgName}</a></p>
|
||
<p style="font-size:12px;color:#71717a;">Lien valable 14 jours. Si vous n'êtes pas le destinataire attendu, ignorez cet email.</p>
|
||
<p style="font-size:11px;color:#a1a1aa;word-break:break-all;">Lien direct : ${inviteUrl}</p>`,
|
||
),
|
||
text: `${intro.replace(/<[^>]+>/g, "")} le CE ${orgName} sur Karbé : ${inviteUrl}`,
|
||
});
|
||
}
|
||
|
||
export async function sendCeApproved(
|
||
to: string,
|
||
firstName: string,
|
||
orgName: string,
|
||
): Promise<void> {
|
||
await sendEmail({
|
||
to,
|
||
subject: `Votre CE « ${orgName} » est validé sur Karbé`,
|
||
html: wrap(
|
||
"Organisation validée",
|
||
`<p>Bonjour ${firstName},</p>
|
||
<p>Votre Comité d'Entreprise <strong>${orgName}</strong> vient d'être validé. Vous pouvez désormais publier vos carbets et activer la location de matériel pour vos membres et le public touriste.</p>
|
||
<p><a href="${SITE_URL}/espace-ce" style="display:inline-block;background:#16a34a;color:white;padding:10px 20px;border-radius:6px;text-decoration:none;font-weight:600;">Accéder à mon espace CE</a></p>`,
|
||
),
|
||
});
|
||
}
|
||
|
||
export async function sendNewRentalProviderRequest(
|
||
providerName: string,
|
||
userEmail: string,
|
||
): Promise<void> {
|
||
const adminEmail = process.env.ADMIN_NOTIFICATION_EMAIL ?? "contact@karbe.cosmolan.fr";
|
||
await sendEmail({
|
||
to: adminEmail,
|
||
subject: `Nouvelle demande prestataire matériel — ${providerName}`,
|
||
html: wrap(
|
||
"Demande de prestataire à valider",
|
||
`<p>Une demande d'inscription en tant que prestataire de location matériel vient d'arriver.</p>
|
||
<ul>
|
||
<li>Nom : <strong>${providerName}</strong></li>
|
||
<li>Email contact : ${userEmail}</li>
|
||
</ul>
|
||
<p><a href="${SITE_URL}/admin/rental-providers" style="display:inline-block;background:#18181b;color:white;padding:10px 20px;border-radius:6px;text-decoration:none;font-weight:600;">Valider sur l'admin Karbé</a></p>
|
||
<p style="font-size:12px;color:#71717a;">Le prestataire reste en attente jusqu'à validation. Ses items ne sont pas publiés tant que <code>approved=false</code>.</p>`,
|
||
),
|
||
});
|
||
}
|
||
|
||
export async function sendPasswordReset(
|
||
to: string,
|
||
resetUrl: string,
|
||
): Promise<void> {
|
||
await sendEmail({
|
||
to,
|
||
subject: "Réinitialisation de votre mot de passe Karbé",
|
||
html: wrap(
|
||
"Réinitialiser votre mot de passe",
|
||
`<p>Vous avez demandé à réinitialiser votre mot de passe Karbé. Cliquez sur le lien ci-dessous pour choisir un nouveau mot de passe (valable 1 heure) :</p>
|
||
<p><a href="${resetUrl}" style="display:inline-block;background:#18181b;color:white;padding:10px 20px;border-radius:6px;text-decoration:none;font-weight:600;">Réinitialiser mon mot de passe</a></p>
|
||
<p style="font-size:12px;color:#71717a;">Si vous n'avez pas fait cette demande, ignorez simplement cet email — votre mot de passe ne change pas.</p>`,
|
||
),
|
||
text: `Réinitialiser votre mot de passe Karbé : ${resetUrl} (valable 1h).`,
|
||
});
|
||
}
|
||
|
||
type RentalLineSummary = { qty: number; itemName: string };
|
||
|
||
function renderLines(lines: RentalLineSummary[]): string {
|
||
return lines.map((l) => `<li>${l.qty}× ${l.itemName}</li>`).join("");
|
||
}
|
||
|
||
export async function sendRentalRequestedTenant(
|
||
to: string,
|
||
firstName: string,
|
||
rentalBookingId: string,
|
||
providerName: string,
|
||
startDate: Date,
|
||
endDate: Date,
|
||
amount: string,
|
||
currency: string,
|
||
lines: RentalLineSummary[],
|
||
): Promise<void> {
|
||
const fmt = (d: Date) =>
|
||
new Intl.DateTimeFormat("fr-FR", { day: "2-digit", month: "long", year: "numeric" }).format(d);
|
||
await sendEmail({
|
||
to,
|
||
subject: `Demande de location matériel — ${providerName}`,
|
||
html: wrap(
|
||
"Votre demande de location est enregistrée",
|
||
`<p>Bonjour ${firstName},</p>
|
||
<p>Votre demande de location auprès de <strong>${providerName}</strong> est bien enregistrée :</p>
|
||
<ul>
|
||
<li>Du ${fmt(startDate)} au ${fmt(endDate)}</li>
|
||
<li>Montant : ${Number(amount).toFixed(2)} ${currency}</li>
|
||
</ul>
|
||
<p><strong>Matériel demandé :</strong></p>
|
||
<ul>${renderLines(lines)}</ul>
|
||
<p>Vous recevrez un nouvel email dès que le paiement sera validé et le prestataire confirmera la préparation du matériel.</p>
|
||
<p><a href="${SITE_URL}/mes-locations" style="display:inline-block;background:#18181b;color:white;padding:10px 20px;border-radius:6px;text-decoration:none;font-weight:600;">Mes locations</a></p>
|
||
<p style="font-size:11px;color:#71717a;">Référence : ${rentalBookingId}</p>`,
|
||
),
|
||
});
|
||
}
|
||
|
||
export async function sendRentalRequestedProvider(
|
||
to: string,
|
||
providerName: string,
|
||
rentalBookingId: string,
|
||
tenantName: string,
|
||
startDate: Date,
|
||
endDate: Date,
|
||
lines: RentalLineSummary[],
|
||
): Promise<void> {
|
||
const fmt = (d: Date) =>
|
||
new Intl.DateTimeFormat("fr-FR", { day: "2-digit", month: "long", year: "numeric" }).format(d);
|
||
await sendEmail({
|
||
to,
|
||
subject: `Nouvelle demande de location — ${tenantName}`,
|
||
html: wrap(
|
||
"Nouvelle demande à préparer",
|
||
`<p>Bonjour ${providerName},</p>
|
||
<p><strong>${tenantName}</strong> vient de réserver du matériel :</p>
|
||
<ul>
|
||
<li>Du ${fmt(startDate)} au ${fmt(endDate)}</li>
|
||
</ul>
|
||
<p><strong>Matériel :</strong></p>
|
||
<ul>${renderLines(lines)}</ul>
|
||
<p>Préparez le matériel pour la remise. Vous recevrez une confirmation paiement une fois le règlement validé.</p>
|
||
<p><a href="${SITE_URL}/espace-prestataire/reservations" style="display:inline-block;background:#18181b;color:white;padding:10px 20px;border-radius:6px;text-decoration:none;font-weight:600;">Mes réservations</a></p>
|
||
<p style="font-size:11px;color:#71717a;">Référence : ${rentalBookingId}</p>`,
|
||
),
|
||
});
|
||
}
|
||
|
||
export async function sendRentalCancelled(
|
||
to: string,
|
||
firstName: string,
|
||
rentalBookingId: string,
|
||
providerName: string,
|
||
refundAmount: string,
|
||
currency: string,
|
||
policyLabel: string,
|
||
cancelledBy: "tenant" | "provider" | "admin",
|
||
): Promise<void> {
|
||
const actor =
|
||
cancelledBy === "tenant"
|
||
? "Vous avez annulé"
|
||
: cancelledBy === "provider"
|
||
? `${providerName} a annulé`
|
||
: "L'équipe Karbé a annulé";
|
||
await sendEmail({
|
||
to,
|
||
subject: `Location annulée — ${providerName}`,
|
||
html: wrap(
|
||
"Location annulée",
|
||
`<p>Bonjour ${firstName},</p>
|
||
<p>${actor} votre location auprès de <strong>${providerName}</strong>.</p>
|
||
<p><strong>Politique appliquée :</strong> ${policyLabel}</p>
|
||
<p><strong>Remboursement :</strong> ${Number(refundAmount).toFixed(2)} ${currency}</p>
|
||
<p>Si un paiement avait été reçu, le remboursement est traité par Stripe sous 3-5 jours ouvrés.</p>
|
||
<p><a href="${SITE_URL}/mes-locations" style="display:inline-block;background:#18181b;color:white;padding:10px 20px;border-radius:6px;text-decoration:none;font-weight:600;">Mes locations</a></p>
|
||
<p style="font-size:11px;color:#71717a;">Référence : ${rentalBookingId}</p>`,
|
||
),
|
||
});
|
||
}
|
||
|
||
export async function sendRentalConfirmed(
|
||
to: string,
|
||
firstName: string,
|
||
rentalBookingId: string,
|
||
providerName: string,
|
||
startDate: Date,
|
||
endDate: Date,
|
||
): Promise<void> {
|
||
const fmt = (d: Date) =>
|
||
new Intl.DateTimeFormat("fr-FR", { day: "2-digit", month: "long", year: "numeric" }).format(d);
|
||
await sendEmail({
|
||
to,
|
||
subject: `Location confirmée — ${providerName}`,
|
||
html: wrap(
|
||
"Votre location est confirmée",
|
||
`<p>Bonjour ${firstName},</p>
|
||
<p>Le paiement de votre location auprès de <strong>${providerName}</strong> du ${fmt(startDate)} au ${fmt(endDate)} est validé.</p>
|
||
<p>Le prestataire vous contactera pour organiser la remise du matériel sur place.</p>
|
||
<p><a href="${SITE_URL}/mes-locations" style="display:inline-block;background:#16a34a;color:white;padding:10px 20px;border-radius:6px;text-decoration:none;font-weight:600;">Voir ma location</a></p>
|
||
<p style="font-size:11px;color:#71717a;">Référence : ${rentalBookingId}</p>`,
|
||
),
|
||
});
|
||
}
|
||
|
||
export async function sendPayoutSent(
|
||
to: string,
|
||
providerName: string,
|
||
periodMonth: Date,
|
||
amount: string,
|
||
reference: string | null,
|
||
): Promise<void> {
|
||
const monthLabel = periodMonth.toLocaleDateString("fr-FR", {
|
||
timeZone: "UTC",
|
||
year: "numeric",
|
||
month: "long",
|
||
});
|
||
await sendEmail({
|
||
to,
|
||
subject: `Reversement Karbé — ${monthLabel}`,
|
||
html: wrap(
|
||
`Reversement ${monthLabel}`,
|
||
`<p>Bonjour ${providerName},</p>
|
||
<p>Le reversement de vos locations matériel pour <strong>${monthLabel}</strong> a été effectué :</p>
|
||
<ul>
|
||
<li>Montant : <strong>${Number(amount).toFixed(2)} EUR</strong></li>
|
||
${reference ? `<li>Référence virement : <code>${reference}</code></li>` : ""}
|
||
</ul>
|
||
<p>Vérifiez votre compte bancaire dans les 1 à 3 jours ouvrés. En cas de question, répondez à cet email.</p>
|
||
<p><a href="${SITE_URL}/espace-prestataire/reservations" style="display:inline-block;background:#18181b;color:white;padding:10px 20px;border-radius:6px;text-decoration:none;font-weight:600;">Voir mes réservations</a></p>`,
|
||
),
|
||
});
|
||
}
|
||
|
||
export async function sendBookingRefunded(
|
||
to: string,
|
||
firstName: string,
|
||
bookingId: string,
|
||
carbetTitle: string,
|
||
amount: string,
|
||
currency: string,
|
||
): Promise<void> {
|
||
await sendEmail({
|
||
to,
|
||
subject: `Remboursement traité — ${carbetTitle}`,
|
||
html: wrap(
|
||
"Remboursement en cours",
|
||
`<p>Bonjour ${firstName},</p>
|
||
<p>Votre réservation pour <strong>${carbetTitle}</strong> a été annulée et le remboursement de <strong>${Number(amount).toFixed(2)} ${currency}</strong> est en cours de traitement par Stripe (3 à 5 jours ouvrés).</p>
|
||
<p><a href="${SITE_URL}/reservations/${bookingId}" style="color:#18181b;">Détails de la réservation</a></p>`,
|
||
),
|
||
});
|
||
}
|