feat(#159): suppressions métier dossiers/parents/enfants/AM/staff.

SuppressionService + DELETE /dossiers/:numero, cascades DELETE /users et
DELETE /enfants?deleteDossier, flag sans_enfant, specs front #160.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-09 18:01:44 +02:00
co-authored by Cursor
parent c8c9cfbc4d
commit 7f23356273
17 changed files with 970 additions and 19 deletions
@@ -1,6 +1,7 @@
import { Test, TestingModule } from '@nestjs/testing';
import { DossiersController } from './dossiers.controller';
import { DossiersService } from './dossiers.service';
import { SuppressionService } from '../suppressions/suppression.service';
import { AuthGuard } from 'src/common/guards/auth.guard';
import { RolesGuard } from 'src/common/guards/roles.guard';
import { StatutUtilisateurType } from 'src/entities/users.entity';
@@ -11,11 +12,17 @@ describe('DossiersController', () => {
listDossiers: jest.fn(),
getDossierByNumero: jest.fn(),
};
const suppressionServiceMock = {
deleteDossier: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [DossiersController],
providers: [{ provide: DossiersService, useValue: dossiersServiceMock }],
providers: [
{ provide: DossiersService, useValue: dossiersServiceMock },
{ provide: SuppressionService, useValue: suppressionServiceMock },
],
})
.overrideGuard(AuthGuard)
.useValue({ canActivate: () => true })
@@ -60,4 +67,19 @@ describe('DossiersController', () => {
expect(dossiersServiceMock.getDossierByNumero).toHaveBeenCalledWith('2026-000001');
expect(res.type).toBe('family');
});
it('remove delegates to suppressionService.deleteDossier', async () => {
const user = { id: 'u1', role: 'gestionnaire' } as never;
suppressionServiceMock.deleteDossier.mockResolvedValue({
type: 'famille',
deleted_user_ids: [],
deleted_enfant_ids: [],
message: 'ok',
});
await controller.remove('2026-000001', user);
expect(suppressionServiceMock.deleteDossier).toHaveBeenCalledWith(
'2026-000001',
user,
);
});
});
@@ -1,4 +1,11 @@
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
import {
Controller,
Delete,
Get,
Param,
Query,
UseGuards,
} from '@nestjs/common';
import {
ApiBearerAuth,
ApiOperation,
@@ -8,10 +15,12 @@ import {
ApiTags,
} from '@nestjs/swagger';
import { Roles } from 'src/common/decorators/roles.decorator';
import { RoleType } from 'src/entities/users.entity';
import { RoleType, Users } from 'src/entities/users.entity';
import { AuthGuard } from 'src/common/guards/auth.guard';
import { RolesGuard } from 'src/common/guards/roles.guard';
import { User } from 'src/common/decorators/user.decorator';
import { DossiersService } from './dossiers.service';
import { SuppressionService } from '../suppressions/suppression.service';
import { DossierUnifieDto } from './dto/dossier-unifie.dto';
import { DossierListItemDto } from './dto/dossier-list-item.dto';
@@ -20,7 +29,10 @@ import { DossierListItemDto } from './dto/dossier-list-item.dto';
@Controller('dossiers')
@UseGuards(AuthGuard, RolesGuard)
export class DossiersController {
constructor(private readonly dossiersService: DossiersService) {}
constructor(
private readonly dossiersService: DossiersService,
private readonly suppressionService: SuppressionService,
) {}
@Get()
@Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE)
@@ -28,7 +40,8 @@ export class DossiersController {
summary: 'Liste unifiée des dossiers (familles + AM) — ticket #153',
description:
'1 entrée = 1 numero_dossier. Types `famille` | `assistante_maternelle`. ' +
'Filtre optionnel `q` (n°, nom, email). Tri : à valider dabord, puis n° décroissant.',
'Filtre optionnel `q` (n°, nom, email). Tri : à valider dabord, puis n° décroissant. ' +
'`sans_enfant` (#159) pour dossiers famille sans enfant.',
})
@ApiQuery({
name: 'q',
@@ -51,4 +64,21 @@ export class DossiersController {
getDossier(@Param('numeroDossier') numeroDossier: string): Promise<DossierUnifieDto> {
return this.dossiersService.getDossierByNumero(numeroDossier);
}
@Delete(':numeroDossier')
@Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE)
@ApiOperation({
summary: 'Supprimer un dossier (famille ou AM) — #159',
description:
'Famille : parents + enfants. AM : compte AM + dossier AM (enfants conservés, placements clos).',
})
@ApiParam({ name: 'numeroDossier', description: 'Numéro de dossier' })
@ApiResponse({ status: 200, description: 'Résultat de suppression' })
@ApiResponse({ status: 404, description: 'Dossier introuvable' })
remove(
@Param('numeroDossier') numeroDossier: string,
@User() currentUser: Users,
) {
return this.suppressionService.deleteDossier(numeroDossier, currentUser);
}
}
@@ -5,6 +5,7 @@ import { JwtModule } from '@nestjs/jwt';
import { Parents } from 'src/entities/parents.entity';
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
import { ParentsModule } from '../parents/parents.module';
import { SuppressionsModule } from '../suppressions/suppressions.module';
import { DossiersController } from './dossiers.controller';
import { DossiersService } from './dossiers.service';
@@ -12,6 +13,7 @@ import { DossiersService } from './dossiers.service';
imports: [
TypeOrmModule.forFeature([Parents, AssistanteMaternelle]),
ParentsModule,
SuppressionsModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
@@ -4,6 +4,7 @@ import { DossiersService } from './dossiers.service';
import { Parents } from 'src/entities/parents.entity';
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
import { ParentsService } from '../parents/parents.service';
import { SuppressionService } from '../suppressions/suppression.service';
import { StatutUtilisateurType } from 'src/entities/users.entity';
describe('DossiersService.listDossiers', () => {
@@ -32,6 +33,9 @@ describe('DossiersService.listDossiers', () => {
const parentsService = {
getDossierFamilleByNumero: jest.fn(),
};
const suppressionService = {
countEnfantsForNumero: jest.fn().mockResolvedValue(1),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
@@ -40,6 +44,7 @@ describe('DossiersService.listDossiers', () => {
{ provide: getRepositoryToken(Parents), useValue: parentsRepo },
{ provide: getRepositoryToken(AssistanteMaternelle), useValue: amRepo },
{ provide: ParentsService, useValue: parentsService },
{ provide: SuppressionService, useValue: suppressionService },
],
}).compile();
@@ -47,6 +52,7 @@ describe('DossiersService.listDossiers', () => {
jest.clearAllMocks();
parentsRepo.createQueryBuilder.mockReturnValue(parentsQb);
amRepo.createQueryBuilder.mockReturnValue(amQb);
suppressionService.countEnfantsForNumero.mockResolvedValue(1);
});
it('aggregates famille (pivot+co-parent) and AM, sorts a_valider first', async () => {
@@ -5,12 +5,13 @@ import { Parents } from 'src/entities/parents.entity';
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
import { StatutUtilisateurType, Users } from 'src/entities/users.entity';
import { ParentsService } from '../parents/parents.service';
import { SuppressionService } from '../suppressions/suppression.service';
import { DossierUnifieDto } from './dto/dossier-unifie.dto';
import { DossierAmCompletDto, DossierAmUserDto } from './dto/dossier-am-complet.dto';
import { DossierListItemDto } from './dto/dossier-list-item.dto';
/**
* Dossiers unifiés — détail (#119) + liste (#153).
* Dossiers unifiés — détail (#119) + liste (#153) + sans_enfant (#159).
*/
@Injectable()
export class DossiersService {
@@ -20,6 +21,7 @@ export class DossiersService {
@InjectRepository(AssistanteMaternelle)
private readonly amRepository: Repository<AssistanteMaternelle>,
private readonly parentsService: ParentsService,
private readonly suppressionService: SuppressionService,
) {}
/**
@@ -43,6 +45,17 @@ export class DossiersService {
return b.numero_dossier.localeCompare(a.numero_dossier, 'fr');
});
for (const item of filtered) {
if (item.type === 'famille') {
const n = await this.suppressionService.countEnfantsForNumero(
item.numero_dossier,
);
item.sans_enfant = n === 0;
} else {
item.sans_enfant = false;
}
}
return filtered;
}
@@ -49,4 +49,10 @@ export class DossierListItemDto {
description: 'Date de référence (MIN cree_le des users du dossier)',
})
date_reference: string | null;
@ApiPropertyOptional({
description:
'True si dossier famille sans enfant lié (#159). Omis ou false pour AM.',
})
sans_enfant?: boolean;
}