feat(#156): POST /assistantes-maternelles/dossier staff (actif + mail MDP).
Factorise createAmDossier depuis register/am ; staff crée un dossier déjà actif et envoie l’e-mail de création de mot de passe. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+51
-2
@@ -1,20 +1,69 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AssistantesMaternellesController } from './assistantes_maternelles.controller';
|
||||
import { AssistantesMaternellesService } from './assistantes_maternelles.service';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { AuthGuard } from 'src/common/guards/auth.guard';
|
||||
import { RolesGuard } from 'src/common/guards/roles.guard';
|
||||
|
||||
describe('AssistantesMaternellesController', () => {
|
||||
let controller: AssistantesMaternellesController;
|
||||
const authServiceMock = {
|
||||
createAmDossierStaff: jest.fn(),
|
||||
};
|
||||
const amServiceMock = {};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AssistantesMaternellesController],
|
||||
providers: [AssistantesMaternellesService],
|
||||
}).compile();
|
||||
providers: [
|
||||
{ provide: AssistantesMaternellesService, useValue: amServiceMock },
|
||||
{ provide: AuthService, useValue: authServiceMock },
|
||||
],
|
||||
})
|
||||
.overrideGuard(AuthGuard)
|
||||
.useValue({ canActivate: () => true })
|
||||
.overrideGuard(RolesGuard)
|
||||
.useValue({ canActivate: () => true })
|
||||
.compile();
|
||||
|
||||
controller = module.get<AssistantesMaternellesController>(AssistantesMaternellesController);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
|
||||
it('createDossier delegates to authService.createAmDossierStaff with CGU accepted', async () => {
|
||||
authServiceMock.createAmDossierStaff.mockResolvedValue({
|
||||
message: 'ok',
|
||||
user_id: 'u1',
|
||||
statut: 'actif',
|
||||
numero_dossier: '2026-000001',
|
||||
});
|
||||
|
||||
const body = {
|
||||
email: 'am.staff@test.fr',
|
||||
prenom: 'Marie',
|
||||
nom: 'TEST',
|
||||
telephone: '0689567890',
|
||||
consentement_photo: false,
|
||||
lieu_naissance_ville: 'Paris',
|
||||
lieu_naissance_pays: 'France',
|
||||
nir: '285017512345678',
|
||||
numero_agrement: 'AGR-TEST-001',
|
||||
capacite_accueil: 3,
|
||||
places_disponibles: 2,
|
||||
};
|
||||
|
||||
const res = await controller.createDossier(body as any);
|
||||
expect(authServiceMock.createAmDossierStaff).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
email: body.email,
|
||||
acceptation_cgu: true,
|
||||
acceptation_privacy: true,
|
||||
}),
|
||||
);
|
||||
expect(res.numero_dossier).toBe('2026-000001');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
Param,
|
||||
Delete,
|
||||
UseGuards,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { AssistantesMaternellesService } from './assistantes_maternelles.service';
|
||||
import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
@@ -16,17 +18,49 @@ import { RoleType, Users } from 'src/entities/users.entity';
|
||||
import { CreateAssistanteDto } from '../user/dto/create_assistante.dto';
|
||||
import { UpdateAssistanteDto } from '../user/dto/update_assistante.dto';
|
||||
import { UpdateAmFicheAdminDto } from './dto/update-am-fiche-admin.dto';
|
||||
import { StaffCreateAmDossierDto } from './dto/staff-create-am-dossier.dto';
|
||||
import { StaffCreateAmDossierResponseDto } from './dto/staff-create-am-dossier-response.dto';
|
||||
import { RolesGuard } from 'src/common/guards/roles.guard';
|
||||
import { AuthGuard } from 'src/common/guards/auth.guard';
|
||||
import { User } from 'src/common/decorators/user.decorator';
|
||||
import { mapAmForApi, mapAmsForApi } from './assistantes_maternelles.mapper';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { RegisterAMCompletDto } from '../auth/dto/register-am-complet.dto';
|
||||
|
||||
@ApiTags("Assistantes Maternelles")
|
||||
@ApiBearerAuth('access-token')
|
||||
@UseGuards(AuthGuard, RolesGuard)
|
||||
@Controller('assistantes-maternelles')
|
||||
export class AssistantesMaternellesController {
|
||||
constructor(private readonly assistantesMaternellesService: AssistantesMaternellesService) { }
|
||||
constructor(
|
||||
private readonly assistantesMaternellesService: AssistantesMaternellesService,
|
||||
private readonly authService: AuthService,
|
||||
) { }
|
||||
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||
@Post('dossier')
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@ApiOperation({
|
||||
summary: 'Créer un dossier AM complet (staff) — ticket #156',
|
||||
description:
|
||||
'Crée user + fiche AM avec statut actif, n° dossier, et envoie l’e-mail de création de mot de passe. ' +
|
||||
'Ne pas utiliser POST /auth/register/am depuis le dashboard.',
|
||||
})
|
||||
@ApiBody({ type: StaffCreateAmDossierDto })
|
||||
@ApiResponse({ status: 201, type: StaffCreateAmDossierResponseDto })
|
||||
@ApiResponse({ status: 400, description: 'Validation métier / NIR' })
|
||||
@ApiResponse({ status: 403, description: 'Rôle non autorisé' })
|
||||
@ApiResponse({ status: 409, description: 'Email / NIR / agrément déjà pris' })
|
||||
async createDossier(
|
||||
@Body() dto: StaffCreateAmDossierDto,
|
||||
): Promise<StaffCreateAmDossierResponseDto> {
|
||||
const registerDto = {
|
||||
...dto,
|
||||
acceptation_cgu: true,
|
||||
acceptation_privacy: true,
|
||||
} as RegisterAMCompletDto;
|
||||
return this.authService.createAmDossierStaff(registerDto);
|
||||
}
|
||||
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
|
||||
@ApiOperation({ summary: 'Créer nounou' })
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { StatutUtilisateurType } from 'src/entities/users.entity';
|
||||
|
||||
/** Réponse 201 POST /assistantes-maternelles/dossier (#156). */
|
||||
export class StaffCreateAmDossierResponseDto {
|
||||
@ApiProperty()
|
||||
message: string;
|
||||
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
user_id: string;
|
||||
|
||||
@ApiProperty({
|
||||
enum: StatutUtilisateurType,
|
||||
example: StatutUtilisateurType.ACTIF,
|
||||
})
|
||||
statut: StatutUtilisateurType;
|
||||
|
||||
@ApiProperty({
|
||||
example: '2026-000042',
|
||||
description: 'Numéro de dossier attribué',
|
||||
})
|
||||
numero_dossier: string;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ApiPropertyOptional, OmitType } from '@nestjs/swagger';
|
||||
import { IsBoolean, IsOptional } from 'class-validator';
|
||||
import { RegisterAMCompletDto } from 'src/routes/auth/dto/register-am-complet.dto';
|
||||
|
||||
/**
|
||||
* Création dossier AM par staff (#156).
|
||||
* Mêmes champs que l'inscription publique, sans CGU/privacy obligatoires
|
||||
* (acceptées côté serveur pour le compte du gestionnaire).
|
||||
*/
|
||||
export class StaffCreateAmDossierDto extends OmitType(RegisterAMCompletDto, [
|
||||
'acceptation_cgu',
|
||||
'acceptation_privacy',
|
||||
] as const) {
|
||||
@ApiPropertyOptional({
|
||||
description: 'Ignoré côté staff (CGU acceptées serveur). Conservé pour compat éventuelle.',
|
||||
default: true,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
acceptation_cgu?: boolean;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Ignoré côté staff (privacy acceptée serveur).',
|
||||
default: true,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
acceptation_privacy?: boolean;
|
||||
}
|
||||
@@ -645,11 +645,28 @@ export class AuthService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Inscription Assistante Maternelle COMPLÈTE - Un seul endpoint (identité + pro + photo + CGU)
|
||||
* Crée User (role AM) + entrée assistantes_maternelles, token création MDP
|
||||
* Cœur partagé création dossier AM (#156).
|
||||
* - public : statut en_attente + mail pending
|
||||
* - staff : statut actif + mail création MDP
|
||||
*/
|
||||
async inscrireAMComplet(dto: RegisterAMCompletDto) {
|
||||
if (!dto.acceptation_cgu || !dto.acceptation_privacy) {
|
||||
async createAmDossier(
|
||||
dto: RegisterAMCompletDto,
|
||||
options: {
|
||||
statut: StatutUtilisateurType;
|
||||
sendPendingEmail: boolean;
|
||||
sendPasswordSetupEmail: boolean;
|
||||
requireCgu: boolean;
|
||||
logContext?: string;
|
||||
},
|
||||
): Promise<{
|
||||
message: string;
|
||||
user_id: string;
|
||||
statut: StatutUtilisateurType;
|
||||
numero_dossier: string;
|
||||
}> {
|
||||
const logCtx = options.logContext ?? 'createAmDossier';
|
||||
|
||||
if (options.requireCgu && (!dto.acceptation_cgu || !dto.acceptation_privacy)) {
|
||||
throw new BadRequestException(
|
||||
"L'acceptation des CGU et de la politique de confidentialité est obligatoire",
|
||||
);
|
||||
@@ -669,8 +686,7 @@ export class AuthService {
|
||||
throw new BadRequestException(nirValidation.error || 'NIR invalide');
|
||||
}
|
||||
if (nirValidation.warning) {
|
||||
// Warning uniquement : on ne bloque pas (AM souvent étrangères, DOM-TOM, Corse)
|
||||
console.warn('[inscrireAMComplet] NIR warning:', nirValidation.warning, 'email=', dto.email);
|
||||
console.warn(`[${logCtx}] NIR warning:`, nirValidation.warning, 'email=', dto.email);
|
||||
}
|
||||
|
||||
const existe = await this.usersService.findByEmailOrNull(dto.email);
|
||||
@@ -721,79 +737,142 @@ export class AuthService {
|
||||
let resultat: { user: Users };
|
||||
try {
|
||||
resultat = await this.usersRepo.manager.transaction(async (manager) => {
|
||||
const { numero: numeroDossier } = await this.numeroDossierService.getNextNumeroDossier(manager);
|
||||
const { numero: numeroDossier } =
|
||||
await this.numeroDossierService.getNextNumeroDossier(manager);
|
||||
|
||||
const user = manager.create(Users, {
|
||||
email: dto.email,
|
||||
prenom: dto.prenom,
|
||||
nom: dto.nom,
|
||||
role: RoleType.ASSISTANTE_MATERNELLE,
|
||||
statut: StatutUtilisateurType.EN_ATTENTE,
|
||||
telephone: dto.telephone,
|
||||
adresse: dto.adresse,
|
||||
code_postal: dto.code_postal,
|
||||
ville: dto.ville,
|
||||
token_creation_mdp: tokenCreationMdp,
|
||||
token_creation_mdp_expire_le: dateExpiration,
|
||||
photo_url: urlPhoto ?? undefined,
|
||||
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 user = manager.create(Users, {
|
||||
email: dto.email,
|
||||
prenom: dto.prenom,
|
||||
nom: dto.nom,
|
||||
role: RoleType.ASSISTANTE_MATERNELLE,
|
||||
statut: options.statut,
|
||||
telephone: dto.telephone,
|
||||
adresse: dto.adresse,
|
||||
code_postal: dto.code_postal,
|
||||
ville: dto.ville,
|
||||
token_creation_mdp: tokenCreationMdp,
|
||||
token_creation_mdp_expire_le: dateExpiration,
|
||||
photo_url: urlPhoto ?? undefined,
|
||||
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);
|
||||
|
||||
const amRepo = manager.getRepository(AssistanteMaternelle);
|
||||
const am = amRepo.create({
|
||||
user_id: userEnregistre.id,
|
||||
approval_number: dto.numero_agrement,
|
||||
nir: nirNormalized,
|
||||
max_children: dto.capacite_accueil,
|
||||
places_available: dto.places_disponibles,
|
||||
biography: dto.biographie,
|
||||
residence_city: dto.ville ?? undefined,
|
||||
agreement_date: dto.date_agrement
|
||||
? new Date(dto.date_agrement)
|
||||
: undefined,
|
||||
available: true,
|
||||
numero_dossier: numeroDossier,
|
||||
});
|
||||
await amRepo.save(am);
|
||||
|
||||
return { user: userEnregistre };
|
||||
});
|
||||
const userEnregistre = await manager.save(Users, user);
|
||||
|
||||
const amRepo = manager.getRepository(AssistanteMaternelle);
|
||||
const am = amRepo.create({
|
||||
user_id: userEnregistre.id,
|
||||
approval_number: dto.numero_agrement,
|
||||
nir: nirNormalized,
|
||||
max_children: dto.capacite_accueil,
|
||||
places_available: dto.places_disponibles,
|
||||
biography: dto.biographie,
|
||||
residence_city: dto.ville ?? undefined,
|
||||
agreement_date: dto.date_agrement ? new Date(dto.date_agrement) : undefined,
|
||||
available: true,
|
||||
numero_dossier: numeroDossier,
|
||||
});
|
||||
await amRepo.save(am);
|
||||
|
||||
return { user: userEnregistre };
|
||||
});
|
||||
} catch (err) {
|
||||
if (this.isPostgresUniqueViolation(err)) {
|
||||
throw new ConflictException('Un compte avec cet email existe déjà (contrainte unique en base).');
|
||||
throw new ConflictException(
|
||||
'Un compte avec cet email existe déjà (contrainte unique en base).',
|
||||
);
|
||||
}
|
||||
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),
|
||||
);
|
||||
if (options.sendPendingEmail) {
|
||||
try {
|
||||
await this.mailService.sendRegistrationPendingEmail(
|
||||
resultat.user.email,
|
||||
resultat.user.prenom ?? '',
|
||||
resultat.user.nom ?? '',
|
||||
numeroDossier,
|
||||
);
|
||||
} catch (err) {
|
||||
this.logger.error(
|
||||
`[${logCtx}] Échec envoi email d'accusé de réception (inscription conservée)`,
|
||||
err instanceof Error ? err.stack : String(err),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.sendPasswordSetupEmail) {
|
||||
try {
|
||||
await this.mailService.sendValidatedAccountPasswordSetupEmail(
|
||||
{
|
||||
email: resultat.user.email,
|
||||
prenom: resultat.user.prenom ?? '',
|
||||
nom: resultat.user.nom ?? '',
|
||||
token: tokenCreationMdp,
|
||||
numeroDossier,
|
||||
},
|
||||
'am',
|
||||
);
|
||||
} catch (err) {
|
||||
this.logger.error(
|
||||
`[${logCtx}] Échec envoi email création MDP (dossier conservé)`,
|
||||
err instanceof Error ? err.stack : String(err),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const message =
|
||||
options.statut === StatutUtilisateurType.ACTIF
|
||||
? 'Dossier AM créé et validé. Un e-mail de création de mot de passe a été envoyé.'
|
||||
: 'Inscription réussie. Votre dossier est en attente de validation par un gestionnaire.';
|
||||
|
||||
return {
|
||||
message:
|
||||
'Inscription réussie. Votre dossier est en attente de validation par un gestionnaire.',
|
||||
message,
|
||||
user_id: resultat.user.id,
|
||||
statut: StatutUtilisateurType.EN_ATTENTE,
|
||||
statut: options.statut,
|
||||
numero_dossier: numeroDossier,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Inscription Assistante Maternelle COMPLÈTE - Un seul endpoint (identité + pro + photo + CGU)
|
||||
* Crée User (role AM) + entrée assistantes_maternelles, token création MDP
|
||||
*/
|
||||
async inscrireAMComplet(dto: RegisterAMCompletDto) {
|
||||
return this.createAmDossier(dto, {
|
||||
statut: StatutUtilisateurType.EN_ATTENTE,
|
||||
sendPendingEmail: true,
|
||||
sendPasswordSetupEmail: false,
|
||||
requireCgu: true,
|
||||
logContext: 'inscrireAMComplet',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Création dossier AM par staff (#156) — statut actif + e-mail création MDP.
|
||||
*/
|
||||
async createAmDossierStaff(dto: RegisterAMCompletDto) {
|
||||
return this.createAmDossier(dto, {
|
||||
statut: StatutUtilisateurType.ACTIF,
|
||||
sendPendingEmail: false,
|
||||
sendPasswordSetupEmail: true,
|
||||
requireCgu: false,
|
||||
logContext: 'createAmDossierStaff',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sauvegarde une photo depuis base64 vers le système de fichiers
|
||||
*/
|
||||
/**
|
||||
* Sauvegarde une photo depuis base64 vers le système de fichiers
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user