Compare commits
2 Commits
671da71752
...
25c10c885a
| Author | SHA1 | Date | |
|---|---|---|---|
| 25c10c885a | |||
| d70577b1c3 |
@ -102,8 +102,7 @@ export class AuthController {
|
|||||||
@ApiResponse({ status: 200, description: 'Dossier resoumis' })
|
@ApiResponse({ status: 200, description: 'Dossier resoumis' })
|
||||||
@ApiResponse({ status: 404, description: 'Token invalide ou expiré' })
|
@ApiResponse({ status: 404, description: 'Token invalide ou expiré' })
|
||||||
async resoumettreReprise(@Body() dto: ResoumettreRepriseDto) {
|
async resoumettreReprise(@Body() dto: ResoumettreRepriseDto) {
|
||||||
const { token, ...fields } = dto;
|
return this.authService.resoumettreReprise(dto);
|
||||||
return this.authService.resoumettreReprise(token, fields);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Public()
|
@Public()
|
||||||
|
|||||||
@ -12,11 +12,25 @@ import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entit
|
|||||||
import { AppConfigModule } from 'src/modules/config';
|
import { AppConfigModule } from 'src/modules/config';
|
||||||
import { NumeroDossierModule } from 'src/modules/numero-dossier/numero-dossier.module';
|
import { NumeroDossierModule } from 'src/modules/numero-dossier/numero-dossier.module';
|
||||||
import { MailModule } from 'src/modules/mail/mail.module';
|
import { MailModule } from 'src/modules/mail/mail.module';
|
||||||
|
import { ParentsModule } from '../parents/parents.module';
|
||||||
|
import { DossiersModule } from '../dossiers/dossiers.module';
|
||||||
|
import { DossierFamille, DossierFamilleEnfant } from 'src/entities/dossier_famille.entity';
|
||||||
|
import { ParentsChildren } from 'src/entities/parents_children.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([Users, Parents, Children, AssistanteMaternelle]),
|
TypeOrmModule.forFeature([
|
||||||
|
Users,
|
||||||
|
Parents,
|
||||||
|
Children,
|
||||||
|
AssistanteMaternelle,
|
||||||
|
DossierFamille,
|
||||||
|
DossierFamilleEnfant,
|
||||||
|
ParentsChildren,
|
||||||
|
]),
|
||||||
forwardRef(() => UserModule),
|
forwardRef(() => UserModule),
|
||||||
|
ParentsModule,
|
||||||
|
DossiersModule,
|
||||||
AppConfigModule,
|
AppConfigModule,
|
||||||
MailModule,
|
MailModule,
|
||||||
NumeroDossierModule,
|
NumeroDossierModule,
|
||||||
|
|||||||
@ -5,11 +5,13 @@ import { getRepositoryToken } from '@nestjs/typeorm';
|
|||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { UserService } from '../user/user.service';
|
import { UserService } from '../user/user.service';
|
||||||
|
import { ParentsService } from '../parents/parents.service';
|
||||||
|
import { DossiersService } from '../dossiers/dossiers.service';
|
||||||
import { AppConfigService } from 'src/modules/config/config.service';
|
import { AppConfigService } from 'src/modules/config/config.service';
|
||||||
import { MailService } from 'src/modules/mail/mail.service';
|
import { MailService } from 'src/modules/mail/mail.service';
|
||||||
import { NumeroDossierService } from 'src/modules/numero-dossier/numero-dossier.service';
|
import { NumeroDossierService } from 'src/modules/numero-dossier/numero-dossier.service';
|
||||||
import { Parents } from 'src/entities/parents.entity';
|
import { Parents } from 'src/entities/parents.entity';
|
||||||
import { Users } from 'src/entities/users.entity';
|
import { Users, RoleType, StatutUtilisateurType } from 'src/entities/users.entity';
|
||||||
import { Children } from 'src/entities/children.entity';
|
import { Children } from 'src/entities/children.entity';
|
||||||
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
|
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
|
||||||
|
|
||||||
@ -19,6 +21,20 @@ describe('AuthService (#118 create-password API)', () => {
|
|||||||
findOne: jest.fn(),
|
findOne: jest.fn(),
|
||||||
createQueryBuilder: jest.fn(),
|
createQueryBuilder: jest.fn(),
|
||||||
update: jest.fn(),
|
update: jest.fn(),
|
||||||
|
manager: { transaction: jest.fn() },
|
||||||
|
};
|
||||||
|
|
||||||
|
const userServiceMock = {
|
||||||
|
findByTokenReprise: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const parentsServiceMock = {
|
||||||
|
getDossierFamilleByNumero: jest.fn(),
|
||||||
|
getFamilyUserIds: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const dossiersServiceMock = {
|
||||||
|
getDossierByNumero: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mailServiceMock = {
|
const mailServiceMock = {
|
||||||
@ -45,7 +61,9 @@ describe('AuthService (#118 create-password API)', () => {
|
|||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
AuthService,
|
AuthService,
|
||||||
{ provide: UserService, useValue: {} },
|
{ provide: UserService, useValue: userServiceMock },
|
||||||
|
{ provide: ParentsService, useValue: parentsServiceMock },
|
||||||
|
{ provide: DossiersService, useValue: dossiersServiceMock },
|
||||||
{ provide: JwtService, useValue: {} },
|
{ provide: JwtService, useValue: {} },
|
||||||
{ provide: ConfigService, useValue: { get: jest.fn() } },
|
{ provide: ConfigService, useValue: { get: jest.fn() } },
|
||||||
{ provide: AppConfigService, useValue: appConfigMock },
|
{ provide: AppConfigService, useValue: appConfigMock },
|
||||||
@ -136,4 +154,69 @@ describe('AuthService (#118 create-password API)', () => {
|
|||||||
expect(exec).toHaveBeenCalled();
|
expect(exec).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Reprise après refus (#112)', () => {
|
||||||
|
const token = '11111111-1111-1111-1111-111111111111';
|
||||||
|
|
||||||
|
it('getRepriseDossier throws when token invalid', async () => {
|
||||||
|
userServiceMock.findByTokenReprise.mockResolvedValue(null);
|
||||||
|
await expect(service.getRepriseDossier(token)).rejects.toThrow(NotFoundException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getRepriseDossier returns famille complète pour parent', async () => {
|
||||||
|
userServiceMock.findByTokenReprise.mockResolvedValue({
|
||||||
|
id: 'p1',
|
||||||
|
email: 'claire@test.fr',
|
||||||
|
prenom: 'Claire',
|
||||||
|
nom: 'MARTIN',
|
||||||
|
role: RoleType.PARENT,
|
||||||
|
numero_dossier: '2026-000021',
|
||||||
|
statut: StatutUtilisateurType.REFUSE,
|
||||||
|
});
|
||||||
|
parentsServiceMock.getDossierFamilleByNumero.mockResolvedValue({
|
||||||
|
numero_dossier: '2026-000021',
|
||||||
|
parents: [{ user_id: 'p1', email: 'claire@test.fr', statut: StatutUtilisateurType.REFUSE }],
|
||||||
|
enfants: [{ id: 'e1', first_name: 'Emma', status: 'actif' }],
|
||||||
|
texte_motivation: 'Motivation test',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await service.getRepriseDossier(token);
|
||||||
|
expect(result.parents).toHaveLength(1);
|
||||||
|
expect(result.enfants).toHaveLength(1);
|
||||||
|
expect(result.texte_motivation).toBe('Motivation test');
|
||||||
|
expect(result.numero_dossier).toBe('2026-000021');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getRepriseDossier returns fiche AM complète', async () => {
|
||||||
|
userServiceMock.findByTokenReprise.mockResolvedValue({
|
||||||
|
id: 'am1',
|
||||||
|
email: 'am@test.fr',
|
||||||
|
role: RoleType.ASSISTANTE_MATERNELLE,
|
||||||
|
numero_dossier: '2026-000003',
|
||||||
|
});
|
||||||
|
dossiersServiceMock.getDossierByNumero.mockResolvedValue({
|
||||||
|
type: 'am',
|
||||||
|
dossier: {
|
||||||
|
numero_dossier: '2026-000003',
|
||||||
|
user: {
|
||||||
|
id: 'am1',
|
||||||
|
email: 'am@test.fr',
|
||||||
|
prenom: 'Marie',
|
||||||
|
nom: 'DUPONT',
|
||||||
|
statut: StatutUtilisateurType.REFUSE,
|
||||||
|
},
|
||||||
|
numero_agrement: 'AGR-1',
|
||||||
|
nir: '123456789012345',
|
||||||
|
biographie: 'Bio',
|
||||||
|
nb_max_enfants: 4,
|
||||||
|
place_disponible: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await service.getRepriseDossier(token);
|
||||||
|
expect(result.numero_agrement).toBe('AGR-1');
|
||||||
|
expect(result.biographie).toBe('Bio');
|
||||||
|
expect(result.nb_max_enfants).toBe(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import {
|
|||||||
BadRequestException,
|
BadRequestException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { QueryFailedError, Repository } from 'typeorm';
|
import { EntityManager, In, QueryFailedError, Repository } from 'typeorm';
|
||||||
import { UserService } from '../user/user.service';
|
import { UserService } from '../user/user.service';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import * as bcrypt from 'bcrypt';
|
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 { StatutDossierType } from 'src/entities/dossiers.entity';
|
||||||
import { LoginDto } from './dto/login.dto';
|
import { LoginDto } from './dto/login.dto';
|
||||||
import { RepriseDossierDto } from './dto/reprise-dossier.dto';
|
import { RepriseDossierDto } from './dto/reprise-dossier.dto';
|
||||||
|
import { ResoumettreRepriseDto } from './dto/resoumettre-reprise.dto';
|
||||||
import { RepriseIdentifyResponseDto } from './dto/reprise-identify.dto';
|
import { RepriseIdentifyResponseDto } from './dto/reprise-identify.dto';
|
||||||
import { AppConfigService } from 'src/modules/config/config.service';
|
import { AppConfigService } from 'src/modules/config/config.service';
|
||||||
import { validateNir } from 'src/common/utils/nir.util';
|
import { validateNir } from 'src/common/utils/nir.util';
|
||||||
import { NumeroDossierService } from 'src/modules/numero-dossier/numero-dossier.service';
|
import { NumeroDossierService } from 'src/modules/numero-dossier/numero-dossier.service';
|
||||||
import { MailService } from 'src/modules/mail/mail.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()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
@ -44,6 +48,8 @@ export class AuthService {
|
|||||||
private readonly appConfigService: AppConfigService,
|
private readonly appConfigService: AppConfigService,
|
||||||
private readonly mailService: MailService,
|
private readonly mailService: MailService,
|
||||||
private readonly numeroDossierService: NumeroDossierService,
|
private readonly numeroDossierService: NumeroDossierService,
|
||||||
|
private readonly parentsService: ParentsService,
|
||||||
|
private readonly dossiersService: DossiersService,
|
||||||
@InjectRepository(Parents)
|
@InjectRepository(Parents)
|
||||||
private readonly parentsRepo: Repository<Parents>,
|
private readonly parentsRepo: Repository<Parents>,
|
||||||
@InjectRepository(Users)
|
@InjectRepository(Users)
|
||||||
@ -873,12 +879,72 @@ export class AuthService {
|
|||||||
return { success: true, message: 'Deconnexion'}
|
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> {
|
async getRepriseDossier(token: string): Promise<RepriseDossierDto> {
|
||||||
const user = await this.usersService.findByTokenReprise(token);
|
const user = await this.usersService.findByTokenReprise(token);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new NotFoundException('Token reprise invalide ou expiré.');
|
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 {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
@ -896,12 +962,270 @@ export class AuthService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** PUT resoumission reprise. Ticket #111 */
|
private mergeAmRepriseDossier(base: RepriseDossierDto, dossier: DossierAmCompletDto): RepriseDossierDto {
|
||||||
async resoumettreReprise(
|
const u = dossier.user;
|
||||||
token: string,
|
return {
|
||||||
dto: { prenom?: string; nom?: string; telephone?: string; adresse?: string; ville?: string; code_postal?: string; photo_url?: string },
|
...base,
|
||||||
): Promise<Users> {
|
email: u.email,
|
||||||
return this.usersService.resoumettreReprise(token, dto);
|
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 */
|
/** POST reprise-identify : numero_dossier + email → type + token. Ticket #111 */
|
||||||
|
|||||||
11
backend/src/routes/auth/dto/enfant-reprise.dto.ts
Normal file
11
backend/src/routes/auth/dto/enfant-reprise.dto.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsNotEmpty, IsUUID } from 'class-validator';
|
||||||
|
import { EnfantInscriptionDto } from './enfant-inscription.dto';
|
||||||
|
|
||||||
|
/** Enfant existant modifiable lors de la resoumission reprise (#112). */
|
||||||
|
export class EnfantRepriseDto extends EnfantInscriptionDto {
|
||||||
|
@ApiProperty({ description: 'UUID enfant existant (obligatoire en reprise)' })
|
||||||
|
@IsUUID()
|
||||||
|
@IsNotEmpty()
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
@ -1,7 +1,11 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { RoleType } from 'src/entities/users.entity';
|
import { RoleType } from 'src/entities/users.entity';
|
||||||
|
import {
|
||||||
|
DossierFamilleEnfantDto,
|
||||||
|
DossierFamilleParentDto,
|
||||||
|
} from '../../parents/dto/dossier-famille-complet.dto';
|
||||||
|
|
||||||
/** Réponse GET /auth/reprise-dossier – données dossier pour préremplir le formulaire reprise. Ticket #111 */
|
/** Réponse GET /auth/reprise-dossier – dossier complet pour préremplir le wizard reprise. #111 + #112 */
|
||||||
export class RepriseDossierDto {
|
export class RepriseDossierDto {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
id: string;
|
id: string;
|
||||||
@ -41,4 +45,47 @@ export class RepriseDossierDto {
|
|||||||
|
|
||||||
@ApiProperty({ required: false })
|
@ApiProperty({ required: false })
|
||||||
situation_familiale?: string;
|
situation_familiale?: string;
|
||||||
|
|
||||||
|
// --- Parent (dossier famille, aligné #119) ---
|
||||||
|
|
||||||
|
@ApiProperty({ type: [DossierFamilleParentDto], required: false })
|
||||||
|
parents?: DossierFamilleParentDto[];
|
||||||
|
|
||||||
|
@ApiProperty({ type: [DossierFamilleEnfantDto], required: false })
|
||||||
|
enfants?: DossierFamilleEnfantDto[];
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, description: 'Motivation / présentation dossier famille' })
|
||||||
|
texte_motivation?: string;
|
||||||
|
|
||||||
|
// --- AM (aligné DossierAmCompletDto) ---
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
consentement_photo?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
date_naissance?: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
lieu_naissance_ville?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
lieu_naissance_pays?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
numero_agrement?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
nir?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
date_agrement?: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
nb_max_enfants?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
place_disponible?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
biographie?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,29 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsOptional, IsString, MaxLength, IsUUID } from 'class-validator';
|
import {
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
MaxLength,
|
||||||
|
IsUUID,
|
||||||
|
IsBoolean,
|
||||||
|
IsArray,
|
||||||
|
ValidateNested,
|
||||||
|
IsInt,
|
||||||
|
Min,
|
||||||
|
Max,
|
||||||
|
IsDateString,
|
||||||
|
Matches,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import { EnfantRepriseDto } from './enfant-reprise.dto';
|
||||||
|
|
||||||
/** Body PUT /auth/reprise-resoumettre – token + champs modifiables. Ticket #111 */
|
/** Body PATCH /auth/reprise-resoumettre – token + champs modifiables du wizard. #111 + #112 */
|
||||||
export class ResoumettreRepriseDto {
|
export class ResoumettreRepriseDto {
|
||||||
@ApiProperty({ description: 'Token reprise (reçu par email)' })
|
@ApiProperty({ description: 'Token reprise (reçu par email)' })
|
||||||
@IsUUID()
|
@IsUUID()
|
||||||
token: string;
|
token: string;
|
||||||
|
|
||||||
|
// --- Identité titulaire (parent principal ou AM) ---
|
||||||
|
|
||||||
@ApiProperty({ required: false })
|
@ApiProperty({ required: false })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
@ -42,8 +59,147 @@ export class ResoumettreRepriseDto {
|
|||||||
@MaxLength(10)
|
@MaxLength(10)
|
||||||
code_postal?: string;
|
code_postal?: string;
|
||||||
|
|
||||||
@ApiProperty({ required: false, description: 'Pour AM' })
|
@ApiProperty({ required: false, description: 'Pour AM (URL photo existante)' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
photo_url?: string;
|
photo_url?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, description: 'Pour AM (nouvelle photo base64)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
photo_base64?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, description: 'Pour AM (nom fichier photo)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
photo_filename?: string;
|
||||||
|
|
||||||
|
// --- Co-parent (reprise parent) ---
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
co_parent_email?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
co_parent_prenom?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
co_parent_nom?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@Matches(/^(\+33|0)[1-9](\d{2}){4}$/, {
|
||||||
|
message: 'Le numéro de téléphone du co-parent doit être valide',
|
||||||
|
})
|
||||||
|
co_parent_telephone?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
co_parent_meme_adresse?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
co_parent_adresse?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
co_parent_code_postal?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
co_parent_ville?: string;
|
||||||
|
|
||||||
|
// --- Motivation dossier famille ---
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, description: 'Alias texte_motivation' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(2000)
|
||||||
|
texte_motivation?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, description: 'Alias presentation_dossier (inscription)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(2000)
|
||||||
|
presentation_dossier?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ type: [EnfantRepriseDto], required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => EnfantRepriseDto)
|
||||||
|
enfants?: EnfantRepriseDto[];
|
||||||
|
|
||||||
|
// --- AM ---
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
consentement_photo?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
date_naissance?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(100)
|
||||||
|
lieu_naissance_ville?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(100)
|
||||||
|
lieu_naissance_pays?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(50)
|
||||||
|
numero_agrement?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@Matches(/^[1-3]\d{4}(?:2A|2B|\d{2})\d{6}\d{2}$/, {
|
||||||
|
message: 'Le NIR doit contenir 15 caractères (chiffres, ou 2A/2B pour la Corse)',
|
||||||
|
})
|
||||||
|
nir?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
date_agrement?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
@Max(10)
|
||||||
|
capacite_accueil?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
@Max(10)
|
||||||
|
places_disponibles?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(2000)
|
||||||
|
biographie?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,6 +54,9 @@ export class DossierFamilleEnfantDto {
|
|||||||
description: 'Consentement affichage photo (colonne consentement_photo)',
|
description: 'Consentement affichage photo (colonne consentement_photo)',
|
||||||
})
|
})
|
||||||
consent_photo?: boolean;
|
consent_photo?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, description: 'Grossesse multiple (est_multiple)' })
|
||||||
|
est_multiple?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Réponse GET /parents/dossier-famille/:numeroDossier – dossier famille complet. Ticket #119 */
|
/** Réponse GET /parents/dossier-famille/:numeroDossier – dossier famille complet. Ticket #119 */
|
||||||
|
|||||||
@ -221,6 +221,7 @@ export class ParentsService {
|
|||||||
status: child.status,
|
status: child.status,
|
||||||
photo_url: child.photo_url ?? undefined,
|
photo_url: child.photo_url ?? undefined,
|
||||||
consent_photo: child.consent_photo,
|
consent_photo: child.consent_photo,
|
||||||
|
est_multiple: child.is_multiple,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
235
docs/tmp/112-back-reprise-alignement-front.md
Normal file
235
docs/tmp/112-back-reprise-alignement-front.md
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
# #112 — Alignement front après évolution back (reprise dossier complet)
|
||||||
|
|
||||||
|
**Branche déployée :** `feature/112-reprise-apres-refus-front`
|
||||||
|
**Commit back :** `d70577b1` — `feat(#112): reprise après refus — dossier complet GET/PATCH`
|
||||||
|
**Date :** 2026-06-16
|
||||||
|
|
||||||
|
Ce document décrit le **contrat API réel** après extension du back, et ce que le front doit encore brancher pour exploiter le dossier complet (au-delà de l’identité seule).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Endpoints (inchangés côté URL)
|
||||||
|
|
||||||
|
| Méthode | Route | Auth |
|
||||||
|
|---------|-------|------|
|
||||||
|
| `GET` | `/api/v1/auth/reprise-dossier?token={uuid}` | Public |
|
||||||
|
| `PATCH` | `/api/v1/auth/reprise-resoumettre` | Public |
|
||||||
|
| `POST` | `/api/v1/auth/reprise-identify` | Public (inchangé) |
|
||||||
|
|
||||||
|
> **Note :** le ticket #111 parlait de `PUT` ; l’implémentation reste en **`PATCH`** (comme avant).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. `GET /auth/reprise-dossier` — réponse enrichie
|
||||||
|
|
||||||
|
### Champs communs (toujours présents)
|
||||||
|
|
||||||
|
Identiques à avant : `id`, `email`, `prenom`, `nom`, `telephone`, `adresse`, `ville`, `code_postal`, `numero_dossier`, `role`, `photo_url`, `genre`, `situation_familiale`.
|
||||||
|
|
||||||
|
### Rôle `parent` (+ champs #119)
|
||||||
|
|
||||||
|
Alignés sur `DossierFamilleCompletDto` :
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"parents": [
|
||||||
|
{
|
||||||
|
"user_id": "uuid",
|
||||||
|
"email": "…",
|
||||||
|
"prenom": "…",
|
||||||
|
"nom": "…",
|
||||||
|
"telephone": "…",
|
||||||
|
"adresse": "…",
|
||||||
|
"ville": "…",
|
||||||
|
"code_postal": "…",
|
||||||
|
"statut": "refuse",
|
||||||
|
"co_parent_id": "uuid-parent-entity"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"enfants": [
|
||||||
|
{
|
||||||
|
"id": "uuid-enfant",
|
||||||
|
"first_name": "Emma",
|
||||||
|
"last_name": "MARTIN",
|
||||||
|
"genre": "F",
|
||||||
|
"status": "actif",
|
||||||
|
"birth_date": "2023-02-15T00:00:00.000Z",
|
||||||
|
"due_date": null,
|
||||||
|
"photo_url": "/uploads/photos/…",
|
||||||
|
"consent_photo": true,
|
||||||
|
"est_multiple": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"texte_motivation": "Nous recherchons…"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Mapping front suggéré :**
|
||||||
|
|
||||||
|
| JSON back | Modèle / wizard parent |
|
||||||
|
|-----------|-------------------------|
|
||||||
|
| `parents[]` | `UserRegistrationData.parent1` + `parent2` (matcher par `email` ou ordre : titulaire = `id` du GET racine) |
|
||||||
|
| `enfants[].first_name` / `last_name` | `ChildData.firstName` / `lastName` |
|
||||||
|
| `enfants[].birth_date` | `ChildData.birthDate` (ISO → `DateTime`) |
|
||||||
|
| `enfants[].due_date` | `ChildData.dueDate` (enfant `a_naitre`) |
|
||||||
|
| `enfants[].status` | `actif` = né, `a_naitre` = à naître |
|
||||||
|
| `enfants[].photo_url` | `ApiConfig.absoluteMediaUrl()` + conserver pour reprise sans re-upload |
|
||||||
|
| `enfants[].id` | **Obligatoire** pour le PATCH (update par id) |
|
||||||
|
| `enfants[].est_multiple` | `grossesse_multiple` si utilisé |
|
||||||
|
| `texte_motivation` | étape présentation / motivation |
|
||||||
|
|
||||||
|
Si `numero_dossier` absent : pas de `parents[]` / `enfants[]` / `texte_motivation` (identité seule).
|
||||||
|
|
||||||
|
### Rôle `assistante_maternelle`
|
||||||
|
|
||||||
|
Champs racine + fiche pro (structure **aplatie**, pas de sous-objet `user`) :
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"consentement_photo": true,
|
||||||
|
"date_naissance": "1985-03-12T00:00:00.000Z",
|
||||||
|
"lieu_naissance_ville": "Paris",
|
||||||
|
"lieu_naissance_pays": "France",
|
||||||
|
"numero_agrement": "AGR-2024-12345",
|
||||||
|
"nir": "123456789012345",
|
||||||
|
"date_agrement": "2024-06-01T00:00:00.000Z",
|
||||||
|
"nb_max_enfants": 4,
|
||||||
|
"place_disponible": 2,
|
||||||
|
"biographie": "…"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Mapping `AmRegistrationData` :**
|
||||||
|
|
||||||
|
| JSON back | Champ front |
|
||||||
|
|-----------|-------------|
|
||||||
|
| `nb_max_enfants` | `capaciteAccueil` |
|
||||||
|
| `place_disponible` | `placesDisponibles` |
|
||||||
|
| `numero_agrement` | `numeroAgrement` |
|
||||||
|
| `biographie` | `biographie` / présentation |
|
||||||
|
| `photo_url` | déjà géré via `RepriseSession.photoUrl` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. `PATCH /auth/reprise-resoumettre` — body étendu
|
||||||
|
|
||||||
|
### Commun
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "token": "uuid-reprise" }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parent — champs à envoyer depuis le wizard
|
||||||
|
|
||||||
|
| Champ PATCH | Source wizard | Notes |
|
||||||
|
|-------------|---------------|-------|
|
||||||
|
| `prenom`, `nom`, `telephone`, `adresse`, `ville`, `code_postal` | Parent 1 (titulaire token) | Champs racine |
|
||||||
|
| `co_parent_prenom`, `co_parent_nom`, `co_parent_telephone` | Parent 2 | |
|
||||||
|
| `co_parent_meme_adresse`, `co_parent_adresse`, `co_parent_code_postal`, `co_parent_ville` | Parent 2 adresse | |
|
||||||
|
| `texte_motivation` **ou** `presentation_dossier` | Étape motivation | Les deux alias acceptés |
|
||||||
|
| `enfants[]` | Liste enfants | Voir ci-dessous |
|
||||||
|
|
||||||
|
**Structure `enfants[]` (miroir inscription + `id` obligatoire) :**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "uuid-enfant-existant",
|
||||||
|
"prenom": "Emma",
|
||||||
|
"nom": "MARTIN",
|
||||||
|
"date_naissance": "2023-02-15",
|
||||||
|
"date_previsionnelle_naissance": null,
|
||||||
|
"genre": "F",
|
||||||
|
"photo_base64": "data:image/jpeg;base64,…",
|
||||||
|
"photo_filename": "emma.jpg",
|
||||||
|
"grossesse_multiple": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **v1 back :** update par `id` uniquement — pas de création/suppression d’enfant.
|
||||||
|
- Si `id` inconnu pour ce dossier → **400** `Enfant inconnu pour ce dossier : {id}`.
|
||||||
|
- Sans nouvelle photo : ne pas envoyer `photo_base64` (l’existant est conservé).
|
||||||
|
|
||||||
|
### AM — champs à envoyer
|
||||||
|
|
||||||
|
| Champ PATCH | Source |
|
||||||
|
|-------------|--------|
|
||||||
|
| Identité + `photo_url` ou `photo_base64` + `photo_filename` | Étapes 1–2 |
|
||||||
|
| `consentement_photo`, `date_naissance`, `lieu_naissance_ville`, `lieu_naissance_pays` | Identité |
|
||||||
|
| `numero_agrement`, `nir`, `date_agrement` | Pro |
|
||||||
|
| `capacite_accueil`, `places_disponibles` | Pro |
|
||||||
|
| `biographie` | Présentation |
|
||||||
|
|
||||||
|
Validation NIR identique à l’inscription si `nir` fourni.
|
||||||
|
|
||||||
|
### Réponse succès (nouveau format)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Dossier resoumis avec succès. Il est de nouveau en attente de validation.",
|
||||||
|
"statut": "en_attente",
|
||||||
|
"user_id": "uuid",
|
||||||
|
"numero_dossier": "2026-000021"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Code HTTP : **200** (pas de corps `Users` brut comme l’ancien back).
|
||||||
|
|
||||||
|
### Effet métier
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Fichiers front à modifier (checklist)
|
||||||
|
|
||||||
|
### Modèles
|
||||||
|
|
||||||
|
- [ ] `lib/models/reprise_dossier.dart` — parser `parents[]`, `enfants[]`, `texte_motivation`, champs AM
|
||||||
|
- [ ] Réutiliser ou mapper vers `DossierFamilleEnfant` / structures existantes (#119 admin) si possible
|
||||||
|
|
||||||
|
### Session / préremplissage
|
||||||
|
|
||||||
|
- [ ] `lib/services/reprise_session.dart`
|
||||||
|
- `applyToParent` : remplir parent1/parent2 depuis `parents[]`, enfants, motivation
|
||||||
|
- `applyToAm` : remplir tous les champs AM
|
||||||
|
|
||||||
|
### API
|
||||||
|
|
||||||
|
- [ ] `lib/services/auth_service.dart` — `resoumettreReprise()` : accepter body complet (parent + AM), pas seulement identité
|
||||||
|
- [ ] Étendre `UserRegistrationData` / `AmRegistrationData` helpers `toReprisePatchBody()` si utile
|
||||||
|
|
||||||
|
### Écrans fin de parcours
|
||||||
|
|
||||||
|
- [ ] `parent_register_step5_screen.dart` — PATCH avec co-parent, enfants, motivation
|
||||||
|
- [ ] `am_register_step4_screen.dart` — PATCH avec fiche AM complète
|
||||||
|
|
||||||
|
### Hors scope back (inchangé)
|
||||||
|
|
||||||
|
RIB / IBAN / attestation CAF (étape 5 wizard parent) : **non persistés** — rien à envoyer en reprise.
|
||||||
|
|
||||||
|
### Non implémenté front (ticket #112 initial)
|
||||||
|
|
||||||
|
- [ ] Modale login « J’ai un numéro de dossier » → `POST /auth/reprise-identify` (back prêt, front absent)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Tests manuels suggérés
|
||||||
|
|
||||||
|
1. Refuser un dossier parent complet (≥1 enfant + co-parent + motivation).
|
||||||
|
2. Ouvrir le lien mail `/reprise?token=…`.
|
||||||
|
3. Vérifier dans DevTools que le GET contient `enfants[]` et `texte_motivation`.
|
||||||
|
4. Après branchement front : wizard prérempli sur toutes les étapes.
|
||||||
|
5. Resoumettre → statut `en_attente` pour les deux parents ; dossier visible file validation admin (#119).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Références code back
|
||||||
|
|
||||||
|
```
|
||||||
|
backend/src/routes/auth/dto/reprise-dossier.dto.ts
|
||||||
|
backend/src/routes/auth/dto/resoumettre-reprise.dto.ts
|
||||||
|
backend/src/routes/auth/dto/enfant-reprise.dto.ts
|
||||||
|
backend/src/routes/auth/auth.service.ts → getRepriseDossier, resoumettreReprise
|
||||||
|
backend/src/routes/parents/dto/dossier-famille-complet.dto.ts
|
||||||
|
```
|
||||||
Loading…
x
Reference in New Issue
Block a user