feat(#112): reprise après refus — dossier complet GET/PATCH

- GET reprise-dossier : parents[], enfants[], texte_motivation (famille) ou fiche AM (#119)
- PATCH reprise-resoumettre : co-parent, enfants (update par id), motivation, fiche AM
- Resoumission famille : tous les parents en en_attente + invalidation token groupée
- DTOs étendus + est_multiple sur enfants dossier famille
- Tests unitaires getRepriseDossier parent/AM

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-16 16:39:53 +02:00
co-authored by Cursor
parent 671da71752
commit d70577b1c3
9 changed files with 655 additions and 17 deletions
+332 -8
View File
@@ -7,7 +7,7 @@ import {
BadRequestException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { QueryFailedError, Repository } from 'typeorm';
import { EntityManager, In, QueryFailedError, Repository } from 'typeorm';
import { UserService } from '../user/user.service';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
@@ -27,11 +27,15 @@ import { DossierFamille, DossierFamilleEnfant } from 'src/entities/dossier_famil
import { StatutDossierType } from 'src/entities/dossiers.entity';
import { LoginDto } from './dto/login.dto';
import { RepriseDossierDto } from './dto/reprise-dossier.dto';
import { ResoumettreRepriseDto } from './dto/resoumettre-reprise.dto';
import { RepriseIdentifyResponseDto } from './dto/reprise-identify.dto';
import { AppConfigService } from 'src/modules/config/config.service';
import { validateNir } from 'src/common/utils/nir.util';
import { NumeroDossierService } from 'src/modules/numero-dossier/numero-dossier.service';
import { MailService } from 'src/modules/mail/mail.service';
import { ParentsService } from '../parents/parents.service';
import { DossiersService } from '../dossiers/dossiers.service';
import { DossierAmCompletDto } from '../dossiers/dto/dossier-am-complet.dto';
@Injectable()
export class AuthService {
@@ -44,6 +48,8 @@ export class AuthService {
private readonly appConfigService: AppConfigService,
private readonly mailService: MailService,
private readonly numeroDossierService: NumeroDossierService,
private readonly parentsService: ParentsService,
private readonly dossiersService: DossiersService,
@InjectRepository(Parents)
private readonly parentsRepo: Repository<Parents>,
@InjectRepository(Users)
@@ -873,12 +879,72 @@ export class AuthService {
return { success: true, message: 'Deconnexion'}
}
/** GET dossier reprise token seul. Ticket #111 */
/** GET dossier reprise token seul. Ticket #111, enrichi #112 */
async getRepriseDossier(token: string): Promise<RepriseDossierDto> {
const user = await this.usersService.findByTokenReprise(token);
if (!user) {
throw new NotFoundException('Token reprise invalide ou expiré.');
}
const base = this.userToRepriseBase(user);
if (user.role === RoleType.PARENT && user.numero_dossier) {
try {
const famille = await this.parentsService.getDossierFamilleByNumero(user.numero_dossier);
return {
...base,
parents: famille.parents,
enfants: famille.enfants,
texte_motivation: famille.texte_motivation,
};
} catch (err) {
if (err instanceof NotFoundException) {
return base;
}
throw err;
}
}
if (user.role === RoleType.ASSISTANTE_MATERNELLE && user.numero_dossier) {
try {
const unifie = await this.dossiersService.getDossierByNumero(user.numero_dossier);
if (unifie.type === 'am') {
return this.mergeAmRepriseDossier(base, unifie.dossier as DossierAmCompletDto);
}
} catch (err) {
if (err instanceof NotFoundException) {
return base;
}
throw err;
}
}
return base;
}
/** PATCH resoumission reprise dossier complet. Ticket #111, enrichi #112 */
async resoumettreReprise(dto: ResoumettreRepriseDto): Promise<{
message: string;
statut: StatutUtilisateurType;
user_id: string;
numero_dossier?: string;
}> {
const user = await this.usersService.findByTokenReprise(dto.token);
if (!user) {
throw new NotFoundException('Token reprise invalide ou expiré.');
}
if (user.role === RoleType.PARENT) {
return this.resoumettreRepriseParent(user, dto);
}
if (user.role === RoleType.ASSISTANTE_MATERNELLE) {
return this.resoumettreRepriseAM(user, dto);
}
throw new BadRequestException('Rôle non pris en charge pour la reprise.');
}
private userToRepriseBase(user: Users): RepriseDossierDto {
return {
id: user.id,
email: user.email,
@@ -896,12 +962,270 @@ export class AuthService {
};
}
/** PUT resoumission reprise. Ticket #111 */
async resoumettreReprise(
token: string,
dto: { prenom?: string; nom?: string; telephone?: string; adresse?: string; ville?: string; code_postal?: string; photo_url?: string },
): Promise<Users> {
return this.usersService.resoumettreReprise(token, dto);
private mergeAmRepriseDossier(base: RepriseDossierDto, dossier: DossierAmCompletDto): RepriseDossierDto {
const u = dossier.user;
return {
...base,
email: u.email,
prenom: u.prenom,
nom: u.nom,
telephone: u.telephone,
adresse: u.adresse,
ville: u.ville,
code_postal: u.code_postal,
photo_url: u.photo_url,
consentement_photo: u.consentement_photo,
date_naissance: u.date_naissance,
lieu_naissance_ville: u.lieu_naissance_ville,
lieu_naissance_pays: u.lieu_naissance_pays,
numero_agrement: dossier.numero_agrement,
nir: dossier.nir,
date_agrement: dossier.date_agrement,
nb_max_enfants: dossier.nb_max_enfants,
place_disponible: dossier.place_disponible,
biographie: dossier.biographie,
};
}
private applyTitulaireUserFields(user: Users, dto: ResoumettreRepriseDto): void {
if (dto.prenom !== undefined) user.prenom = dto.prenom;
if (dto.nom !== undefined) user.nom = dto.nom;
if (dto.telephone !== undefined) user.telephone = dto.telephone;
if (dto.adresse !== undefined) user.adresse = dto.adresse;
if (dto.ville !== undefined) user.ville = dto.ville;
if (dto.code_postal !== undefined) user.code_postal = dto.code_postal;
}
private async resoumettreRepriseParent(
titulaire: Users,
dto: ResoumettreRepriseDto,
): Promise<{ message: string; statut: StatutUtilisateurType; user_id: string; numero_dossier?: string }> {
const motivation =
dto.texte_motivation?.trim() ??
dto.presentation_dossier?.trim();
return this.usersRepo.manager.transaction(async (manager) => {
const familyUsers = titulaire.numero_dossier
? await manager.find(Users, {
where: {
role: RoleType.PARENT,
numero_dossier: titulaire.numero_dossier,
},
})
: [titulaire];
const titulaireLive = familyUsers.find((u) => u.id === titulaire.id) ?? titulaire;
this.applyTitulaireUserFields(titulaireLive, dto);
const coParentUser = familyUsers.find((u) => u.id !== titulaire.id);
if (coParentUser) {
if (dto.co_parent_prenom !== undefined) coParentUser.prenom = dto.co_parent_prenom;
if (dto.co_parent_nom !== undefined) coParentUser.nom = dto.co_parent_nom;
if (dto.co_parent_telephone !== undefined) coParentUser.telephone = dto.co_parent_telephone;
if (dto.co_parent_meme_adresse) {
coParentUser.adresse = titulaireLive.adresse;
coParentUser.code_postal = titulaireLive.code_postal;
coParentUser.ville = titulaireLive.ville;
} else {
if (dto.co_parent_adresse !== undefined) coParentUser.adresse = dto.co_parent_adresse;
if (dto.co_parent_code_postal !== undefined) coParentUser.code_postal = dto.co_parent_code_postal;
if (dto.co_parent_ville !== undefined) coParentUser.ville = dto.co_parent_ville;
}
}
if (titulaire.numero_dossier && motivation !== undefined) {
const dossierFamille = await manager.findOne(DossierFamille, {
where: { numero_dossier: titulaire.numero_dossier },
});
if (dossierFamille) {
dossierFamille.presentation = motivation || undefined;
await manager.save(DossierFamille, dossierFamille);
}
}
if (dto.enfants?.length) {
const familyChildIds = await this.getFamilyChildrenIds(manager, titulaireLive.id);
for (const enfantDto of dto.enfants) {
if (!familyChildIds.has(enfantDto.id)) {
throw new BadRequestException(
`Enfant inconnu pour ce dossier : ${enfantDto.id}`,
);
}
const enfant = await manager.findOne(Children, { where: { id: enfantDto.id } });
if (!enfant) {
throw new BadRequestException(`Enfant introuvable : ${enfantDto.id}`);
}
if (enfantDto.prenom !== undefined) enfant.first_name = enfantDto.prenom;
if (enfantDto.nom !== undefined) enfant.last_name = enfantDto.nom;
if (enfantDto.genre !== undefined) enfant.gender = enfantDto.genre;
if (enfantDto.date_naissance !== undefined) {
enfant.birth_date = new Date(enfantDto.date_naissance);
enfant.status = StatutEnfantType.ACTIF;
}
if (enfantDto.date_previsionnelle_naissance !== undefined) {
enfant.due_date = new Date(enfantDto.date_previsionnelle_naissance);
if (!enfantDto.date_naissance) {
enfant.status = StatutEnfantType.A_NAITRE;
}
}
if (enfantDto.grossesse_multiple !== undefined) {
enfant.is_multiple = enfantDto.grossesse_multiple;
}
if (enfantDto.photo_base64 && enfantDto.photo_filename) {
enfant.photo_url = await this.sauvegarderPhotoDepuisBase64(
enfantDto.photo_base64,
enfantDto.photo_filename,
);
}
await manager.save(Children, enfant);
}
}
for (const familyUser of familyUsers) {
familyUser.statut = StatutUtilisateurType.EN_ATTENTE;
familyUser.token_reprise = undefined;
familyUser.token_reprise_expire_le = undefined;
await manager.save(Users, familyUser);
}
return {
message: 'Dossier resoumis avec succès. Il est de nouveau en attente de validation.',
statut: StatutUtilisateurType.EN_ATTENTE,
user_id: titulaireLive.id,
numero_dossier: titulaireLive.numero_dossier,
};
});
}
private async resoumettreRepriseAM(
user: Users,
dto: ResoumettreRepriseDto,
): Promise<{ message: string; statut: StatutUtilisateurType; user_id: string; numero_dossier?: string }> {
if (
dto.capacite_accueil !== undefined &&
dto.places_disponibles !== undefined &&
dto.places_disponibles > dto.capacite_accueil
) {
throw new BadRequestException(
'Le nombre de places disponibles ne peut pas dépasser la capacité d\'accueil.',
);
}
let urlPhoto: string | undefined;
const photoB64 = (dto.photo_base64 ?? '').trim();
if (photoB64) {
const nomFichier = (dto.photo_filename ?? '').trim() || 'photo_am.jpg';
urlPhoto = await this.sauvegarderPhotoDepuisBase64(photoB64, nomFichier);
} else if (dto.photo_url !== undefined) {
urlPhoto = dto.photo_url;
}
if (dto.nir !== undefined) {
const nirNormalized = dto.nir.replace(/\s/g, '').toUpperCase();
const nirValidation = validateNir(nirNormalized, {
dateNaissance: dto.date_naissance ?? user.date_naissance?.toISOString().slice(0, 10),
});
if (!nirValidation.valid) {
throw new BadRequestException(nirValidation.error || 'NIR invalide');
}
}
return this.usersRepo.manager.transaction(async (manager) => {
const userLive = await manager.findOne(Users, { where: { id: user.id } });
if (!userLive) {
throw new NotFoundException('Utilisateur introuvable.');
}
this.applyTitulaireUserFields(userLive, dto);
if (urlPhoto !== undefined) userLive.photo_url = urlPhoto;
if (dto.consentement_photo !== undefined) {
userLive.consentement_photo = dto.consentement_photo;
if (dto.consentement_photo) {
userLive.date_consentement_photo = new Date();
}
}
if (dto.date_naissance !== undefined) {
userLive.date_naissance = new Date(dto.date_naissance);
}
if (dto.lieu_naissance_ville !== undefined) {
userLive.lieu_naissance_ville = dto.lieu_naissance_ville;
}
if (dto.lieu_naissance_pays !== undefined) {
userLive.lieu_naissance_pays = dto.lieu_naissance_pays;
}
const am = await manager.findOne(AssistanteMaternelle, { where: { user_id: user.id } });
if (!am) {
throw new NotFoundException('Fiche assistante maternelle introuvable.');
}
if (dto.nir !== undefined) {
const nirNormalized = dto.nir.replace(/\s/g, '').toUpperCase();
const nirDejaUtilise = await manager.findOne(AssistanteMaternelle, {
where: { nir: nirNormalized },
});
if (nirDejaUtilise && nirDejaUtilise.user_id !== user.id) {
throw new ConflictException(
'Un compte assistante maternelle avec ce numéro NIR existe déjà.',
);
}
am.nir = nirNormalized;
}
if (dto.numero_agrement !== undefined) {
const agrement = dto.numero_agrement.trim();
const agrementDejaUtilise = await manager.findOne(AssistanteMaternelle, {
where: { approval_number: agrement },
});
if (agrementDejaUtilise && agrementDejaUtilise.user_id !== user.id) {
throw new ConflictException(
'Un compte assistante maternelle avec ce numéro d\'agrément existe déjà.',
);
}
am.approval_number = agrement;
}
if (dto.date_agrement !== undefined) {
am.agreement_date = new Date(dto.date_agrement);
}
if (dto.capacite_accueil !== undefined) {
am.max_children = dto.capacite_accueil;
}
if (dto.places_disponibles !== undefined) {
am.places_available = dto.places_disponibles;
}
if (dto.biographie !== undefined) {
am.biography = dto.biographie;
}
userLive.statut = StatutUtilisateurType.EN_ATTENTE;
userLive.token_reprise = undefined;
userLive.token_reprise_expire_le = undefined;
await manager.save(AssistanteMaternelle, am);
await manager.save(Users, userLive);
return {
message: 'Dossier resoumis avec succès. Il est de nouveau en attente de validation.',
statut: StatutUtilisateurType.EN_ATTENTE,
user_id: userLive.id,
numero_dossier: userLive.numero_dossier,
};
});
}
private async getFamilyChildrenIds(
manager: EntityManager,
parentUserId: string,
): Promise<Set<string>> {
const familyUserIds = await this.parentsService.getFamilyUserIds(parentUserId);
const links = await manager.find(ParentsChildren, {
where: { parentId: In(familyUserIds) },
});
return new Set(links.map((l) => l.enfantId));
}
/** POST reprise-identify : numero_dossier + email → type + token. Ticket #111 */