From 69381a41ba19a78384286ae7650f96aaee463b29 Mon Sep 17 00:00:00 2001 From: MatthieuTD <39524319+MatthieuTD@users.noreply.github.com> Date: Mon, 22 Sep 2025 10:21:06 +0200 Subject: [PATCH] test: add machine creation e2e flow and CI --- .github/workflows/ci.yml | 24 + README.md | 57 ++ test/app.e2e-spec.ts | 1070 +++++++++++++++++++++++++++++++++++++- 3 files changed, 1140 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4f28e3b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +name: CI + +on: + push: + branches: + - main + - develop + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + - name: Install dependencies + run: npm ci + - name: Run unit tests + run: npm test -- --runInBand + - name: Run e2e tests + run: npm run test:e2e -- --runInBand diff --git a/README.md b/README.md index 3e91758..673a683 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,63 @@ Site → Machine → Composant → Sous-composant → ... - `PATCH /api/machines/:id` - Modifier une machine - `DELETE /api/machines/:id` - Supprimer une machine +#### Payloads `componentSelections` / `pieceSelections` + +Lors de la création d'une machine à partir d'un type, il est possible de fournir des sélections de composants et de pièces qui viendront remplir les exigences définies dans le type de machine. + +```json +{ + "name": "Presse HP-2000", + "siteId": "", + "typeMachineId": "", + "componentSelections": [ + { + "requirementId": "", + "componentModelId": "", + "definition": { + "name": "Bloc moteur série X", + "reference": "COMP-001", + "emplacement": "Module A", + "prix": "12000.00", + "customFields": [ + { + "name": "Puissance nominale", + "type": "text", + "required": true, + "defaultValue": "7 kW" + } + ] + } + } + ], + "pieceSelections": [ + { + "requirementId": "", + "pieceModelId": "", + "definition": { + "name": "Kit maintenance niveau 1", + "reference": "KIT-001", + "customFields": [ + { + "name": "Référence fournisseur", + "type": "text", + "defaultValue": "STD-002" + } + ] + } + } + ] +} +``` + +Principales règles de validation : + +- `requirementId` doit correspondre à une exigence déclarée dans le type de machine (composant ou pièce). +- Le nombre de sélections pour une exigence doit respecter `minCount` et `maxCount` (si défini). Les exigences marquées `required` imposent au moins une sélection. +- Si `allowNewModels` vaut `false`, il est obligatoire de fournir un `componentModelId`/`pieceModelId` existant. Sinon un `definition` sans modèle peut être utilisé pour créer un nouvel élément. +- Les modèles sélectionnés doivent appartenir au type attendu (`typeComposantId` ou `typePieceId`) sous peine d'échec de la création. +- Les champs personnalisés du `definition.customFields` permettent de surcharger la valeur par défaut définie au niveau du type; la valeur est automatiquement injectée dans les `customFieldValues` de la machine, du composant ou de la pièce créée. + ### Composants - `GET /api/composants` - Liste tous les composants - `GET /api/composants/:id` - Détails d'un composant diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts index 4df6580..f8b9c37 100644 --- a/test/app.e2e-spec.ts +++ b/test/app.e2e-spec.ts @@ -1,25 +1,1073 @@ -import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; 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; +type Nullable = T | null; + +type SiteRecord = { + id: string; + name: string; + contactName: string; + contactPhone: string; + contactAddress: string; + contactPostalCode: string; + contactCity: string; + createdAt: Date; + updatedAt: Date; +}; + +type TypeComposantRecord = { + id: string; + name: string; + description: Nullable; + createdAt: Date; + updatedAt: Date; +}; + +type TypePieceRecord = { + id: string; + name: string; + description: Nullable; + createdAt: Date; + updatedAt: Date; +}; + +type TypeMachineRecord = { + id: string; + name: string; + description: Nullable; + category: Nullable; + maintenanceFrequency: Nullable; + components: any; + machinePieces: any; + specifications: any; + createdAt: Date; + updatedAt: Date; +}; + +type TypeMachineComponentRequirementRecord = { + id: string; + typeMachineId: string; + typeComposantId: string; + label: Nullable; + minCount: number; + maxCount: Nullable; + required: boolean; + allowNewModels: boolean; +}; + +type TypeMachinePieceRequirementRecord = { + id: string; + typeMachineId: string; + typePieceId: string; + label: Nullable; + minCount: number; + maxCount: Nullable; + required: boolean; + allowNewModels: boolean; +}; + +type MachineRecord = { + id: string; + name: string; + reference: Nullable; + constructeurId: Nullable; + prix: Nullable; + emplacement: Nullable; + siteId: string; + typeMachineId: Nullable; + createdAt: Date; + updatedAt: Date; +}; + +type ComposantRecord = { + id: string; + name: string; + reference: Nullable; + prix: Nullable; + emplacement: Nullable; + machineId: Nullable; + parentComposantId: Nullable; + typeComposantId: Nullable; + composantModelId: Nullable; + typeMachineComponentRequirementId: Nullable; + constructeurId: Nullable; + createdAt: Date; + updatedAt: Date; +}; + +type PieceRecord = { + id: string; + name: string; + reference: Nullable; + prix: Nullable; + emplacement: Nullable; + machineId: Nullable; + composantId: Nullable; + typePieceId: Nullable; + pieceModelId: Nullable; + typeMachinePieceRequirementId: Nullable; + constructeurId: Nullable; + createdAt: Date; + updatedAt: Date; +}; + +type CustomFieldRecord = { + id: string; + name: string; + type: string; + required: boolean; + defaultValue: Nullable; + options: string[]; + typeMachineId: Nullable; + typeComposantId: Nullable; + typePieceId: Nullable; + createdAt: Date; + updatedAt: Date; +}; + +type CustomFieldValueRecord = { + id: string; + value: string; + customFieldId: string; + machineId: Nullable; + composantId: Nullable; + pieceId: Nullable; + createdAt: Date; + updatedAt: Date; +}; + +type ProfileRecord = { + id: string; + firstName: string; + lastName: string; + isActive: boolean; + createdAt: Date; + updatedAt: Date; +}; + +function generateId(prefix: string): string { + return `${prefix}_${Math.random().toString(36).slice(2, 12)}`; +} + +class InMemoryPrismaService { + private sites: SiteRecord[] = []; + private typeComposants: TypeComposantRecord[] = []; + private typePieces: TypePieceRecord[] = []; + private typeMachines: TypeMachineRecord[] = []; + private typeMachineComponentRequirements: TypeMachineComponentRequirementRecord[] = []; + private typeMachinePieceRequirements: TypeMachinePieceRequirementRecord[] = []; + private machines: MachineRecord[] = []; + private composants: ComposantRecord[] = []; + private pieces: PieceRecord[] = []; + private customFields: CustomFieldRecord[] = []; + private customFieldValues: CustomFieldValueRecord[] = []; + private profiles: ProfileRecord[] = []; + + async onModuleInit() {} + async onModuleDestroy() {} + async $connect() {} + async $disconnect() {} + + async $transaction(fn: (tx: this) => Promise): Promise { + return fn(this); + } + + site = { + create: async ({ data }: any) => { + const now = new Date(); + const record: SiteRecord = { + id: generateId('site'), + name: data.name, + contactName: data.contactName, + contactPhone: data.contactPhone, + contactAddress: data.contactAddress, + contactPostalCode: data.contactPostalCode, + contactCity: data.contactCity, + createdAt: now, + updatedAt: now, + }; + this.sites.push(record); + return { ...record }; + }, + findMany: async ({ include }: any = {}) => { + return this.sites.map((site) => this.buildSite(site, include)); + }, + findUnique: async ({ where, include }: any) => { + const site = this.sites.find((item) => item.id === where.id); + if (!site) { + return null; + } + return this.buildSite(site, include); + }, + update: async ({ where, data }: any) => { + const site = this.sites.find((item) => item.id === where.id); + if (!site) { + throw new Error('Site not found'); + } + Object.assign(site, data, { updatedAt: new Date() }); + return { ...site }; + }, + delete: async ({ where }: any) => { + const index = this.sites.findIndex((item) => item.id === where.id); + if (index === -1) { + throw new Error('Site not found'); + } + const [deleted] = this.sites.splice(index, 1); + return { ...deleted }; + }, + }; + + typeComposant = { + create: async ({ data, include }: any) => { + const now = new Date(); + const record: TypeComposantRecord = { + id: generateId('type_comp'), + name: data.name, + description: data.description ?? null, + createdAt: now, + updatedAt: now, + }; + this.typeComposants.push(record); + + if (data.customFields?.create) { + for (const field of data.customFields.create) { + this.customFields.push({ + id: generateId('cf'), + name: field.name, + type: field.type, + required: field.required ?? false, + defaultValue: field.defaultValue ?? null, + options: field.options ?? [], + typeMachineId: null, + typeComposantId: record.id, + typePieceId: null, + createdAt: now, + updatedAt: now, + }); + } + } + + return include?.customFields + ? { + ...record, + customFields: this.customFields.filter((field) => field.typeComposantId === record.id), + } + : { ...record }; + }, + findFirst: async ({ where }: any) => { + if (!where) { + return this.typeComposants[0] ?? null; + } + if (where.id) { + return this.typeComposants.find((item) => item.id === where.id) ?? null; + } + if (where.name) { + return this.typeComposants.find((item) => item.name === where.name) ?? null; + } + return null; + }, + findMany: async () => { + return this.typeComposants.map((item) => ({ ...item })); + }, + findUnique: async ({ where }: any) => { + return this.typeComposants.find((item) => item.id === where.id) ?? null; + }, + }; + + typePiece = { + create: async ({ data, include }: any) => { + const now = new Date(); + const record: TypePieceRecord = { + id: generateId('type_piece'), + name: data.name, + description: data.description ?? null, + createdAt: now, + updatedAt: now, + }; + this.typePieces.push(record); + + if (data.customFields?.create) { + for (const field of data.customFields.create) { + this.customFields.push({ + id: generateId('cf'), + name: field.name, + type: field.type, + required: field.required ?? false, + defaultValue: field.defaultValue ?? null, + options: field.options ?? [], + typeMachineId: null, + typeComposantId: null, + typePieceId: record.id, + createdAt: now, + updatedAt: now, + }); + } + } + + return include?.customFields + ? { + ...record, + customFields: this.customFields.filter((field) => field.typePieceId === record.id), + } + : { ...record }; + }, + findFirst: async ({ where }: any) => { + if (!where) { + return this.typePieces[0] ?? null; + } + if (where.id) { + return this.typePieces.find((item) => item.id === where.id) ?? null; + } + if (where.name) { + return this.typePieces.find((item) => item.name === where.name) ?? null; + } + return null; + }, + findUnique: async ({ where }: any) => { + return this.typePieces.find((item) => item.id === where.id) ?? null; + }, + }; + + typeMachine = { + create: async ({ data, include }: any) => { + const now = new Date(); + const record: TypeMachineRecord = { + id: generateId('type_machine'), + name: data.name, + description: data.description ?? null, + category: data.category ?? null, + maintenanceFrequency: data.maintenanceFrequency ?? null, + components: data.components ?? null, + machinePieces: data.machinePieces ?? null, + specifications: data.specifications ?? null, + createdAt: now, + updatedAt: now, + }; + this.typeMachines.push(record); + + if (data.customFields?.create) { + for (const field of data.customFields.create) { + this.customFields.push({ + id: generateId('cf'), + name: field.name, + type: field.type, + required: field.required ?? false, + defaultValue: field.defaultValue ?? null, + options: field.options ?? [], + typeMachineId: record.id, + typeComposantId: null, + typePieceId: null, + createdAt: now, + updatedAt: now, + }); + } + } + + let componentRequirements: TypeMachineComponentRequirementRecord[] = []; + if (data.componentRequirements?.create) { + componentRequirements = data.componentRequirements.create.map((requirement) => { + const req: TypeMachineComponentRequirementRecord = { + id: generateId('tmc_req'), + typeMachineId: record.id, + typeComposantId: requirement.typeComposant.connect.id, + label: requirement.label ?? null, + minCount: requirement.minCount ?? 1, + maxCount: requirement.maxCount ?? null, + required: requirement.required ?? true, + allowNewModels: requirement.allowNewModels ?? true, + }; + this.typeMachineComponentRequirements.push(req); + return req; + }); + } + + let pieceRequirements: TypeMachinePieceRequirementRecord[] = []; + if (data.pieceRequirements?.create) { + pieceRequirements = data.pieceRequirements.create.map((requirement) => { + const req: TypeMachinePieceRequirementRecord = { + id: generateId('tmp_req'), + typeMachineId: record.id, + typePieceId: requirement.typePiece.connect.id, + label: requirement.label ?? null, + minCount: requirement.minCount ?? 0, + maxCount: requirement.maxCount ?? null, + required: requirement.required ?? false, + allowNewModels: requirement.allowNewModels ?? true, + }; + this.typeMachinePieceRequirements.push(req); + return req; + }); + } + + return this.buildTypeMachine(record, include ?? {}, componentRequirements, pieceRequirements); + }, + findUnique: async ({ where, include }: any) => { + const typeMachine = this.typeMachines.find((item) => item.id === where.id); + if (!typeMachine) { + return null; + } + const componentRequirements = this.typeMachineComponentRequirements.filter( + (item) => item.typeMachineId === typeMachine.id, + ); + const pieceRequirements = this.typeMachinePieceRequirements.filter( + (item) => item.typeMachineId === typeMachine.id, + ); + return this.buildTypeMachine(typeMachine, include ?? {}, componentRequirements, pieceRequirements); + }, + findMany: async ({ include }: any = {}) => { + return this.typeMachines.map((item) => { + const componentRequirements = this.typeMachineComponentRequirements.filter( + (req) => req.typeMachineId === item.id, + ); + const pieceRequirements = this.typeMachinePieceRequirements.filter((req) => req.typeMachineId === item.id); + return this.buildTypeMachine(item, include ?? {}, componentRequirements, pieceRequirements); + }); + }, + }; + + machine = { + create: async ({ data, include }: any) => { + const now = new Date(); + const record: MachineRecord = { + id: generateId('machine'), + name: data.name, + reference: data.reference ?? null, + constructeurId: data.constructeurId ?? null, + prix: data.prix ?? null, + emplacement: data.emplacement ?? null, + siteId: data.siteId, + typeMachineId: data.typeMachineId ?? null, + createdAt: now, + updatedAt: now, + }; + this.machines.push(record); + return this.buildMachine(record, include ?? {}); + }, + findUnique: async ({ where, include }: any) => { + const machine = this.machines.find((item) => item.id === where.id); + if (!machine) { + return null; + } + return this.buildMachine(machine, include ?? {}); + }, + findMany: async ({ include, where }: any = {}) => { + let machines = this.machines; + if (where?.siteId) { + machines = machines.filter((machine) => machine.siteId === where.siteId); + } + return machines.map((machine) => this.buildMachine(machine, include ?? {})); + }, + delete: async ({ where }: any) => { + const index = this.machines.findIndex((item) => item.id === where.id); + if (index === -1) { + throw new Error('Machine not found'); + } + const [deleted] = this.machines.splice(index, 1); + return { ...deleted }; + }, + }; + + composant = { + create: async ({ data }: any) => { + const now = new Date(); + const record: ComposantRecord = { + id: generateId('component'), + name: data.name, + reference: data.reference ?? null, + prix: data.prix ?? null, + emplacement: data.emplacement ?? null, + machineId: data.machineId ?? null, + parentComposantId: data.parentComposantId ?? null, + typeComposantId: data.typeComposantId ?? null, + composantModelId: data.composantModelId ?? null, + typeMachineComponentRequirementId: data.typeMachineComponentRequirementId ?? null, + constructeurId: data.constructeurId ?? null, + createdAt: now, + updatedAt: now, + }; + this.composants.push(record); + return { ...record }; + }, + findMany: async ({ where }: any) => { + let composants = this.composants; + if (where?.machineId !== undefined) { + composants = composants.filter((item) => item.machineId === where.machineId); + } + if (where?.parentComposantId !== undefined) { + composants = composants.filter((item) => item.parentComposantId === where.parentComposantId); + } + return composants.map((item) => ({ ...item })); + }, + }; + + piece = { + create: async ({ data }: any) => { + const now = new Date(); + const record: PieceRecord = { + id: generateId('piece'), + name: data.name, + reference: data.reference ?? null, + prix: data.prix ?? null, + emplacement: data.emplacement ?? null, + machineId: data.machineId ?? null, + composantId: data.composantId ?? null, + typePieceId: data.typePieceId ?? null, + pieceModelId: data.pieceModelId ?? null, + typeMachinePieceRequirementId: data.typeMachinePieceRequirementId ?? null, + constructeurId: data.constructeurId ?? null, + createdAt: now, + updatedAt: now, + }; + this.pieces.push(record); + return { ...record }; + }, + findMany: async ({ where }: any) => { + let pieces = this.pieces; + if (where?.machineId !== undefined) { + pieces = pieces.filter((item) => item.machineId === where.machineId); + } + if (where?.composantId !== undefined) { + pieces = pieces.filter((item) => item.composantId === where.composantId); + } + return pieces.map((item) => ({ ...item })); + }, + }; + + customField = { + create: async ({ data }: any) => { + const now = new Date(); + const record: CustomFieldRecord = { + id: generateId('cf'), + name: data.name, + type: data.type, + required: data.required ?? false, + defaultValue: data.defaultValue ?? null, + options: data.options ?? [], + typeMachineId: data.typeMachineId ?? null, + typeComposantId: data.typeComposantId ?? null, + typePieceId: data.typePieceId ?? null, + createdAt: now, + updatedAt: now, + }; + this.customFields.push(record); + return { ...record }; + }, + findMany: async ({ where }: any) => { + return this.customFields + .filter((field) => { + if (!where) return true; + return Object.entries(where).every(([key, value]) => (field as any)[key] === value); + }) + .map((field) => ({ ...field })); + }, + deleteMany: async ({ where }: any) => { + const before = this.customFields.length; + this.customFields = this.customFields.filter((field) => { + return !Object.entries(where).every(([key, value]) => (field as any)[key] === value); + }); + return { count: before - this.customFields.length }; + }, + }; + + customFieldValue = { + create: async ({ data, include }: any) => { + const now = new Date(); + const record: CustomFieldValueRecord = { + id: generateId('cfv'), + value: data.value, + customFieldId: data.customFieldId, + machineId: data.machineId ?? null, + composantId: data.composantId ?? null, + pieceId: data.pieceId ?? null, + createdAt: now, + updatedAt: now, + }; + this.customFieldValues.push(record); + return this.buildCustomFieldValue(record, include ?? {}); + }, + findMany: async ({ where, include }: any) => { + const records = this.customFieldValues.filter((value) => { + if (!where) return true; + return Object.entries(where).every(([key, v]) => (value as any)[key] === v); + }); + return records.map((record) => this.buildCustomFieldValue(record, include ?? {})); + }, + findUnique: async ({ where, include }: any) => { + const record = this.customFieldValues.find((value) => value.id === where.id); + if (!record) { + return null; + } + return this.buildCustomFieldValue(record, include ?? {}); + }, + findFirst: async ({ where }: any) => { + return ( + this.customFieldValues.find((value) => + Object.entries(where).every(([key, v]) => (value as any)[key] === v), + ) ?? null + ); + }, + update: async ({ where, data, include }: any) => { + const record = this.customFieldValues.find((value) => value.id === where.id); + if (!record) { + throw new Error('Custom field value not found'); + } + if (data.value !== undefined) { + record.value = data.value; + } + record.updatedAt = new Date(); + return this.buildCustomFieldValue(record, include ?? {}); + }, + delete: async ({ where }: any) => { + const index = this.customFieldValues.findIndex((value) => value.id === where.id); + if (index === -1) { + throw new Error('Custom field value not found'); + } + const [deleted] = this.customFieldValues.splice(index, 1); + return { ...deleted }; + }, + }; + + profile = { + count: async ({ where }: any) => { + return this.profiles.filter((profile) => { + if (!where) return true; + return Object.entries(where).every(([key, value]) => (profile as any)[key] === value); + }).length; + }, + findMany: async ({ where, orderBy, select }: any) => { + let profiles = this.profiles; + if (where) { + profiles = profiles.filter((profile) => + Object.entries(where).every(([key, value]) => (profile as any)[key] === value), + ); + } + if (orderBy?.createdAt === 'asc') { + profiles = [...profiles].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()); + } + return profiles.map((profile) => this.applySelect(profile, select)); + }, + findFirst: async ({ where, select }: any) => { + const profile = this.profiles.find((item) => + Object.entries(where).every(([key, value]) => (item as any)[key] === value), + ); + return profile ? this.applySelect(profile, select) : null; + }, + findUnique: async ({ where, select }: any) => { + const profile = this.profiles.find((item) => item.id === where.id); + return profile ? this.applySelect(profile, select) : null; + }, + create: async ({ data, select }: any) => { + const now = new Date(); + const record: ProfileRecord = { + id: generateId('profile'), + firstName: data.firstName, + lastName: data.lastName, + isActive: data.isActive ?? true, + createdAt: now, + updatedAt: now, + }; + this.profiles.push(record); + return this.applySelect(record, select); + }, + update: async ({ where, data, select }: any) => { + const profile = this.profiles.find((item) => item.id === where.id); + if (!profile) { + throw new Error('Profile not found'); + } + Object.assign(profile, data, { updatedAt: new Date() }); + return this.applySelect(profile, select); + }, + }; + + private applySelect>(record: T, select?: any) { + if (!select) { + return { ...record }; + } + const result: any = {}; + for (const [key, enabled] of Object.entries(select)) { + if (enabled) { + result[key] = record[key as keyof T]; + } + } + return result; + } + + private buildSite(site: SiteRecord, include: any) { + const base: any = { ...site }; + if (include?.machines) { + const machines = this.machines.filter((machine) => machine.siteId === site.id); + base.machines = machines.map((machine) => this.buildMachine(machine, include.machines.include ?? {})); + } + if (include?.documents) { + base.documents = []; + } + return base; + } + + private buildTypeMachine( + record: TypeMachineRecord, + include: any, + componentRequirements: TypeMachineComponentRequirementRecord[], + pieceRequirements: TypeMachinePieceRequirementRecord[], + ) { + const base: any = { ...record }; + if (include?.customFields) { + base.customFields = this.customFields.filter((field) => field.typeMachineId === record.id); + } + if (include?.componentRequirements) { + base.componentRequirements = componentRequirements.map((requirement) => ({ + ...requirement, + typeComposant: include.componentRequirements.include?.typeComposant + ? this.typeComposants.find((item) => item.id === requirement.typeComposantId) ?? null + : undefined, + })); + } + if (include?.pieceRequirements) { + base.pieceRequirements = pieceRequirements.map((requirement) => ({ + ...requirement, + typePiece: include.pieceRequirements.include?.typePiece + ? this.typePieces.find((item) => item.id === requirement.typePieceId) ?? null + : undefined, + })); + } + if (include?.machines) { + base.machines = this.machines + .filter((machine) => machine.typeMachineId === record.id) + .map((machine) => this.buildMachine(machine, include.machines.include ?? {})); + } + return base; + } + + private buildMachine(machine: MachineRecord, include: any) { + const base: any = { ...machine }; + + if (include?.site) { + base.site = this.sites.find((site) => site.id === machine.siteId) ?? null; + } + + if (include?.typeMachine) { + const typeMachine = machine.typeMachineId + ? this.typeMachines.find((item) => item.id === machine.typeMachineId) + : null; + if (typeMachine) { + const componentRequirements = this.typeMachineComponentRequirements.filter( + (req) => req.typeMachineId === typeMachine.id, + ); + const pieceRequirements = this.typeMachinePieceRequirements.filter( + (req) => req.typeMachineId === typeMachine.id, + ); + base.typeMachine = this.buildTypeMachine( + typeMachine, + include.typeMachine.include ?? {}, + componentRequirements, + pieceRequirements, + ); + } else { + base.typeMachine = null; + } + } + + if (include?.constructeur) { + base.constructeur = null; + } + + if (include?.composants) { + const composants = this.composants.filter((component) => component.machineId === machine.id); + base.composants = composants + .filter((component) => component.parentComposantId === null) + .map((component) => this.buildComponent(component, include.composants.include ?? {})); + } + + if (include?.pieces) { + const machinePieces = this.pieces.filter((piece) => piece.machineId === machine.id && piece.composantId === null); + base.pieces = machinePieces.map((piece) => this.buildPiece(piece, include.pieces.include ?? {})); + } + + if (include?.customFieldValues) { + const values = this.customFieldValues.filter((value) => value.machineId === machine.id); + base.customFieldValues = values.map((value) => this.buildCustomFieldValue(value, include.customFieldValues.include ?? {})); + } + + if (include?.documents) { + base.documents = []; + } + + return base; + } + + private buildComponent(component: ComposantRecord, include: any) { + const base: any = { ...component }; + + if (include?.typeComposant) { + base.typeComposant = component.typeComposantId + ? this.typeComposants.find((item) => item.id === component.typeComposantId) ?? null + : null; + } + + if (include?.composantModel) { + base.composantModel = null; + } + + if (include?.typeMachineComponentRequirement) { + const requirement = component.typeMachineComponentRequirementId + ? this.typeMachineComponentRequirements.find((item) => item.id === component.typeMachineComponentRequirementId) ?? null + : null; + base.typeMachineComponentRequirement = requirement + ? { + ...requirement, + typeComposant: include.typeMachineComponentRequirement.include?.typeComposant + ? this.typeComposants.find((item) => item.id === requirement.typeComposantId) ?? null + : undefined, + } + : null; + } + + if (include?.sousComposants) { + const children = this.composants.filter((item) => item.parentComposantId === component.id); + base.sousComposants = children.map((child) => this.buildComponent(child, include.sousComposants.include ?? {})); + } + + if (include?.customFieldValues) { + const values = this.customFieldValues.filter((value) => value.composantId === component.id); + base.customFieldValues = values.map((value) => this.buildCustomFieldValue(value, include.customFieldValues.include ?? {})); + } + + if (include?.constructeur) { + base.constructeur = null; + } + + if (include?.pieces) { + const relatedPieces = this.pieces.filter((piece) => piece.composantId === component.id); + base.pieces = relatedPieces.map((piece) => this.buildPiece(piece, include.pieces.include ?? {})); + } + + return base; + } + + private buildPiece(piece: PieceRecord, include: any) { + const base: any = { ...piece }; + + if (include?.customFieldValues) { + const values = this.customFieldValues.filter((value) => value.pieceId === piece.id); + base.customFieldValues = values.map((value) => this.buildCustomFieldValue(value, include.customFieldValues.include ?? {})); + } + + if (include?.constructeur) { + base.constructeur = null; + } + + if (include?.pieceModel) { + base.pieceModel = null; + } + + if (include?.typeMachinePieceRequirement) { + const requirement = piece.typeMachinePieceRequirementId + ? this.typeMachinePieceRequirements.find((item) => item.id === piece.typeMachinePieceRequirementId) ?? null + : null; + base.typeMachinePieceRequirement = requirement + ? { + ...requirement, + typePiece: include.typeMachinePieceRequirement.include?.typePiece + ? this.typePieces.find((item) => item.id === requirement.typePieceId) ?? null + : undefined, + } + : null; + } + + if (include?.typePiece) { + base.typePiece = piece.typePieceId + ? this.typePieces.find((item) => item.id === piece.typePieceId) ?? null + : null; + } + + return base; + } + + private buildCustomFieldValue(record: CustomFieldValueRecord, include: any) { + const base: any = { ...record }; + if (include?.customField) { + base.customField = this.customFields.find((field) => field.id === record.customFieldId) ?? null; + } + return base; + } +} + +describe('Inventory flow (e2e)', () => { + let app: INestApplication; + let prismaStub: InMemoryPrismaService; + + beforeAll(async () => { + prismaStub = new InMemoryPrismaService(); - beforeEach(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], - }).compile(); + }) + .overrideProvider(PrismaService) + .useValue(prismaStub) + .compile(); app = moduleFixture.createNestApplication(); await app.init(); }); - it('/ (GET)', () => { - return request(app.getHttpServer()) - .get('/') - .expect(200) - .expect('Hello World!'); + afterAll(async () => { + await app.close(); + }); + + it('should create a type, create a machine with new component/piece selections, and edit technical fields', async () => { + const siteResponse = await request(app.getHttpServer()).post('/sites').send({ + name: 'Site Principal', + contactName: 'Responsable Maintenance', + contactPhone: '+33 1 23 45 67 89', + contactAddress: '1 rue de la Paix', + contactPostalCode: '75000', + contactCity: 'Paris', + }); + + expect(siteResponse.status).toBe(201); + const siteId = siteResponse.body.id; + expect(siteId).toBeDefined(); + + const typeComposantResponse = await request(app.getHttpServer()) + .post('/types/composants') + .send({ + name: 'Bloc moteur', + description: 'Sous-ensemble principal', + customFields: [ + { + name: 'Puissance nominale', + type: 'text', + required: true, + defaultValue: '5 kW', + }, + ], + }); + + expect(typeComposantResponse.status).toBe(201); + const typeComposantId = typeComposantResponse.body.id; + expect(typeComposantId).toBeDefined(); + + const typePieceResponse = await request(app.getHttpServer()) + .post('/types/pieces') + .send({ + name: 'Kit maintenance', + description: 'Kit standard', + customFields: [ + { + name: 'Référence fournisseur', + type: 'text', + defaultValue: 'STD-001', + }, + ], + }); + + expect(typePieceResponse.status).toBe(201); + const typePieceId = typePieceResponse.body.id; + expect(typePieceId).toBeDefined(); + + const typeMachineResponse = await request(app.getHttpServer()) + .post('/types/machines') + .send({ + name: 'Presse hydraulique', + description: 'Presse pour la ligne A', + componentRequirements: [ + { + typeComposantId, + label: 'Bloc moteur obligatoire', + minCount: 1, + required: true, + allowNewModels: true, + }, + ], + pieceRequirements: [ + { + typePieceId, + label: 'Kit de maintenance', + minCount: 1, + required: true, + allowNewModels: true, + }, + ], + }); + + expect(typeMachineResponse.status).toBe(201); + const typeMachine = typeMachineResponse.body; + const componentRequirementId = typeMachine.componentRequirements[0].id; + const pieceRequirementId = typeMachine.pieceRequirements[0].id; + + const machineResponse = await request(app.getHttpServer()) + .post('/machines') + .send({ + name: 'Presse HP-2000', + siteId, + typeMachineId: typeMachine.id, + componentSelections: [ + { + requirementId: componentRequirementId, + definition: { + name: 'Bloc moteur série X', + reference: 'COMP-001', + customFields: [ + { + name: 'Puissance nominale', + type: 'text', + required: true, + defaultValue: '7 kW', + }, + ], + }, + }, + ], + pieceSelections: [ + { + requirementId: pieceRequirementId, + definition: { + name: 'Kit maintenance niveau 1', + reference: 'KIT-001', + customFields: [ + { + name: 'Référence fournisseur', + type: 'text', + defaultValue: 'STD-002', + }, + ], + }, + }, + ], + }); + + expect(machineResponse.status).toBe(201); + const machineId = machineResponse.body.id; + + const machineDetailsResponse = await request(app.getHttpServer()).get(`/machines/${machineId}`); + expect(machineDetailsResponse.status).toBe(200); + const machine = machineDetailsResponse.body; + + expect(machine.composants).toHaveLength(1); + expect(machine.pieces).toHaveLength(1); + + const component = machine.composants[0]; + expect(component.name).toBe('Bloc moteur série X'); + expect(component.customFieldValues[0].value).toBe('7 kW'); + + const piece = machine.pieces[0]; + expect(piece.name).toBe('Kit maintenance niveau 1'); + expect(piece.customFieldValues[0].value).toBe('STD-002'); + + const customFieldValueId = component.customFieldValues[0].id; + const updateResponse = await request(app.getHttpServer()) + .patch(`/custom-fields/values/${customFieldValueId}`) + .send({ value: '8 kW' }); + + expect(updateResponse.status).toBe(200); + expect(updateResponse.body.value).toBe('8 kW'); + + const refreshedMachineResponse = await request(app.getHttpServer()).get(`/machines/${machine.id}`); + expect(refreshedMachineResponse.status).toBe(200); + const refreshedComponent = refreshedMachineResponse.body.composants[0]; + expect(refreshedComponent.customFieldValues[0].value).toBe('8 kW'); }); });