Skip to content

Commit a9294fb

Browse files
committed
refactor(mail): derive text body from html and move CSE receipt to upload
- Templates now return only { subject, html }. sendMail uses html-to-text to produce the plain-text alternative at send time, removing the duplication between the two representations. - CSE opinion receipt is now triggered by a successful POST on /api/upload (flowType=cse_opinion) instead of cseOpinion.saveOpinions, so the acknowledgement matches the moment the document is actually deposited.
1 parent c688e70 commit a9294fb

14 files changed

Lines changed: 138 additions & 73 deletions

maildev-all-3-emails.png

56.5 KB
Loading

maildev-email.png

73.9 KB
Loading

maildev-seconde-declaration.png

91.4 KB
Loading

packages/app/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"@trpc/server": "^11.16.0",
4646
"drizzle-orm": "^0.45.2",
4747
"exceljs": "^4.4.0",
48+
"html-to-text": "^9.0.5",
4849
"next": "^16.2.3",
4950
"next-auth": "4.24.13",
5051
"nodemailer": "^7.0.13",
@@ -66,6 +67,7 @@
6667
"@testing-library/jest-dom": "^6.9.1",
6768
"@testing-library/react": "^16.3.2",
6869
"@testing-library/user-event": "^14.6.1",
70+
"@types/html-to-text": "^9.0.4",
6971
"@types/node": "^25.6.0",
7072
"@types/nodemailer": "^8.0.0",
7173
"@types/react": "^19.2.14",

packages/app/src/app/api/upload/route.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,19 @@ export async function POST(request: Request): Promise<Response> {
207207
userAgent: requestContext.userAgent,
208208
durationMs: Date.now() - startedAt,
209209
});
210+
if (flowType === "cse_opinion" && userEmail) {
211+
void (async () => {
212+
const { sendReceipt } = await import("~/modules/mail/server");
213+
await sendReceipt({
214+
kind: "cseOpinion",
215+
to: userEmail,
216+
siren,
217+
year,
218+
userId,
219+
isResend: false,
220+
});
221+
})();
222+
}
210223
return Response.json({
211224
fileId: result.fileId,
212225
fileName: result.fileName,

packages/app/src/modules/mail/__tests__/sendMail.test.ts

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,32 +34,29 @@ describe("sendMail", () => {
3434
const result = await sendMail({
3535
to: "a@b.fr",
3636
subject: "s",
37-
text: "t",
3837
html: "<p>t</p>",
3938
});
4039
expect(result).toEqual({ status: "disabled" });
4140
expect(sendMailMock).not.toHaveBeenCalled();
4241
});
4342

44-
it("sends the email when enabled", async () => {
43+
it("sends the email with text derived from html", async () => {
4544
mockEnv(true);
4645
sendMailMock.mockResolvedValue({ messageId: "msg-1" });
4746
const { sendMail } = await import("../sendMail");
4847
const result = await sendMail({
4948
to: "a@b.fr",
5049
subject: "s",
51-
text: "t",
52-
html: "<p>t</p>",
50+
html: "<p>Bonjour <strong>Egapro</strong></p>",
5351
});
5452
expect(result).toEqual({ status: "sent", messageId: "msg-1" });
55-
expect(sendMailMock).toHaveBeenCalledWith({
56-
from: "no-reply@test",
57-
to: "a@b.fr",
58-
subject: "s",
59-
text: "t",
60-
html: "<p>t</p>",
61-
attachments: undefined,
62-
});
53+
const payload = sendMailMock.mock.calls[0]?.[0];
54+
expect(payload.from).toBe("no-reply@test");
55+
expect(payload.to).toBe("a@b.fr");
56+
expect(payload.html).toBe("<p>Bonjour <strong>Egapro</strong></p>");
57+
expect(payload.text).toContain("Bonjour");
58+
expect(payload.text).toContain("Egapro");
59+
expect(payload.text).not.toContain("<");
6360
});
6461

6562
it("returns error status when transporter throws", async () => {
@@ -69,7 +66,6 @@ describe("sendMail", () => {
6966
const result = await sendMail({
7067
to: "a@b.fr",
7168
subject: "s",
72-
text: "t",
7369
html: "<p>t</p>",
7470
});
7571
expect(result).toEqual({ status: "error", error: "smtp down" });
@@ -82,7 +78,6 @@ describe("sendMail", () => {
8278
await sendMail({
8379
to: "a@b.fr",
8480
subject: "s",
85-
text: "t",
8681
html: "<p>t</p>",
8782
attachments: [
8883
{

packages/app/src/modules/mail/__tests__/templates.test.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ describe("mail templates", () => {
99
const t = buildDeclarationReceipt({ siren: "552100554", year: 2024 });
1010
expect(t.subject).toContain("2024");
1111
expect(t.html).toContain("552 100 554");
12-
expect(t.text).toContain("552 100 554");
13-
expect(t.text).toContain("2024");
1412
});
1513

1614
it("builds a second declaration receipt", () => {

packages/app/src/modules/mail/sendMail.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import "server-only";
2+
import { convert as htmlToText } from "html-to-text";
23
import { env } from "~/env.js";
34
import { getTransporter } from "./transporter";
45
import type { MailAttachment } from "./types";
56

67
export type SendMailInput = {
78
to: string;
89
subject: string;
9-
text: string;
1010
html: string;
1111
attachments?: MailAttachment[];
1212
};
@@ -26,7 +26,7 @@ export async function sendMail(input: SendMailInput): Promise<SendMailResult> {
2626
from: env.MAIL_FROM,
2727
to: input.to,
2828
subject: input.subject,
29-
text: input.text,
29+
text: htmlToText(input.html, { wordwrap: 80 }),
3030
html: input.html,
3131
attachments: input.attachments?.map((a) => ({
3232
filename: a.filename,

packages/app/src/modules/mail/sendReceipt.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ export async function sendReceipt(input: SendReceiptInput): Promise<void> {
3333
const result = await sendMail({
3434
to,
3535
subject: template.subject,
36-
text: template.text,
3736
html: template.html,
3837
attachments,
3938
});

packages/app/src/modules/mail/templates/cseOpinionReceipt.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,6 @@ import { escapeHtml, wrapEmail } from "./shell";
55
export function buildCseOpinionReceipt({ siren, year }: ReceiptContext) {
66
const subject = `Accusé de réception — Avis du CSE ${year}`;
77
const prettySiren = formatSiren(siren);
8-
const text = `Bonjour,
9-
10-
Nous accusons réception du dépôt de l'avis du CSE sur les indicateurs de l'égalité professionnelle pour l'année ${year}.
11-
12-
Entreprise (SIREN) : ${prettySiren}
13-
Année de déclaration : ${year}
14-
15-
Le récapitulatif de votre dépôt est joint à cet e-mail.
16-
17-
Cordialement,
18-
L'équipe Egapro`;
19-
208
const html = wrapEmail(
219
subject,
2210
`<p>Bonjour,</p>
@@ -25,9 +13,8 @@ L'équipe Egapro`;
2513
<li><strong>Entreprise (SIREN)&nbsp;:</strong> ${escapeHtml(prettySiren)}</li>
2614
<li><strong>Année de déclaration&nbsp;:</strong> ${year}</li>
2715
</ul>
28-
<p>Le récapitulatif de votre dépôt est joint à cet e-mail.</p>
2916
<p>Cordialement,<br>L'équipe Egapro</p>`,
3017
);
3118

32-
return { subject, text, html };
19+
return { subject, html };
3320
}

0 commit comments

Comments
 (0)