Merge branch 'master' into codex/add-e2e-tests-for-type-creation-and-editing
This commit is contained in:
@@ -1,19 +1,84 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ComposantsService } from './composants.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreateComposantDto } from '../shared/dto/composant.dto';
|
||||
|
||||
describe('ComposantsService', () => {
|
||||
let service: ComposantsService;
|
||||
let prisma: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
prisma = {
|
||||
composant: {
|
||||
create: jest.fn(),
|
||||
findUnique: jest.fn(),
|
||||
},
|
||||
machine: {
|
||||
findUnique: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [ComposantsService, PrismaService],
|
||||
providers: [ComposantsService, { provide: PrismaService, useValue: prisma }],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ComposantsService>(ComposantsService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should create a component when requirement matches the machine skeleton', async () => {
|
||||
const dto: CreateComposantDto = {
|
||||
name: 'Comp A',
|
||||
machineId: 'machine-1',
|
||||
typeComposantId: 'type-comp-1',
|
||||
typeMachineComponentRequirementId: 'req-1',
|
||||
};
|
||||
|
||||
prisma.machine.findUnique.mockResolvedValue({
|
||||
id: 'machine-1',
|
||||
typeMachine: {
|
||||
componentRequirements: [
|
||||
{ id: 'req-1', typeComposantId: 'type-comp-1' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const created = { id: 'component-1' };
|
||||
prisma.composant.create.mockResolvedValue(created);
|
||||
|
||||
await expect(service.create(dto)).resolves.toEqual(created);
|
||||
|
||||
expect(prisma.composant.create).toHaveBeenCalled();
|
||||
expect(
|
||||
prisma.composant.create.mock.calls[0][0].data.typeComposantId,
|
||||
).toBe('type-comp-1');
|
||||
});
|
||||
|
||||
it('should refuse creation when requirement does not belong to machine skeleton', async () => {
|
||||
const dto: CreateComposantDto = {
|
||||
name: 'Comp A',
|
||||
machineId: 'machine-1',
|
||||
typeComposantId: 'type-comp-1',
|
||||
typeMachineComponentRequirementId: 'req-2',
|
||||
};
|
||||
|
||||
prisma.machine.findUnique.mockResolvedValue({
|
||||
id: 'machine-1',
|
||||
typeMachine: {
|
||||
componentRequirements: [
|
||||
{ id: 'req-1', typeComposantId: 'type-comp-1' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await expect(service.create(dto)).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
|
||||
expect(prisma.composant.create).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreateComposantDto, UpdateComposantDto } from '../shared/dto/composant.dto';
|
||||
|
||||
@@ -7,8 +7,75 @@ export class ComposantsService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async create(createComposantDto: CreateComposantDto) {
|
||||
const requirementId = createComposantDto.typeMachineComponentRequirementId;
|
||||
|
||||
let machineId = createComposantDto.machineId;
|
||||
|
||||
if (createComposantDto.parentComposantId) {
|
||||
const parentMachineId = await this.resolveMachineIdFromComposant(
|
||||
createComposantDto.parentComposantId,
|
||||
);
|
||||
|
||||
if (machineId && machineId !== parentMachineId) {
|
||||
throw new BadRequestException(
|
||||
'Le composant parent ne correspond pas à la machine ciblée.',
|
||||
);
|
||||
}
|
||||
|
||||
machineId = parentMachineId;
|
||||
}
|
||||
|
||||
if (!machineId) {
|
||||
throw new BadRequestException(
|
||||
'Un machineId ou un parentComposantId valide est requis pour créer un composant.',
|
||||
);
|
||||
}
|
||||
|
||||
const machine = await this.prisma.machine.findUnique({
|
||||
where: { id: machineId },
|
||||
include: {
|
||||
typeMachine: {
|
||||
include: {
|
||||
componentRequirements: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!machine || !machine.typeMachine) {
|
||||
throw new BadRequestException(
|
||||
'La machine ciblée doit être associée à un type de machine pour valider les requirements.',
|
||||
);
|
||||
}
|
||||
|
||||
const requirement = machine.typeMachine.componentRequirements.find(
|
||||
(componentRequirement) => componentRequirement.id === requirementId,
|
||||
);
|
||||
|
||||
if (!requirement) {
|
||||
throw new BadRequestException(
|
||||
'Le requirement de composant fourni ne correspond pas au squelette de la machine.',
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
createComposantDto.typeComposantId &&
|
||||
createComposantDto.typeComposantId !== requirement.typeComposantId
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
'Le type de composant fourni ne correspond pas au requirement pour cette machine.',
|
||||
);
|
||||
}
|
||||
|
||||
const data = {
|
||||
...createComposantDto,
|
||||
machineId,
|
||||
typeComposantId:
|
||||
createComposantDto.typeComposantId ?? requirement.typeComposantId,
|
||||
};
|
||||
|
||||
return this.prisma.composant.create({
|
||||
data: createComposantDto,
|
||||
data,
|
||||
include: {
|
||||
machine: true,
|
||||
parentComposant: true,
|
||||
@@ -437,6 +504,37 @@ export class ComposantsService {
|
||||
});
|
||||
}
|
||||
|
||||
private async resolveMachineIdFromComposant(
|
||||
composantId: string,
|
||||
): Promise<string> {
|
||||
const composant = await this.prisma.composant.findUnique({
|
||||
where: { id: composantId },
|
||||
select: {
|
||||
id: true,
|
||||
machineId: true,
|
||||
parentComposantId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!composant) {
|
||||
throw new BadRequestException(
|
||||
'Le composant parent spécifié est introuvable.',
|
||||
);
|
||||
}
|
||||
|
||||
if (composant.machineId) {
|
||||
return composant.machineId;
|
||||
}
|
||||
|
||||
if (composant.parentComposantId) {
|
||||
return this.resolveMachineIdFromComposant(composant.parentComposantId);
|
||||
}
|
||||
|
||||
throw new BadRequestException(
|
||||
'Impossible de déterminer la machine associée au composant parent.',
|
||||
);
|
||||
}
|
||||
|
||||
async remove(id: string) {
|
||||
return this.prisma.composant.delete({
|
||||
where: { id },
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
|
||||
import { CustomFieldsService } from './custom-fields.service';
|
||||
import { CreateCustomFieldValueDto, UpdateCustomFieldValueDto } from '../shared/dto/custom-field.dto';
|
||||
import {
|
||||
CreateCustomFieldValueDto,
|
||||
UpdateCustomFieldValueDto,
|
||||
CustomFieldEntityParamsDto,
|
||||
UpsertCustomFieldValueDto,
|
||||
} from '../shared/dto/custom-field.dto';
|
||||
|
||||
@Controller('custom-fields')
|
||||
export class CustomFieldsController {
|
||||
@@ -12,11 +17,11 @@ export class CustomFieldsController {
|
||||
}
|
||||
|
||||
@Get('values/:entityType/:entityId')
|
||||
findCustomFieldValuesByEntity(
|
||||
@Param('entityType') entityType: string,
|
||||
@Param('entityId') entityId: string,
|
||||
) {
|
||||
return this.customFieldsService.findCustomFieldValuesByEntity(entityType, entityId);
|
||||
findCustomFieldValuesByEntity(@Param() params: CustomFieldEntityParamsDto) {
|
||||
return this.customFieldsService.findCustomFieldValuesByEntity(
|
||||
params.entityType,
|
||||
params.entityId,
|
||||
);
|
||||
}
|
||||
|
||||
@Get('values/:id')
|
||||
@@ -38,12 +43,7 @@ export class CustomFieldsController {
|
||||
}
|
||||
|
||||
@Post('values/upsert')
|
||||
upsertCustomFieldValue(@Body() body: {
|
||||
customFieldId: string;
|
||||
entityType: string;
|
||||
entityId: string;
|
||||
value: string;
|
||||
}) {
|
||||
upsertCustomFieldValue(@Body() body: UpsertCustomFieldValueDto) {
|
||||
return this.customFieldsService.upsertCustomFieldValue(
|
||||
body.customFieldId,
|
||||
body.entityType,
|
||||
@@ -51,4 +51,4 @@ export class CustomFieldsController {
|
||||
body.value,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
103
src/custom-fields/custom-fields.service.spec.ts
Normal file
103
src/custom-fields/custom-fields.service.spec.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { CustomFieldsService } from './custom-fields.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CustomFieldEntityType } from '../shared/dto/custom-field.dto';
|
||||
|
||||
describe('CustomFieldsService', () => {
|
||||
let service: CustomFieldsService;
|
||||
let prisma: {
|
||||
machine: { findUnique: jest.Mock };
|
||||
composant: { findUnique: jest.Mock };
|
||||
piece: { findUnique: jest.Mock };
|
||||
customField: { findFirst: jest.Mock };
|
||||
customFieldValue: {
|
||||
findFirst: jest.Mock;
|
||||
update: jest.Mock;
|
||||
create: jest.Mock;
|
||||
};
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
prisma = {
|
||||
machine: { findUnique: jest.fn() },
|
||||
composant: { findUnique: jest.fn() },
|
||||
piece: { findUnique: jest.fn() },
|
||||
customField: { findFirst: jest.fn() },
|
||||
customFieldValue: {
|
||||
findFirst: jest.fn(),
|
||||
update: jest.fn(),
|
||||
create: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
service = new CustomFieldsService(prisma as unknown as PrismaService);
|
||||
});
|
||||
|
||||
describe('upsertCustomFieldValue', () => {
|
||||
it('should reject when the custom field is not allowed for the machine', async () => {
|
||||
prisma.machine.findUnique.mockResolvedValue({ typeMachineId: 'type-1' });
|
||||
prisma.customField.findFirst.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.upsertCustomFieldValue(
|
||||
'custom-field-1',
|
||||
CustomFieldEntityType.MACHINE,
|
||||
'machine-1',
|
||||
'value',
|
||||
),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(prisma.customField.findFirst).toHaveBeenCalledWith({
|
||||
where: {
|
||||
id: 'custom-field-1',
|
||||
typeMachineId: 'type-1',
|
||||
},
|
||||
});
|
||||
expect(prisma.customFieldValue.findFirst).not.toHaveBeenCalled();
|
||||
expect(prisma.customFieldValue.update).not.toHaveBeenCalled();
|
||||
expect(prisma.customFieldValue.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update an existing value when the custom field is allowed', async () => {
|
||||
prisma.machine.findUnique.mockResolvedValue({ typeMachineId: 'type-1' });
|
||||
prisma.customField.findFirst.mockResolvedValue({ id: 'custom-field-1' });
|
||||
prisma.customFieldValue.findFirst.mockResolvedValue({ id: 'value-1' });
|
||||
prisma.customFieldValue.update.mockResolvedValue({
|
||||
id: 'value-1',
|
||||
value: 'updated',
|
||||
customField: { id: 'custom-field-1' },
|
||||
});
|
||||
|
||||
const result = await service.upsertCustomFieldValue(
|
||||
'custom-field-1',
|
||||
CustomFieldEntityType.MACHINE,
|
||||
'machine-1',
|
||||
'updated',
|
||||
);
|
||||
|
||||
expect(prisma.customField.findFirst).toHaveBeenCalledWith({
|
||||
where: {
|
||||
id: 'custom-field-1',
|
||||
typeMachineId: 'type-1',
|
||||
},
|
||||
});
|
||||
expect(prisma.customFieldValue.findFirst).toHaveBeenCalledWith({
|
||||
where: {
|
||||
customFieldId: 'custom-field-1',
|
||||
machineId: 'machine-1',
|
||||
},
|
||||
});
|
||||
expect(prisma.customFieldValue.update).toHaveBeenCalledWith({
|
||||
where: { id: 'value-1' },
|
||||
data: { value: 'updated' },
|
||||
include: { customField: true },
|
||||
});
|
||||
expect(prisma.customFieldValue.create).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({
|
||||
id: 'value-1',
|
||||
value: 'updated',
|
||||
customField: { id: 'custom-field-1' },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreateCustomFieldValueDto, UpdateCustomFieldValueDto } from '../shared/dto/custom-field.dto';
|
||||
import {
|
||||
CreateCustomFieldValueDto,
|
||||
UpdateCustomFieldValueDto,
|
||||
CustomFieldEntityType,
|
||||
} from '../shared/dto/custom-field.dto';
|
||||
|
||||
@Injectable()
|
||||
export class CustomFieldsService {
|
||||
@@ -17,9 +21,99 @@ export class CustomFieldsService {
|
||||
}
|
||||
|
||||
// Trouver toutes les valeurs de champs personnalisés pour une entité
|
||||
async findCustomFieldValuesByEntity(entityType: string, entityId: string) {
|
||||
private getCustomFieldValueKey(entityType: CustomFieldEntityType) {
|
||||
switch (entityType) {
|
||||
case CustomFieldEntityType.MACHINE:
|
||||
return 'machineId' as const;
|
||||
case CustomFieldEntityType.COMPOSANT:
|
||||
return 'composantId' as const;
|
||||
case CustomFieldEntityType.PIECE:
|
||||
return 'pieceId' as const;
|
||||
default:
|
||||
throw new BadRequestException('Type d\'entité de champ personnalisé invalide.');
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveEntityContext(entityType: CustomFieldEntityType, entityId: string) {
|
||||
switch (entityType) {
|
||||
case CustomFieldEntityType.MACHINE: {
|
||||
const machine = await this.prisma.machine.findUnique({
|
||||
where: { id: entityId },
|
||||
select: { typeMachineId: true },
|
||||
});
|
||||
|
||||
if (!machine) {
|
||||
throw new NotFoundException('Machine introuvable.');
|
||||
}
|
||||
|
||||
if (!machine.typeMachineId) {
|
||||
throw new BadRequestException(
|
||||
'La machine ne possède pas de type associé pour les champs personnalisés.',
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
typeId: machine.typeMachineId,
|
||||
customFieldTypeField: 'typeMachineId' as const,
|
||||
valueKey: 'machineId' as const,
|
||||
};
|
||||
}
|
||||
case CustomFieldEntityType.COMPOSANT: {
|
||||
const composant = await this.prisma.composant.findUnique({
|
||||
where: { id: entityId },
|
||||
select: { typeComposantId: true },
|
||||
});
|
||||
|
||||
if (!composant) {
|
||||
throw new NotFoundException('Composant introuvable.');
|
||||
}
|
||||
|
||||
if (!composant.typeComposantId) {
|
||||
throw new BadRequestException(
|
||||
'Le composant ne possède pas de type associé pour les champs personnalisés.',
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
typeId: composant.typeComposantId,
|
||||
customFieldTypeField: 'typeComposantId' as const,
|
||||
valueKey: 'composantId' as const,
|
||||
};
|
||||
}
|
||||
case CustomFieldEntityType.PIECE: {
|
||||
const piece = await this.prisma.piece.findUnique({
|
||||
where: { id: entityId },
|
||||
select: { typePieceId: true },
|
||||
});
|
||||
|
||||
if (!piece) {
|
||||
throw new NotFoundException('Pièce introuvable.');
|
||||
}
|
||||
|
||||
if (!piece.typePieceId) {
|
||||
throw new BadRequestException(
|
||||
'La pièce ne possède pas de type associé pour les champs personnalisés.',
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
typeId: piece.typePieceId,
|
||||
customFieldTypeField: 'typePieceId' as const,
|
||||
valueKey: 'pieceId' as const,
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new BadRequestException('Type d\'entité de champ personnalisé invalide.');
|
||||
}
|
||||
}
|
||||
|
||||
async findCustomFieldValuesByEntity(
|
||||
entityType: CustomFieldEntityType,
|
||||
entityId: string,
|
||||
) {
|
||||
const key = this.getCustomFieldValueKey(entityType);
|
||||
const whereClause = {
|
||||
[entityType + 'Id']: entityId,
|
||||
[key]: entityId,
|
||||
};
|
||||
|
||||
return this.prisma.customFieldValue.findMany({
|
||||
@@ -59,12 +153,35 @@ export class CustomFieldsService {
|
||||
}
|
||||
|
||||
// Créer ou mettre à jour une valeur de champ personnalisé
|
||||
async upsertCustomFieldValue(customFieldId: string, entityType: string, entityId: string, value: string) {
|
||||
async upsertCustomFieldValue(
|
||||
customFieldId: string,
|
||||
entityType: CustomFieldEntityType,
|
||||
entityId: string,
|
||||
value: string,
|
||||
) {
|
||||
const { typeId, customFieldTypeField, valueKey } = await this.resolveEntityContext(
|
||||
entityType,
|
||||
entityId,
|
||||
);
|
||||
|
||||
const allowedCustomField = await this.prisma.customField.findFirst({
|
||||
where: {
|
||||
id: customFieldId,
|
||||
[customFieldTypeField]: typeId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!allowedCustomField) {
|
||||
throw new BadRequestException(
|
||||
'Le champ personnalisé n\'est pas autorisé pour cette entité.',
|
||||
);
|
||||
}
|
||||
|
||||
// D'abord, essayer de trouver une valeur existante
|
||||
const existingValue = await this.prisma.customFieldValue.findFirst({
|
||||
where: {
|
||||
customFieldId,
|
||||
[entityType + 'Id']: entityId,
|
||||
[valueKey]: entityId,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -83,7 +200,7 @@ export class CustomFieldsService {
|
||||
data: {
|
||||
customFieldId,
|
||||
value,
|
||||
[entityType + 'Id']: entityId,
|
||||
[valueKey]: entityId,
|
||||
},
|
||||
include: {
|
||||
customField: true,
|
||||
@@ -91,4 +208,4 @@ export class CustomFieldsService {
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<string, MachineComponentSelectionDto[]>();
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,85 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { PiecesService } from './pieces.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreatePieceDto } from '../shared/dto/piece.dto';
|
||||
|
||||
describe('PiecesService', () => {
|
||||
let service: PiecesService;
|
||||
let prisma: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
prisma = {
|
||||
piece: {
|
||||
create: jest.fn(),
|
||||
},
|
||||
machine: {
|
||||
findUnique: jest.fn(),
|
||||
},
|
||||
composant: {
|
||||
findUnique: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [PiecesService, PrismaService],
|
||||
providers: [PiecesService, { provide: PrismaService, useValue: prisma }],
|
||||
}).compile();
|
||||
|
||||
service = module.get<PiecesService>(PiecesService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should create a piece when requirement matches the machine skeleton', async () => {
|
||||
const dto: CreatePieceDto = {
|
||||
name: 'Piece A',
|
||||
machineId: 'machine-1',
|
||||
typePieceId: 'type-piece-1',
|
||||
typeMachinePieceRequirementId: 'req-1',
|
||||
};
|
||||
|
||||
prisma.machine.findUnique.mockResolvedValue({
|
||||
id: 'machine-1',
|
||||
typeMachine: {
|
||||
pieceRequirements: [
|
||||
{ id: 'req-1', typePieceId: 'type-piece-1' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const created = { id: 'piece-1' };
|
||||
prisma.piece.create.mockResolvedValue(created);
|
||||
|
||||
await expect(service.create(dto)).resolves.toEqual(created);
|
||||
expect(prisma.piece.create).toHaveBeenCalled();
|
||||
expect(prisma.piece.create.mock.calls[0][0].data.machineId).toBe(
|
||||
'machine-1',
|
||||
);
|
||||
});
|
||||
|
||||
it('should refuse creation when requirement does not belong to machine skeleton', async () => {
|
||||
const dto: CreatePieceDto = {
|
||||
name: 'Piece A',
|
||||
machineId: 'machine-1',
|
||||
typePieceId: 'type-piece-1',
|
||||
typeMachinePieceRequirementId: 'req-2',
|
||||
};
|
||||
|
||||
prisma.machine.findUnique.mockResolvedValue({
|
||||
id: 'machine-1',
|
||||
typeMachine: {
|
||||
pieceRequirements: [
|
||||
{ id: 'req-1', typePieceId: 'type-piece-1' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await expect(service.create(dto)).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
|
||||
expect(prisma.piece.create).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreatePieceDto, UpdatePieceDto } from '../shared/dto/piece.dto';
|
||||
|
||||
@@ -7,8 +7,74 @@ export class PiecesService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async create(createPieceDto: CreatePieceDto) {
|
||||
const requirementId = createPieceDto.typeMachinePieceRequirementId;
|
||||
|
||||
let machineId = createPieceDto.machineId;
|
||||
|
||||
if (createPieceDto.composantId) {
|
||||
const composantMachineId = await this.resolveMachineIdFromComposant(
|
||||
createPieceDto.composantId,
|
||||
);
|
||||
|
||||
if (machineId && machineId !== composantMachineId) {
|
||||
throw new BadRequestException(
|
||||
'Le composant ciblé appartient à une autre machine que celle fournie.',
|
||||
);
|
||||
}
|
||||
|
||||
machineId = composantMachineId;
|
||||
}
|
||||
|
||||
if (!machineId) {
|
||||
throw new BadRequestException(
|
||||
'Un machineId ou un composantId valide est requis pour créer une pièce.',
|
||||
);
|
||||
}
|
||||
|
||||
const machine = await this.prisma.machine.findUnique({
|
||||
where: { id: machineId },
|
||||
include: {
|
||||
typeMachine: {
|
||||
include: {
|
||||
pieceRequirements: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!machine || !machine.typeMachine) {
|
||||
throw new BadRequestException(
|
||||
'La machine ciblée doit être associée à un type de machine pour valider les requirements.',
|
||||
);
|
||||
}
|
||||
|
||||
const requirement = machine.typeMachine.pieceRequirements.find(
|
||||
(pieceRequirement) => pieceRequirement.id === requirementId,
|
||||
);
|
||||
|
||||
if (!requirement) {
|
||||
throw new BadRequestException(
|
||||
'Le requirement de pièce fourni ne correspond pas au squelette de la machine.',
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
createPieceDto.typePieceId &&
|
||||
createPieceDto.typePieceId !== requirement.typePieceId
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
'Le type de pièce fourni ne correspond pas au requirement pour cette machine.',
|
||||
);
|
||||
}
|
||||
|
||||
const data = {
|
||||
...createPieceDto,
|
||||
machineId,
|
||||
typePieceId: createPieceDto.typePieceId ?? requirement.typePieceId,
|
||||
};
|
||||
|
||||
return this.prisma.piece.create({
|
||||
data: createPieceDto,
|
||||
data,
|
||||
include: {
|
||||
machine: true,
|
||||
composant: true,
|
||||
@@ -101,6 +167,37 @@ export class PiecesService {
|
||||
});
|
||||
}
|
||||
|
||||
private async resolveMachineIdFromComposant(
|
||||
composantId: string,
|
||||
): Promise<string> {
|
||||
const composant = await this.prisma.composant.findUnique({
|
||||
where: { id: composantId },
|
||||
select: {
|
||||
id: true,
|
||||
machineId: true,
|
||||
parentComposantId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!composant) {
|
||||
throw new BadRequestException(
|
||||
'Le composant spécifié est introuvable.',
|
||||
);
|
||||
}
|
||||
|
||||
if (composant.machineId) {
|
||||
return composant.machineId;
|
||||
}
|
||||
|
||||
if (composant.parentComposantId) {
|
||||
return this.resolveMachineIdFromComposant(composant.parentComposantId);
|
||||
}
|
||||
|
||||
throw new BadRequestException(
|
||||
'Impossible de déterminer la machine associée à ce composant.',
|
||||
);
|
||||
}
|
||||
|
||||
async findByComposant(composantId: string) {
|
||||
return this.prisma.piece.findMany({
|
||||
where: { composantId },
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { IsString, IsOptional, IsNumber } from 'class-validator';
|
||||
import { IsString, IsOptional, IsNumber, ValidateIf } from 'class-validator';
|
||||
import { Transform } from 'class-transformer';
|
||||
|
||||
export class CreateComposantDto {
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateIf((dto) => !dto.parentComposantId)
|
||||
@IsString()
|
||||
machineId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateIf((dto) => !dto.machineId)
|
||||
@IsString()
|
||||
parentComposantId?: string;
|
||||
|
||||
@@ -34,6 +34,9 @@ export class CreateComposantDto {
|
||||
@IsString()
|
||||
typeComposantId?: string;
|
||||
|
||||
@IsString()
|
||||
typeMachineComponentRequirementId: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
composantModelId?: string;
|
||||
|
||||
@@ -1,4 +1,36 @@
|
||||
import { IsString, IsOptional, IsNotEmpty } from 'class-validator';
|
||||
import { IsString, IsOptional, IsNotEmpty, IsEnum } from 'class-validator';
|
||||
|
||||
export enum CustomFieldEntityType {
|
||||
MACHINE = 'machine',
|
||||
COMPOSANT = 'composant',
|
||||
PIECE = 'piece',
|
||||
}
|
||||
|
||||
export class CustomFieldEntityParamsDto {
|
||||
@IsEnum(CustomFieldEntityType)
|
||||
entityType: CustomFieldEntityType;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
entityId: string;
|
||||
}
|
||||
|
||||
export class UpsertCustomFieldValueDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
customFieldId: string;
|
||||
|
||||
@IsEnum(CustomFieldEntityType)
|
||||
entityType: CustomFieldEntityType;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
entityId: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
value: string;
|
||||
}
|
||||
|
||||
export class CreateCustomFieldValueDto {
|
||||
@IsString()
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { IsString, IsOptional, IsNumber } from 'class-validator';
|
||||
import { IsString, IsOptional, IsNumber, ValidateIf } from 'class-validator';
|
||||
import { Transform } from 'class-transformer';
|
||||
|
||||
export class CreatePieceDto {
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateIf((dto) => !dto.composantId)
|
||||
@IsString()
|
||||
machineId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateIf((dto) => !dto.machineId)
|
||||
@IsString()
|
||||
composantId?: string;
|
||||
|
||||
@@ -34,6 +34,9 @@ export class CreatePieceDto {
|
||||
@IsString()
|
||||
typePieceId?: string;
|
||||
|
||||
@IsString()
|
||||
typeMachinePieceRequirementId: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
pieceModelId?: string;
|
||||
|
||||
@@ -902,6 +902,7 @@ describe('Inventory flow (e2e)', () => {
|
||||
beforeAll(async () => {
|
||||
prismaStub = new InMemoryPrismaService();
|
||||
|
||||
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
})
|
||||
@@ -911,6 +912,22 @@ describe('Inventory flow (e2e)', () => {
|
||||
|
||||
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',
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -1070,4 +1087,109 @@ describe('Inventory flow (e2e)', () => {
|
||||
const refreshedComponent = refreshedMachineResponse.body.composants[0];
|
||||
expect(refreshedComponent.customFieldValues[0].value).toBe('8 kW');
|
||||
});
|
||||
|
||||
describe('POST /composants', () => {
|
||||
it('accepts creation when requirement matches the machine skeleton', async () => {
|
||||
prisma.machine.findUnique.mockResolvedValue({
|
||||
id: 'machine-1',
|
||||
typeMachine: {
|
||||
componentRequirements: [
|
||||
{ id: 'req-1', typeComposantId: 'type-comp-1' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const created = { id: 'component-1' };
|
||||
prisma.composant.create.mockResolvedValue(created);
|
||||
|
||||
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(prisma.composant.create).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('refuses creation when requirement is not part of the machine skeleton', async () => {
|
||||
prisma.machine.findUnique.mockResolvedValue({
|
||||
id: 'machine-1',
|
||||
typeMachine: {
|
||||
componentRequirements: [
|
||||
{ id: 'req-1', typeComposantId: 'type-comp-1' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post('/composants')
|
||||
.send({
|
||||
name: 'Comp A',
|
||||
machineId: 'machine-1',
|
||||
typeComposantId: 'type-comp-1',
|
||||
typeMachineComponentRequirementId: 'req-2',
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
expect(prisma.composant.create).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /pieces', () => {
|
||||
it('accepts creation when requirement matches the machine skeleton', async () => {
|
||||
prisma.machine.findUnique.mockResolvedValue({
|
||||
id: 'machine-1',
|
||||
typeMachine: {
|
||||
pieceRequirements: [
|
||||
{ id: 'req-1', typePieceId: 'type-piece-1' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const created = { id: 'piece-1' };
|
||||
prisma.piece.create.mockResolvedValue(created);
|
||||
|
||||
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(prisma.piece.create).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('refuses creation when requirement is not part of the machine skeleton', async () => {
|
||||
prisma.machine.findUnique.mockResolvedValue({
|
||||
id: 'machine-1',
|
||||
typeMachine: {
|
||||
pieceRequirements: [
|
||||
{ id: 'req-1', typePieceId: 'type-piece-1' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post('/pieces')
|
||||
.send({
|
||||
name: 'Piece A',
|
||||
machineId: 'machine-1',
|
||||
typePieceId: 'type-piece-1',
|
||||
typeMachinePieceRequirementId: 'req-2',
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
expect(prisma.piece.create).not.toHaveBeenCalled();
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user