feat(#110): refus dossier propagé aux co-parents + mail aligné

- `refuseUser` charge tous les parents du même numéro de dossier, leur
  applique le même token_reprise (7 jours), trace une validation refus
  par compte et envoie un mail de refus à chacun. Le refus devient une
  action dossier : peu importe quel parent ouvre ensuite le lien.
- Échec d'envoi mail loggé sans bloquer les autres destinataires.
- Mail de refus aligné sur le look des autres templates (titre/bouton
  verts, bloc motif gestionnaire en encart, échappement HTML cohérent,
  appUrl normalisée).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-13 19:44:30 +02:00
co-authored by Cursor
parent b6e83bf9ef
commit 9eb62ddd13
2 changed files with 72 additions and 33 deletions
+56 -25
View File
@@ -332,7 +332,7 @@ export class UserService {
return savedUser;
}
/** Refuser un compte (en_attente -> refuse) ; tracé validations, token reprise, email. Ticket #110 */
/** Refuser un dossier (en_attente -> refuse) ; tracé validations, token reprise partagé, emails. Ticket #110 */
async refuseUser(user_id: string, currentUser: Users, comment?: string): Promise<Users> {
if (![RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE].includes(currentUser.role)) {
throw new ForbiddenException('Accès réservé aux super admins, administrateurs et gestionnaires');
@@ -340,40 +340,71 @@ export class UserService {
const user = await this.usersRepository.findOne({ where: { id: user_id } });
if (!user) throw new NotFoundException('Utilisateur introuvable');
if (user.statut !== StatutUtilisateurType.EN_ATTENTE) {
throw new BadRequestException('Seul un compte en attente peut être refusé.');
throw new BadRequestException('Seul un dossier en attente peut être refusé.');
}
const tokenReprise = crypto.randomUUID();
const expireLe = new Date();
expireLe.setDate(expireLe.getDate() + 7);
user.statut = StatutUtilisateurType.REFUSE;
user.token_reprise = tokenReprise;
user.token_reprise_expire_le = expireLe;
const savedUser = await this.usersRepository.save(user);
const usersDossier = user.role === RoleType.PARENT && user.numero_dossier
? await this.usersRepository.find({
where: {
role: RoleType.PARENT,
numero_dossier: user.numero_dossier,
},
})
: [user];
const validation = this.validationRepository.create({
user: savedUser,
type: 'refus_compte',
status: StatutValidationType.REFUSE,
validated_by: currentUser,
comment,
});
await this.validationRepository.save(validation);
try {
await this.mailService.sendRefusEmail(
savedUser.email,
savedUser.prenom ?? '',
savedUser.nom ?? '',
comment,
tokenReprise,
const usersARefuser = usersDossier.length ? usersDossier : [user];
const utilisateurInvalide = usersARefuser.find(
(u) => u.statut !== StatutUtilisateurType.EN_ATTENTE,
);
if (utilisateurInvalide) {
throw new BadRequestException(
'Tous les comptes du dossier doivent être en attente pour refuser le dossier.',
);
} catch (err) {
this.logger.warn(`Envoi email refus échoué pour ${savedUser.email}`, err);
}
return savedUser;
const savedUsers = await this.usersRepository.manager.transaction(async (manager) => {
const updatedUsers: Users[] = [];
for (const userARefuser of usersARefuser) {
userARefuser.statut = StatutUtilisateurType.REFUSE;
userARefuser.token_reprise = tokenReprise;
userARefuser.token_reprise_expire_le = expireLe;
const savedUser = await manager.save(Users, userARefuser);
updatedUsers.push(savedUser);
const validation = manager.create(Validation, {
user: savedUser,
type: 'refus_compte',
status: StatutValidationType.REFUSE,
validated_by: currentUser,
comment,
});
await manager.save(Validation, validation);
}
return updatedUsers;
});
for (const savedUser of savedUsers) {
try {
await this.mailService.sendRefusEmail(
savedUser.email,
savedUser.prenom ?? '',
savedUser.nom ?? '',
comment,
tokenReprise,
);
} catch (err) {
this.logger.warn(`Envoi email refus échoué pour ${savedUser.email}`, err);
}
}
return savedUsers.find((savedUser) => savedUser.id === user.id) ?? savedUsers[0];
}
/**