import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import * as request from 'supertest'; import { AppModule } from './../src/app.module'; import { PrismaService } from '../src/prisma/prisma.service'; 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; constructeurIds: string[]; prix: Nullable; siteId: string; typeMachineId: Nullable; createdAt: Date; updatedAt: Date; }; type ComposantRecord = { id: string; name: string; reference: Nullable; prix: Nullable; machineId: Nullable; parentComposantId: Nullable; typeComposantId: Nullable; typeMachineComponentRequirementId: Nullable; constructeurIds: string[]; createdAt: Date; updatedAt: Date; }; type PieceRecord = { id: string; name: string; reference: Nullable; prix: Nullable; machineId: Nullable; composantId: Nullable; typePieceId: Nullable; typeMachinePieceRequirementId: Nullable; constructeurIds: string[]; createdAt: Date; updatedAt: Date; }; type ConstructeurRecord = { id: string; name: string; email: Nullable; phone: Nullable; createdAt: Date; updatedAt: Date; }; type MachineComponentLinkRecord = { id: string; machineId: string; composantId: string; parentLinkId: Nullable; typeMachineComponentRequirementId: Nullable; nameOverride: Nullable; referenceOverride: Nullable; prixOverride: Nullable; createdAt: Date; updatedAt: Date; }; type MachinePieceLinkRecord = { id: string; machineId: string; pieceId: string; parentLinkId: Nullable; typeMachinePieceRequirementId: Nullable; nameOverride: Nullable; referenceOverride: Nullable; prixOverride: Nullable; createdAt: Date; updatedAt: Date; }; type ModelTypeRecord = { id: string; name: string; code: string; category: 'COMPONENT' | 'PIECE'; description: Nullable; notes: Nullable; createdAt: Date; updatedAt: Date; }; type CustomFieldRecord = { id: string; name: string; type: string; required: boolean; 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 modelTypes: ModelTypeRecord[] = []; private modelTypeCodeCounter = 0; private typeMachines: TypeMachineRecord[] = []; private typeMachineComponentRequirements: TypeMachineComponentRequirementRecord[] = []; private typeMachinePieceRequirements: TypeMachinePieceRequirementRecord[] = []; private machines: MachineRecord[] = []; private composants: ComposantRecord[] = []; private pieces: PieceRecord[] = []; private machineComponentLinks: MachineComponentLinkRecord[] = []; private machinePieceLinks: MachinePieceLinkRecord[] = []; private customFields: CustomFieldRecord[] = []; private customFieldValues: CustomFieldValueRecord[] = []; private profiles: ProfileRecord[] = []; private constructeurs: ConstructeurRecord[] = []; private readonly constructeurLinkOrientation: Record< string, { parentColumn: 'A' | 'B'; constructeurColumn: 'A' | 'B' } > = { _MachineConstructeurs: { parentColumn: 'A', constructeurColumn: 'B' }, _ComposantConstructeurs: { parentColumn: 'A', constructeurColumn: 'B' }, _PieceConstructeurs: { parentColumn: 'A', constructeurColumn: 'B' }, }; async onModuleInit() {} async onModuleDestroy() {} async $connect() {} async $disconnect() {} async $transaction(fn: (tx: this) => Promise): Promise { return fn(this); } reset() { this.sites = []; this.typeComposants = []; this.typePieces = []; this.modelTypes = []; this.modelTypeCodeCounter = 0; this.typeMachines = []; this.typeMachineComponentRequirements = []; this.typeMachinePieceRequirements = []; this.machines = []; this.composants = []; this.pieces = []; this.machineComponentLinks = []; this.machinePieceLinks = []; this.customFields = []; this.customFieldValues = []; this.profiles = []; this.constructeurs = []; } private readonly modelTypeDelegate = { create: async ({ data, include }: any) => { const now = new Date(); const category: 'COMPONENT' | 'PIECE' = data.category ?? 'COMPONENT'; const record: ModelTypeRecord = { id: generateId('model_type'), name: data.name, code: data.code ?? this.generateModelTypeCode(data.name), category, description: data.description ?? null, notes: data.notes ?? null, createdAt: now, updatedAt: now, }; this.modelTypes.push(record); if (data.customFields?.create) { for (const field of data.customFields.create) { this.createCustomFieldForModel(record.id, 'COMPONENT', field); } } if (data.pieceCustomFields?.create) { for (const field of data.pieceCustomFields.create) { this.createCustomFieldForModel(record.id, 'PIECE', field); } } this.syncModelType(record); const resolvedInclude = typeof include === 'object' ? include : {}; return this.buildModelType(record, resolvedInclude); }, findMany: async ({ where, include, orderBy }: any = {}) => { let records = [...this.modelTypes]; if (where) { records = records.filter((item) => this.matchesModelTypeWhere(item, where), ); } if (orderBy?.name) { records.sort((a, b) => orderBy.name === 'desc' ? b.name.localeCompare(a.name) : a.name.localeCompare(b.name), ); } const resolvedInclude = typeof include === 'object' ? include : {}; return records.map((record) => this.buildModelType(record, resolvedInclude), ); }, findFirst: async ({ where, include }: any = {}) => { const record = this.modelTypes.find((item) => this.matchesModelTypeWhere(item, where), ); const resolvedInclude = typeof include === 'object' ? include : {}; return record ? this.buildModelType(record, resolvedInclude) : null; }, findUnique: async ({ where, include }: any) => { const record = this.modelTypes.find((item) => { if (!where) return false; if (where.id) { return item.id === where.id; } if (where.code) { return item.code === where.code; } return this.matchesModelTypeWhere(item, where); }); const resolvedInclude = typeof include === 'object' ? include : {}; return record ? this.buildModelType(record, resolvedInclude) : null; }, update: async ({ where, data, include }: any) => { const record = this.modelTypes.find((item) => this.matchesModelTypeWhere(item, where), ); if (!record) { throw new Error('ModelType not found'); } if (data.name !== undefined) { record.name = this.applyUpdateValue(data.name); } if (data.description !== undefined) { record.description = this.applyUpdateValue(data.description); } if (data.notes !== undefined) { record.notes = this.applyUpdateValue(data.notes); } if (data.code !== undefined) { record.code = this.applyUpdateValue(data.code); } record.updatedAt = new Date(); this.syncModelType(record); const resolvedInclude = typeof include === 'object' ? include : {}; return this.buildModelType(record, resolvedInclude); }, delete: async ({ where }: any) => { const index = this.modelTypes.findIndex((item) => this.matchesModelTypeWhere(item, where), ); if (index === -1) { throw new Error('ModelType not found'); } const [record] = this.modelTypes.splice(index, 1); this.removeModelType(record); this.customFields = this.customFields.filter( (field) => field.typeComposantId !== record.id && field.typePieceId !== record.id, ); return { ...record }; }, }; get modelType() { return this.modelTypeDelegate; } 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, 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, 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, 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 constructeurIds = this.extractConstructeurIds(data.constructeurs); const record: MachineRecord = { id: generateId('machine'), name: data.name, reference: data.reference ?? null, constructeurIds, prix: data.prix ?? 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 constructeurIds = this.extractConstructeurIds(data.constructeurs); const record: ComposantRecord = { id: generateId('component'), name: data.name, reference: data.reference ?? null, prix: data.prix ?? null, machineId: data.machineId ?? null, parentComposantId: data.parentComposantId ?? null, typeComposantId: data.typeComposantId ?? null, typeMachineComponentRequirementId: data.typeMachineComponentRequirementId ?? null, constructeurIds, 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 constructeurIds = this.extractConstructeurIds(data.constructeurs); const record: PieceRecord = { id: generateId('piece'), name: data.name, reference: data.reference ?? null, prix: data.prix ?? null, machineId: data.machineId ?? null, composantId: data.composantId ?? null, typePieceId: data.typePieceId ?? null, typeMachinePieceRequirementId: data.typeMachinePieceRequirementId ?? null, constructeurIds, 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 })); }, }; machineComponentLink = { create: async ({ data, include }: any) => { const now = new Date(); const record: MachineComponentLinkRecord = { id: data.id ?? generateId('mcl'), machineId: data.machineId, composantId: data.composantId, parentLinkId: data.parentLinkId ?? null, typeMachineComponentRequirementId: data.typeMachineComponentRequirementId ?? null, nameOverride: data.nameOverride !== undefined ? data.nameOverride : null, referenceOverride: data.referenceOverride !== undefined ? data.referenceOverride : null, prixOverride: data.prixOverride !== undefined && data.prixOverride !== null ? String(data.prixOverride) : (data.prixOverride ?? null), createdAt: now, updatedAt: now, }; this.machineComponentLinks.push(record); return this.buildMachineComponentLink(record, include ?? {}); }, deleteMany: async ({ where }: any) => { const before = this.machineComponentLinks.length; this.machineComponentLinks = this.machineComponentLinks.filter((link) => { if (where?.machineId !== undefined) { return link.machineId !== where.machineId; } return true; }); return { count: before - this.machineComponentLinks.length }; }, findMany: async ({ where, include }: any = {}) => { let links = this.machineComponentLinks; if (where?.machineId) { links = links.filter((link) => link.machineId === where.machineId); } if (where?.parentLinkId === null) { links = links.filter((link) => link.parentLinkId === null); } else if (where?.parentLinkId) { links = links.filter( (link) => link.parentLinkId === where.parentLinkId, ); } return links.map((link) => this.buildMachineComponentLink(link, include ?? {}), ); }, }; machinePieceLink = { create: async ({ data, include }: any) => { const now = new Date(); const record: MachinePieceLinkRecord = { id: data.id ?? generateId('mpl'), machineId: data.machineId, pieceId: data.pieceId, parentLinkId: data.parentLinkId ?? null, typeMachinePieceRequirementId: data.typeMachinePieceRequirementId ?? null, nameOverride: data.nameOverride !== undefined ? data.nameOverride : null, referenceOverride: data.referenceOverride !== undefined ? data.referenceOverride : null, prixOverride: data.prixOverride !== undefined && data.prixOverride !== null ? String(data.prixOverride) : (data.prixOverride ?? null), createdAt: now, updatedAt: now, }; this.machinePieceLinks.push(record); return this.buildMachinePieceLink(record, include ?? {}); }, deleteMany: async ({ where }: any) => { const before = this.machinePieceLinks.length; this.machinePieceLinks = this.machinePieceLinks.filter((link) => { if (where?.machineId !== undefined) { return link.machineId !== where.machineId; } return true; }); return { count: before - this.machinePieceLinks.length }; }, findMany: async ({ where, include }: any = {}) => { let links = this.machinePieceLinks; if (where?.machineId) { links = links.filter((link) => link.machineId === where.machineId); } if (where?.parentLinkId === null) { links = links.filter((link) => link.parentLinkId === null); } else if (where?.parentLinkId) { links = links.filter( (link) => link.parentLinkId === where.parentLinkId, ); } return links.map((link) => this.buildMachinePieceLink(link, include ?? {}), ); }, }; 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, 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 }; }, createMany: async ({ data }: any) => { const now = new Date(); const records = data.map((item: any) => { const record: CustomFieldRecord = { id: generateId('cf'), name: item.name, type: item.type, required: item.required ?? false, options: item.options ?? [], typeMachineId: item.typeMachineId ?? null, typeComposantId: item.typeComposantId ?? null, typePieceId: item.typePieceId ?? null, createdAt: now, updatedAt: now, }; this.customFields.push(record); return record; }); return { count: records.length }; }, 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 }; }, }; constructeur = { findMany: async ({ where, select, orderBy }: any = {}) => { let records = [...this.constructeurs]; if (where?.id?.in) { const ids = Array.isArray(where.id.in) ? where.id.in : []; records = records.filter((item) => ids.includes(item.id)); } if (where?.OR && Array.isArray(where.OR)) { const termCandidate = where.OR.find( (clause: any) => clause?.name?.contains, ); const term = termCandidate?.name?.contains ?? termCandidate?.email?.contains ?? termCandidate?.phone?.contains ?? ''; const normalized = term.toLowerCase().trim(); if (normalized) { records = records.filter((record) => { const nameMatch = record.name.toLowerCase().includes(normalized); const emailMatch = record.email?.toLowerCase().includes(normalized) ?? false; const phoneMatch = record.phone?.toLowerCase().includes(normalized) ?? false; return nameMatch || emailMatch || phoneMatch; }); } } if (orderBy?.name) { records = [...records].sort((a, b) => orderBy.name === 'desc' ? b.name.localeCompare(a.name) : a.name.localeCompare(b.name), ); } return records.map((record) => this.applySelect(record, select)); }, findUnique: async ({ where, select }: any) => { const record = this.constructeurs.find((item) => item.id === where?.id); return record ? this.applySelect(record, select) : null; }, findFirst: async ({ where, select }: any = {}) => { if (where?.name?.equals) { const target = where.name.equals.toLowerCase(); const record = this.constructeurs.find( (item) => item.name.toLowerCase() === target, ); return record ? this.applySelect(record, select) : null; } return null; }, create: async ({ data, select }: any) => { const now = new Date(); const record: ConstructeurRecord = { id: data.id ?? generateId('constructeur'), name: data.name?.trim?.() ?? '', email: data.email?.trim?.() ?? null, phone: data.phone?.trim?.() ?? null, createdAt: now, updatedAt: now, }; this.constructeurs.push(record); return this.applySelect(record, select); }, update: async ({ where, data, select }: any) => { const record = this.constructeurs.find((item) => item.id === where?.id); if (!record) { throw new Error('Constructeur not found'); } if (data.name !== undefined) { record.name = this.applyUpdateValue(data.name)?.trim?.() ?? record.name; } if (data.email !== undefined) { record.email = this.applyUpdateValue(data.email)?.trim?.() ?? null; } if (data.phone !== undefined) { record.phone = this.applyUpdateValue(data.phone)?.trim?.() ?? null; } record.updatedAt = new Date(); return this.applySelect(record, select); }, delete: async ({ where, select }: any) => { const index = this.constructeurs.findIndex( (item) => item.id === where?.id, ); if (index === -1) { throw new Error('Constructeur not found'); } const [deleted] = this.constructeurs.splice(index, 1); return this.applySelect(deleted, select); }, }; async __getConstructeurLinkOrientation(table: string) { return ( this.constructeurLinkOrientation[table] ?? { parentColumn: 'A', constructeurColumn: 'B', } ); } 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 matchesModelTypeWhere(record: ModelTypeRecord, where?: any) { if (!where) { return true; } if (where.AND && Array.isArray(where.AND)) { return where.AND.every((clause: any) => this.matchesModelTypeWhere(record, clause), ); } if (where.OR && Array.isArray(where.OR)) { return where.OR.some((clause: any) => this.matchesModelTypeWhere(record, clause), ); } if (where.NOT && Array.isArray(where.NOT)) { return where.NOT.every( (clause: any) => !this.matchesModelTypeWhere(record, clause), ); } return Object.entries(where).every(([key, value]) => { if (value === undefined) { return true; } if ( key === 'id' || key === 'code' || key === 'category' || key === 'name' ) { return (record as any)[key] === value; } return true; }); } private createCustomFieldForModel( modelTypeId: string, category: 'COMPONENT' | 'PIECE', field: any, ) { const now = new Date(); const record: CustomFieldRecord = { id: generateId('cf'), name: field.name, type: field.type, required: field.required ?? false, options: field.options ?? [], typeMachineId: null, typeComposantId: category === 'COMPONENT' ? modelTypeId : null, typePieceId: category === 'PIECE' ? modelTypeId : null, createdAt: now, updatedAt: now, }; this.customFields.push(record); return record; } private syncModelType(record: ModelTypeRecord) { const base = { id: record.id, name: record.name, description: record.description ?? null, createdAt: record.createdAt, updatedAt: record.updatedAt, }; if (record.category === 'COMPONENT') { const existing = this.typeComposants.find( (item) => item.id === record.id, ); if (existing) { existing.name = base.name; existing.description = base.description; existing.updatedAt = record.updatedAt; } else { this.typeComposants.push({ ...base }); } } else { const existing = this.typePieces.find((item) => item.id === record.id); if (existing) { existing.name = base.name; existing.description = base.description; existing.updatedAt = record.updatedAt; } else { this.typePieces.push({ ...base }); } } } private removeModelType(record: ModelTypeRecord) { if (record.category === 'COMPONENT') { this.typeComposants = this.typeComposants.filter( (item) => item.id !== record.id, ); } else { this.typePieces = this.typePieces.filter((item) => item.id !== record.id); } } private generateModelTypeCode(name: string) { const base = (name || 'type') .toLowerCase() .normalize('NFD') .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') || 'type'; let candidate = base; let suffix = 1; while (this.modelTypes.some((item) => item.code === candidate)) { candidate = `${base}-${suffix++}`; } return candidate; } private applyUpdateValue(value: any): T { if (value && typeof value === 'object' && 'set' in value) { return value.set as T; } return value as T; } private buildModelType(record: ModelTypeRecord, include: any) { const base: any = { ...record }; const customFieldsInclude = include?.customFields; if (customFieldsInclude) { const select = typeof customFieldsInclude === 'object' ? customFieldsInclude.select : undefined; base.customFields = this.customFields .filter((field) => field.typeComposantId === record.id) .map((field) => this.applySelect(field, select)); } const pieceCustomFieldsInclude = include?.pieceCustomFields; if (pieceCustomFieldsInclude) { const select = typeof pieceCustomFieldsInclude === 'object' ? pieceCustomFieldsInclude.select : undefined; base.pieceCustomFields = this.customFields .filter((field) => field.typePieceId === record.id) .map((field) => this.applySelect(field, select)); } if (include?.composants) { base.composants = this.composants .filter((item) => item.typeComposantId === record.id) .map((item) => ({ ...item })); } if (include?.pieceRequirements) { base.pieceRequirements = []; } if (include?.pieces) { base.pieces = this.pieces .filter((item) => item.typePieceId === record.id) .map((item) => ({ ...item })); } if (include?.componentRequirements) { base.componentRequirements = []; } return base; } 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 extractConstructeurIds(input: any): string[] { if (!input) { return []; } const source = Array.isArray(input.set) ? input.set : Array.isArray(input.connect) ? input.connect : []; return source .map((entry: any) => (typeof entry?.id === 'string' ? entry.id : null)) .filter((id: string | null): id is string => Boolean(id)); } private mapConstructeurs(ids: string[] = []) { if (!Array.isArray(ids) || ids.length === 0) { return []; } return ids .map((id) => this.constructeurs.find((constructeur) => constructeur.id === id), ) .filter((item): item is (typeof this.constructeurs)[number] => Boolean(item), ) .map((item) => ({ ...item })); } async __syncConstructeurLinks( table: string, parentId: string, ids: string[], ): Promise { const filtered = ids.filter((id) => this.constructeurs.some((item) => item.id === id), ); switch (table) { case '_MachineConstructeurs': this.assignConstructeurs(this.machines, parentId, filtered); break; case '_ComposantConstructeurs': this.assignConstructeurs(this.composants, parentId, filtered); break; case '_PieceConstructeurs': this.assignConstructeurs(this.pieces, parentId, filtered); break; default: throw new Error(`Unsupported constructeur link table: ${table}`); } } private assignConstructeurs< T extends { id: string; constructeurIds: string[] }, >(collection: T[], parentId: string, ids: string[]) { const record = collection.find((item) => item.id === parentId); if (!record) { return; } record.constructeurIds = [...ids]; } 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?.constructeurs) { base.constructeurs = this.mapConstructeurs(machine.constructeurIds); } if (include?.componentLinks) { const links = this.machineComponentLinks.filter( (link) => link.machineId === machine.id, ); base.componentLinks = links.map((link) => this.buildMachineComponentLink( link, include.componentLinks.include ?? {}, ), ); } if (include?.pieceLinks) { const links = this.machinePieceLinks.filter( (link) => link.machineId === machine.id, ); base.pieceLinks = links.map((link) => this.buildMachinePieceLink(link, include.pieceLinks.include ?? {}), ); } 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 buildMachineComponentLink( link: MachineComponentLinkRecord, include: any, ) { const base: any = { ...link }; if (include?.composant) { const composant = this.composants.find((item) => item.id === link.composantId) ?? null; base.composant = composant ? this.buildComponent(composant, include.composant.include ?? {}) : null; } if (include?.typeMachineComponentRequirement) { const requirement = link.typeMachineComponentRequirementId ? (this.typeMachineComponentRequirements.find( (item) => item.id === link.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?.pieceLinks) { const nestedInclude = include.pieceLinks.include ?? {}; const pieces = this.machinePieceLinks.filter( (pieceLink) => pieceLink.parentLinkId === link.id, ); base.pieceLinks = pieces.map((pieceLink) => this.buildMachinePieceLink(pieceLink, nestedInclude), ); } return base; } private buildMachinePieceLink(link: MachinePieceLinkRecord, include: any) { const base: any = { ...link }; if (include?.piece) { const piece = this.pieces.find((item) => item.id === link.pieceId) ?? null; base.piece = piece ? this.buildPiece(piece, include.piece.include ?? {}) : null; } if (include?.typeMachinePieceRequirement) { const requirement = link.typeMachinePieceRequirementId ? (this.typeMachinePieceRequirements.find( (item) => item.id === link.typeMachinePieceRequirementId, ) ?? null) : null; base.typeMachinePieceRequirement = requirement ? { ...requirement, typePiece: include.typeMachinePieceRequirement.include?.typePiece ? (this.typePieces.find( (item) => item.id === requirement.typePieceId, ) ?? null) : undefined, } : null; } 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?.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?.constructeurs) { base.constructeurs = this.mapConstructeurs(component.constructeurIds); } 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?.constructeurs) { base.constructeurs = this.mapConstructeurs(piece.constructeurIds); } 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; let prisma: InMemoryPrismaService; beforeAll(async () => { prismaStub = new InMemoryPrismaService(); const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }) .overrideProvider(PrismaService) .useValue(prismaStub) .compile(); app = moduleFixture.createNestApplication(); await app.init(); prisma = prismaStub; }); afterAll(async () => { await app.close(); }); beforeEach(() => { prisma.reset(); }); afterEach(() => { jest.restoreAllMocks(); }); 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, }, ], }); 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', }, ], }); 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 baseComponent = await prisma.composant.create({ data: { name: 'Bloc moteur standard', reference: 'COMP-BASE', typeComposantId, }, }); const basePiece = await prisma.piece.create({ data: { name: 'Kit maintenance standard', reference: 'KIT-BASE', typePieceId, }, }); const machineResponse = await request(app.getHttpServer()) .post('/machines') .send({ name: 'Presse HP-2000', siteId, typeMachineId: typeMachine.id, componentLinks: [ { requirementId: componentRequirementId, composantId: baseComponent.id, overrides: { name: 'Bloc moteur série X', reference: 'COMP-001', prix: '12000.00', }, }, ], pieceLinks: [ { requirementId: pieceRequirementId, pieceId: basePiece.id, overrides: { name: 'Kit maintenance niveau 1', reference: 'KIT-001', }, }, ], }); 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.componentLinks).toHaveLength(1); expect(machine.pieceLinks).toHaveLength(1); const componentLink = machine.componentLinks[0]; expect(componentLink.composantId).toBe(baseComponent.id); expect(componentLink.overrides.name).toBe('Bloc moteur série X'); expect(componentLink.composant.name).toBe('Bloc moteur série X'); expect(componentLink.originalComposant.name).toBe('Bloc moteur standard'); const pieceLink = machine.pieceLinks[0]; expect(pieceLink.pieceId).toBe(basePiece.id); expect(pieceLink.overrides.name).toBe('Kit maintenance niveau 1'); expect(pieceLink.piece.name).toBe('Kit maintenance niveau 1'); }); describe('POST /composants', () => { it('accepts creation when requirement matches the machine skeleton', async () => { jest.spyOn(prisma.machine, 'findUnique').mockResolvedValue({ id: 'machine-1', typeMachine: { componentRequirements: [ { id: 'req-1', typeComposantId: 'type-comp-1' }, ], }, } as any); const created = { id: 'component-1' }; const createSpy = jest .spyOn(prisma.composant, 'create') .mockResolvedValue(created as any); const response = await request(app.getHttpServer()) .post('/composants') .send({ name: 'Comp A', machineId: 'machine-1', typeComposantId: 'type-comp-1', typeMachineComponentRequirementId: 'req-1', }) .expect(201); expect(response.body).toEqual(created); expect(createSpy).toHaveBeenCalled(); }); it('refuses creation when requirement is not part of the machine skeleton', async () => { jest.spyOn(prisma.machine, 'findUnique').mockResolvedValue({ id: 'machine-1', typeMachine: { componentRequirements: [ { id: 'req-1', typeComposantId: 'type-comp-1' }, ], }, } as any); const createSpy = jest.spyOn(prisma.composant, 'create'); await request(app.getHttpServer()) .post('/composants') .send({ name: 'Comp A', machineId: 'machine-1', typeComposantId: 'type-comp-1', typeMachineComponentRequirementId: 'req-2', }) .expect(400); expect(createSpy).not.toHaveBeenCalled(); }); }); describe('POST /pieces', () => { it('accepts creation when requirement matches the machine skeleton', async () => { jest.spyOn(prisma.machine, 'findUnique').mockResolvedValue({ id: 'machine-1', typeMachine: { pieceRequirements: [{ id: 'req-1', typePieceId: 'type-piece-1' }], }, } as any); const created = { id: 'piece-1' }; const createSpy = jest .spyOn(prisma.piece, 'create') .mockResolvedValue(created as any); const response = await request(app.getHttpServer()) .post('/pieces') .send({ name: 'Piece A', machineId: 'machine-1', typePieceId: 'type-piece-1', typeMachinePieceRequirementId: 'req-1', }) .expect(201); expect(response.body).toEqual(created); expect(createSpy).toHaveBeenCalled(); }); it('refuses creation when requirement is not part of the machine skeleton', async () => { jest.spyOn(prisma.machine, 'findUnique').mockResolvedValue({ id: 'machine-1', typeMachine: { pieceRequirements: [{ id: 'req-1', typePieceId: 'type-piece-1' }], }, } as any); const createSpy = jest.spyOn(prisma.piece, 'create'); await request(app.getHttpServer()) .post('/pieces') .send({ name: 'Piece A', machineId: 'machine-1', typePieceId: 'type-piece-1', typeMachinePieceRequirementId: 'req-2', }) .expect(400); expect(createSpy).not.toHaveBeenCalled(); }); }); });