feat(#112): email accusé resoumission aux parents
Après PATCH reprise-resoumettre (parent), envoi à chaque parent du dossier : confirmation resoumission + rappel numero_dossier. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
c438009286
commit
9d54d9b19b
@ -98,4 +98,21 @@ describe('MailService (ticket #28)', () => {
|
|||||||
expect(html).toContain(`https://app.test/reset-password?token=${encodeURIComponent(token)}`);
|
expect(html).toContain(`https://app.test/reset-password?token=${encodeURIComponent(token)}`);
|
||||||
expect(html).not.toContain('create-password');
|
expect(html).not.toContain('create-password');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('sendResoumissionPendingEmail — accusé resoumission avec n° dossier', async () => {
|
||||||
|
await service.sendResoumissionPendingEmail(
|
||||||
|
'parent@test.fr',
|
||||||
|
'Claire',
|
||||||
|
'MARTIN',
|
||||||
|
'2026-000024',
|
||||||
|
);
|
||||||
|
expect(sendEmailSpy).toHaveBeenCalledTimes(1);
|
||||||
|
const [to, subject, html] = sendEmailSpy.mock.calls[0];
|
||||||
|
expect(to).toBe('parent@test.fr');
|
||||||
|
expect(subject).toContain('resoumis');
|
||||||
|
expect(subject).toContain('2026-000024');
|
||||||
|
expect(html).toContain('Claire');
|
||||||
|
expect(html).toContain('2026-000024');
|
||||||
|
expect(html).toContain('en attente de validation');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -301,6 +301,43 @@ export class MailService {
|
|||||||
await this.sendEmail(to, subject, html);
|
await this.sendEmail(to, subject, html);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accusé de resoumission après refus (reprise #112) : dossier à nouveau en attente de validation.
|
||||||
|
*/
|
||||||
|
async sendResoumissionPendingEmail(
|
||||||
|
to: string,
|
||||||
|
prenom: string,
|
||||||
|
nom: string,
|
||||||
|
numeroDossier: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const appName = this.configService.get<string>('app_name', "P'titsPas");
|
||||||
|
const appUrl = this.configService.get<string>('app_url', 'https://app.ptits-pas.fr');
|
||||||
|
|
||||||
|
const safe = (s: string) =>
|
||||||
|
(s || '')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"');
|
||||||
|
|
||||||
|
const subject = `Votre dossier a été resoumis — ${safe(numeroDossier)}`;
|
||||||
|
const html = `
|
||||||
|
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||||
|
<h2 style="color: #4CAF50;">Bonjour ${safe(prenom)} ${safe(nom)},</h2>
|
||||||
|
<p>Nous avons bien reçu la <strong>resoumission</strong> de votre dossier sur <strong>${safe(appName)}</strong>.</p>
|
||||||
|
<p><strong>Numéro de dossier :</strong> ${safe(numeroDossier)}</p>
|
||||||
|
<p>Votre dossier est de nouveau <strong>en attente de validation</strong> par notre équipe. Vous recevrez un email lorsqu'il aura été traité.</p>
|
||||||
|
<div style="text-align: center; margin: 30px 0;">
|
||||||
|
<a href="${appUrl}" style="background-color: #4CAF50; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; font-weight: bold;">Accéder au site</a>
|
||||||
|
</div>
|
||||||
|
<hr style="border: 1px solid #eee; margin: 20px 0;">
|
||||||
|
<p style="color: #666; font-size: 12px;">Cet email a été envoyé automatiquement. Merci de ne pas y répondre.</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
await this.sendEmail(to, subject, html);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Email de refus de dossier avec lien reprise (token).
|
* Email de refus de dossier avec lien reprise (token).
|
||||||
* Ticket #110 – Refus sans suppression
|
* Ticket #110 – Refus sans suppression
|
||||||
|
|||||||
@ -1004,7 +1004,7 @@ export class AuthService {
|
|||||||
dto.texte_motivation?.trim() ??
|
dto.texte_motivation?.trim() ??
|
||||||
dto.presentation_dossier?.trim();
|
dto.presentation_dossier?.trim();
|
||||||
|
|
||||||
return this.usersRepo.manager.transaction(async (manager) => {
|
const result = await this.usersRepo.manager.transaction(async (manager) => {
|
||||||
const familyUsers = titulaire.numero_dossier
|
const familyUsers = titulaire.numero_dossier
|
||||||
? await manager.find(Users, {
|
? await manager.find(Users, {
|
||||||
where: {
|
where: {
|
||||||
@ -1098,8 +1098,29 @@ export class AuthService {
|
|||||||
statut: StatutUtilisateurType.EN_ATTENTE,
|
statut: StatutUtilisateurType.EN_ATTENTE,
|
||||||
user_id: titulaireLive.id,
|
user_id: titulaireLive.id,
|
||||||
numero_dossier: titulaireLive.numero_dossier,
|
numero_dossier: titulaireLive.numero_dossier,
|
||||||
|
parentsPourEmail: familyUsers,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const numeroDossier = result.numero_dossier ?? '';
|
||||||
|
try {
|
||||||
|
for (const parent of result.parentsPourEmail) {
|
||||||
|
await this.mailService.sendResoumissionPendingEmail(
|
||||||
|
parent.email,
|
||||||
|
parent.prenom ?? '',
|
||||||
|
parent.nom ?? '',
|
||||||
|
numeroDossier,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(
|
||||||
|
"[resoumettreRepriseParent] Échec envoi email accusé resoumission (dossier conservé)",
|
||||||
|
err instanceof Error ? err.stack : String(err),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { parentsPourEmail: _parentsPourEmail, ...response } = result;
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async resoumettreRepriseAM(
|
private async resoumettreRepriseAM(
|
||||||
|
|||||||
@ -179,6 +179,15 @@ Code HTTP : **200** (pas de corps `Users` brut comme l’ancien back).
|
|||||||
- **Parent :** tous les users `role=parent` avec le même `numero_dossier` passent en `en_attente` ; `token_reprise` invalidé sur **tous** (symétrique refus #110).
|
- **Parent :** tous les users `role=parent` avec le même `numero_dossier` passent en `en_attente` ; `token_reprise` invalidé sur **tous** (symétrique refus #110).
|
||||||
- **AM :** un seul user.
|
- **AM :** un seul user.
|
||||||
|
|
||||||
|
### E-mail accusé resoumission (parent)
|
||||||
|
|
||||||
|
Après `PATCH` réussi, un e-mail est envoyé à **chaque parent** du dossier (`sendResoumissionPendingEmail`) :
|
||||||
|
- confirmation de resoumission ;
|
||||||
|
- rappel du **numéro de dossier** ;
|
||||||
|
- mention « en attente de validation ».
|
||||||
|
|
||||||
|
Échec SMTP : logué, **ne bloque pas** la resoumission (même règle que l'inscription initiale).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Fichiers front à modifier (checklist)
|
## 4. Fichiers front à modifier (checklist)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user