feat(am): alignement inscription/validation + BDD init

- Lieu naissance obligatoire (DTO), persistance Users + migration SQL
- BDD.sql: colonnes lieu_naissance_*, tables dossier_famille / dossier_famille_enfants
- inscrireAMComplet: places_available = capacite_accueil
- GET dossier AM: user.lieu_naissance_*, consentement_photo
- docs/TEMP_FRONT_alignement_AM.md pour équipe Flutter
- Parité: email accusé AM, RegisterAmResponseDto (numero_dossier)

Made-with: Cursor
This commit is contained in:
2026-04-13 13:14:38 +02:00
parent d69768806c
commit cdae9b5f7c
11 changed files with 169 additions and 14 deletions
+6
View File
@@ -138,6 +138,12 @@ export class Users {
@Column({ name: 'date_naissance', type: 'date', nullable: true })
date_naissance?: Date;
@Column({ name: 'lieu_naissance_ville', length: 100, nullable: true })
lieu_naissance_ville?: string;
@Column({ name: 'lieu_naissance_pays', length: 100, nullable: true })
lieu_naissance_pays?: string;
@CreateDateColumn({ name: 'cree_le', type: 'timestamptz' })
cree_le: Date;
+3 -3
View File
@@ -99,10 +99,10 @@ export class MailService {
}
/**
* Accusé de réception inscription parent : demande enregistrée + n° de dossier.
* Lutilisateur est en attente de validation ; pas de lien création MDP à ce stade.
* Accusé de réception inscription (parent ou assistante maternelle) :
* demande enregistrée + n° de dossier. En attente de validation ; pas de lien création MDP à ce stade.
*/
async sendParentRegistrationPendingEmail(
async sendRegistrationPendingEmail(
to: string,
prenom: string,
nom: string,
+3 -2
View File
@@ -5,6 +5,7 @@ import { Public } from 'src/common/decorators/public.decorator';
import { RegisterDto } from './dto/register.dto';
import { RegisterParentCompletDto } from './dto/register-parent-complet.dto';
import { RegisterAMCompletDto } from './dto/register-am-complet.dto';
import { RegisterAmResponseDto } from './dto/register-am-response.dto';
import { ChangePasswordRequiredDto } from './dto/change-password.dto';
import { ApiBearerAuth, ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
import { AuthGuard } from 'src/common/guards/auth.guard';
@@ -62,10 +63,10 @@ export class AuthController {
description:
'Crée User AM + entrée assistantes_maternelles (identité + infos pro + photo + CGU) en une transaction. Si photo_base64 est fourni sans photo_filename, le serveur utilise le nom par défaut photo_am.jpg (extension du fichier stocké = type du data-URL).',
})
@ApiResponse({ status: 201, description: 'Inscription réussie - Dossier en attente de validation' })
@ApiResponse({ status: 201, description: 'Inscription réussie - Dossier en attente de validation', type: RegisterAmResponseDto })
@ApiResponse({ status: 400, description: 'Données invalides ou CGU non acceptées' })
@ApiResponse({ status: 409, description: 'Email déjà utilisé' })
async inscrireAMComplet(@Body() dto: RegisterAMCompletDto) {
async inscrireAMComplet(@Body() dto: RegisterAMCompletDto): Promise<RegisterAmResponseDto> {
return this.authService.inscrireAMComplet(dto);
}
+22 -2
View File
@@ -376,14 +376,14 @@ export class AuthService {
const numeroDossier = resultat.parent1.numero_dossier ?? '';
try {
await this.mailService.sendParentRegistrationPendingEmail(
await this.mailService.sendRegistrationPendingEmail(
resultat.parent1.email,
resultat.parent1.prenom ?? '',
resultat.parent1.nom ?? '',
numeroDossier,
);
if (resultat.parent2) {
await this.mailService.sendParentRegistrationPendingEmail(
await this.mailService.sendRegistrationPendingEmail(
resultat.parent2.email,
resultat.parent2.prenom ?? '',
resultat.parent2.nom ?? '',
@@ -496,6 +496,8 @@ export class AuthService {
consentement_photo: dto.consentement_photo,
date_consentement_photo: dateConsentementPhoto,
date_naissance: dto.date_naissance ? new Date(dto.date_naissance) : undefined,
lieu_naissance_ville: dto.lieu_naissance_ville,
lieu_naissance_pays: dto.lieu_naissance_pays,
numero_dossier: numeroDossier,
});
const userEnregistre = await manager.save(Users, user);
@@ -506,6 +508,7 @@ export class AuthService {
approval_number: dto.numero_agrement,
nir: nirNormalized,
max_children: dto.capacite_accueil,
places_available: dto.capacite_accueil,
biography: dto.biographie,
residence_city: dto.ville ?? undefined,
agreement_date: dto.date_agrement ? new Date(dto.date_agrement) : undefined,
@@ -523,11 +526,28 @@ export class AuthService {
throw err;
}
const numeroDossier = resultat.user.numero_dossier ?? '';
try {
await this.mailService.sendRegistrationPendingEmail(
resultat.user.email,
resultat.user.prenom ?? '',
resultat.user.nom ?? '',
numeroDossier,
);
} catch (err) {
this.logger.error(
"[inscrireAMComplet] Échec envoi email d'accusé de réception (inscription conservée)",
err instanceof Error ? err.stack : String(err),
);
}
return {
message:
'Inscription réussie. Votre dossier est en attente de validation par un gestionnaire.',
user_id: resultat.user.id,
statut: StatutUtilisateurType.EN_ATTENTE,
numero_dossier: numeroDossier,
};
}
@@ -1,4 +1,5 @@
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import {
IsEmail,
IsNotEmpty,
@@ -96,17 +97,21 @@ export class RegisterAMCompletDto {
@IsDateString()
date_naissance?: string;
@ApiProperty({ example: 'Paris', required: false, description: 'Ville de naissance' })
@IsOptional()
@ApiProperty({ example: 'Paris', description: 'Ville de naissance (obligatoire)' })
@Transform(({ value }) => (typeof value === 'string' ? value.trim() : value))
@IsString()
@IsNotEmpty({ message: 'La ville de naissance est requise' })
@MinLength(2, { message: 'La ville de naissance doit contenir au moins 2 caractères' })
@MaxLength(100)
lieu_naissance_ville?: string;
lieu_naissance_ville: string;
@ApiProperty({ example: 'France', required: false, description: 'Pays de naissance' })
@IsOptional()
@ApiProperty({ example: 'France', description: 'Pays de naissance (obligatoire)' })
@Transform(({ value }) => (typeof value === 'string' ? value.trim() : value))
@IsString()
@IsNotEmpty({ message: 'Le pays de naissance est requis' })
@MinLength(2, { message: 'Le pays de naissance doit contenir au moins 2 caractères' })
@MaxLength(100)
lieu_naissance_pays?: string;
lieu_naissance_pays: string;
@ApiProperty({ example: '123456789012345', description: 'NIR 15 caractères (chiffres, ou 2A/2B pour la Corse)' })
@IsString()
@@ -0,0 +1,17 @@
import { ApiProperty } from '@nestjs/swagger';
import { StatutUtilisateurType } from 'src/entities/users.entity';
/** Réponse 201 POST /auth/register/am (alignée sur les champs clés de /auth/register/parent). */
export class RegisterAmResponseDto {
@ApiProperty()
message: string;
@ApiProperty({ format: 'uuid' })
user_id: string;
@ApiProperty({ enum: StatutUtilisateurType, example: StatutUtilisateurType.EN_ATTENTE })
statut: StatutUtilisateurType;
@ApiProperty({ example: '2026-000015', description: 'Numéro de dossier attribué à linscription' })
numero_dossier: string;
}
@@ -62,7 +62,23 @@ export class DossiersService {
throw new NotFoundException('Aucun dossier trouvé pour ce numéro.');
}
private toDossierAmUserDto(user: { id: string; email: string; prenom?: string; nom?: string; telephone?: string; adresse?: string; ville?: string; code_postal?: string; profession?: string; date_naissance?: Date; photo_url?: string; statut: any }): DossierAmUserDto {
private toDossierAmUserDto(user: {
id: string;
email: string;
prenom?: string;
nom?: string;
telephone?: string;
adresse?: string;
ville?: string;
code_postal?: string;
profession?: string;
date_naissance?: Date;
lieu_naissance_ville?: string;
lieu_naissance_pays?: string;
photo_url?: string;
consentement_photo?: boolean;
statut: any;
}): DossierAmUserDto {
return {
id: user.id,
email: user.email,
@@ -74,7 +90,10 @@ export class DossiersService {
code_postal: user.code_postal,
profession: user.profession,
date_naissance: user.date_naissance,
lieu_naissance_ville: user.lieu_naissance_ville,
lieu_naissance_pays: user.lieu_naissance_pays,
photo_url: user.photo_url,
consentement_photo: user.consentement_photo,
statut: user.statut,
};
}
@@ -23,8 +23,14 @@ export class DossierAmUserDto {
profession?: string;
@ApiProperty({ required: false })
date_naissance?: Date;
@ApiProperty({ required: false, description: 'Ville de naissance' })
lieu_naissance_ville?: string;
@ApiProperty({ required: false, description: 'Pays de naissance' })
lieu_naissance_pays?: string;
@ApiProperty({ required: false })
photo_url?: string;
@ApiProperty({ required: false, description: 'Consentement utilisation photo' })
consentement_photo?: boolean;
@ApiProperty({ enum: StatutUtilisateurType })
statut: StatutUtilisateurType;
}