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:
parent
d69768806c
commit
cdae9b5f7c
@ -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;
|
||||
|
||||
|
||||
@ -99,10 +99,10 @@ export class MailService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Accusé de réception inscription parent : demande enregistrée + n° de dossier.
|
||||
* L’utilisateur 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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
17
backend/src/routes/auth/dto/register-am-response.dto.ts
Normal file
17
backend/src/routes/auth/dto/register-am-response.dto.ts
Normal file
@ -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é à l’inscription' })
|
||||
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;
|
||||
}
|
||||
|
||||
@ -55,6 +55,8 @@ CREATE TABLE utilisateurs (
|
||||
telephone VARCHAR(20), -- Unifié (mobile privilégié)
|
||||
adresse TEXT,
|
||||
date_naissance DATE,
|
||||
lieu_naissance_ville VARCHAR(100),
|
||||
lieu_naissance_pays VARCHAR(100),
|
||||
photo_url TEXT, -- Obligatoire pour AM, non utilisé pour parents
|
||||
consentement_photo BOOLEAN DEFAULT false,
|
||||
date_consentement_photo TIMESTAMPTZ,
|
||||
@ -125,6 +127,33 @@ CREATE TABLE enfants_parents (
|
||||
PRIMARY KEY (id_parent, id_enfant)
|
||||
);
|
||||
|
||||
-- ==========================================================
|
||||
-- Table : dossier_famille (inscription parent, schéma simplifié — ticket #119)
|
||||
-- ==========================================================
|
||||
CREATE TABLE dossier_famille (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
numero_dossier VARCHAR(20) NOT NULL,
|
||||
id_parent UUID NOT NULL REFERENCES parents(id_utilisateur) ON DELETE CASCADE,
|
||||
presentation TEXT,
|
||||
statut statut_dossier_type NOT NULL DEFAULT 'envoye',
|
||||
cree_le TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
modifie_le TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_dossier_famille_numero ON dossier_famille(numero_dossier);
|
||||
CREATE INDEX idx_dossier_famille_id_parent ON dossier_famille(id_parent);
|
||||
|
||||
-- ==========================================================
|
||||
-- Table : dossier_famille_enfants
|
||||
-- ==========================================================
|
||||
CREATE TABLE dossier_famille_enfants (
|
||||
id_dossier_famille UUID NOT NULL REFERENCES dossier_famille(id) ON DELETE CASCADE,
|
||||
id_enfant UUID NOT NULL REFERENCES enfants(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (id_dossier_famille, id_enfant)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_dossier_famille_enfants_enfant ON dossier_famille_enfants(id_enfant);
|
||||
|
||||
-- ==========================================================
|
||||
-- Table : dossiers
|
||||
-- ==========================================================
|
||||
@ -376,6 +405,10 @@ ALTER TABLE utilisateurs ADD COLUMN IF NOT EXISTS token_reprise VARCHAR(255) NUL
|
||||
ALTER TABLE utilisateurs ADD COLUMN IF NOT EXISTS token_reprise_expire_le TIMESTAMPTZ NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_utilisateurs_token_reprise ON utilisateurs(token_reprise) WHERE token_reprise IS NOT NULL;
|
||||
|
||||
-- Lieu de naissance (aligné CREATE TABLE utilisateurs — idempotent si colonnes déjà présentes)
|
||||
ALTER TABLE utilisateurs ADD COLUMN IF NOT EXISTS lieu_naissance_ville VARCHAR(100) NULL;
|
||||
ALTER TABLE utilisateurs ADD COLUMN IF NOT EXISTS lieu_naissance_pays VARCHAR(100) NULL;
|
||||
|
||||
-- ==========================================================
|
||||
-- Seed : Documents légaux génériques v1
|
||||
-- ==========================================================
|
||||
|
||||
4
database/migrations/2026_lieu_naissance_utilisateurs.sql
Normal file
4
database/migrations/2026_lieu_naissance_utilisateurs.sql
Normal file
@ -0,0 +1,4 @@
|
||||
-- Lieu de naissance (inscription AM, affichage dossier / validation). Nullable pour l'historique.
|
||||
ALTER TABLE utilisateurs
|
||||
ADD COLUMN IF NOT EXISTS lieu_naissance_ville VARCHAR(100) NULL,
|
||||
ADD COLUMN IF NOT EXISTS lieu_naissance_pays VARCHAR(100) NULL;
|
||||
44
docs/TEMP_FRONT_alignement_AM.md
Normal file
44
docs/TEMP_FRONT_alignement_AM.md
Normal file
@ -0,0 +1,44 @@
|
||||
# TEMP — Alignement front / API (inscription AM & validation gestionnaire)
|
||||
|
||||
> **Fichier temporaire** : à supprimer ou renommer une fois le front livré.
|
||||
|
||||
Ce document décrit les changements **côté API** et ce que **Flutter** doit faire pour rester aligné. Aucune modification front n’a été faite dans le chantier backend associé.
|
||||
|
||||
## 1. `POST /auth/register/am` — lieu de naissance obligatoire
|
||||
|
||||
- **`lieu_naissance_ville`** et **`lieu_naissance_pays`** sont **obligatoires** (non vides après trim, min. **2 caractères** chacun, max 100).
|
||||
- Réponses **400** si manquants ou invalides (messages class-validator).
|
||||
- **Action front** : champs obligatoires dans le parcours AM (étapes identité / naissance), validation UI avant envoi ; afficher les erreurs renvoyées par l’API.
|
||||
|
||||
## 2. Réponse `GET /dossiers/:numeroDossier` (type `am`)
|
||||
|
||||
Sous `dossier.user`, l’API peut inclure :
|
||||
|
||||
| Clé JSON | Description |
|
||||
|----------|-------------|
|
||||
| `date_naissance` | Date (si renseignée à l’inscription) |
|
||||
| `lieu_naissance_ville` | Ville de naissance |
|
||||
| `lieu_naissance_pays` | Pays de naissance |
|
||||
| `consentement_photo` | Booléen (exposé dans `dossier.user`) |
|
||||
|
||||
À la **racine** de `dossier` (objet AM), champs déjà renvoyés par le backend : `disponible`, `annees_experience`, `specialite`, `nb_max_enfants`, `place_disponible`, etc.
|
||||
|
||||
**Action front** :
|
||||
|
||||
- Étendre **`AppUser.fromJson` / `toJson`** (`lib/models/user.dart`) pour mapper `date_naissance`, `lieu_naissance_ville`, `lieu_naissance_pays`, `consentement_photo`.
|
||||
- Étendre **`DossierAM.fromJson`** (`lib/models/dossier_unifie.dart`) pour parser `disponible`, `annees_experience`, `specialite` à la racine du dossier (noms snake_case comme dans la réponse JSON Nest).
|
||||
|
||||
## 3. `ValidationAmWizard` (admin)
|
||||
|
||||
Afficher pour cohérence avec le formulaire d’inscription :
|
||||
|
||||
- **Informations personnelles** : date de naissance, ville / pays de naissance, consentement photo (Oui/Non).
|
||||
- **Informations professionnelles** : disponibilité, années d’expérience, spécialité (afficher « – » si `null`).
|
||||
|
||||
## 4. `place_disponible` à l’inscription
|
||||
|
||||
- Le backend initialise **`place_disponible`** sur la fiche AM à la **même valeur** que **`capacite_accueil`** à la création. Le wizard peut donc afficher une valeur cohérente avec la capacité sans champ séparé côté public.
|
||||
|
||||
---
|
||||
|
||||
*Dernière mise à jour : alignement backend branche `feature/120-inscription-am-photo-backend`.*
|
||||
Loading…
x
Reference in New Issue
Block a user