diff --git a/src/machines/machines.controller.ts b/src/machines/machines.controller.ts index 4988f4b..60dc435 100644 --- a/src/machines/machines.controller.ts +++ b/src/machines/machines.controller.ts @@ -1,6 +1,10 @@ import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common'; import { MachinesService } from './machines.service'; -import { CreateMachineDto, UpdateMachineDto } from '../shared/dto/machine.dto'; +import { + CreateMachineDto, + UpdateMachineDto, + ReconfigureMachineDto, +} from '../shared/dto/machine.dto'; @Controller('machines') export class MachinesController { @@ -26,6 +30,14 @@ export class MachinesController { return this.machinesService.update(id, updateMachineDto); } + @Patch(':id/skeleton') + reconfigure( + @Param('id') id: string, + @Body() reconfigureMachineDto: ReconfigureMachineDto, + ) { + return this.machinesService.reconfigure(id, reconfigureMachineDto); + } + @Delete(':id') remove(@Param('id') id: string) { return this.machinesService.remove(id); diff --git a/src/machines/machines.service.ts b/src/machines/machines.service.ts index 5ea07c6..5cd4262 100644 --- a/src/machines/machines.service.ts +++ b/src/machines/machines.service.ts @@ -1,53 +1,125 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; -import { - CreateMachineDto, +import { + CreateMachineDto, UpdateMachineDto, + ReconfigureMachineDto, MachineComponentSelectionDto, - MachinePieceSelectionDto + MachinePieceSelectionDto, } from '../shared/dto/machine.dto'; +const TYPE_MACHINE_CONFIGURATION_INCLUDE = { + customFields: true, + componentRequirements: { + include: { + typeComposant: true, + }, + }, + pieceRequirements: { + include: { + typePiece: true, + }, + }, +}; + +const MACHINE_DEFAULT_INCLUDE = { + site: true, + typeMachine: { + include: TYPE_MACHINE_CONFIGURATION_INCLUDE, + }, + constructeur: true, + composants: { + include: { + typeComposant: true, + composantModel: true, + typeMachineComponentRequirement: { + include: { + typeComposant: true, + }, + }, + sousComposants: true, + customFieldValues: { + include: { + customField: true, + }, + }, + constructeur: true, + pieces: { + include: { + customFieldValues: { + include: { + customField: true, + }, + }, + constructeur: true, + pieceModel: true, + typeMachinePieceRequirement: { + include: { + typePiece: true, + }, + }, + }, + }, + }, + }, + pieces: { + include: { + customFieldValues: { + include: { + customField: true, + }, + }, + constructeur: true, + pieceModel: true, + typeMachinePieceRequirement: { + include: { + typePiece: true, + }, + }, + }, + }, + customFieldValues: { + include: { + customField: true, + }, + }, + documents: true, +}; + @Injectable() export class MachinesService { constructor(private prisma: PrismaService) {} - async create(createMachineDto: CreateMachineDto) { - const { - componentSelections = [], - pieceSelections = [], - ...machineData - } = createMachineDto; - - if (!machineData.typeMachineId) { - throw new Error('typeMachineId est requis pour créer une machine à partir d\'un squelette.'); - } - + private async getTypeMachineConfiguration(typeMachineId: string) { const typeMachine = await this.prisma.typeMachine.findUnique({ - where: { id: machineData.typeMachineId }, - include: { - customFields: true, - componentRequirements: { - include: { - typeComposant: true, - }, - }, - pieceRequirements: { - include: { - typePiece: true, - }, - }, - }, + where: { id: typeMachineId }, + include: TYPE_MACHINE_CONFIGURATION_INCLUDE, }); if (!typeMachine) { throw new Error('Type de machine non trouvé'); } + return typeMachine; + } + + private async buildConfigurationContext( + typeMachine: any, + componentSelections: MachineComponentSelectionDto[], + pieceSelections: MachinePieceSelectionDto[], + ) { + const componentRequirements = (Array.isArray(typeMachine.componentRequirements) + ? typeMachine.componentRequirements + : []) as any[]; + const pieceRequirements = (Array.isArray(typeMachine.pieceRequirements) + ? typeMachine.pieceRequirements + : []) as any[]; + const componentRequirementMap = new Map( - typeMachine.componentRequirements.map((requirement) => [requirement.id, requirement]), + componentRequirements.map((requirement: any) => [requirement.id, requirement]), ); const pieceRequirementMap = new Map( - typeMachine.pieceRequirements.map((requirement) => [requirement.id, requirement]), + pieceRequirements.map((requirement: any) => [requirement.id, requirement]), ); const componentSelectionMap = new Map(); @@ -94,7 +166,7 @@ export class MachinesService { : []; const pieceModelMap = new Map(pieceModels.map((model) => [model.id, model])); - for (const requirement of typeMachine.componentRequirements) { + for (const requirement of componentRequirements) { const selections = componentSelectionMap.get(requirement.id) ?? []; const min = requirement.minCount ?? (requirement.required ? 1 : 0); const max = requirement.maxCount ?? undefined; @@ -121,7 +193,7 @@ export class MachinesService { } } - for (const requirement of typeMachine.pieceRequirements) { + for (const requirement of pieceRequirements) { const selections = pieceSelectionMap.get(requirement.id) ?? []; const min = requirement.minCount ?? (requirement.required ? 1 : 0); const max = requirement.maxCount ?? undefined; @@ -186,7 +258,42 @@ export class MachinesService { } } - return await this.prisma.$transaction(async (prisma) => { + return { + componentSelectionMap, + pieceSelectionMap, + componentModelMap, + pieceModelMap, + }; + } + + async create(createMachineDto: CreateMachineDto) { + const { + componentSelections = [], + pieceSelections = [], + ...machineData + } = createMachineDto; + + if (!machineData.typeMachineId) { + throw new Error('typeMachineId est requis pour créer une machine à partir d\'un squelette.'); + } + + const typeMachine = await this.getTypeMachineConfiguration(machineData.typeMachineId); + + const { + componentSelectionMap, + pieceSelectionMap, + componentModelMap, + pieceModelMap, + } = await this.buildConfigurationContext(typeMachine, componentSelections, pieceSelections); + + const componentRequirements = (Array.isArray(typeMachine.componentRequirements) + ? typeMachine.componentRequirements + : []) as any[]; + const pieceRequirements = (Array.isArray(typeMachine.pieceRequirements) + ? typeMachine.pieceRequirements + : []) as any[]; + + return this.prisma.$transaction(async (prisma) => { const machine = await prisma.machine.create({ data: machineData, include: { @@ -196,8 +303,8 @@ export class MachinesService { }, }); - if (typeMachine.componentRequirements.length > 0) { - for (const requirement of typeMachine.componentRequirements) { + if (componentRequirements.length > 0) { + for (const requirement of componentRequirements) { const selections = componentSelectionMap.get(requirement.id) ?? []; for (const selection of selections) { const model = selection.componentModelId ? componentModelMap.get(selection.componentModelId) : undefined; @@ -212,8 +319,8 @@ export class MachinesService { } } - if (typeMachine.pieceRequirements.length > 0) { - for (const requirement of typeMachine.pieceRequirements) { + if (pieceRequirements.length > 0) { + for (const requirement of pieceRequirements) { const selections = pieceSelectionMap.get(requirement.id) ?? []; for (const selection of selections) { const model = selection.pieceModelId ? pieceModelMap.get(selection.pieceModelId) : undefined; @@ -234,76 +341,7 @@ export class MachinesService { return prisma.machine.findUnique({ where: { id: machine.id }, - include: { - site: true, - typeMachine: { - include: { - customFields: true, - componentRequirements: { - include: { - typeComposant: true, - }, - }, - pieceRequirements: { - include: { - typePiece: true, - }, - }, - }, - }, - constructeur: true, - composants: { - include: { - typeComposant: true, - composantModel: true, - typeMachineComponentRequirement: { - include: { - typeComposant: true, - }, - }, - sousComposants: true, - pieces: { - include: { - customFieldValues: { - include: { - customField: true, - }, - }, - constructeur: true, - pieceModel: true, - typeMachinePieceRequirement: { - include: { - typePiece: true, - }, - }, - }, - }, - constructeur: true, - }, - }, - pieces: { - include: { - customFieldValues: { - include: { - customField: true, - }, - }, - constructeur: true, - pieceModel: true, - typeMachinePieceRequirement: { - include: { - typePiece: true, - }, - }, - }, - }, - customFieldValues: { - include: { - customField: true, - }, - }, - documents: true, - }, + include: MACHINE_DEFAULT_INCLUDE, }); }); } @@ -660,239 +698,146 @@ export class MachinesService { async findAll() { return this.prisma.machine.findMany({ - include: { - site: true, - typeMachine: { - include: { - customFields: true, - componentRequirements: { - include: { - typeComposant: true, - }, - }, - pieceRequirements: { - include: { - typePiece: true, - }, - }, - }, - }, - constructeur: true, - composants: { - include: { - typeComposant: true, - composantModel: true, - typeMachineComponentRequirement: { - include: { - typeComposant: true, - }, - }, - sousComposants: true, - customFieldValues: { - include: { - customField: true, - }, - }, - constructeur: true, - pieces: { - include: { - customFieldValues: { - include: { - customField: true, - }, - }, - constructeur: true, - pieceModel: true, - typeMachinePieceRequirement: { - include: { - typePiece: true, - }, - }, - }, - }, - }, - }, - pieces: { - include: { - customFieldValues: { - include: { - customField: true, - }, - }, - constructeur: true, - pieceModel: true, - typeMachinePieceRequirement: { - include: { - typePiece: true, - }, - }, - }, - }, - customFieldValues: { - include: { - customField: true, - }, - }, - documents: true, - }, + include: MACHINE_DEFAULT_INCLUDE, }); } async findOne(id: string) { return this.prisma.machine.findUnique({ + where: { id }, + include: MACHINE_DEFAULT_INCLUDE, + }); + } + + async reconfigure(id: string, reconfigureMachineDto: ReconfigureMachineDto) { + const { + componentSelections = [], + pieceSelections = [], + } = reconfigureMachineDto; + + const machine = await this.prisma.machine.findUnique({ where: { id }, include: { - site: true, typeMachine: { - include: { - customFields: true, - componentRequirements: { - include: { - typeComposant: true, - }, - }, - pieceRequirements: { - include: { - typePiece: true, - }, - }, - }, + include: TYPE_MACHINE_CONFIGURATION_INCLUDE, }, - constructeur: true, - composants: { - include: { - typeComposant: true, - composantModel: true, - typeMachineComponentRequirement: { - include: { - typeComposant: true, - }, - }, - sousComposants: true, - customFieldValues: { - include: { - customField: true, - }, - }, - constructeur: true, - pieces: { - include: { - customFieldValues: { - include: { - customField: true, - }, - }, - constructeur: true, - pieceModel: true, - typeMachinePieceRequirement: { - include: { - typePiece: true, - }, - }, - }, - }, - }, - }, - pieces: { - include: { - customFieldValues: { - include: { - customField: true, - }, - }, - constructeur: true, - pieceModel: true, - typeMachinePieceRequirement: { - include: { - typePiece: true, - }, - }, - }, - }, - customFieldValues: { - include: { - customField: true, - }, - }, - documents: true, }, }); + + if (!machine) { + throw new Error('Machine non trouvée'); + } + + if (!machine.typeMachineId || !machine.typeMachine) { + throw new Error('Impossible de reconfigurer une machine sans type de machine associé.'); + } + + const typeMachine = machine.typeMachine; + + const { + componentSelectionMap, + pieceSelectionMap, + componentModelMap, + pieceModelMap, + } = await this.buildConfigurationContext(typeMachine, componentSelections, pieceSelections); + + const componentRequirements = (Array.isArray(typeMachine.componentRequirements) + ? typeMachine.componentRequirements + : []) as any[]; + const pieceRequirements = (Array.isArray(typeMachine.pieceRequirements) + ? typeMachine.pieceRequirements + : []) as any[]; + + return this.prisma.$transaction(async (prisma) => { + await prisma.customFieldValue.deleteMany({ + where: { + OR: [ + { + composant: { + machineId: id, + typeMachineComponentRequirementId: { not: null }, + }, + }, + { + piece: { + machineId: id, + typeMachinePieceRequirementId: { not: null }, + }, + }, + { + piece: { + composant: { + machineId: id, + typeMachineComponentRequirementId: { not: null }, + }, + }, + }, + ], + }, + }); + + await prisma.piece.deleteMany({ + where: { + machineId: id, + typeMachinePieceRequirementId: { not: null }, + }, + }); + + await prisma.composant.deleteMany({ + where: { + machineId: id, + typeMachineComponentRequirementId: { not: null }, + }, + }); + + if (componentRequirements.length > 0) { + for (const requirement of componentRequirements) { + const selections = componentSelectionMap.get(requirement.id) ?? []; + for (const selection of selections) { + const model = selection.componentModelId + ? componentModelMap.get(selection.componentModelId) + : undefined; + const definition = this.normalizeComponentSelection(selection, requirement, model); + await this.createComponentsFromType(prisma, id, [definition]); + } + } + } else { + const legacyComponents = (typeMachine as any).components; + if (legacyComponents) { + await this.createComponentsFromType(prisma, id, legacyComponents); + } + } + + if (pieceRequirements.length > 0) { + for (const requirement of pieceRequirements) { + const selections = pieceSelectionMap.get(requirement.id) ?? []; + for (const selection of selections) { + const model = selection.pieceModelId + ? pieceModelMap.get(selection.pieceModelId) + : undefined; + const definition = this.normalizePieceSelection(selection, requirement, model); + await this.createMachinePiecesFromType(prisma, id, [definition]); + } + } + } else { + const legacyPieces = (typeMachine as any).machinePieces; + if (legacyPieces) { + await this.createMachinePiecesFromType(prisma, id, legacyPieces); + } + } + + return prisma.machine.findUnique({ + where: { id }, + include: MACHINE_DEFAULT_INCLUDE, + }); + }); } async update(id: string, updateMachineDto: UpdateMachineDto) { return this.prisma.machine.update({ where: { id }, data: updateMachineDto, - include: { - site: true, - typeMachine: { - include: { - customFields: true, - componentRequirements: { - include: { - typeComposant: true, - }, - }, - pieceRequirements: { - include: { - typePiece: true, - }, - }, - }, - }, - constructeur: true, - composants: { - include: { - typeComposant: true, - composantModel: true, - typeMachineComponentRequirement: { - include: { - typeComposant: true, - }, - }, - sousComposants: true, - constructeur: true, - pieces: { - include: { - constructeur: true, - pieceModel: true, - typeMachinePieceRequirement: { - include: { - typePiece: true, - }, - }, - customFieldValues: { - include: { - customField: true, - }, - }, - }, - }, - }, - }, - pieces: { - include: { - constructeur: true, - pieceModel: true, - typeMachinePieceRequirement: { - include: { - typePiece: true, - }, - }, - customFieldValues: { - include: { - customField: true, - }, - }, - }, - }, - customFieldValues: { - include: { - customField: true, - }, - }, - documents: true, - }, + include: MACHINE_DEFAULT_INCLUDE, }); } diff --git a/src/shared/dto/machine.dto.ts b/src/shared/dto/machine.dto.ts index 01f2168..a798000 100644 --- a/src/shared/dto/machine.dto.ts +++ b/src/shared/dto/machine.dto.ts @@ -90,4 +90,18 @@ export class UpdateMachineDto { @IsOptional() @IsString() typeMachineId?: string; -} +} + +export class ReconfigureMachineDto { + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => MachineComponentSelectionDto) + componentSelections?: MachineComponentSelectionDto[]; + + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => MachinePieceSelectionDto) + pieceSelections?: MachinePieceSelectionDto[]; +} diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts index 4df6580..c20dbd2 100644 --- a/test/app.e2e-spec.ts +++ b/test/app.e2e-spec.ts @@ -3,17 +3,31 @@ import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; import { App } from 'supertest/types'; import { AppModule } from './../src/app.module'; +import { PrismaService } from '../src/prisma/prisma.service'; describe('AppController (e2e)', () => { let app: INestApplication; + let prisma: PrismaService; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); await app.init(); + + prisma = app.get(PrismaService); + }); + + afterAll(async () => { + await app.close(); + }); + + beforeEach(async () => { + await prisma.$executeRawUnsafe( + 'TRUNCATE TABLE custom_field_values, documents, pieces, composants, machines, composant_models, piece_models, type_machine_component_requirements, type_machine_piece_requirements, custom_fields, type_machines, type_composants, type_pieces, constructeurs, sites RESTART IDENTITY CASCADE', + ); }); it('/ (GET)', () => { @@ -22,4 +36,256 @@ describe('AppController (e2e)', () => { .expect(200) .expect('Hello World!'); }); + + describe('Machines skeleton reconfiguration', () => { + it('reconfigures machine skeleton according to updated requirements', async () => { + const site = await prisma.site.create({ + data: { + name: 'Site principal', + contactName: 'Jane Doe', + contactPhone: '0102030405', + contactAddress: '1 rue Principale', + contactPostalCode: '75000', + contactCity: 'Paris', + }, + }); + + const typeComposant = await prisma.typeComposant.create({ + data: { + name: 'Module', + description: 'Module principal', + }, + }); + + await prisma.customField.create({ + data: { + name: 'Serial', + type: 'text', + required: false, + options: [], + typeComposantId: typeComposant.id, + }, + }); + + const typePiece = await prisma.typePiece.create({ + data: { + name: 'Pièce de rechange', + description: 'Pièce critique', + }, + }); + + await prisma.customField.create({ + data: { + name: 'Batch', + type: 'text', + required: false, + options: [], + typePieceId: typePiece.id, + }, + }); + + const componentModelV1 = await prisma.composantModel.create({ + data: { + name: 'Module V1', + typeComposantId: typeComposant.id, + structure: { + name: 'Module V1', + customFields: [{ name: 'Serial', defaultValue: 'SERIAL-V1' }], + }, + }, + }); + + const componentModelV2 = await prisma.composantModel.create({ + data: { + name: 'Module V2', + typeComposantId: typeComposant.id, + structure: { + name: 'Module V2', + customFields: [{ name: 'Serial', defaultValue: 'SERIAL-V2' }], + }, + }, + }); + + const pieceModelV1 = await prisma.pieceModel.create({ + data: { + name: 'Pièce V1', + typePieceId: typePiece.id, + structure: { + name: 'Pièce V1', + customFields: [{ name: 'Batch', defaultValue: 'BATCH-V1' }], + }, + }, + }); + + const pieceModelV2 = await prisma.pieceModel.create({ + data: { + name: 'Pièce V2', + typePieceId: typePiece.id, + structure: { + name: 'Pièce V2', + customFields: [{ name: 'Batch', defaultValue: 'BATCH-V2' }], + }, + }, + }); + + const typeMachine = await prisma.typeMachine.create({ + data: { + name: 'Type Machine A', + componentRequirements: { + create: [ + { + label: 'Module initial', + minCount: 1, + maxCount: 1, + required: true, + allowNewModels: false, + typeComposantId: typeComposant.id, + }, + ], + }, + pieceRequirements: { + create: [ + { + label: 'Pièce initiale', + minCount: 1, + maxCount: 1, + required: true, + allowNewModels: false, + typePieceId: typePiece.id, + }, + ], + }, + }, + include: { + componentRequirements: true, + pieceRequirements: true, + }, + }); + + const initialComponentRequirement = typeMachine.componentRequirements[0]; + const initialPieceRequirement = typeMachine.pieceRequirements[0]; + + const createResponse = await request(app.getHttpServer()) + .post('/machines') + .send({ + name: 'Machine Alpha', + siteId: site.id, + typeMachineId: typeMachine.id, + componentSelections: [ + { + requirementId: initialComponentRequirement.id, + componentModelId: componentModelV1.id, + }, + ], + pieceSelections: [ + { + requirementId: initialPieceRequirement.id, + pieceModelId: pieceModelV1.id, + }, + ], + }) + .expect(201); + + const machineId = createResponse.body.id as string; + + const initialComponents = await prisma.composant.findMany({ where: { machineId } }); + expect(initialComponents).toHaveLength(1); + const initialComponentId = initialComponents[0].id; + + const initialPieces = await prisma.piece.findMany({ where: { machineId } }); + expect(initialPieces).toHaveLength(1); + const initialPieceId = initialPieces[0].id; + + expect( + await prisma.customFieldValue.count({ where: { composantId: initialComponentId } }), + ).toBe(1); + expect(await prisma.customFieldValue.count({ where: { pieceId: initialPieceId } })).toBe(1); + + await prisma.typeMachineComponentRequirement.deleteMany({ + where: { typeMachineId: typeMachine.id }, + }); + await prisma.typeMachinePieceRequirement.deleteMany({ + where: { typeMachineId: typeMachine.id }, + }); + + const newComponentRequirement = await prisma.typeMachineComponentRequirement.create({ + data: { + label: 'Modules mis à jour', + minCount: 2, + maxCount: 2, + required: true, + allowNewModels: true, + typeMachineId: typeMachine.id, + typeComposantId: typeComposant.id, + }, + }); + + const newPieceRequirement = await prisma.typeMachinePieceRequirement.create({ + data: { + label: 'Pièce mise à jour', + minCount: 1, + maxCount: 1, + required: true, + allowNewModels: false, + typeMachineId: typeMachine.id, + typePieceId: typePiece.id, + }, + }); + + const reconfigureResponse = await request(app.getHttpServer()) + .patch(`/machines/${machineId}/skeleton`) + .send({ + componentSelections: [ + { + requirementId: newComponentRequirement.id, + componentModelId: componentModelV2.id, + }, + { + requirementId: newComponentRequirement.id, + definition: { + name: 'Module personnalisé', + customFields: [{ name: 'Serial', defaultValue: 'SERIAL-CUSTOM' }], + }, + }, + ], + pieceSelections: [ + { + requirementId: newPieceRequirement.id, + pieceModelId: pieceModelV2.id, + }, + ], + }) + .expect(200); + + expect(reconfigureResponse.body.composants).toHaveLength(2); + expect(reconfigureResponse.body.pieces).toHaveLength(1); + + const componentsAfter = await prisma.composant.findMany({ where: { machineId } }); + expect(componentsAfter).toHaveLength(2); + const componentIdsAfter = componentsAfter.map((component) => component.id); + expect(componentIdsAfter).not.toContain(initialComponentId); + componentsAfter.forEach((component) => { + expect(component.typeMachineComponentRequirementId).toBe(newComponentRequirement.id); + }); + + const componentValueCount = await prisma.customFieldValue.count({ + where: { composantId: { in: componentIdsAfter } }, + }); + expect(componentValueCount).toBe(2); + expect( + await prisma.customFieldValue.count({ where: { composantId: initialComponentId } }), + ).toBe(0); + + const piecesAfter = await prisma.piece.findMany({ where: { machineId } }); + expect(piecesAfter).toHaveLength(1); + const [updatedPiece] = piecesAfter; + expect(updatedPiece.typeMachinePieceRequirementId).toBe(newPieceRequirement.id); + expect(updatedPiece.id).not.toBe(initialPieceId); + + expect( + await prisma.customFieldValue.count({ where: { pieceId: updatedPiece.id } }), + ).toBe(1); + expect(await prisma.customFieldValue.count({ where: { pieceId: initialPieceId } })).toBe(0); + }); + }); });