feat: add product domain and machine integration
- extend Prisma schema with products, product constructs and link tables\n- introduce product service, DTOs and includes with constructeur support\n- integrate product selections across model type skeletons, composants, pièces and machines\n- validate product requirements when building machine skeletons and payloads
This commit is contained in:
75
prisma/migrations/20251108120000_add_products/migration.sql
Normal file
75
prisma/migrations/20251108120000_add_products/migration.sql
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
-- AlterEnum
|
||||||
|
ALTER TYPE "ModelCategory" ADD VALUE IF NOT EXISTS 'PRODUCT';
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "ModelType" ADD COLUMN "productSkeleton" JSONB;
|
||||||
|
|
||||||
|
ALTER TABLE "composants" ADD COLUMN "productId" TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE "pieces" ADD COLUMN "productId" TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE "documents" ADD COLUMN "productId" TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE "custom_fields" ADD COLUMN "typeProductId" TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE "custom_field_values" ADD COLUMN "productId" TEXT;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "products" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"reference" TEXT,
|
||||||
|
"supplierPrice" DECIMAL(10,2),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"typeProductId" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "products_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "type_machine_product_requirements" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"label" TEXT,
|
||||||
|
"minCount" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"maxCount" INTEGER,
|
||||||
|
"required" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"allowNewModels" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"orderIndex" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"typeMachineId" TEXT NOT NULL,
|
||||||
|
"typeProductId" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "type_machine_product_requirements_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "_ProductConstructeurs" (
|
||||||
|
"A" TEXT NOT NULL,
|
||||||
|
"B" TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "products_name_key" ON "products"("name");
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "_ProductConstructeurs_AB_unique" ON "_ProductConstructeurs"("A", "B");
|
||||||
|
CREATE INDEX "_ProductConstructeurs_B_index" ON "_ProductConstructeurs"("B");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "products" ADD CONSTRAINT "products_typeProductId_fkey" FOREIGN KEY ("typeProductId") REFERENCES "ModelType"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE "composants" ADD CONSTRAINT "composants_productId_fkey" FOREIGN KEY ("productId") REFERENCES "products"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE "pieces" ADD CONSTRAINT "pieces_productId_fkey" FOREIGN KEY ("productId") REFERENCES "products"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE "documents" ADD CONSTRAINT "documents_productId_fkey" FOREIGN KEY ("productId") REFERENCES "products"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE "custom_fields" ADD CONSTRAINT "custom_fields_typeProductId_fkey" FOREIGN KEY ("typeProductId") REFERENCES "ModelType"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE "custom_field_values" ADD CONSTRAINT "custom_field_values_productId_fkey" FOREIGN KEY ("productId") REFERENCES "products"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE "type_machine_product_requirements" ADD CONSTRAINT "type_machine_product_requirements_typeMachineId_fkey" FOREIGN KEY ("typeMachineId") REFERENCES "type_machines"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE "type_machine_product_requirements" ADD CONSTRAINT "type_machine_product_requirements_typeProductId_fkey" FOREIGN KEY ("typeProductId") REFERENCES "ModelType"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE "_ProductConstructeurs" ADD CONSTRAINT "_ProductConstructeurs_A_fkey" FOREIGN KEY ("A") REFERENCES "products"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "_ProductConstructeurs" ADD CONSTRAINT "_ProductConstructeurs_B_fkey" FOREIGN KEY ("B") REFERENCES "constructeurs"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "machine_product_links" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"machineId" TEXT NOT NULL,
|
||||||
|
"productId" TEXT NOT NULL,
|
||||||
|
"typeMachineProductRequirementId" TEXT,
|
||||||
|
"parentLinkId" TEXT,
|
||||||
|
"parentComponentLinkId" TEXT,
|
||||||
|
"parentPieceLinkId" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "machine_product_links_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "machine_product_links" ADD CONSTRAINT "machine_product_links_machineId_fkey" FOREIGN KEY ("machineId") REFERENCES "machines"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "machine_product_links" ADD CONSTRAINT "machine_product_links_productId_fkey" FOREIGN KEY ("productId") REFERENCES "products"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "machine_product_links" ADD CONSTRAINT "machine_product_links_typeMachineProductRequirementId_fkey" FOREIGN KEY ("typeMachineProductRequirementId") REFERENCES "type_machine_product_requirements"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "machine_product_links" ADD CONSTRAINT "machine_product_links_parentLinkId_fkey" FOREIGN KEY ("parentLinkId") REFERENCES "machine_product_links"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "machine_product_links" ADD CONSTRAINT "machine_product_links_parentComponentLinkId_fkey" FOREIGN KEY ("parentComponentLinkId") REFERENCES "machine_component_links"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "machine_product_links" ADD CONSTRAINT "machine_product_links_parentPieceLinkId_fkey" FOREIGN KEY ("parentPieceLinkId") REFERENCES "machine_piece_links"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "machine_product_links_machineId_idx" ON "machine_product_links"("machineId");
|
||||||
|
CREATE INDEX "machine_product_links_productId_idx" ON "machine_product_links"("productId");
|
||||||
@@ -47,6 +47,7 @@ model TypeMachine {
|
|||||||
customFields CustomField[] @relation("TypeMachineCustomFields")
|
customFields CustomField[] @relation("TypeMachineCustomFields")
|
||||||
componentRequirements TypeMachineComponentRequirement[]
|
componentRequirements TypeMachineComponentRequirement[]
|
||||||
pieceRequirements TypeMachinePieceRequirement[]
|
pieceRequirements TypeMachinePieceRequirement[]
|
||||||
|
productRequirements TypeMachineProductRequirement[]
|
||||||
|
|
||||||
@@map("type_machines")
|
@@map("type_machines")
|
||||||
}
|
}
|
||||||
@@ -70,6 +71,7 @@ model Machine {
|
|||||||
|
|
||||||
componentLinks MachineComponentLink[]
|
componentLinks MachineComponentLink[]
|
||||||
pieceLinks MachinePieceLink[]
|
pieceLinks MachinePieceLink[]
|
||||||
|
productLinks MachineProductLink[]
|
||||||
documents Document[] @relation("MachineDocuments")
|
documents Document[] @relation("MachineDocuments")
|
||||||
customFieldValues CustomFieldValue[] @relation("MachineCustomFieldValues")
|
customFieldValues CustomFieldValue[] @relation("MachineCustomFieldValues")
|
||||||
|
|
||||||
@@ -88,6 +90,9 @@ model Composant {
|
|||||||
typeComposantId String?
|
typeComposantId String?
|
||||||
typeComposant ModelType? @relation("ModelTypeComponentAssignments", fields: [typeComposantId], references: [id])
|
typeComposant ModelType? @relation("ModelTypeComponentAssignments", fields: [typeComposantId], references: [id])
|
||||||
|
|
||||||
|
productId String?
|
||||||
|
product Product? @relation(fields: [productId], references: [id], onDelete: SetNull)
|
||||||
|
|
||||||
constructeurs Constructeur[] @relation("ComposantConstructeurs")
|
constructeurs Constructeur[] @relation("ComposantConstructeurs")
|
||||||
|
|
||||||
documents Document[] @relation("ComposantDocuments")
|
documents Document[] @relation("ComposantDocuments")
|
||||||
@@ -108,6 +113,9 @@ model Piece {
|
|||||||
typePieceId String?
|
typePieceId String?
|
||||||
typePiece ModelType? @relation("ModelTypePieceAssignments", fields: [typePieceId], references: [id])
|
typePiece ModelType? @relation("ModelTypePieceAssignments", fields: [typePieceId], references: [id])
|
||||||
|
|
||||||
|
productId String?
|
||||||
|
product Product? @relation(fields: [productId], references: [id], onDelete: SetNull)
|
||||||
|
|
||||||
constructeurs Constructeur[] @relation("PieceConstructeurs")
|
constructeurs Constructeur[] @relation("PieceConstructeurs")
|
||||||
|
|
||||||
documents Document[] @relation("PieceDocuments")
|
documents Document[] @relation("PieceDocuments")
|
||||||
@@ -117,6 +125,27 @@ model Piece {
|
|||||||
@@map("pieces")
|
@@map("pieces")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Product {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String @unique
|
||||||
|
reference String?
|
||||||
|
supplierPrice Decimal? @db.Decimal(10, 2)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
typeProductId String?
|
||||||
|
typeProduct ModelType? @relation("ModelTypeProductAssignments", fields: [typeProductId], references: [id])
|
||||||
|
|
||||||
|
constructeurs Constructeur[] @relation("ProductConstructeurs")
|
||||||
|
documents Document[] @relation("ProductDocuments")
|
||||||
|
customFieldValues CustomFieldValue[] @relation("ProductCustomFieldValues")
|
||||||
|
pieces Piece[]
|
||||||
|
composants Composant[]
|
||||||
|
machineLinks MachineProductLink[]
|
||||||
|
|
||||||
|
@@map("products")
|
||||||
|
}
|
||||||
|
|
||||||
model MachineComponentLink {
|
model MachineComponentLink {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
machineId String
|
machineId String
|
||||||
@@ -135,6 +164,7 @@ model MachineComponentLink {
|
|||||||
childLinks MachineComponentLink[] @relation("MachineComponentLinkHierarchy")
|
childLinks MachineComponentLink[] @relation("MachineComponentLinkHierarchy")
|
||||||
typeMachineComponentRequirement TypeMachineComponentRequirement? @relation("ComponentRequirementLinks", fields: [typeMachineComponentRequirementId], references: [id], onDelete: SetNull)
|
typeMachineComponentRequirement TypeMachineComponentRequirement? @relation("ComponentRequirementLinks", fields: [typeMachineComponentRequirementId], references: [id], onDelete: SetNull)
|
||||||
pieceLinks MachinePieceLink[] @relation("ComponentLinkPieceLinks")
|
pieceLinks MachinePieceLink[] @relation("ComponentLinkPieceLinks")
|
||||||
|
productLinks MachineProductLink[] @relation("ComponentLinkProductLinks")
|
||||||
|
|
||||||
@@map("machine_component_links")
|
@@map("machine_component_links")
|
||||||
}
|
}
|
||||||
@@ -155,13 +185,37 @@ model MachinePieceLink {
|
|||||||
piece Piece @relation(fields: [pieceId], references: [id], onDelete: Cascade)
|
piece Piece @relation(fields: [pieceId], references: [id], onDelete: Cascade)
|
||||||
parentLink MachineComponentLink? @relation("ComponentLinkPieceLinks", fields: [parentLinkId], references: [id], onDelete: Cascade)
|
parentLink MachineComponentLink? @relation("ComponentLinkPieceLinks", fields: [parentLinkId], references: [id], onDelete: Cascade)
|
||||||
typeMachinePieceRequirement TypeMachinePieceRequirement? @relation("PieceRequirementLinks", fields: [typeMachinePieceRequirementId], references: [id], onDelete: SetNull)
|
typeMachinePieceRequirement TypeMachinePieceRequirement? @relation("PieceRequirementLinks", fields: [typeMachinePieceRequirementId], references: [id], onDelete: SetNull)
|
||||||
|
productLinks MachineProductLink[] @relation("PieceLinkProductLinks")
|
||||||
|
|
||||||
@@map("machine_piece_links")
|
@@map("machine_piece_links")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model MachineProductLink {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
machineId String
|
||||||
|
productId String
|
||||||
|
typeMachineProductRequirementId String?
|
||||||
|
parentLinkId String?
|
||||||
|
parentComponentLinkId String?
|
||||||
|
parentPieceLinkId String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
machine Machine @relation(fields: [machineId], references: [id], onDelete: Cascade)
|
||||||
|
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||||
|
typeMachineProductRequirement TypeMachineProductRequirement? @relation("ProductRequirementLinks", fields: [typeMachineProductRequirementId], references: [id], onDelete: SetNull)
|
||||||
|
parentLink MachineProductLink? @relation("MachineProductLinkHierarchy", fields: [parentLinkId], references: [id], onDelete: Cascade)
|
||||||
|
childLinks MachineProductLink[] @relation("MachineProductLinkHierarchy")
|
||||||
|
parentComponentLink MachineComponentLink? @relation("ComponentLinkProductLinks", fields: [parentComponentLinkId], references: [id], onDelete: Cascade)
|
||||||
|
parentPieceLink MachinePieceLink? @relation("PieceLinkProductLinks", fields: [parentPieceLinkId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@map("machine_product_links")
|
||||||
|
}
|
||||||
|
|
||||||
enum ModelCategory {
|
enum ModelCategory {
|
||||||
COMPONENT
|
COMPONENT
|
||||||
PIECE
|
PIECE
|
||||||
|
PRODUCT
|
||||||
}
|
}
|
||||||
|
|
||||||
model ModelType {
|
model ModelType {
|
||||||
@@ -173,15 +227,19 @@ model ModelType {
|
|||||||
description String? @db.Text
|
description String? @db.Text
|
||||||
componentSkeleton Json?
|
componentSkeleton Json?
|
||||||
pieceSkeleton Json?
|
pieceSkeleton Json?
|
||||||
|
productSkeleton Json?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
composants Composant[] @relation("ModelTypeComponentAssignments")
|
composants Composant[] @relation("ModelTypeComponentAssignments")
|
||||||
componentRequirements TypeMachineComponentRequirement[] @relation("ModelTypeComponentRequirements")
|
componentRequirements TypeMachineComponentRequirement[] @relation("ModelTypeComponentRequirements")
|
||||||
customFields CustomField[] @relation("ModelTypeCustomFields")
|
customFields CustomField[] @relation("ModelTypeCustomFields")
|
||||||
|
productCustomFields CustomField[] @relation("ModelTypeProductCustomFields")
|
||||||
pieceRequirements TypeMachinePieceRequirement[] @relation("ModelTypePieceRequirements")
|
pieceRequirements TypeMachinePieceRequirement[] @relation("ModelTypePieceRequirements")
|
||||||
pieces Piece[] @relation("ModelTypePieceAssignments")
|
pieces Piece[] @relation("ModelTypePieceAssignments")
|
||||||
pieceCustomFields CustomField[] @relation("ModelTypePieceCustomFields")
|
pieceCustomFields CustomField[] @relation("ModelTypePieceCustomFields")
|
||||||
|
products Product[] @relation("ModelTypeProductAssignments")
|
||||||
|
productRequirements TypeMachineProductRequirement[] @relation("ModelTypeProductRequirements")
|
||||||
|
|
||||||
@@unique([category, name])
|
@@unique([category, name])
|
||||||
}
|
}
|
||||||
@@ -197,6 +255,7 @@ model Constructeur {
|
|||||||
machines Machine[] @relation("MachineConstructeurs")
|
machines Machine[] @relation("MachineConstructeurs")
|
||||||
composants Composant[] @relation("ComposantConstructeurs")
|
composants Composant[] @relation("ComposantConstructeurs")
|
||||||
pieces Piece[] @relation("PieceConstructeurs")
|
pieces Piece[] @relation("PieceConstructeurs")
|
||||||
|
products Product[] @relation("ProductConstructeurs")
|
||||||
|
|
||||||
@@map("constructeurs")
|
@@map("constructeurs")
|
||||||
}
|
}
|
||||||
@@ -232,6 +291,9 @@ model Document {
|
|||||||
pieceId String?
|
pieceId String?
|
||||||
piece Piece? @relation("PieceDocuments", fields: [pieceId], references: [id], onDelete: Cascade)
|
piece Piece? @relation("PieceDocuments", fields: [pieceId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
productId String?
|
||||||
|
product Product? @relation("ProductDocuments", fields: [productId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
siteId String?
|
siteId String?
|
||||||
site Site? @relation("SiteDocuments", fields: [siteId], references: [id], onDelete: Cascade)
|
site Site? @relation("SiteDocuments", fields: [siteId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@ -259,6 +321,9 @@ model CustomField {
|
|||||||
typePieceId String?
|
typePieceId String?
|
||||||
typePiece ModelType? @relation("ModelTypePieceCustomFields", fields: [typePieceId], references: [id], onDelete: Cascade)
|
typePiece ModelType? @relation("ModelTypePieceCustomFields", fields: [typePieceId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
typeProductId String?
|
||||||
|
typeProduct ModelType? @relation("ModelTypeProductCustomFields", fields: [typeProductId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
// Relations avec les valeurs
|
// Relations avec les valeurs
|
||||||
customFieldValues CustomFieldValue[]
|
customFieldValues CustomFieldValue[]
|
||||||
|
|
||||||
@@ -284,6 +349,9 @@ model CustomFieldValue {
|
|||||||
pieceId String?
|
pieceId String?
|
||||||
piece Piece? @relation("PieceCustomFieldValues", fields: [pieceId], references: [id], onDelete: Cascade)
|
piece Piece? @relation("PieceCustomFieldValues", fields: [pieceId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
productId String?
|
||||||
|
product Product? @relation("ProductCustomFieldValues", fields: [productId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@map("custom_field_values")
|
@@map("custom_field_values")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,3 +398,24 @@ model TypeMachinePieceRequirement {
|
|||||||
|
|
||||||
@@map("type_machine_piece_requirements")
|
@@map("type_machine_piece_requirements")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model TypeMachineProductRequirement {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
label String?
|
||||||
|
minCount Int @default(0)
|
||||||
|
maxCount Int?
|
||||||
|
required Boolean @default(false)
|
||||||
|
allowNewModels Boolean @default(true)
|
||||||
|
orderIndex Int @default(0)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
typeMachineId String
|
||||||
|
typeMachine TypeMachine @relation(fields: [typeMachineId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
typeProductId String
|
||||||
|
typeProduct ModelType @relation("ModelTypeProductRequirements", fields: [typeProductId], references: [id])
|
||||||
|
machineProductLinks MachineProductLink[] @relation("ProductRequirementLinks")
|
||||||
|
|
||||||
|
@@map("type_machine_product_requirements")
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ async function deleteExistingData() {
|
|||||||
await prisma.machinePieceLink.deleteMany();
|
await prisma.machinePieceLink.deleteMany();
|
||||||
await prisma.machine.deleteMany();
|
await prisma.machine.deleteMany();
|
||||||
await prisma.customFieldValue.deleteMany();
|
await prisma.customFieldValue.deleteMany();
|
||||||
|
await prisma.product.deleteMany();
|
||||||
await prisma.composant.deleteMany();
|
await prisma.composant.deleteMany();
|
||||||
await prisma.piece.deleteMany();
|
await prisma.piece.deleteMany();
|
||||||
|
await prisma.typeMachineProductRequirement.deleteMany();
|
||||||
|
|
||||||
await prisma.modelType.deleteMany({
|
await prisma.modelType.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
@@ -22,6 +24,7 @@ async function deleteExistingData() {
|
|||||||
'cooling-module',
|
'cooling-module',
|
||||||
'structural-frame',
|
'structural-frame',
|
||||||
'hydraulic-power-unit',
|
'hydraulic-power-unit',
|
||||||
|
'hydraulic-product',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -239,6 +242,135 @@ async function createComponent(options: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createProductType(
|
||||||
|
name: string,
|
||||||
|
code: string,
|
||||||
|
description: string,
|
||||||
|
fields: Array<{
|
||||||
|
name: string;
|
||||||
|
type: 'text' | 'number' | 'select' | 'boolean' | 'date';
|
||||||
|
required?: boolean;
|
||||||
|
options?: string[];
|
||||||
|
}>,
|
||||||
|
skeleton?: Record<string, unknown>,
|
||||||
|
) {
|
||||||
|
const type = await prisma.modelType.create({
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
code,
|
||||||
|
category: 'PRODUCT',
|
||||||
|
description,
|
||||||
|
productSkeleton: skeleton
|
||||||
|
? (skeleton as Prisma.InputJsonValue)
|
||||||
|
: Prisma.JsonNull,
|
||||||
|
productCustomFields: {
|
||||||
|
create: fields.map((field, index) => ({
|
||||||
|
name: field.name,
|
||||||
|
type: field.type,
|
||||||
|
required: field.required ?? false,
|
||||||
|
options: field.options ?? [],
|
||||||
|
orderIndex: index,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const customFields = await prisma.customField.findMany({
|
||||||
|
where: { typeProductId: type.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
const fieldMap: CreatedFields = {};
|
||||||
|
customFields.forEach((field) => {
|
||||||
|
fieldMap[field.name] = field.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { type, fieldMap };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createProduct(options: {
|
||||||
|
name: string;
|
||||||
|
reference?: string;
|
||||||
|
supplierPrice?: number | null;
|
||||||
|
constructeurIds?: string[] | null;
|
||||||
|
typeId?: string | null;
|
||||||
|
fieldValues?: Record<string, string>;
|
||||||
|
}) {
|
||||||
|
const fieldValues = options.fieldValues ?? {};
|
||||||
|
|
||||||
|
const customFields = options.typeId
|
||||||
|
? await prisma.customField.findMany({
|
||||||
|
where: { typeProductId: options.typeId },
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const customFieldValues = Object.entries(fieldValues).flatMap(
|
||||||
|
([fieldName, value]) => {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = customFields.find((field) => field.name === fieldName);
|
||||||
|
if (!target) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
value,
|
||||||
|
customFieldId: target.id,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const constructeurIds = Array.isArray(options.constructeurIds)
|
||||||
|
? Array.from(
|
||||||
|
new Set(
|
||||||
|
options.constructeurIds
|
||||||
|
.filter((value): value is string => typeof value === 'string')
|
||||||
|
.map((value) => value.trim())
|
||||||
|
.filter((value) => value.length > 0),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const data: Prisma.ProductCreateInput = {
|
||||||
|
name: options.name,
|
||||||
|
reference: options.reference ?? null,
|
||||||
|
supplierPrice:
|
||||||
|
options.supplierPrice === undefined || options.supplierPrice === null
|
||||||
|
? null
|
||||||
|
: new Prisma.Decimal(options.supplierPrice),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.typeId) {
|
||||||
|
data.typeProduct = {
|
||||||
|
connect: { id: options.typeId },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (constructeurIds.length) {
|
||||||
|
data.constructeurs = {
|
||||||
|
connect: constructeurIds.map((id) => ({ id })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (customFieldValues.length) {
|
||||||
|
data.customFieldValues = {
|
||||||
|
create: customFieldValues.map((entry) => ({
|
||||||
|
value: entry.value,
|
||||||
|
customField: {
|
||||||
|
connect: { id: entry.customFieldId },
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return prisma.product.create({
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
console.log('Nettoyage des données existantes…');
|
console.log('Nettoyage des données existantes…');
|
||||||
await deleteExistingData();
|
await deleteExistingData();
|
||||||
@@ -371,6 +503,63 @@ async function main() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('Création des types de produits…');
|
||||||
|
const hydraulicProductFields: {
|
||||||
|
name: string;
|
||||||
|
type: 'text' | 'number' | 'select' | 'boolean' | 'date';
|
||||||
|
required?: boolean;
|
||||||
|
options?: string[];
|
||||||
|
}[] = [
|
||||||
|
{ name: 'Fournisseur', type: 'text', required: true },
|
||||||
|
{ name: 'Garantie (mois)', type: 'number', required: true },
|
||||||
|
{
|
||||||
|
name: 'Délai d’approvisionnement (jours)',
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const hydraulicProductType = await createProductType(
|
||||||
|
'Produit hydraulique standard',
|
||||||
|
'hydraulic-product',
|
||||||
|
'Produits compatibles avec les centrales hydrauliques',
|
||||||
|
hydraulicProductFields,
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('Création des produits…');
|
||||||
|
const pumpProduct = await createProduct({
|
||||||
|
name: 'Pompe PX-300 Fournisseur A',
|
||||||
|
reference: 'PRD-PX-300-A',
|
||||||
|
supplierPrice: 1520,
|
||||||
|
typeId: hydraulicProductType.type.id,
|
||||||
|
fieldValues: {
|
||||||
|
Fournisseur: 'HydrauParts',
|
||||||
|
'Garantie (mois)': '24',
|
||||||
|
'Délai d’approvisionnement (jours)': '21',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const coolingProduct = await createProduct({
|
||||||
|
name: 'Module de refroidissement AC-50 - OEM',
|
||||||
|
reference: 'PRD-AC-50',
|
||||||
|
supplierPrice: 1980,
|
||||||
|
typeId: hydraulicProductType.type.id,
|
||||||
|
fieldValues: {
|
||||||
|
Fournisseur: 'ThermoTech',
|
||||||
|
'Garantie (mois)': '18',
|
||||||
|
'Délai d’approvisionnement (jours)': '28',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Association des produits aux pièces…');
|
||||||
|
await prisma.piece.update({
|
||||||
|
where: { id: pumpPiece.id },
|
||||||
|
data: {
|
||||||
|
product: {
|
||||||
|
connect: { id: pumpProduct.id },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
console.log('Création des types de composants…');
|
console.log('Création des types de composants…');
|
||||||
const coolingComponentFields: {
|
const coolingComponentFields: {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -509,6 +698,15 @@ async function main() {
|
|||||||
} as Prisma.InputJsonValue,
|
} as Prisma.InputJsonValue,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await prisma.composant.update({
|
||||||
|
where: { id: coolingModule.id },
|
||||||
|
data: {
|
||||||
|
product: {
|
||||||
|
connect: { id: coolingProduct.id },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const structuralFrame = await createComponent({
|
const structuralFrame = await createComponent({
|
||||||
name: 'Châssis structurel XC-800',
|
name: 'Châssis structurel XC-800',
|
||||||
reference: 'FRAME-XC800',
|
reference: 'FRAME-XC800',
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { ConstructeursModule } from './constructeurs/constructeurs.module';
|
|||||||
import { ProfilesModule } from './profiles/profiles.module';
|
import { ProfilesModule } from './profiles/profiles.module';
|
||||||
import { SessionModule } from './session/session.module';
|
import { SessionModule } from './session/session.module';
|
||||||
import { ModelTypeModule } from './model-type/model-type.module';
|
import { ModelTypeModule } from './model-type/model-type.module';
|
||||||
|
import { ProductsModule } from './products/products.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -32,6 +33,7 @@ import { ModelTypeModule } from './model-type/model-type.module';
|
|||||||
ProfilesModule,
|
ProfilesModule,
|
||||||
SessionModule,
|
SessionModule,
|
||||||
ModelTypeModule,
|
ModelTypeModule,
|
||||||
|
ProductsModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [AppService],
|
||||||
|
|||||||
@@ -23,6 +23,17 @@ export const COMPONENT_WITH_RELATIONS_INCLUDE = {
|
|||||||
customField: { select: CUSTOM_FIELD_SELECT },
|
customField: { select: CUSTOM_FIELD_SELECT },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
product: {
|
||||||
|
include: {
|
||||||
|
constructeurs: true,
|
||||||
|
customFieldValues: {
|
||||||
|
include: {
|
||||||
|
customField: { select: CUSTOM_FIELD_SELECT },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
documents: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
machineLinks: {
|
machineLinks: {
|
||||||
include: {
|
include: {
|
||||||
machine: true,
|
machine: true,
|
||||||
|
|||||||
32
src/common/constants/product-includes.ts
Normal file
32
src/common/constants/product-includes.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
|
||||||
|
export const PRODUCT_WITH_RELATIONS_INCLUDE = {
|
||||||
|
typeProduct: {
|
||||||
|
include: {
|
||||||
|
productCustomFields: {
|
||||||
|
orderBy: { orderIndex: 'asc' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
constructeurs: true,
|
||||||
|
documents: true,
|
||||||
|
customFieldValues: {
|
||||||
|
include: {
|
||||||
|
customField: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pieces: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
reference: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
composants: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
reference: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Prisma.ProductInclude;
|
||||||
@@ -26,6 +26,16 @@ const baseDto = {
|
|||||||
typePieceId: 'piece-id',
|
typePieceId: 'piece-id',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
productRequirements: [
|
||||||
|
{
|
||||||
|
label: 'Product',
|
||||||
|
minCount: 1,
|
||||||
|
maxCount: 3,
|
||||||
|
required: true,
|
||||||
|
allowNewModels: true,
|
||||||
|
typeProductId: 'product-id',
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('TypeMachineMapper', () => {
|
describe('TypeMachineMapper', () => {
|
||||||
@@ -52,6 +62,14 @@ describe('TypeMachineMapper', () => {
|
|||||||
allowNewModels: true,
|
allowNewModels: true,
|
||||||
orderIndex: 0,
|
orderIndex: 0,
|
||||||
});
|
});
|
||||||
|
expect(input.productRequirements?.create?.[0]).toMatchObject({
|
||||||
|
label: 'Product',
|
||||||
|
minCount: 1,
|
||||||
|
maxCount: 3,
|
||||||
|
required: true,
|
||||||
|
allowNewModels: true,
|
||||||
|
orderIndex: 0,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should map custom field inputs for create many', () => {
|
it('should map custom field inputs for create many', () => {
|
||||||
@@ -76,6 +94,9 @@ describe('TypeMachineMapper', () => {
|
|||||||
const piece = TypeMachineMapper.mapPieceRequirementInputs(
|
const piece = TypeMachineMapper.mapPieceRequirementInputs(
|
||||||
baseDto.pieceRequirements as any,
|
baseDto.pieceRequirements as any,
|
||||||
);
|
);
|
||||||
|
const product = TypeMachineMapper.mapProductRequirementInputs(
|
||||||
|
baseDto.productRequirements as any,
|
||||||
|
);
|
||||||
|
|
||||||
expect(component[0]).toMatchObject({
|
expect(component[0]).toMatchObject({
|
||||||
typeComposantId: 'comp-id',
|
typeComposantId: 'comp-id',
|
||||||
@@ -89,5 +110,11 @@ describe('TypeMachineMapper', () => {
|
|||||||
maxCount: 2,
|
maxCount: 2,
|
||||||
orderIndex: 0,
|
orderIndex: 0,
|
||||||
});
|
});
|
||||||
|
expect(product[0]).toMatchObject({
|
||||||
|
typeProductId: 'product-id',
|
||||||
|
minCount: 1,
|
||||||
|
maxCount: 3,
|
||||||
|
orderIndex: 0,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type RequirementDto = {
|
|||||||
allowNewModels?: boolean | null;
|
allowNewModels?: boolean | null;
|
||||||
typeComposantId?: string;
|
typeComposantId?: string;
|
||||||
typePieceId?: string;
|
typePieceId?: string;
|
||||||
|
typeProductId?: string;
|
||||||
orderIndex?: number | null;
|
orderIndex?: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -29,6 +30,10 @@ export const TYPE_MACHINE_DEFAULT_INCLUDE: Prisma.TypeMachineInclude = {
|
|||||||
include: { typePiece: true },
|
include: { typePiece: true },
|
||||||
orderBy: { orderIndex: 'asc' },
|
orderBy: { orderIndex: 'asc' },
|
||||||
},
|
},
|
||||||
|
productRequirements: {
|
||||||
|
include: { typeProduct: true },
|
||||||
|
orderBy: { orderIndex: 'asc' },
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TYPE_MACHINE_WITH_MACHINES_INCLUDE: Prisma.TypeMachineInclude = {
|
export const TYPE_MACHINE_WITH_MACHINES_INCLUDE: Prisma.TypeMachineInclude = {
|
||||||
@@ -40,8 +45,13 @@ export class TypeMachineMapper {
|
|||||||
static toCreateInput(
|
static toCreateInput(
|
||||||
dto: CreateTypeMachineDto,
|
dto: CreateTypeMachineDto,
|
||||||
): Prisma.TypeMachineCreateInput {
|
): Prisma.TypeMachineCreateInput {
|
||||||
const { customFields, componentRequirements, pieceRequirements, ...data } =
|
const {
|
||||||
dto;
|
customFields,
|
||||||
|
componentRequirements,
|
||||||
|
pieceRequirements,
|
||||||
|
productRequirements,
|
||||||
|
...data
|
||||||
|
} = dto;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
@@ -50,14 +60,20 @@ export class TypeMachineMapper {
|
|||||||
componentRequirements,
|
componentRequirements,
|
||||||
),
|
),
|
||||||
pieceRequirements: this.mapPieceRequirements(pieceRequirements),
|
pieceRequirements: this.mapPieceRequirements(pieceRequirements),
|
||||||
|
productRequirements: this.mapProductRequirements(productRequirements),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static toUpdateData(
|
static toUpdateData(
|
||||||
dto: UpdateTypeMachineDto,
|
dto: UpdateTypeMachineDto,
|
||||||
): Prisma.TypeMachineUpdateInput {
|
): Prisma.TypeMachineUpdateInput {
|
||||||
const { customFields, componentRequirements, pieceRequirements, ...data } =
|
const {
|
||||||
dto;
|
customFields,
|
||||||
|
componentRequirements,
|
||||||
|
pieceRequirements,
|
||||||
|
productRequirements,
|
||||||
|
...data
|
||||||
|
} = dto;
|
||||||
|
|
||||||
const payload: Prisma.TypeMachineUpdateInput = { ...data };
|
const payload: Prisma.TypeMachineUpdateInput = { ...data };
|
||||||
|
|
||||||
@@ -73,6 +89,10 @@ export class TypeMachineMapper {
|
|||||||
payload.pieceRequirements = undefined;
|
payload.pieceRequirements = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (productRequirements !== undefined) {
|
||||||
|
payload.productRequirements = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,4 +219,50 @@ export class TypeMachineMapper {
|
|||||||
typePieceId: requirement.typePieceId!,
|
typePieceId: requirement.typePieceId!,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static mapProductRequirements(
|
||||||
|
requirements?: RequirementDto[] | null,
|
||||||
|
):
|
||||||
|
| Prisma.TypeMachineProductRequirementCreateNestedManyWithoutTypeMachineInput
|
||||||
|
| undefined {
|
||||||
|
if (!requirements || requirements.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
create: requirements.map((requirement, index) => ({
|
||||||
|
label: requirement.label ?? null,
|
||||||
|
minCount: requirement.minCount ?? 0,
|
||||||
|
maxCount: requirement.maxCount ?? null,
|
||||||
|
required: requirement.required ?? false,
|
||||||
|
allowNewModels: requirement.allowNewModels ?? true,
|
||||||
|
orderIndex: requirement.orderIndex ?? index,
|
||||||
|
typeProduct: requirement.typeProductId
|
||||||
|
? {
|
||||||
|
connect: { id: requirement.typeProductId },
|
||||||
|
}
|
||||||
|
: (() => {
|
||||||
|
throw new Error(
|
||||||
|
'typeProductId est requis pour créer une contrainte produit.',
|
||||||
|
);
|
||||||
|
})(),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static mapProductRequirementInputs(requirements?: RequirementDto[] | null) {
|
||||||
|
if (!requirements || requirements.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return requirements.map((requirement, index) => ({
|
||||||
|
label: requirement.label ?? null,
|
||||||
|
minCount: requirement.minCount ?? 0,
|
||||||
|
maxCount: requirement.maxCount ?? null,
|
||||||
|
required: requirement.required ?? false,
|
||||||
|
allowNewModels: requirement.allowNewModels ?? true,
|
||||||
|
orderIndex: requirement.orderIndex ?? index,
|
||||||
|
typeProductId: requirement.typeProductId!,
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ type PieceRequirementInput = Omit<
|
|||||||
'id' | 'typeMachineId'
|
'id' | 'typeMachineId'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
type ProductRequirementInput = Omit<
|
||||||
|
Prisma.TypeMachineProductRequirementCreateManyInput,
|
||||||
|
'id' | 'typeMachineId'
|
||||||
|
>;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TypeMachinesRepository {
|
export class TypeMachinesRepository {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
@@ -132,6 +137,28 @@ export class TypeMachinesRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteProductRequirements(typeMachineId: string) {
|
||||||
|
await this.client.typeMachineProductRequirement.deleteMany({
|
||||||
|
where: { typeMachineId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async createProductRequirements(
|
||||||
|
typeMachineId: string,
|
||||||
|
requirements: ProductRequirementInput[],
|
||||||
|
) {
|
||||||
|
if (!requirements.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.client.typeMachineProductRequirement.createMany({
|
||||||
|
data: requirements.map((requirement) => ({
|
||||||
|
...requirement,
|
||||||
|
typeMachineId,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async findMachinesUsingType(typeMachineId: string) {
|
async findMachinesUsingType(typeMachineId: string) {
|
||||||
return this.client.machine.findMany({
|
return this.client.machine.findMany({
|
||||||
where: { typeMachineId },
|
where: { typeMachineId },
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const DEFAULT_ORIENTATIONS: Record<string, LinkOrientation> = {
|
|||||||
_MachineConstructeurs: { parentColumn: 'A', constructeurColumn: 'B' },
|
_MachineConstructeurs: { parentColumn: 'A', constructeurColumn: 'B' },
|
||||||
_ComposantConstructeurs: { parentColumn: 'A', constructeurColumn: 'B' },
|
_ComposantConstructeurs: { parentColumn: 'A', constructeurColumn: 'B' },
|
||||||
_PieceConstructeurs: { parentColumn: 'A', constructeurColumn: 'B' },
|
_PieceConstructeurs: { parentColumn: 'A', constructeurColumn: 'B' },
|
||||||
|
_ProductConstructeurs: { parentColumn: 'A', constructeurColumn: 'B' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const sanitizeTableName = (tableName: string): string => {
|
const sanitizeTableName = (tableName: string): string => {
|
||||||
@@ -22,7 +23,12 @@ const sanitizeTableName = (tableName: string): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ORIENTATION_CACHE = new Map<string, LinkOrientation>();
|
const ORIENTATION_CACHE = new Map<string, LinkOrientation>();
|
||||||
const KNOWN_PARENT_TABLES = new Set(['machines', 'composants', 'pieces']);
|
const KNOWN_PARENT_TABLES = new Set([
|
||||||
|
'machines',
|
||||||
|
'composants',
|
||||||
|
'pieces',
|
||||||
|
'products',
|
||||||
|
]);
|
||||||
const oppositeColumn = (column: 'A' | 'B'): 'A' | 'B' =>
|
const oppositeColumn = (column: 'A' | 'B'): 'A' | 'B' =>
|
||||||
column === 'A' ? 'B' : 'A';
|
column === 'A' ? 'B' : 'A';
|
||||||
|
|
||||||
@@ -39,11 +45,12 @@ async function resolveOrientation(
|
|||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof prisma.__getConstructeurLinkOrientation === 'function') {
|
if (typeof prisma.__getConstructeurLinkOrientation === 'function') {
|
||||||
const orientation = await prisma.__getConstructeurLinkOrientation(tableName);
|
const orientation =
|
||||||
ORIENTATION_CACHE.set(tableName, orientation);
|
await prisma.__getConstructeurLinkOrientation(tableName);
|
||||||
return orientation;
|
ORIENTATION_CACHE.set(tableName, orientation);
|
||||||
}
|
return orientation;
|
||||||
|
}
|
||||||
|
|
||||||
const rows = await prisma.$queryRaw<
|
const rows = await prisma.$queryRaw<
|
||||||
Array<{ column_name: string; foreign_table_name: string }>
|
Array<{ column_name: string; foreign_table_name: string }>
|
||||||
@@ -103,11 +110,10 @@ async function resolveOrientation(
|
|||||||
|
|
||||||
if (!parentColumn || !constructeurColumn) {
|
if (!parentColumn || !constructeurColumn) {
|
||||||
const columns = rows
|
const columns = rows
|
||||||
.map(
|
.map((row) => row.column_name?.toUpperCase?.() as 'A' | 'B' | undefined)
|
||||||
(row) =>
|
.filter(
|
||||||
row.column_name?.toUpperCase?.() as 'A' | 'B' | undefined,
|
(column): column is 'A' | 'B' => column === 'A' || column === 'B',
|
||||||
)
|
);
|
||||||
.filter((column): column is 'A' | 'B' => column === 'A' || column === 'B');
|
|
||||||
|
|
||||||
if (columns.length === 2) {
|
if (columns.length === 2) {
|
||||||
if (!parentColumn) {
|
if (!parentColumn) {
|
||||||
@@ -204,8 +210,8 @@ export async function syncConstructeurLinks(
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const valueTuples = targetConstructeurIds.map((constructeurId) =>
|
const valueTuples = targetConstructeurIds.map(
|
||||||
Prisma.sql`(${parentId}, ${constructeurId})`,
|
(constructeurId) => Prisma.sql`(${parentId}, ${constructeurId})`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await prisma.$executeRaw(
|
await prisma.$executeRaw(
|
||||||
|
|||||||
@@ -76,6 +76,59 @@ export function normalizeComponentModelStructure(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const products = toArray((structure as any)?.products).map((product) => {
|
||||||
|
const candidate = product as Record<string, unknown> | null | undefined;
|
||||||
|
|
||||||
|
if (candidate?.typeProductId) {
|
||||||
|
const normalized: ComponentModelStructure['products'][number] = {
|
||||||
|
typeProductId:
|
||||||
|
ensureString(candidate.typeProductId).trim() || 'UNKNOWN',
|
||||||
|
role: sanitizeRole(candidate.role),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (candidate?.familyCode) {
|
||||||
|
const familyCode = ensureString(candidate.familyCode).trim();
|
||||||
|
if (familyCode) {
|
||||||
|
(normalized as Record<string, unknown>).familyCode = familyCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidate?.typeProductLabel) {
|
||||||
|
const label = ensureString(candidate.typeProductLabel).trim();
|
||||||
|
if (label) {
|
||||||
|
(normalized as Record<string, unknown>).typeProductLabel = label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidate?.reference) {
|
||||||
|
const reference = ensureString(candidate.reference).trim();
|
||||||
|
if (reference) {
|
||||||
|
(normalized as Record<string, unknown>).reference = reference;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidate?.familyCode) {
|
||||||
|
return {
|
||||||
|
familyCode: ensureString(candidate.familyCode).trim() || 'UNKNOWN',
|
||||||
|
role: sanitizeRole(candidate.role),
|
||||||
|
} as ComponentModelStructure['products'][number];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
familyCode:
|
||||||
|
ensureString(
|
||||||
|
candidate?.familyCode ??
|
||||||
|
candidate?.name ??
|
||||||
|
candidate?.typeProductLabel ??
|
||||||
|
'UNKNOWN',
|
||||||
|
).trim() || 'UNKNOWN',
|
||||||
|
role: sanitizeRole(candidate?.role),
|
||||||
|
} as ComponentModelStructure['products'][number];
|
||||||
|
});
|
||||||
|
|
||||||
const rawSubcomponents = toArray(
|
const rawSubcomponents = toArray(
|
||||||
(structure as any)?.subcomponents ?? (structure as any)?.subComponents,
|
(structure as any)?.subcomponents ?? (structure as any)?.subComponents,
|
||||||
);
|
);
|
||||||
@@ -115,6 +168,7 @@ export function normalizeComponentModelStructure(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
pieces,
|
pieces,
|
||||||
|
products,
|
||||||
customFields,
|
customFields,
|
||||||
subcomponents,
|
subcomponents,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ describe('ComposantsService', () => {
|
|||||||
const dto: CreateComposantDto = {
|
const dto: CreateComposantDto = {
|
||||||
name: 'Comp A',
|
name: 'Comp A',
|
||||||
typeComposantId: 'type-1',
|
typeComposantId: 'type-1',
|
||||||
|
productId: ' product-1 ',
|
||||||
};
|
};
|
||||||
|
|
||||||
prisma.composant.create.mockResolvedValue({ id: 'comp-1', name: dto.name });
|
prisma.composant.create.mockResolvedValue({ id: 'comp-1', name: dto.name });
|
||||||
@@ -42,11 +43,14 @@ describe('ComposantsService', () => {
|
|||||||
const result = await service.create(dto);
|
const result = await service.create(dto);
|
||||||
|
|
||||||
expect(prisma.composant.create).toHaveBeenCalled();
|
expect(prisma.composant.create).toHaveBeenCalled();
|
||||||
|
expect(prisma.composant.create.mock.calls[0][0].data.product).toEqual({
|
||||||
|
connect: { id: 'product-1' },
|
||||||
|
});
|
||||||
expect(result).toMatchObject({ id: 'comp-1' });
|
expect(result).toMatchObject({ id: 'comp-1' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates a component', async () => {
|
it('updates a component', async () => {
|
||||||
const dto: UpdateComposantDto = { name: 'Updated' };
|
const dto: UpdateComposantDto = { name: 'Updated', productId: '' };
|
||||||
|
|
||||||
prisma.composant.update.mockResolvedValue({
|
prisma.composant.update.mockResolvedValue({
|
||||||
id: 'comp-1',
|
id: 'comp-1',
|
||||||
@@ -56,5 +60,8 @@ describe('ComposantsService', () => {
|
|||||||
await service.update('comp-1', dto);
|
await service.update('comp-1', dto);
|
||||||
|
|
||||||
expect(prisma.composant.update).toHaveBeenCalled();
|
expect(prisma.composant.update).toHaveBeenCalled();
|
||||||
|
expect(prisma.composant.update.mock.calls[0][0].data.product).toEqual({
|
||||||
|
disconnect: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -40,6 +40,15 @@ export class ComposantsService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (createComposantDto.productId) {
|
||||||
|
const normalizedProductId = createComposantDto.productId.trim();
|
||||||
|
if (normalizedProductId) {
|
||||||
|
data.product = {
|
||||||
|
connect: { id: normalizedProductId },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (createComposantDto.structure !== undefined) {
|
if (createComposantDto.structure !== undefined) {
|
||||||
data.structure = createComposantDto.structure as Prisma.InputJsonValue;
|
data.structure = createComposantDto.structure as Prisma.InputJsonValue;
|
||||||
}
|
}
|
||||||
@@ -49,9 +58,8 @@ export class ComposantsService {
|
|||||||
|
|
||||||
async create(createComposantDto: CreateComposantDto) {
|
async create(createComposantDto: CreateComposantDto) {
|
||||||
try {
|
try {
|
||||||
const { data, constructeurIds } = await this.buildCreateInput(
|
const { data, constructeurIds } =
|
||||||
createComposantDto,
|
await this.buildCreateInput(createComposantDto);
|
||||||
);
|
|
||||||
const created = await this.prisma.composant.create({
|
const created = await this.prisma.composant.create({
|
||||||
data,
|
data,
|
||||||
include: COMPONENT_WITH_RELATIONS_INCLUDE,
|
include: COMPONENT_WITH_RELATIONS_INCLUDE,
|
||||||
@@ -73,9 +81,11 @@ export class ComposantsService {
|
|||||||
})) as ComposantWithRelations | null;
|
})) as ComposantWithRelations | null;
|
||||||
|
|
||||||
if (refreshed && syncedConstructeurIds.length > 0) {
|
if (refreshed && syncedConstructeurIds.length > 0) {
|
||||||
(refreshed as ComposantWithRelations & {
|
(
|
||||||
constructeurIds?: string[];
|
refreshed as ComposantWithRelations & {
|
||||||
}).constructeurIds = [...syncedConstructeurIds];
|
constructeurIds?: string[];
|
||||||
|
}
|
||||||
|
).constructeurIds = [...syncedConstructeurIds];
|
||||||
}
|
}
|
||||||
|
|
||||||
return refreshed;
|
return refreshed;
|
||||||
@@ -118,9 +128,8 @@ export class ComposantsService {
|
|||||||
const constructeurIds = this.normalizeConstructeurIds(
|
const constructeurIds = this.normalizeConstructeurIds(
|
||||||
updateComposantDto.constructeurIds,
|
updateComposantDto.constructeurIds,
|
||||||
);
|
);
|
||||||
resolvedConstructeurIds = await this.resolveExistingConstructeurIds(
|
resolvedConstructeurIds =
|
||||||
constructeurIds,
|
await this.resolveExistingConstructeurIds(constructeurIds);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updateComposantDto.typeComposantId !== undefined) {
|
if (updateComposantDto.typeComposantId !== undefined) {
|
||||||
@@ -129,6 +138,16 @@ export class ComposantsService {
|
|||||||
: { disconnect: true };
|
: { disconnect: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (updateComposantDto.productId !== undefined) {
|
||||||
|
const normalizedProductId =
|
||||||
|
typeof updateComposantDto.productId === 'string'
|
||||||
|
? updateComposantDto.productId.trim()
|
||||||
|
: null;
|
||||||
|
data.product = normalizedProductId
|
||||||
|
? { connect: { id: normalizedProductId } }
|
||||||
|
: { disconnect: true };
|
||||||
|
}
|
||||||
|
|
||||||
if (updateComposantDto.structure !== undefined) {
|
if (updateComposantDto.structure !== undefined) {
|
||||||
data.structure = updateComposantDto.structure as Prisma.InputJsonValue;
|
data.structure = updateComposantDto.structure as Prisma.InputJsonValue;
|
||||||
}
|
}
|
||||||
@@ -157,9 +176,11 @@ export class ComposantsService {
|
|||||||
})) as ComposantWithRelations | null;
|
})) as ComposantWithRelations | null;
|
||||||
|
|
||||||
if (refreshed && syncedConstructeurIds) {
|
if (refreshed && syncedConstructeurIds) {
|
||||||
(refreshed as ComposantWithRelations & {
|
(
|
||||||
constructeurIds?: string[];
|
refreshed as ComposantWithRelations & {
|
||||||
}).constructeurIds = [...syncedConstructeurIds];
|
constructeurIds?: string[];
|
||||||
|
}
|
||||||
|
).constructeurIds = [...syncedConstructeurIds];
|
||||||
}
|
}
|
||||||
|
|
||||||
return refreshed;
|
return refreshed;
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ export class CustomFieldsService {
|
|||||||
return 'composantId' as const;
|
return 'composantId' as const;
|
||||||
case CustomFieldEntityType.PIECE:
|
case CustomFieldEntityType.PIECE:
|
||||||
return 'pieceId' as const;
|
return 'pieceId' as const;
|
||||||
|
case CustomFieldEntityType.PRODUCT:
|
||||||
|
return 'productId' as const;
|
||||||
default:
|
default:
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
"Type d'entité de champ personnalisé invalide.",
|
"Type d'entité de champ personnalisé invalide.",
|
||||||
@@ -114,6 +116,28 @@ export class CustomFieldsService {
|
|||||||
valueKey: 'pieceId' as const,
|
valueKey: 'pieceId' as const,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case CustomFieldEntityType.PRODUCT: {
|
||||||
|
const product = await this.prisma.product.findUnique({
|
||||||
|
where: { id: entityId },
|
||||||
|
select: { typeProductId: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
throw new NotFoundException('Produit introuvable.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!product.typeProductId) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'Le produit ne possède pas de type associé pour les champs personnalisés.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
typeId: product.typeProductId,
|
||||||
|
customFieldTypeField: 'typeProductId' as const,
|
||||||
|
valueKey: 'productId' as const,
|
||||||
|
};
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
"Type d'entité de champ personnalisé invalide.",
|
"Type d'entité de champ personnalisé invalide.",
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ export class DocumentsController {
|
|||||||
return this.documentsService.findByPiece(pieceId);
|
return this.documentsService.findByPiece(pieceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('product/:productId')
|
||||||
|
findByProduct(@Param('productId') productId: string) {
|
||||||
|
return this.documentsService.findByProduct(productId);
|
||||||
|
}
|
||||||
|
|
||||||
@Get('site/:siteId')
|
@Get('site/:siteId')
|
||||||
findBySite(@Param('siteId') siteId: string) {
|
findBySite(@Param('siteId') siteId: string) {
|
||||||
return this.documentsService.findBySite(siteId);
|
return this.documentsService.findBySite(siteId);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export class DocumentsService {
|
|||||||
machine: true,
|
machine: true,
|
||||||
composant: true,
|
composant: true,
|
||||||
piece: true,
|
piece: true,
|
||||||
|
product: true,
|
||||||
site: true,
|
site: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -27,6 +28,7 @@ export class DocumentsService {
|
|||||||
machine: true,
|
machine: true,
|
||||||
composant: true,
|
composant: true,
|
||||||
piece: true,
|
piece: true,
|
||||||
|
product: true,
|
||||||
site: true,
|
site: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -39,6 +41,7 @@ export class DocumentsService {
|
|||||||
machine: true,
|
machine: true,
|
||||||
composant: true,
|
composant: true,
|
||||||
piece: true,
|
piece: true,
|
||||||
|
product: true,
|
||||||
site: true,
|
site: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -51,6 +54,7 @@ export class DocumentsService {
|
|||||||
machine: true,
|
machine: true,
|
||||||
composant: true,
|
composant: true,
|
||||||
piece: true,
|
piece: true,
|
||||||
|
product: true,
|
||||||
site: true,
|
site: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -63,6 +67,20 @@ export class DocumentsService {
|
|||||||
machine: true,
|
machine: true,
|
||||||
composant: true,
|
composant: true,
|
||||||
piece: true,
|
piece: true,
|
||||||
|
product: true,
|
||||||
|
site: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByProduct(productId: string) {
|
||||||
|
return this.prisma.document.findMany({
|
||||||
|
where: { productId },
|
||||||
|
include: {
|
||||||
|
machine: true,
|
||||||
|
composant: true,
|
||||||
|
piece: true,
|
||||||
|
product: true,
|
||||||
site: true,
|
site: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -75,6 +93,7 @@ export class DocumentsService {
|
|||||||
machine: true,
|
machine: true,
|
||||||
composant: true,
|
composant: true,
|
||||||
piece: true,
|
piece: true,
|
||||||
|
product: true,
|
||||||
site: true,
|
site: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -202,4 +202,52 @@ describe('MachinesService', () => {
|
|||||||
expect(result?.pieceLinks[0].piece.name).toBe('Root piece name');
|
expect(result?.pieceLinks[0].piece.name).toBe('Root piece name');
|
||||||
expect(result?.pieceLinks[0].overrides.reference).toBe('RP-001');
|
expect(result?.pieceLinks[0].overrides.reference).toBe('RP-001');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('validateProductRequirements', () => {
|
||||||
|
const buildRequirement = (overrides: Partial<any> = {}) =>
|
||||||
|
({
|
||||||
|
id: 'req-1',
|
||||||
|
label: 'Hydraulic kits',
|
||||||
|
minCount: 1,
|
||||||
|
maxCount: 2,
|
||||||
|
required: true,
|
||||||
|
allowNewModels: true,
|
||||||
|
typeProductId: 'product-type-1',
|
||||||
|
typeProduct: { name: 'Hydraulic kit' },
|
||||||
|
...overrides,
|
||||||
|
}) as any;
|
||||||
|
|
||||||
|
const callValidate = (
|
||||||
|
requirement: any,
|
||||||
|
componentUsage: Record<string, number>,
|
||||||
|
pieceUsage: Record<string, number>,
|
||||||
|
) => {
|
||||||
|
const map = new Map([[requirement.id, requirement]]);
|
||||||
|
const componentMap = new Map(Object.entries(componentUsage));
|
||||||
|
const pieceMap = new Map(Object.entries(pieceUsage));
|
||||||
|
(service as any).validateProductRequirements(map, componentMap, pieceMap);
|
||||||
|
};
|
||||||
|
|
||||||
|
it('does nothing when usage satisfies min and max constraints', () => {
|
||||||
|
expect(() =>
|
||||||
|
callValidate(buildRequirement(), { 'product-type-1': 1 }, {}),
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when minimum requirement is not met', () => {
|
||||||
|
expect(() => callValidate(buildRequirement(), {}, {})).toThrow(
|
||||||
|
/requiert au moins 1 sélection/i,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when usage exceeds maximum', () => {
|
||||||
|
expect(() =>
|
||||||
|
callValidate(
|
||||||
|
buildRequirement({ maxCount: 2 }),
|
||||||
|
{ 'product-type-1': 2 },
|
||||||
|
{ 'product-type-1': 1 },
|
||||||
|
),
|
||||||
|
).toThrow(/ne peut pas dépasser 2 sélection/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
ReconfigureMachineDto,
|
ReconfigureMachineDto,
|
||||||
MachineComponentLinkInput,
|
MachineComponentLinkInput,
|
||||||
MachinePieceLinkInput,
|
MachinePieceLinkInput,
|
||||||
|
MachineProductLinkInput,
|
||||||
} from '../shared/dto/machine.dto';
|
} from '../shared/dto/machine.dto';
|
||||||
import { buildComponentHierarchy } from '../common/utils/component-tree.util';
|
import { buildComponentHierarchy } from '../common/utils/component-tree.util';
|
||||||
import {
|
import {
|
||||||
@@ -51,6 +52,18 @@ const TYPE_MACHINE_CONFIGURATION_INCLUDE: Prisma.TypeMachineInclude = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
productRequirements: {
|
||||||
|
include: {
|
||||||
|
typeProduct: {
|
||||||
|
include: {
|
||||||
|
productCustomFields: {
|
||||||
|
orderBy: { orderIndex: 'asc' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { orderIndex: 'asc' },
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const MACHINE_PIECE_LINK_INCLUDE: Prisma.MachinePieceLinkInclude = {
|
const MACHINE_PIECE_LINK_INCLUDE: Prisma.MachinePieceLinkInclude = {
|
||||||
@@ -69,6 +82,17 @@ const MACHINE_PIECE_LINK_INCLUDE: Prisma.MachinePieceLinkInclude = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
product: {
|
||||||
|
include: {
|
||||||
|
constructeurs: true,
|
||||||
|
customFieldValues: {
|
||||||
|
include: {
|
||||||
|
customField: { select: CUSTOM_FIELD_SELECT },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
documents: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
documents: true,
|
documents: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -104,6 +128,17 @@ const buildComponentLinkInclude = (
|
|||||||
customField: { select: CUSTOM_FIELD_SELECT },
|
customField: { select: CUSTOM_FIELD_SELECT },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
product: {
|
||||||
|
include: {
|
||||||
|
constructeurs: true,
|
||||||
|
customFieldValues: {
|
||||||
|
include: {
|
||||||
|
customField: { select: CUSTOM_FIELD_SELECT },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
documents: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
documents: true,
|
documents: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -134,6 +169,20 @@ const buildComponentLinkInclude = (
|
|||||||
|
|
||||||
const MACHINE_COMPONENT_LINK_INCLUDE = buildComponentLinkInclude();
|
const MACHINE_COMPONENT_LINK_INCLUDE = buildComponentLinkInclude();
|
||||||
|
|
||||||
|
const MACHINE_PRODUCT_LINK_INCLUDE = {
|
||||||
|
product: {
|
||||||
|
include: {
|
||||||
|
constructeurs: true,
|
||||||
|
typeProduct: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
typeMachineProductRequirement: {
|
||||||
|
include: {
|
||||||
|
typeProduct: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Prisma.MachineProductLinkInclude;
|
||||||
|
|
||||||
const MACHINE_DEFAULT_INCLUDE = {
|
const MACHINE_DEFAULT_INCLUDE = {
|
||||||
site: true,
|
site: true,
|
||||||
typeMachine: {
|
typeMachine: {
|
||||||
@@ -146,6 +195,9 @@ const MACHINE_DEFAULT_INCLUDE = {
|
|||||||
pieceLinks: {
|
pieceLinks: {
|
||||||
include: MACHINE_PIECE_LINK_INCLUDE,
|
include: MACHINE_PIECE_LINK_INCLUDE,
|
||||||
},
|
},
|
||||||
|
productLinks: {
|
||||||
|
include: MACHINE_PRODUCT_LINK_INCLUDE,
|
||||||
|
},
|
||||||
customFieldValues: {
|
customFieldValues: {
|
||||||
include: {
|
include: {
|
||||||
customField: { select: CUSTOM_FIELD_SELECT },
|
customField: { select: CUSTOM_FIELD_SELECT },
|
||||||
@@ -166,6 +218,10 @@ type MachinePieceLinkWithRelations = Prisma.MachinePieceLinkGetPayload<{
|
|||||||
include: typeof MACHINE_PIECE_LINK_INCLUDE;
|
include: typeof MACHINE_PIECE_LINK_INCLUDE;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
type MachineProductLinkWithRelations = Prisma.MachineProductLinkGetPayload<{
|
||||||
|
include: typeof MACHINE_PRODUCT_LINK_INCLUDE;
|
||||||
|
}>;
|
||||||
|
|
||||||
type LinkOverride = {
|
type LinkOverride = {
|
||||||
name: string | null;
|
name: string | null;
|
||||||
reference: string | null;
|
reference: string | null;
|
||||||
@@ -215,18 +271,49 @@ type PieceRequirementWithType = Prisma.TypeMachinePieceRequirementGetPayload<{
|
|||||||
include: { typePiece: true };
|
include: { typePiece: true };
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
type ProductRequirementWithType =
|
||||||
|
Prisma.TypeMachineProductRequirementGetPayload<{
|
||||||
|
include: { typeProduct: true };
|
||||||
|
}>;
|
||||||
|
|
||||||
type ComponentWithType = Prisma.ComposantGetPayload<{
|
type ComponentWithType = Prisma.ComposantGetPayload<{
|
||||||
include: { typeComposant: true };
|
include: {
|
||||||
|
typeComposant: true;
|
||||||
|
product: {
|
||||||
|
select: {
|
||||||
|
id: true;
|
||||||
|
typeProductId: true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
type PieceWithType = Prisma.PieceGetPayload<{
|
type PieceWithType = Prisma.PieceGetPayload<{
|
||||||
include: { typePiece: true; constructeurs: true };
|
include: {
|
||||||
|
typePiece: true;
|
||||||
|
constructeurs: true;
|
||||||
|
product: {
|
||||||
|
select: {
|
||||||
|
id: true;
|
||||||
|
typeProductId: true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
type CreatedComponentLinkInfo = {
|
type CreatedComponentLinkInfo = {
|
||||||
id: string;
|
id: string;
|
||||||
composantId: string;
|
composantId: string;
|
||||||
requirementId: string | null;
|
requirementId: string | null;
|
||||||
|
productTypeId: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ComponentLinkIndex = {
|
||||||
|
createdLinks: Map<string, CreatedComponentLinkInfo>;
|
||||||
|
byComponentId: Map<string, CreatedComponentLinkInfo[]>;
|
||||||
|
byRequirementId: Map<string, CreatedComponentLinkInfo[]>;
|
||||||
|
productUsage: Map<string, number>;
|
||||||
|
autoPieceProductUsage: Map<string, number>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PendingComponentLink = {
|
type PendingComponentLink = {
|
||||||
@@ -248,6 +335,22 @@ type CreatedPieceLinkInfo = {
|
|||||||
pieceId: string;
|
pieceId: string;
|
||||||
requirementId: string;
|
requirementId: string;
|
||||||
parentLinkId: string | null;
|
parentLinkId: string | null;
|
||||||
|
productTypeId: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CreatedProductLinkInfo = {
|
||||||
|
id: string;
|
||||||
|
productId: string;
|
||||||
|
requirementId: string;
|
||||||
|
productTypeId: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PendingProductLink = {
|
||||||
|
raw: MachineProductLinkInput;
|
||||||
|
assignedId: string;
|
||||||
|
requirement: ProductRequirementWithType;
|
||||||
|
productId: string;
|
||||||
|
position: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PendingPieceLink = {
|
type PendingPieceLink = {
|
||||||
@@ -408,6 +511,7 @@ export class MachinesService {
|
|||||||
pieceLinks: HydratedPieceLink[];
|
pieceLinks: HydratedPieceLink[];
|
||||||
constructeurIds: string[];
|
constructeurIds: string[];
|
||||||
constructeurs: MachineWithRelations['constructeurs'];
|
constructeurs: MachineWithRelations['constructeurs'];
|
||||||
|
productLinks: MachineProductLinkWithRelations[];
|
||||||
})
|
})
|
||||||
| null {
|
| null {
|
||||||
if (!machine) {
|
if (!machine) {
|
||||||
@@ -431,6 +535,7 @@ export class MachinesService {
|
|||||||
pieceLinks: HydratedPieceLink[];
|
pieceLinks: HydratedPieceLink[];
|
||||||
constructeurIds: string[];
|
constructeurIds: string[];
|
||||||
constructeurs: MachineWithRelations['constructeurs'];
|
constructeurs: MachineWithRelations['constructeurs'];
|
||||||
|
productLinks: MachineProductLinkWithRelations[];
|
||||||
};
|
};
|
||||||
|
|
||||||
hydratedMachine.componentLinks = componentLinks;
|
hydratedMachine.componentLinks = componentLinks;
|
||||||
@@ -441,6 +546,7 @@ export class MachinesService {
|
|||||||
)
|
)
|
||||||
.filter((id): id is string => Boolean(id));
|
.filter((id): id is string => Boolean(id));
|
||||||
hydratedMachine.constructeurs = resolvedConstructeurs;
|
hydratedMachine.constructeurs = resolvedConstructeurs;
|
||||||
|
hydratedMachine.productLinks = machine.productLinks ?? [];
|
||||||
|
|
||||||
return hydratedMachine;
|
return hydratedMachine;
|
||||||
}
|
}
|
||||||
@@ -452,6 +558,7 @@ export class MachinesService {
|
|||||||
pieceLinks: HydratedPieceLink[];
|
pieceLinks: HydratedPieceLink[];
|
||||||
constructeurIds: string[];
|
constructeurIds: string[];
|
||||||
constructeurs: MachineWithRelations['constructeurs'];
|
constructeurs: MachineWithRelations['constructeurs'];
|
||||||
|
productLinks: MachineProductLinkWithRelations[];
|
||||||
})[] {
|
})[] {
|
||||||
return machines.map((machine) => this.hydrateMachine(machine)!);
|
return machines.map((machine) => this.hydrateMachine(machine)!);
|
||||||
}
|
}
|
||||||
@@ -481,7 +588,8 @@ export class MachinesService {
|
|||||||
.filter((id): id is string => Boolean(id));
|
.filter((id): id is string => Boolean(id));
|
||||||
|
|
||||||
const initialIds =
|
const initialIds =
|
||||||
Array.isArray(machine.constructeurIds) && machine.constructeurIds.length > 0
|
Array.isArray(machine.constructeurIds) &&
|
||||||
|
machine.constructeurIds.length > 0
|
||||||
? machine.constructeurIds
|
? machine.constructeurIds
|
||||||
: idsFromConstructeurs;
|
: idsFromConstructeurs;
|
||||||
|
|
||||||
@@ -515,20 +623,15 @@ export class MachinesService {
|
|||||||
|
|
||||||
const orderedConstructeurs = resolvedIds
|
const orderedConstructeurs = resolvedIds
|
||||||
.map((id) => byId.get(id))
|
.map((id) => byId.get(id))
|
||||||
.filter(
|
.filter((record): record is (typeof constructeurs)[number] =>
|
||||||
(
|
Boolean(record),
|
||||||
record,
|
|
||||||
): record is (typeof constructeurs)[number] =>
|
|
||||||
Boolean(record),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
machine.constructeurs =
|
machine.constructeurs = orderedConstructeurs;
|
||||||
orderedConstructeurs as MachineWithRelations['constructeurs'];
|
|
||||||
|
|
||||||
return machine;
|
return machine;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private slugifyName(name: string): string {
|
private slugifyName(name: string): string {
|
||||||
return name
|
return name
|
||||||
.normalize('NFD')
|
.normalize('NFD')
|
||||||
@@ -576,6 +679,7 @@ export class MachinesService {
|
|||||||
typeMachine: TypeMachineConfiguration,
|
typeMachine: TypeMachineConfiguration,
|
||||||
componentLinks: MachineComponentLinkInput[],
|
componentLinks: MachineComponentLinkInput[],
|
||||||
pieceLinks: MachinePieceLinkInput[],
|
pieceLinks: MachinePieceLinkInput[],
|
||||||
|
productLinks: MachineProductLinkInput[],
|
||||||
) {
|
) {
|
||||||
const componentRequirements = (
|
const componentRequirements = (
|
||||||
Array.isArray(typeMachine.componentRequirements)
|
Array.isArray(typeMachine.componentRequirements)
|
||||||
@@ -587,6 +691,11 @@ export class MachinesService {
|
|||||||
? typeMachine.pieceRequirements
|
? typeMachine.pieceRequirements
|
||||||
: []
|
: []
|
||||||
) as PieceRequirementWithType[];
|
) as PieceRequirementWithType[];
|
||||||
|
const productRequirements = (
|
||||||
|
Array.isArray(typeMachine.productRequirements)
|
||||||
|
? typeMachine.productRequirements
|
||||||
|
: []
|
||||||
|
) as ProductRequirementWithType[];
|
||||||
|
|
||||||
const componentRequirementMap = new Map(
|
const componentRequirementMap = new Map(
|
||||||
componentRequirements.map((requirement) => [requirement.id, requirement]),
|
componentRequirements.map((requirement) => [requirement.id, requirement]),
|
||||||
@@ -594,6 +703,9 @@ export class MachinesService {
|
|||||||
const pieceRequirementMap = new Map(
|
const pieceRequirementMap = new Map(
|
||||||
pieceRequirements.map((requirement) => [requirement.id, requirement]),
|
pieceRequirements.map((requirement) => [requirement.id, requirement]),
|
||||||
);
|
);
|
||||||
|
const productRequirementMap = new Map(
|
||||||
|
productRequirements.map((requirement) => [requirement.id, requirement]),
|
||||||
|
);
|
||||||
|
|
||||||
const componentLinksByRequirement = new Map<
|
const componentLinksByRequirement = new Map<
|
||||||
string,
|
string,
|
||||||
@@ -623,6 +735,10 @@ export class MachinesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pieceLinksByRequirement = new Map<string, MachinePieceLinkInput[]>();
|
const pieceLinksByRequirement = new Map<string, MachinePieceLinkInput[]>();
|
||||||
|
const productLinksByRequirement = new Map<
|
||||||
|
string,
|
||||||
|
MachineProductLinkInput[]
|
||||||
|
>();
|
||||||
for (const link of pieceLinks) {
|
for (const link of pieceLinks) {
|
||||||
const requirement = pieceRequirementMap.get(link.requirementId);
|
const requirement = pieceRequirementMap.get(link.requirementId);
|
||||||
if (!requirement) {
|
if (!requirement) {
|
||||||
@@ -693,11 +809,27 @@ export class MachinesService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const link of productLinks) {
|
||||||
|
const requirement = productRequirementMap.get(link.requirementId);
|
||||||
|
if (!requirement) {
|
||||||
|
throw new Error(
|
||||||
|
`Lien de produit invalide: requirementId=${link.requirementId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!productLinksByRequirement.has(requirement.id)) {
|
||||||
|
productLinksByRequirement.set(requirement.id, []);
|
||||||
|
}
|
||||||
|
productLinksByRequirement.get(requirement.id)!.push(link);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
componentRequirementMap,
|
componentRequirementMap,
|
||||||
pieceRequirementMap,
|
pieceRequirementMap,
|
||||||
|
productRequirementMap,
|
||||||
componentLinksByRequirement,
|
componentLinksByRequirement,
|
||||||
pieceLinksByRequirement,
|
pieceLinksByRequirement,
|
||||||
|
productLinksByRequirement,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -709,6 +841,69 @@ export class MachinesService {
|
|||||||
return value as Record<string, unknown>;
|
return value as Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private validateProductRequirements(
|
||||||
|
productRequirementMap: Map<string, ProductRequirementWithType>,
|
||||||
|
componentUsage: Map<string, number>,
|
||||||
|
pieceUsage: Map<string, number>,
|
||||||
|
directUsage: Map<string, number>,
|
||||||
|
productLinksByRequirement: Map<string, MachineProductLinkInput[]>,
|
||||||
|
) {
|
||||||
|
if (productRequirementMap.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalUsage = new Map<string, number>();
|
||||||
|
const accumulate = (source: Map<string, number>) => {
|
||||||
|
for (const [typeProductId, count] of source.entries()) {
|
||||||
|
totalUsage.set(
|
||||||
|
typeProductId,
|
||||||
|
(totalUsage.get(typeProductId) ?? 0) + count,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
accumulate(componentUsage);
|
||||||
|
accumulate(pieceUsage);
|
||||||
|
accumulate(directUsage);
|
||||||
|
|
||||||
|
for (const requirement of productRequirementMap.values()) {
|
||||||
|
const typeProductId = requirement.typeProductId;
|
||||||
|
if (!typeProductId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const directSelections =
|
||||||
|
productLinksByRequirement.get(requirement.id)?.length ?? 0;
|
||||||
|
const count = totalUsage.get(typeProductId) ?? 0;
|
||||||
|
const min = requirement.minCount ?? (requirement.required ? 1 : 0);
|
||||||
|
const max = requirement.maxCount ?? undefined;
|
||||||
|
|
||||||
|
const label =
|
||||||
|
requirement.label?.trim() ||
|
||||||
|
requirement.typeProduct?.name ||
|
||||||
|
requirement.typeProduct?.code ||
|
||||||
|
requirement.id;
|
||||||
|
|
||||||
|
if (count < min) {
|
||||||
|
throw new Error(
|
||||||
|
`Le groupe de produits "${label}" requiert au moins ${min} sélection(s) mais seulement ${count} ont été fournis.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (max !== undefined && count > max) {
|
||||||
|
throw new Error(
|
||||||
|
`Le groupe de produits "${label}" ne peut pas dépasser ${max} sélection(s).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (max !== undefined && directSelections > max) {
|
||||||
|
throw new Error(
|
||||||
|
`Le groupe de produits "${label}" ne peut pas dépasser ${max} sélection(s) directes.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private extractString(value: unknown): string | undefined {
|
private extractString(value: unknown): string | undefined {
|
||||||
if (typeof value !== 'string') {
|
if (typeof value !== 'string') {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -903,12 +1098,16 @@ export class MachinesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private describeRequirement(
|
private describeRequirement(
|
||||||
requirement: ComponentRequirementWithType | PieceRequirementWithType,
|
requirement:
|
||||||
|
| ComponentRequirementWithType
|
||||||
|
| PieceRequirementWithType
|
||||||
|
| ProductRequirementWithType,
|
||||||
): string {
|
): string {
|
||||||
return (
|
return (
|
||||||
requirement.label ||
|
requirement.label ||
|
||||||
(requirement as ComponentRequirementWithType).typeComposant?.name ||
|
(requirement as ComponentRequirementWithType).typeComposant?.name ||
|
||||||
(requirement as PieceRequirementWithType).typePiece?.name ||
|
(requirement as PieceRequirementWithType).typePiece?.name ||
|
||||||
|
(requirement as ProductRequirementWithType).typeProduct?.name ||
|
||||||
requirement.id
|
requirement.id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1066,6 +1265,8 @@ export class MachinesService {
|
|||||||
createdLinks: Map<string, CreatedComponentLinkInfo>,
|
createdLinks: Map<string, CreatedComponentLinkInfo>,
|
||||||
byComponentId: Map<string, CreatedComponentLinkInfo[]>,
|
byComponentId: Map<string, CreatedComponentLinkInfo[]>,
|
||||||
componentMap: Map<string, ComponentWithType>,
|
componentMap: Map<string, ComponentWithType>,
|
||||||
|
productUsage: Map<string, number>,
|
||||||
|
autoPieceProductUsage: Map<string, number>,
|
||||||
) {
|
) {
|
||||||
if (createdLinks.size === 0) {
|
if (createdLinks.size === 0) {
|
||||||
return;
|
return;
|
||||||
@@ -1083,6 +1284,12 @@ export class MachinesService {
|
|||||||
where: { id: componentId },
|
where: { id: componentId },
|
||||||
include: {
|
include: {
|
||||||
typeComposant: true,
|
typeComposant: true,
|
||||||
|
product: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
typeProductId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1104,6 +1311,12 @@ export class MachinesService {
|
|||||||
include: {
|
include: {
|
||||||
typePiece: true,
|
typePiece: true,
|
||||||
constructeurs: true,
|
constructeurs: true,
|
||||||
|
product: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
typeProductId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1249,6 +1462,14 @@ export class MachinesService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pieceProductTypeId = piece.product?.typeProductId ?? null;
|
||||||
|
if (pieceProductTypeId) {
|
||||||
|
autoPieceProductUsage.set(
|
||||||
|
pieceProductTypeId,
|
||||||
|
(autoPieceProductUsage.get(pieceProductTypeId) ?? 0) + 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
createdPieceKeys.add(pieceKey);
|
createdPieceKeys.add(pieceKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1307,10 +1528,20 @@ export class MachinesService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const childProductTypeId =
|
||||||
|
childComponent.product?.typeProductId ?? null;
|
||||||
|
if (childProductTypeId) {
|
||||||
|
productUsage.set(
|
||||||
|
childProductTypeId,
|
||||||
|
(productUsage.get(childProductTypeId) ?? 0) + 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const created: CreatedComponentLinkInfo = {
|
const created: CreatedComponentLinkInfo = {
|
||||||
id: assignedId,
|
id: assignedId,
|
||||||
composantId: selectedComponentId,
|
composantId: selectedComponentId,
|
||||||
requirementId: null,
|
requirementId: null,
|
||||||
|
productTypeId: childProductTypeId,
|
||||||
};
|
};
|
||||||
|
|
||||||
createdLinks.set(assignedId, created);
|
createdLinks.set(assignedId, created);
|
||||||
@@ -1331,13 +1562,15 @@ export class MachinesService {
|
|||||||
machineId: string,
|
machineId: string,
|
||||||
componentRequirementMap: Map<string, ComponentRequirementWithType>,
|
componentRequirementMap: Map<string, ComponentRequirementWithType>,
|
||||||
componentLinks: MachineComponentLinkInput[],
|
componentLinks: MachineComponentLinkInput[],
|
||||||
) {
|
): Promise<ComponentLinkIndex> {
|
||||||
const links = Array.isArray(componentLinks) ? componentLinks : [];
|
const links = Array.isArray(componentLinks) ? componentLinks : [];
|
||||||
if (links.length === 0) {
|
if (links.length === 0) {
|
||||||
return {
|
return {
|
||||||
createdLinks: new Map<string, CreatedComponentLinkInfo>(),
|
createdLinks: new Map<string, CreatedComponentLinkInfo>(),
|
||||||
byComponentId: new Map<string, CreatedComponentLinkInfo[]>(),
|
byComponentId: new Map<string, CreatedComponentLinkInfo[]>(),
|
||||||
byRequirementId: new Map<string, CreatedComponentLinkInfo[]>(),
|
byRequirementId: new Map<string, CreatedComponentLinkInfo[]>(),
|
||||||
|
productUsage: new Map<string, number>(),
|
||||||
|
autoPieceProductUsage: new Map<string, number>(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1375,7 +1608,15 @@ export class MachinesService {
|
|||||||
|
|
||||||
const components = await prisma.composant.findMany({
|
const components = await prisma.composant.findMany({
|
||||||
where: { id: { in: Array.from(componentIds) } },
|
where: { id: { in: Array.from(componentIds) } },
|
||||||
include: { typeComposant: true },
|
include: {
|
||||||
|
typeComposant: true,
|
||||||
|
product: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
typeProductId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const componentMap = new Map<string, ComponentWithType>(
|
const componentMap = new Map<string, ComponentWithType>(
|
||||||
components.map((component) => [component.id, component]),
|
components.map((component) => [component.id, component]),
|
||||||
@@ -1412,6 +1653,8 @@ export class MachinesService {
|
|||||||
const createdLinks = new Map<string, CreatedComponentLinkInfo>();
|
const createdLinks = new Map<string, CreatedComponentLinkInfo>();
|
||||||
const byComponentId = new Map<string, CreatedComponentLinkInfo[]>();
|
const byComponentId = new Map<string, CreatedComponentLinkInfo[]>();
|
||||||
const byRequirementId = new Map<string, CreatedComponentLinkInfo[]>();
|
const byRequirementId = new Map<string, CreatedComponentLinkInfo[]>();
|
||||||
|
const productUsage = new Map<string, number>();
|
||||||
|
const autoPieceProductUsage = new Map<string, number>();
|
||||||
|
|
||||||
while (pending.size > 0) {
|
while (pending.size > 0) {
|
||||||
let progress = false;
|
let progress = false;
|
||||||
@@ -1454,6 +1697,7 @@ export class MachinesService {
|
|||||||
id: entry.assignedId,
|
id: entry.assignedId,
|
||||||
composantId: entry.componentId,
|
composantId: entry.componentId,
|
||||||
requirementId: entry.requirement.id,
|
requirementId: entry.requirement.id,
|
||||||
|
productTypeId: entry.component?.product?.typeProductId ?? null,
|
||||||
};
|
};
|
||||||
|
|
||||||
createdLinks.set(entry.assignedId, created);
|
createdLinks.set(entry.assignedId, created);
|
||||||
@@ -1468,6 +1712,14 @@ export class MachinesService {
|
|||||||
}
|
}
|
||||||
byRequirementId.get(entry.requirement.id)!.push(created);
|
byRequirementId.get(entry.requirement.id)!.push(created);
|
||||||
|
|
||||||
|
const productTypeId = entry.component?.product?.typeProductId ?? null;
|
||||||
|
if (productTypeId) {
|
||||||
|
productUsage.set(
|
||||||
|
productTypeId,
|
||||||
|
(productUsage.get(productTypeId) ?? 0) + 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
pending.delete(id);
|
pending.delete(id);
|
||||||
progress = true;
|
progress = true;
|
||||||
}
|
}
|
||||||
@@ -1485,9 +1737,17 @@ export class MachinesService {
|
|||||||
createdLinks,
|
createdLinks,
|
||||||
byComponentId,
|
byComponentId,
|
||||||
componentMap,
|
componentMap,
|
||||||
|
productUsage,
|
||||||
|
autoPieceProductUsage,
|
||||||
);
|
);
|
||||||
|
|
||||||
return { createdLinks, byComponentId, byRequirementId };
|
return {
|
||||||
|
createdLinks,
|
||||||
|
byComponentId,
|
||||||
|
byRequirementId,
|
||||||
|
productUsage,
|
||||||
|
autoPieceProductUsage,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private resolveComponentParentReference(
|
private resolveComponentParentReference(
|
||||||
@@ -1570,15 +1830,17 @@ export class MachinesService {
|
|||||||
machineId: string,
|
machineId: string,
|
||||||
pieceRequirementMap: Map<string, PieceRequirementWithType>,
|
pieceRequirementMap: Map<string, PieceRequirementWithType>,
|
||||||
pieceLinks: MachinePieceLinkInput[],
|
pieceLinks: MachinePieceLinkInput[],
|
||||||
componentLinkIndex: {
|
componentLinkIndex: ComponentLinkIndex,
|
||||||
createdLinks: Map<string, CreatedComponentLinkInfo>;
|
): Promise<{
|
||||||
byComponentId: Map<string, CreatedComponentLinkInfo[]>;
|
createdLinks: Map<string, CreatedPieceLinkInfo>;
|
||||||
byRequirementId: Map<string, CreatedComponentLinkInfo[]>;
|
productUsage: Map<string, number>;
|
||||||
},
|
}> {
|
||||||
) {
|
|
||||||
const links = Array.isArray(pieceLinks) ? pieceLinks : [];
|
const links = Array.isArray(pieceLinks) ? pieceLinks : [];
|
||||||
if (links.length === 0) {
|
if (links.length === 0) {
|
||||||
return new Map<string, CreatedPieceLinkInfo>();
|
return {
|
||||||
|
createdLinks: new Map<string, CreatedPieceLinkInfo>(),
|
||||||
|
productUsage: new Map<string, number>(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const pieceIds = new Set<string>();
|
const pieceIds = new Set<string>();
|
||||||
@@ -1612,7 +1874,16 @@ export class MachinesService {
|
|||||||
|
|
||||||
const pieces = await prisma.piece.findMany({
|
const pieces = await prisma.piece.findMany({
|
||||||
where: { id: { in: Array.from(pieceIds) } },
|
where: { id: { in: Array.from(pieceIds) } },
|
||||||
include: { typePiece: true, constructeurs: true },
|
include: {
|
||||||
|
typePiece: true,
|
||||||
|
constructeurs: true,
|
||||||
|
product: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
typeProductId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const pieceMap = new Map<string, PieceWithType>(
|
const pieceMap = new Map<string, PieceWithType>(
|
||||||
pieces.map((piece) => [piece.id, piece]),
|
pieces.map((piece) => [piece.id, piece]),
|
||||||
@@ -1643,6 +1914,7 @@ export class MachinesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createdLinks = new Map<string, CreatedPieceLinkInfo>();
|
const createdLinks = new Map<string, CreatedPieceLinkInfo>();
|
||||||
|
const productUsage = new Map<string, number>();
|
||||||
|
|
||||||
for (const entry of pendingEntries) {
|
for (const entry of pendingEntries) {
|
||||||
const parentId = this.resolvePieceParentReference(
|
const parentId = this.resolvePieceParentReference(
|
||||||
@@ -1675,19 +1947,145 @@ export class MachinesService {
|
|||||||
pieceId: entry.pieceId,
|
pieceId: entry.pieceId,
|
||||||
requirementId: entry.requirement.id,
|
requirementId: entry.requirement.id,
|
||||||
parentLinkId: parentId ?? null,
|
parentLinkId: parentId ?? null,
|
||||||
|
productTypeId: entry.piece?.product?.typeProductId ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const productTypeId = entry.piece?.product?.typeProductId ?? null;
|
||||||
|
if (productTypeId) {
|
||||||
|
productUsage.set(
|
||||||
|
productTypeId,
|
||||||
|
(productUsage.get(productTypeId) ?? 0) + 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return createdLinks;
|
return { createdLinks, productUsage };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createProductLinksForMachine(
|
||||||
|
prisma: Prisma.TransactionClient | PrismaService,
|
||||||
|
machineId: string,
|
||||||
|
productRequirementMap: Map<string, ProductRequirementWithType>,
|
||||||
|
productLinks: MachineProductLinkInput[],
|
||||||
|
): Promise<{
|
||||||
|
createdLinks: Map<string, CreatedProductLinkInfo>;
|
||||||
|
productUsage: Map<string, number>;
|
||||||
|
}> {
|
||||||
|
const links = Array.isArray(productLinks) ? productLinks : [];
|
||||||
|
if (links.length === 0) {
|
||||||
|
return {
|
||||||
|
createdLinks: new Map<string, CreatedProductLinkInfo>(),
|
||||||
|
productUsage: new Map<string, number>(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const productIds = new Set<string>();
|
||||||
|
const pendingEntries: PendingProductLink[] = [];
|
||||||
|
|
||||||
|
links.forEach((link, index) => {
|
||||||
|
const requirement = productRequirementMap.get(link.requirementId);
|
||||||
|
if (!requirement) {
|
||||||
|
throw new Error(
|
||||||
|
`Requirement de produit introuvable (${link.requirementId}).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const productId = this.extractString(link.productId);
|
||||||
|
if (!productId) {
|
||||||
|
throw new Error(
|
||||||
|
`productId manquant pour le lien de produit #${index + 1} (${this.describeRequirement(requirement)}).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
productIds.add(productId);
|
||||||
|
|
||||||
|
pendingEntries.push({
|
||||||
|
raw: link,
|
||||||
|
assignedId: this.resolveLinkIdentifier(link) ?? randomUUID(),
|
||||||
|
requirement,
|
||||||
|
productId,
|
||||||
|
position: index,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const products = await prisma.product.findMany({
|
||||||
|
where: { id: { in: Array.from(productIds) } },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
typeProductId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const productMap = new Map(
|
||||||
|
products.map((product) => [product.id, product]),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const entry of pendingEntries) {
|
||||||
|
const product = productMap.get(entry.productId);
|
||||||
|
if (!product) {
|
||||||
|
throw new Error(
|
||||||
|
`Produit introuvable (${entry.productId}) pour le lien de produit #${entry.position + 1}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
entry.requirement.typeProductId &&
|
||||||
|
product.typeProductId &&
|
||||||
|
product.typeProductId !== entry.requirement.typeProductId
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`Le produit sélectionné n'appartient pas à la catégorie attendue pour "${this.describeRequirement(entry.requirement)}".`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdLinks = new Map<string, CreatedProductLinkInfo>();
|
||||||
|
const productUsage = new Map<string, number>();
|
||||||
|
|
||||||
|
for (const entry of pendingEntries) {
|
||||||
|
const product = productMap.get(entry.productId);
|
||||||
|
if (!product) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createData: Prisma.MachineProductLinkUncheckedCreateInput = {
|
||||||
|
id: entry.assignedId,
|
||||||
|
machineId,
|
||||||
|
productId: entry.productId,
|
||||||
|
typeMachineProductRequirementId: entry.requirement.id,
|
||||||
|
parentLinkId: this.extractString(entry.raw.parentLinkId),
|
||||||
|
parentComponentLinkId: this.extractString(
|
||||||
|
entry.raw.parentComponentLinkId,
|
||||||
|
),
|
||||||
|
parentPieceLinkId: this.extractString(entry.raw.parentPieceLinkId),
|
||||||
|
};
|
||||||
|
|
||||||
|
await prisma.machineProductLink.create({ data: createData });
|
||||||
|
|
||||||
|
const created: CreatedProductLinkInfo = {
|
||||||
|
id: entry.assignedId,
|
||||||
|
productId: entry.productId,
|
||||||
|
requirementId: entry.requirement.id,
|
||||||
|
productTypeId: product.typeProductId ?? null,
|
||||||
|
};
|
||||||
|
|
||||||
|
createdLinks.set(entry.assignedId, created);
|
||||||
|
|
||||||
|
const typeProductId = product.typeProductId ?? null;
|
||||||
|
if (typeProductId) {
|
||||||
|
productUsage.set(
|
||||||
|
typeProductId,
|
||||||
|
(productUsage.get(typeProductId) ?? 0) + 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { createdLinks, productUsage };
|
||||||
}
|
}
|
||||||
|
|
||||||
private resolvePieceParentReference(
|
private resolvePieceParentReference(
|
||||||
link: MachinePieceLinkInput,
|
link: MachinePieceLinkInput,
|
||||||
componentLinkIndex: {
|
componentLinkIndex: ComponentLinkIndex,
|
||||||
createdLinks: Map<string, CreatedComponentLinkInfo>;
|
|
||||||
byComponentId: Map<string, CreatedComponentLinkInfo[]>;
|
|
||||||
byRequirementId: Map<string, CreatedComponentLinkInfo[]>;
|
|
||||||
},
|
|
||||||
): string | null {
|
): string | null {
|
||||||
const explicitParentId = this.extractString(
|
const explicitParentId = this.extractString(
|
||||||
link.parentComponentLinkId ?? link.parentLinkId,
|
link.parentComponentLinkId ?? link.parentLinkId,
|
||||||
@@ -1813,6 +2211,7 @@ export class MachinesService {
|
|||||||
const {
|
const {
|
||||||
componentLinks = [],
|
componentLinks = [],
|
||||||
pieceLinks = [],
|
pieceLinks = [],
|
||||||
|
productLinks = [],
|
||||||
constructeurIds,
|
constructeurIds,
|
||||||
...machineData
|
...machineData
|
||||||
} = createMachineDto;
|
} = createMachineDto;
|
||||||
@@ -1834,8 +2233,17 @@ export class MachinesService {
|
|||||||
machineData.typeMachineId,
|
machineData.typeMachineId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { componentRequirementMap, pieceRequirementMap } =
|
const {
|
||||||
this.buildConfigurationContext(typeMachine, componentLinks, pieceLinks);
|
componentRequirementMap,
|
||||||
|
pieceRequirementMap,
|
||||||
|
productRequirementMap,
|
||||||
|
productLinksByRequirement,
|
||||||
|
} = this.buildConfigurationContext(
|
||||||
|
typeMachine,
|
||||||
|
componentLinks,
|
||||||
|
pieceLinks,
|
||||||
|
productLinks,
|
||||||
|
);
|
||||||
|
|
||||||
const baseMachine = await this.prisma.machine.create({
|
const baseMachine = await this.prisma.machine.create({
|
||||||
data: machineData,
|
data: machineData,
|
||||||
@@ -1870,13 +2278,41 @@ export class MachinesService {
|
|||||||
componentLinks,
|
componentLinks,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.createPieceLinksForMachine(
|
const pieceLinkResult = await this.createPieceLinksForMachine(
|
||||||
this.prisma,
|
this.prisma,
|
||||||
baseMachine.id,
|
baseMachine.id,
|
||||||
pieceRequirementMap,
|
pieceRequirementMap,
|
||||||
pieceLinks,
|
pieceLinks,
|
||||||
componentIndex,
|
componentIndex,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const productLinkResult = await this.createProductLinksForMachine(
|
||||||
|
this.prisma,
|
||||||
|
baseMachine.id,
|
||||||
|
productRequirementMap,
|
||||||
|
productLinks,
|
||||||
|
);
|
||||||
|
|
||||||
|
const combinedPieceUsage = new Map(pieceLinkResult.productUsage);
|
||||||
|
for (const [
|
||||||
|
typeProductId,
|
||||||
|
count,
|
||||||
|
] of componentIndex.autoPieceProductUsage) {
|
||||||
|
combinedPieceUsage.set(
|
||||||
|
typeProductId,
|
||||||
|
(combinedPieceUsage.get(typeProductId) ?? 0) + count,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const combinedProductUsage = new Map(productLinkResult.productUsage);
|
||||||
|
|
||||||
|
this.validateProductRequirements(
|
||||||
|
productRequirementMap,
|
||||||
|
componentIndex.productUsage,
|
||||||
|
combinedPieceUsage,
|
||||||
|
combinedProductUsage,
|
||||||
|
productLinksByRequirement,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await this.prisma.machine
|
await this.prisma.machine
|
||||||
.delete({ where: { id: baseMachine.id } })
|
.delete({ where: { id: baseMachine.id } })
|
||||||
@@ -1904,8 +2340,8 @@ export class MachinesService {
|
|||||||
const enriched = await Promise.all(
|
const enriched = await Promise.all(
|
||||||
hydrated.map((machine) => this.ensureMachineConstructeurs(machine)),
|
hydrated.map((machine) => this.ensureMachineConstructeurs(machine)),
|
||||||
);
|
);
|
||||||
return enriched.filter(
|
return enriched.filter((machine): machine is NonNullable<typeof machine> =>
|
||||||
(machine): machine is NonNullable<typeof machine> => Boolean(machine),
|
Boolean(machine),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1919,7 +2355,11 @@ export class MachinesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async reconfigure(id: string, reconfigureMachineDto: ReconfigureMachineDto) {
|
async reconfigure(id: string, reconfigureMachineDto: ReconfigureMachineDto) {
|
||||||
const { componentLinks = [], pieceLinks = [] } = reconfigureMachineDto;
|
const {
|
||||||
|
componentLinks = [],
|
||||||
|
pieceLinks = [],
|
||||||
|
productLinks = [],
|
||||||
|
} = reconfigureMachineDto;
|
||||||
|
|
||||||
const machine = await this.prisma.machine.findUnique({
|
const machine = await this.prisma.machine.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
@@ -1942,12 +2382,22 @@ export class MachinesService {
|
|||||||
|
|
||||||
const typeMachine = machine.typeMachine as TypeMachineConfiguration;
|
const typeMachine = machine.typeMachine as TypeMachineConfiguration;
|
||||||
|
|
||||||
const { componentRequirementMap, pieceRequirementMap } =
|
const {
|
||||||
this.buildConfigurationContext(typeMachine, componentLinks, pieceLinks);
|
componentRequirementMap,
|
||||||
|
pieceRequirementMap,
|
||||||
|
productRequirementMap,
|
||||||
|
productLinksByRequirement,
|
||||||
|
} = this.buildConfigurationContext(
|
||||||
|
typeMachine,
|
||||||
|
componentLinks,
|
||||||
|
pieceLinks,
|
||||||
|
productLinks,
|
||||||
|
);
|
||||||
|
|
||||||
await this.prisma.$transaction(async (tx) => {
|
await this.prisma.$transaction(async (tx) => {
|
||||||
await tx.machinePieceLink.deleteMany({ where: { machineId: id } });
|
await tx.machinePieceLink.deleteMany({ where: { machineId: id } });
|
||||||
await tx.machineComponentLink.deleteMany({ where: { machineId: id } });
|
await tx.machineComponentLink.deleteMany({ where: { machineId: id } });
|
||||||
|
await tx.machineProductLink.deleteMany({ where: { machineId: id } });
|
||||||
|
|
||||||
const componentIndex = await this.createComponentLinksForMachine(
|
const componentIndex = await this.createComponentLinksForMachine(
|
||||||
tx,
|
tx,
|
||||||
@@ -1956,13 +2406,41 @@ export class MachinesService {
|
|||||||
componentLinks,
|
componentLinks,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.createPieceLinksForMachine(
|
const pieceLinkResult = await this.createPieceLinksForMachine(
|
||||||
tx,
|
tx,
|
||||||
id,
|
id,
|
||||||
pieceRequirementMap,
|
pieceRequirementMap,
|
||||||
pieceLinks,
|
pieceLinks,
|
||||||
componentIndex,
|
componentIndex,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const productLinkResult = await this.createProductLinksForMachine(
|
||||||
|
tx,
|
||||||
|
id,
|
||||||
|
productRequirementMap,
|
||||||
|
productLinks,
|
||||||
|
);
|
||||||
|
|
||||||
|
const combinedPieceUsage = new Map(pieceLinkResult.productUsage);
|
||||||
|
for (const [
|
||||||
|
typeProductId,
|
||||||
|
count,
|
||||||
|
] of componentIndex.autoPieceProductUsage) {
|
||||||
|
combinedPieceUsage.set(
|
||||||
|
typeProductId,
|
||||||
|
(combinedPieceUsage.get(typeProductId) ?? 0) + count,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const combinedProductUsage = new Map(productLinkResult.productUsage);
|
||||||
|
|
||||||
|
this.validateProductRequirements(
|
||||||
|
productRequirementMap,
|
||||||
|
componentIndex.productUsage,
|
||||||
|
combinedPieceUsage,
|
||||||
|
combinedProductUsage,
|
||||||
|
productLinksByRequirement,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const updatedMachine = await this.prisma.machine.findUnique({
|
const updatedMachine = await this.prisma.machine.findUnique({
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { IsEnum, IsOptional, IsString, Length, Matches } from 'class-validator';
|
|||||||
export enum ModelCategory {
|
export enum ModelCategory {
|
||||||
COMPONENT = 'COMPONENT',
|
COMPONENT = 'COMPONENT',
|
||||||
PIECE = 'PIECE',
|
PIECE = 'PIECE',
|
||||||
|
PRODUCT = 'PRODUCT',
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreateModelTypeDto {
|
export class CreateModelTypeDto {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { UpdateModelTypeDto } from './dto/update-model-type.dto';
|
|||||||
import {
|
import {
|
||||||
ComponentModelStructureSchema,
|
ComponentModelStructureSchema,
|
||||||
PieceModelStructureSchema,
|
PieceModelStructureSchema,
|
||||||
|
ProductModelStructureSchema,
|
||||||
} from '../shared/schemas/inventory';
|
} from '../shared/schemas/inventory';
|
||||||
|
|
||||||
type SortField = 'name' | 'code' | 'createdAt';
|
type SortField = 'name' | 'code' | 'createdAt';
|
||||||
@@ -112,12 +113,22 @@ export class ModelTypeService {
|
|||||||
if (normalizedStructure !== undefined) {
|
if (normalizedStructure !== undefined) {
|
||||||
const skeletonValue =
|
const skeletonValue =
|
||||||
normalizedStructure === null ? Prisma.JsonNull : normalizedStructure;
|
normalizedStructure === null ? Prisma.JsonNull : normalizedStructure;
|
||||||
if (rest.category === ModelCategory.COMPONENT) {
|
switch (rest.category) {
|
||||||
data.componentSkeleton = skeletonValue;
|
case ModelCategory.COMPONENT:
|
||||||
data.pieceSkeleton = Prisma.JsonNull;
|
data.componentSkeleton = skeletonValue;
|
||||||
} else {
|
data.pieceSkeleton = Prisma.JsonNull;
|
||||||
data.pieceSkeleton = skeletonValue;
|
data.productSkeleton = Prisma.JsonNull;
|
||||||
data.componentSkeleton = Prisma.JsonNull;
|
break;
|
||||||
|
case ModelCategory.PIECE:
|
||||||
|
data.pieceSkeleton = skeletonValue;
|
||||||
|
data.componentSkeleton = Prisma.JsonNull;
|
||||||
|
data.productSkeleton = Prisma.JsonNull;
|
||||||
|
break;
|
||||||
|
case ModelCategory.PRODUCT:
|
||||||
|
data.productSkeleton = skeletonValue;
|
||||||
|
data.componentSkeleton = Prisma.JsonNull;
|
||||||
|
data.pieceSkeleton = Prisma.JsonNull;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,12 +183,22 @@ export class ModelTypeService {
|
|||||||
if (normalizedStructure !== undefined) {
|
if (normalizedStructure !== undefined) {
|
||||||
const skeletonValue =
|
const skeletonValue =
|
||||||
normalizedStructure === null ? Prisma.JsonNull : normalizedStructure;
|
normalizedStructure === null ? Prisma.JsonNull : normalizedStructure;
|
||||||
if (targetCategory === ModelCategory.COMPONENT) {
|
switch (targetCategory) {
|
||||||
data.componentSkeleton = skeletonValue;
|
case ModelCategory.COMPONENT:
|
||||||
data.pieceSkeleton = Prisma.JsonNull;
|
data.componentSkeleton = skeletonValue;
|
||||||
} else {
|
data.pieceSkeleton = Prisma.JsonNull;
|
||||||
data.pieceSkeleton = skeletonValue;
|
data.productSkeleton = Prisma.JsonNull;
|
||||||
data.componentSkeleton = Prisma.JsonNull;
|
break;
|
||||||
|
case ModelCategory.PIECE:
|
||||||
|
data.pieceSkeleton = skeletonValue;
|
||||||
|
data.componentSkeleton = Prisma.JsonNull;
|
||||||
|
data.productSkeleton = Prisma.JsonNull;
|
||||||
|
break;
|
||||||
|
case ModelCategory.PRODUCT:
|
||||||
|
data.productSkeleton = skeletonValue;
|
||||||
|
data.componentSkeleton = Prisma.JsonNull;
|
||||||
|
data.pieceSkeleton = Prisma.JsonNull;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,7 +291,12 @@ export class ModelTypeService {
|
|||||||
structure,
|
structure,
|
||||||
) as Prisma.InputJsonValue;
|
) as Prisma.InputJsonValue;
|
||||||
}
|
}
|
||||||
return PieceModelStructureSchema.parse(
|
if (category === ModelCategory.PIECE) {
|
||||||
|
return PieceModelStructureSchema.parse(
|
||||||
|
structure,
|
||||||
|
) as Prisma.InputJsonValue;
|
||||||
|
}
|
||||||
|
return ProductModelStructureSchema.parse(
|
||||||
structure,
|
structure,
|
||||||
) as Prisma.InputJsonValue;
|
) as Prisma.InputJsonValue;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -281,10 +307,24 @@ export class ModelTypeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private mapModelType(modelType: PrismaModelType) {
|
private mapModelType(modelType: PrismaModelType) {
|
||||||
const structure =
|
let structure: Prisma.InputJsonValue | null;
|
||||||
modelType.category === ModelCategory.COMPONENT
|
switch (modelType.category as ModelCategory) {
|
||||||
? (modelType.componentSkeleton ?? null)
|
case ModelCategory.COMPONENT:
|
||||||
: (modelType.pieceSkeleton ?? null);
|
structure = (modelType.componentSkeleton ??
|
||||||
|
null) as Prisma.InputJsonValue | null;
|
||||||
|
break;
|
||||||
|
case ModelCategory.PIECE:
|
||||||
|
structure = (modelType.pieceSkeleton ??
|
||||||
|
null) as Prisma.InputJsonValue | null;
|
||||||
|
break;
|
||||||
|
case ModelCategory.PRODUCT:
|
||||||
|
structure = (modelType.productSkeleton ??
|
||||||
|
null) as Prisma.InputJsonValue | null;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
structure = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...modelType,
|
...modelType,
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ describe('PiecesService', () => {
|
|||||||
const dto: CreatePieceDto = {
|
const dto: CreatePieceDto = {
|
||||||
name: 'Piece A',
|
name: 'Piece A',
|
||||||
typePieceId: 'type-piece-1',
|
typePieceId: 'type-piece-1',
|
||||||
|
productId: ' product-1 ',
|
||||||
};
|
};
|
||||||
|
|
||||||
prisma.piece.create.mockResolvedValue({ id: 'piece-1', name: dto.name });
|
prisma.piece.create.mockResolvedValue({ id: 'piece-1', name: dto.name });
|
||||||
@@ -51,11 +52,14 @@ describe('PiecesService', () => {
|
|||||||
const result = await service.create(dto);
|
const result = await service.create(dto);
|
||||||
|
|
||||||
expect(prisma.piece.create).toHaveBeenCalled();
|
expect(prisma.piece.create).toHaveBeenCalled();
|
||||||
|
expect(prisma.piece.create.mock.calls[0][0].data.product).toEqual({
|
||||||
|
connect: { id: 'product-1' },
|
||||||
|
});
|
||||||
expect(result).toMatchObject({ id: 'piece-1' });
|
expect(result).toMatchObject({ id: 'piece-1' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates a piece', async () => {
|
it('updates a piece', async () => {
|
||||||
const dto: UpdatePieceDto = { name: 'Updated piece' };
|
const dto: UpdatePieceDto = { name: 'Updated piece', productId: '' };
|
||||||
|
|
||||||
prisma.piece.update.mockResolvedValue({
|
prisma.piece.update.mockResolvedValue({
|
||||||
id: 'piece-1',
|
id: 'piece-1',
|
||||||
@@ -71,5 +75,8 @@ describe('PiecesService', () => {
|
|||||||
await service.update('piece-1', dto);
|
await service.update('piece-1', dto);
|
||||||
|
|
||||||
expect(prisma.piece.update).toHaveBeenCalled();
|
expect(prisma.piece.update).toHaveBeenCalled();
|
||||||
|
expect(prisma.piece.update.mock.calls[0][0].data.product).toEqual({
|
||||||
|
disconnect: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,6 +21,18 @@ const PIECE_WITH_RELATIONS_INCLUDE = {
|
|||||||
customField: true,
|
customField: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
product: {
|
||||||
|
include: {
|
||||||
|
typeProduct: true,
|
||||||
|
constructeurs: true,
|
||||||
|
customFieldValues: {
|
||||||
|
include: {
|
||||||
|
customField: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
documents: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
machineLinks: {
|
machineLinks: {
|
||||||
include: {
|
include: {
|
||||||
machine: true,
|
machine: true,
|
||||||
@@ -55,43 +67,63 @@ export class PiecesService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (createPieceDto.productId) {
|
||||||
|
const normalizedProductId = createPieceDto.productId.trim();
|
||||||
|
if (normalizedProductId) {
|
||||||
|
data.product = {
|
||||||
|
connect: { id: normalizedProductId },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { data, constructeurIds: resolvedConstructeurIds };
|
return { data, constructeurIds: resolvedConstructeurIds };
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(createPieceDto: CreatePieceDto) {
|
async create(createPieceDto: CreatePieceDto) {
|
||||||
try {
|
try {
|
||||||
const { data, constructeurIds } = await this.buildCreateInput(
|
const { data, constructeurIds } =
|
||||||
createPieceDto,
|
await this.buildCreateInput(createPieceDto);
|
||||||
|
|
||||||
|
const { pieceId, syncedConstructeurIds } = await this.prisma.$transaction(
|
||||||
|
async (tx) => {
|
||||||
|
const created = await tx.piece.create({
|
||||||
|
data,
|
||||||
|
include: PIECE_WITH_RELATIONS_INCLUDE,
|
||||||
|
});
|
||||||
|
|
||||||
|
let synced: string[] = [];
|
||||||
|
if (constructeurIds.length > 0) {
|
||||||
|
synced = await syncConstructeurLinks(
|
||||||
|
tx,
|
||||||
|
'_PieceConstructeurs',
|
||||||
|
created.id,
|
||||||
|
constructeurIds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.applyPieceSkeleton({
|
||||||
|
pieceId: created.id,
|
||||||
|
typePiece: created.typePiece as PieceTypeWithSkeleton | null,
|
||||||
|
product: created.product,
|
||||||
|
prisma: tx,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
pieceId: created.id,
|
||||||
|
syncedConstructeurIds: synced,
|
||||||
|
};
|
||||||
|
},
|
||||||
);
|
);
|
||||||
const created = await this.prisma.piece.create({
|
|
||||||
data,
|
|
||||||
include: PIECE_WITH_RELATIONS_INCLUDE,
|
|
||||||
});
|
|
||||||
|
|
||||||
let syncedConstructeurIds: string[] = [];
|
|
||||||
if (constructeurIds.length > 0) {
|
|
||||||
syncedConstructeurIds = await syncConstructeurLinks(
|
|
||||||
this.prisma,
|
|
||||||
'_PieceConstructeurs',
|
|
||||||
created.id,
|
|
||||||
constructeurIds,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.applyPieceSkeleton({
|
|
||||||
pieceId: created.id,
|
|
||||||
typePiece: created.typePiece as PieceTypeWithSkeleton | null,
|
|
||||||
prisma: this.prisma,
|
|
||||||
});
|
|
||||||
|
|
||||||
const refreshed = await this.prisma.piece.findUnique({
|
const refreshed = await this.prisma.piece.findUnique({
|
||||||
where: { id: created.id },
|
where: { id: pieceId },
|
||||||
include: PIECE_WITH_RELATIONS_INCLUDE,
|
include: PIECE_WITH_RELATIONS_INCLUDE,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (refreshed && syncedConstructeurIds.length > 0) {
|
if (refreshed && syncedConstructeurIds.length > 0) {
|
||||||
(refreshed as typeof refreshed & { constructeurIds?: string[] }).constructeurIds =
|
(
|
||||||
[...syncedConstructeurIds];
|
refreshed as typeof refreshed & { constructeurIds?: string[] }
|
||||||
|
).constructeurIds = [...syncedConstructeurIds];
|
||||||
}
|
}
|
||||||
|
|
||||||
return refreshed;
|
return refreshed;
|
||||||
@@ -134,9 +166,8 @@ export class PiecesService {
|
|||||||
const constructeurIds = this.normalizeConstructeurIds(
|
const constructeurIds = this.normalizeConstructeurIds(
|
||||||
updatePieceDto.constructeurIds,
|
updatePieceDto.constructeurIds,
|
||||||
);
|
);
|
||||||
resolvedConstructeurIds = await this.resolveExistingConstructeurIds(
|
resolvedConstructeurIds =
|
||||||
constructeurIds,
|
await this.resolveExistingConstructeurIds(constructeurIds);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updatePieceDto.typePieceId !== undefined) {
|
if (updatePieceDto.typePieceId !== undefined) {
|
||||||
@@ -145,6 +176,16 @@ export class PiecesService {
|
|||||||
: { disconnect: true };
|
: { disconnect: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (updatePieceDto.productId !== undefined) {
|
||||||
|
const normalizedProductId =
|
||||||
|
typeof updatePieceDto.productId === 'string'
|
||||||
|
? updatePieceDto.productId.trim()
|
||||||
|
: null;
|
||||||
|
data.product = normalizedProductId
|
||||||
|
? { connect: { id: normalizedProductId } }
|
||||||
|
: { disconnect: true };
|
||||||
|
}
|
||||||
|
|
||||||
let syncedConstructeurIds: string[] | undefined;
|
let syncedConstructeurIds: string[] | undefined;
|
||||||
try {
|
try {
|
||||||
await this.prisma.$transaction(async (tx) => {
|
await this.prisma.$transaction(async (tx) => {
|
||||||
@@ -166,6 +207,7 @@ export class PiecesService {
|
|||||||
await this.applyPieceSkeleton({
|
await this.applyPieceSkeleton({
|
||||||
pieceId: updated.id,
|
pieceId: updated.id,
|
||||||
typePiece: updated.typePiece as PieceTypeWithSkeleton | null,
|
typePiece: updated.typePiece as PieceTypeWithSkeleton | null,
|
||||||
|
product: updated.product,
|
||||||
prisma: tx,
|
prisma: tx,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -176,8 +218,9 @@ export class PiecesService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (refreshed && syncedConstructeurIds) {
|
if (refreshed && syncedConstructeurIds) {
|
||||||
(refreshed as typeof refreshed & { constructeurIds?: string[] }).constructeurIds =
|
(
|
||||||
[...syncedConstructeurIds];
|
refreshed as typeof refreshed & { constructeurIds?: string[] }
|
||||||
|
).constructeurIds = [...syncedConstructeurIds];
|
||||||
}
|
}
|
||||||
|
|
||||||
return refreshed;
|
return refreshed;
|
||||||
@@ -247,10 +290,15 @@ export class PiecesService {
|
|||||||
private async applyPieceSkeleton({
|
private async applyPieceSkeleton({
|
||||||
pieceId,
|
pieceId,
|
||||||
typePiece,
|
typePiece,
|
||||||
|
product,
|
||||||
prisma,
|
prisma,
|
||||||
}: {
|
}: {
|
||||||
pieceId: string;
|
pieceId: string;
|
||||||
typePiece: PieceTypeWithSkeleton | null;
|
typePiece: PieceTypeWithSkeleton | null;
|
||||||
|
product: {
|
||||||
|
typeProductId: string | null;
|
||||||
|
typeProduct?: { code: string | null } | null;
|
||||||
|
} | null;
|
||||||
prisma: Prisma.TransactionClient | PrismaService;
|
prisma: Prisma.TransactionClient | PrismaService;
|
||||||
}) {
|
}) {
|
||||||
if (!typePiece?.id) {
|
if (!typePiece?.id) {
|
||||||
@@ -267,6 +315,13 @@ export class PiecesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const customFields = skeleton.customFields ?? [];
|
const customFields = skeleton.customFields ?? [];
|
||||||
|
const productRequirements: PieceProductRequirement[] = Array.isArray(
|
||||||
|
skeleton.products,
|
||||||
|
)
|
||||||
|
? skeleton.products.filter(
|
||||||
|
(entry): entry is PieceProductRequirement => !!entry,
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
await this.ensurePieceCustomFieldDefinitions(
|
await this.ensurePieceCustomFieldDefinitions(
|
||||||
prisma,
|
prisma,
|
||||||
@@ -279,6 +334,99 @@ export class PiecesService {
|
|||||||
typePiece.id,
|
typePiece.id,
|
||||||
customFields,
|
customFields,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (productRequirements.length > 0) {
|
||||||
|
await this.ensurePieceProductCompliance({
|
||||||
|
prisma,
|
||||||
|
pieceId,
|
||||||
|
product,
|
||||||
|
requirements: productRequirements,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensurePieceProductCompliance({
|
||||||
|
prisma,
|
||||||
|
pieceId,
|
||||||
|
product,
|
||||||
|
requirements,
|
||||||
|
}: {
|
||||||
|
prisma: Prisma.TransactionClient | PrismaService;
|
||||||
|
pieceId: string;
|
||||||
|
product: {
|
||||||
|
typeProductId: string | null;
|
||||||
|
typeProduct?: { code: string | null } | null;
|
||||||
|
} | null;
|
||||||
|
requirements: PieceProductRequirement[];
|
||||||
|
}) {
|
||||||
|
const effectiveProduct =
|
||||||
|
product ??
|
||||||
|
(
|
||||||
|
await prisma.piece.findUnique({
|
||||||
|
where: { id: pieceId },
|
||||||
|
select: {
|
||||||
|
product: {
|
||||||
|
select: {
|
||||||
|
typeProductId: true,
|
||||||
|
typeProduct: {
|
||||||
|
select: { code: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)?.product;
|
||||||
|
|
||||||
|
if (!effectiveProduct) {
|
||||||
|
throw new ConflictException(
|
||||||
|
'Ce type de pièce impose la sélection d’un produit catalogue.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const matches = requirements.some((requirement) =>
|
||||||
|
this.doesProductMatchRequirement(effectiveProduct, requirement),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!matches) {
|
||||||
|
throw new ConflictException(
|
||||||
|
'Le produit associé ne respecte pas les exigences définies par le squelette.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private doesProductMatchRequirement(
|
||||||
|
product: {
|
||||||
|
typeProductId: string | null;
|
||||||
|
typeProduct?: { code: string | null } | null;
|
||||||
|
},
|
||||||
|
requirement: PieceProductRequirement,
|
||||||
|
): boolean {
|
||||||
|
if (!requirement) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('typeProductId' in requirement && requirement.typeProductId) {
|
||||||
|
const expectedId = requirement.typeProductId.trim();
|
||||||
|
if (!expectedId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const currentId = product.typeProductId
|
||||||
|
? product.typeProductId.trim()
|
||||||
|
: '';
|
||||||
|
return currentId === expectedId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('familyCode' in requirement && requirement.familyCode) {
|
||||||
|
const expectedCode = requirement.familyCode.trim().toLowerCase();
|
||||||
|
if (!expectedCode) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const productCode =
|
||||||
|
product.typeProduct?.code?.trim().toLowerCase() ?? null;
|
||||||
|
return productCode === expectedCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private normalizeConstructeurIds(ids?: string[] | null): string[] {
|
private normalizeConstructeurIds(ids?: string[] | null): string[] {
|
||||||
@@ -529,3 +677,7 @@ type PieceTypeWithSkeleton = Prisma.ModelTypeGetPayload<{
|
|||||||
type PieceCustomFieldEntry = NonNullable<
|
type PieceCustomFieldEntry = NonNullable<
|
||||||
PieceModelStructure['customFields']
|
PieceModelStructure['customFields']
|
||||||
>[number];
|
>[number];
|
||||||
|
|
||||||
|
type PieceProductRequirement = NonNullable<
|
||||||
|
PieceModelStructure['products']
|
||||||
|
>[number];
|
||||||
|
|||||||
49
src/products/dto/list-products.dto.ts
Normal file
49
src/products/dto/list-products.dto.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { IsEnum, IsInt, IsOptional, IsString, Max, Min } from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
|
export enum ProductSortField {
|
||||||
|
NAME = 'name',
|
||||||
|
REFERENCE = 'reference',
|
||||||
|
CREATED_AT = 'createdAt',
|
||||||
|
SUPPLIER_PRICE = 'supplierPrice',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SortDirection {
|
||||||
|
ASC = 'asc',
|
||||||
|
DESC = 'desc',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ListProductsQueryDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
q?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
typeProductId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
constructeurId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
@Max(100)
|
||||||
|
limit?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
offset?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(ProductSortField)
|
||||||
|
sort?: ProductSortField;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(SortDirection)
|
||||||
|
dir?: SortDirection;
|
||||||
|
}
|
||||||
43
src/products/products.controller.ts
Normal file
43
src/products/products.controller.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ProductsService } from './products.service';
|
||||||
|
import { CreateProductDto, UpdateProductDto } from '../shared/dto/product.dto';
|
||||||
|
import { ListProductsQueryDto } from './dto/list-products.dto';
|
||||||
|
|
||||||
|
@Controller('products')
|
||||||
|
export class ProductsController {
|
||||||
|
constructor(private readonly productsService: ProductsService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
list(@Query() query: ListProductsQueryDto) {
|
||||||
|
return this.productsService.list(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
findOne(@Param('id') id: string) {
|
||||||
|
return this.productsService.findOne(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
create(@Body() createProductDto: CreateProductDto) {
|
||||||
|
return this.productsService.create(createProductDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
update(@Param('id') id: string, @Body() updateProductDto: UpdateProductDto) {
|
||||||
|
return this.productsService.update(id, updateProductDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
remove(@Param('id') id: string) {
|
||||||
|
return this.productsService.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/products/products.module.ts
Normal file
9
src/products/products.module.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ProductsController } from './products.controller';
|
||||||
|
import { ProductsService } from './products.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [ProductsController],
|
||||||
|
providers: [ProductsService],
|
||||||
|
})
|
||||||
|
export class ProductsModule {}
|
||||||
240
src/products/products.service.spec.ts
Normal file
240
src/products/products.service.spec.ts
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { ConflictException } from '@nestjs/common';
|
||||||
|
import { Prisma, Product } from '@prisma/client';
|
||||||
|
import { ProductsService } from './products.service';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { syncConstructeurLinks } from '../common/utils/constructeur-link.util';
|
||||||
|
import { CreateProductDto, UpdateProductDto } from '../shared/dto/product.dto';
|
||||||
|
import { ProductSortField, SortDirection } from './dto/list-products.dto';
|
||||||
|
|
||||||
|
jest.mock('../common/utils/constructeur-link.util', () => ({
|
||||||
|
syncConstructeurLinks: jest.fn().mockResolvedValue([]),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('ProductsService', () => {
|
||||||
|
let service: ProductsService;
|
||||||
|
let prisma: {
|
||||||
|
product: any;
|
||||||
|
constructeur: any;
|
||||||
|
piece: any;
|
||||||
|
composant: any;
|
||||||
|
document: any;
|
||||||
|
$transaction: jest.Mock;
|
||||||
|
};
|
||||||
|
const mockSyncConstructeurLinks = syncConstructeurLinks as jest.Mock;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
prisma = {
|
||||||
|
product: {
|
||||||
|
findMany: jest.fn(),
|
||||||
|
count: jest.fn(),
|
||||||
|
findUnique: jest.fn(),
|
||||||
|
create: jest.fn(),
|
||||||
|
update: jest.fn(),
|
||||||
|
delete: jest.fn(),
|
||||||
|
},
|
||||||
|
constructeur: {
|
||||||
|
findMany: jest.fn(),
|
||||||
|
},
|
||||||
|
piece: {
|
||||||
|
count: jest.fn(),
|
||||||
|
},
|
||||||
|
composant: {
|
||||||
|
count: jest.fn(),
|
||||||
|
},
|
||||||
|
document: {
|
||||||
|
count: jest.fn(),
|
||||||
|
},
|
||||||
|
$transaction: jest.fn((arg: any) => {
|
||||||
|
if (Array.isArray(arg)) {
|
||||||
|
return Promise.all(arg);
|
||||||
|
}
|
||||||
|
if (typeof arg === 'function') {
|
||||||
|
return arg(prisma);
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
ProductsService,
|
||||||
|
{ provide: PrismaService, useValue: prisma },
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<ProductsService>(ProductsService);
|
||||||
|
mockSyncConstructeurLinks.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('list', () => {
|
||||||
|
it('returns products with mapped constructeur ids and pagination meta', async () => {
|
||||||
|
const product: Product & {
|
||||||
|
constructeurs: Array<{ id: string }>;
|
||||||
|
documents: any[];
|
||||||
|
customFieldValues: any[];
|
||||||
|
pieces: any[];
|
||||||
|
composants: any[];
|
||||||
|
typeProduct: null;
|
||||||
|
} = {
|
||||||
|
id: 'prod-1',
|
||||||
|
name: 'Product 1',
|
||||||
|
reference: 'P-001',
|
||||||
|
supplierPrice: new Prisma.Decimal(120),
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
typeProductId: null,
|
||||||
|
constructeurs: [{ id: 'const-1' }],
|
||||||
|
documents: [],
|
||||||
|
customFieldValues: [],
|
||||||
|
pieces: [],
|
||||||
|
composants: [],
|
||||||
|
typeProduct: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
prisma.product.findMany.mockResolvedValue([product]);
|
||||||
|
prisma.product.count.mockResolvedValue(1);
|
||||||
|
|
||||||
|
const result = await service.list({
|
||||||
|
q: ' Product ',
|
||||||
|
limit: 200,
|
||||||
|
sort: ProductSortField.NAME,
|
||||||
|
dir: SortDirection.ASC,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(prisma.product.findMany).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
where: expect.objectContaining({
|
||||||
|
OR: expect.any(Array),
|
||||||
|
}),
|
||||||
|
take: 100, // capped
|
||||||
|
orderBy: { name: 'asc' },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(result.total).toBe(1);
|
||||||
|
expect(result.items[0]).toMatchObject({
|
||||||
|
id: 'prod-1',
|
||||||
|
constructeurIds: ['const-1'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('create', () => {
|
||||||
|
it('persists a product and synchronizes constructeurs', async () => {
|
||||||
|
const dto: CreateProductDto = {
|
||||||
|
name: 'New Product',
|
||||||
|
supplierPrice: 150.5,
|
||||||
|
constructeurIds: ['const-1', 'const-1', ''],
|
||||||
|
typeProductId: 'type-1',
|
||||||
|
};
|
||||||
|
|
||||||
|
prisma.constructeur.findMany.mockResolvedValue([{ id: 'const-1' }]);
|
||||||
|
prisma.product.create.mockResolvedValue({
|
||||||
|
id: 'prod-1',
|
||||||
|
});
|
||||||
|
prisma.product.findUnique.mockResolvedValue({
|
||||||
|
id: 'prod-1',
|
||||||
|
name: dto.name,
|
||||||
|
reference: null,
|
||||||
|
supplierPrice: new Prisma.Decimal(150.5),
|
||||||
|
typeProductId: dto.typeProductId,
|
||||||
|
constructeurs: [{ id: 'const-1' }],
|
||||||
|
documents: [],
|
||||||
|
customFieldValues: [],
|
||||||
|
pieces: [],
|
||||||
|
composants: [],
|
||||||
|
typeProduct: null,
|
||||||
|
});
|
||||||
|
mockSyncConstructeurLinks.mockResolvedValue(['const-1']);
|
||||||
|
|
||||||
|
const created = await service.create(dto);
|
||||||
|
|
||||||
|
expect(prisma.product.create).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: expect.objectContaining({
|
||||||
|
name: 'New Product',
|
||||||
|
supplierPrice: expect.any(Prisma.Decimal),
|
||||||
|
typeProduct: { connect: { id: 'type-1' } },
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
String(prisma.product.create.mock.calls[0][0].data.supplierPrice),
|
||||||
|
).toBe('150.5');
|
||||||
|
expect(mockSyncConstructeurLinks).toHaveBeenCalledWith(
|
||||||
|
expect.any(Object),
|
||||||
|
'_ProductConstructeurs',
|
||||||
|
'prod-1',
|
||||||
|
['const-1'],
|
||||||
|
);
|
||||||
|
expect(created.constructeurIds).toEqual(['const-1']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('update', () => {
|
||||||
|
it('updates product fields and synchronizes constructeurs when provided', async () => {
|
||||||
|
const dto: UpdateProductDto = {
|
||||||
|
supplierPrice: null,
|
||||||
|
constructeurIds: ['const-2'],
|
||||||
|
typeProductId: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
prisma.constructeur.findMany.mockResolvedValue([{ id: 'const-2' }]);
|
||||||
|
prisma.product.findUnique.mockResolvedValue({
|
||||||
|
id: 'prod-1',
|
||||||
|
name: 'Existing product',
|
||||||
|
reference: null,
|
||||||
|
supplierPrice: null,
|
||||||
|
typeProductId: null,
|
||||||
|
constructeurs: [{ id: 'const-2' }],
|
||||||
|
documents: [],
|
||||||
|
customFieldValues: [],
|
||||||
|
pieces: [],
|
||||||
|
composants: [],
|
||||||
|
typeProduct: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
await service.update('prod-1', dto);
|
||||||
|
|
||||||
|
expect(prisma.product.update).toHaveBeenCalledWith({
|
||||||
|
where: { id: 'prod-1' },
|
||||||
|
data: expect.objectContaining({
|
||||||
|
supplierPrice: null,
|
||||||
|
typeProduct: { disconnect: true },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expect(mockSyncConstructeurLinks).toHaveBeenCalledWith(
|
||||||
|
expect.any(Object),
|
||||||
|
'_ProductConstructeurs',
|
||||||
|
'prod-1',
|
||||||
|
['const-2'],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('remove', () => {
|
||||||
|
it('throws when product is still referenced', async () => {
|
||||||
|
prisma.piece.count.mockResolvedValue(1);
|
||||||
|
prisma.composant.count.mockResolvedValue(0);
|
||||||
|
prisma.document.count.mockResolvedValue(2);
|
||||||
|
|
||||||
|
await expect(service.remove('prod-1')).rejects.toBeInstanceOf(
|
||||||
|
ConflictException,
|
||||||
|
);
|
||||||
|
expect(prisma.product.delete).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes product when no references remain', async () => {
|
||||||
|
prisma.piece.count.mockResolvedValue(0);
|
||||||
|
prisma.composant.count.mockResolvedValue(0);
|
||||||
|
prisma.document.count.mockResolvedValue(0);
|
||||||
|
prisma.product.delete.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await service.remove('prod-1');
|
||||||
|
|
||||||
|
expect(prisma.product.delete).toHaveBeenCalledWith({
|
||||||
|
where: { id: 'prod-1' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
322
src/products/products.service.ts
Normal file
322
src/products/products.service.ts
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
import {
|
||||||
|
ConflictException,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { CreateProductDto, UpdateProductDto } from '../shared/dto/product.dto';
|
||||||
|
import { PRODUCT_WITH_RELATIONS_INCLUDE } from '../common/constants/product-includes';
|
||||||
|
import { syncConstructeurLinks } from '../common/utils/constructeur-link.util';
|
||||||
|
import {
|
||||||
|
ListProductsQueryDto,
|
||||||
|
ProductSortField,
|
||||||
|
SortDirection,
|
||||||
|
} from './dto/list-products.dto';
|
||||||
|
|
||||||
|
type ProductWithRelations = Prisma.ProductGetPayload<{
|
||||||
|
include: typeof PRODUCT_WITH_RELATIONS_INCLUDE;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ProductsService {
|
||||||
|
private readonly allowedSortFields: ProductSortField[] = [
|
||||||
|
ProductSortField.CREATED_AT,
|
||||||
|
ProductSortField.NAME,
|
||||||
|
ProductSortField.REFERENCE,
|
||||||
|
ProductSortField.SUPPLIER_PRICE,
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async list(params: ListProductsQueryDto) {
|
||||||
|
const {
|
||||||
|
q,
|
||||||
|
typeProductId,
|
||||||
|
constructeurId,
|
||||||
|
limit = 20,
|
||||||
|
offset = 0,
|
||||||
|
sort = ProductSortField.CREATED_AT,
|
||||||
|
dir = SortDirection.DESC,
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
const cappedLimit = Math.min(Math.max(limit, 1), 100);
|
||||||
|
const safeOffset = Math.max(offset, 0);
|
||||||
|
|
||||||
|
const orderByField = this.allowedSortFields.includes(sort)
|
||||||
|
? sort
|
||||||
|
: ProductSortField.CREATED_AT;
|
||||||
|
const orderByDir: SortDirection =
|
||||||
|
dir === SortDirection.ASC ? SortDirection.ASC : SortDirection.DESC;
|
||||||
|
|
||||||
|
const where: Prisma.ProductWhereInput = {};
|
||||||
|
|
||||||
|
if (q?.trim()) {
|
||||||
|
const term = q.trim();
|
||||||
|
where.OR = [
|
||||||
|
{ name: { contains: term, mode: 'insensitive' } },
|
||||||
|
{ reference: { contains: term, mode: 'insensitive' } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeProductId) {
|
||||||
|
where.typeProductId = typeProductId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (constructeurId) {
|
||||||
|
where.constructeurs = {
|
||||||
|
some: { id: constructeurId },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const [items, total] = await this.prisma.$transaction([
|
||||||
|
this.prisma.product.findMany({
|
||||||
|
where,
|
||||||
|
include: PRODUCT_WITH_RELATIONS_INCLUDE,
|
||||||
|
orderBy: {
|
||||||
|
[orderByField]: orderByDir,
|
||||||
|
},
|
||||||
|
skip: safeOffset,
|
||||||
|
take: cappedLimit,
|
||||||
|
}),
|
||||||
|
this.prisma.product.count({ where }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: items.map((item) => this.mapProduct(item)),
|
||||||
|
total,
|
||||||
|
offset: safeOffset,
|
||||||
|
limit: cappedLimit,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: string) {
|
||||||
|
const product = await this.prisma.product.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: PRODUCT_WITH_RELATIONS_INCLUDE,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
throw new NotFoundException('Produit introuvable.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.mapProduct(product);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(createProductDto: CreateProductDto) {
|
||||||
|
try {
|
||||||
|
const data: Prisma.ProductCreateInput = {
|
||||||
|
name: createProductDto.name,
|
||||||
|
reference: createProductDto.reference ?? null,
|
||||||
|
supplierPrice:
|
||||||
|
createProductDto.supplierPrice === undefined ||
|
||||||
|
createProductDto.supplierPrice === null
|
||||||
|
? null
|
||||||
|
: new Prisma.Decimal(createProductDto.supplierPrice),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (createProductDto.typeProductId) {
|
||||||
|
data.typeProduct = {
|
||||||
|
connect: { id: createProductDto.typeProductId },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const constructeurIds = this.normalizeConstructeurIds(
|
||||||
|
createProductDto.constructeurIds,
|
||||||
|
);
|
||||||
|
const resolvedConstructeurIds =
|
||||||
|
await this.resolveExistingConstructeurIds(constructeurIds);
|
||||||
|
|
||||||
|
const created = await this.prisma.product.create({
|
||||||
|
data,
|
||||||
|
include: PRODUCT_WITH_RELATIONS_INCLUDE,
|
||||||
|
});
|
||||||
|
|
||||||
|
let syncedConstructeurIds: string[] = [];
|
||||||
|
if (resolvedConstructeurIds.length > 0) {
|
||||||
|
syncedConstructeurIds = await syncConstructeurLinks(
|
||||||
|
this.prisma,
|
||||||
|
'_ProductConstructeurs',
|
||||||
|
created.id,
|
||||||
|
resolvedConstructeurIds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshed = await this.prisma.product.findUnique({
|
||||||
|
where: { id: created.id },
|
||||||
|
include: PRODUCT_WITH_RELATIONS_INCLUDE,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!refreshed) {
|
||||||
|
return this.mapProduct(created);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapped = this.mapProduct(refreshed);
|
||||||
|
if (syncedConstructeurIds.length > 0) {
|
||||||
|
mapped.constructeurIds = [...syncedConstructeurIds];
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapped;
|
||||||
|
} catch (error) {
|
||||||
|
this.handlePrismaError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, updateProductDto: UpdateProductDto) {
|
||||||
|
try {
|
||||||
|
const data: Prisma.ProductUpdateInput = {};
|
||||||
|
|
||||||
|
if (updateProductDto.name !== undefined) {
|
||||||
|
data.name = updateProductDto.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateProductDto.reference !== undefined) {
|
||||||
|
data.reference = updateProductDto.reference;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateProductDto.supplierPrice !== undefined) {
|
||||||
|
data.supplierPrice =
|
||||||
|
updateProductDto.supplierPrice === null
|
||||||
|
? null
|
||||||
|
: new Prisma.Decimal(updateProductDto.supplierPrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateProductDto.typeProductId !== undefined) {
|
||||||
|
data.typeProduct = updateProductDto.typeProductId
|
||||||
|
? { connect: { id: updateProductDto.typeProductId } }
|
||||||
|
: { disconnect: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
let resolvedConstructeurIds: string[] | undefined;
|
||||||
|
if (updateProductDto.constructeurIds !== undefined) {
|
||||||
|
const constructeurIds = this.normalizeConstructeurIds(
|
||||||
|
updateProductDto.constructeurIds,
|
||||||
|
);
|
||||||
|
resolvedConstructeurIds =
|
||||||
|
await this.resolveExistingConstructeurIds(constructeurIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
let syncedConstructeurIds: string[] | undefined;
|
||||||
|
|
||||||
|
await this.prisma.$transaction(async (tx) => {
|
||||||
|
await tx.product.update({
|
||||||
|
where: { id },
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resolvedConstructeurIds !== undefined) {
|
||||||
|
syncedConstructeurIds = await syncConstructeurLinks(
|
||||||
|
tx,
|
||||||
|
'_ProductConstructeurs',
|
||||||
|
id,
|
||||||
|
resolvedConstructeurIds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const refreshed = await this.prisma.product.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: PRODUCT_WITH_RELATIONS_INCLUDE,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!refreshed) {
|
||||||
|
throw new NotFoundException('Produit introuvable.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapped = this.mapProduct(refreshed);
|
||||||
|
if (syncedConstructeurIds) {
|
||||||
|
mapped.constructeurIds = [...syncedConstructeurIds];
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapped;
|
||||||
|
} catch (error) {
|
||||||
|
this.handlePrismaError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: string) {
|
||||||
|
const [pieceCount, componentCount, documentCount] = await Promise.all([
|
||||||
|
this.prisma.piece.count({
|
||||||
|
where: { productId: id },
|
||||||
|
}),
|
||||||
|
this.prisma.composant.count({
|
||||||
|
where: { productId: id },
|
||||||
|
}),
|
||||||
|
this.prisma.document.count({
|
||||||
|
where: { productId: id },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const blockingReasons: string[] = [];
|
||||||
|
if (pieceCount > 0) {
|
||||||
|
blockingReasons.push(`${pieceCount} pièce${pieceCount > 1 ? 's' : ''}`);
|
||||||
|
}
|
||||||
|
if (componentCount > 0) {
|
||||||
|
blockingReasons.push(
|
||||||
|
`${componentCount} composant${componentCount > 1 ? 's' : ''}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (documentCount > 0) {
|
||||||
|
blockingReasons.push(
|
||||||
|
`${documentCount} document${documentCount > 1 ? 's' : ''}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blockingReasons.length > 0) {
|
||||||
|
throw new ConflictException(
|
||||||
|
`Impossible de supprimer ce produit car il est encore lié à ${blockingReasons.join(
|
||||||
|
', ',
|
||||||
|
)}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.prisma.product.delete({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeConstructeurIds(ids?: string[] | null): string[] {
|
||||||
|
if (!Array.isArray(ids)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return Array.from(
|
||||||
|
new Set(
|
||||||
|
ids
|
||||||
|
.map((value) => (typeof value === 'string' ? value.trim() : ''))
|
||||||
|
.filter((value) => value.length > 0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async resolveExistingConstructeurIds(ids: string[]) {
|
||||||
|
if (ids.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const existing = await this.prisma.constructeur.findMany({
|
||||||
|
where: { id: { in: ids } },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
const existingIds = new Set(existing.map(({ id }) => id));
|
||||||
|
return ids.filter((id) => existingIds.has(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapProduct(product: ProductWithRelations) {
|
||||||
|
return {
|
||||||
|
...product,
|
||||||
|
constructeurIds: product.constructeurs.map((item) => item.id),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private handlePrismaError(error: unknown): never {
|
||||||
|
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
|
if (error.code === 'P2002') {
|
||||||
|
throw new ConflictException('Un produit avec ce nom existe déjà.');
|
||||||
|
}
|
||||||
|
if (error.code === 'P2025') {
|
||||||
|
throw new NotFoundException('Produit introuvable.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,6 +45,10 @@ export class CreateComposantDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsObject()
|
@IsObject()
|
||||||
structure?: Record<string, any>;
|
structure?: Record<string, any>;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
productId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateComposantDto {
|
export class UpdateComposantDto {
|
||||||
@@ -73,4 +77,9 @@ export class UpdateComposantDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsObject()
|
@IsObject()
|
||||||
structure?: Record<string, any>;
|
structure?: Record<string, any>;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@Transform(({ value }) => (value === '' ? null : value))
|
||||||
|
@IsString()
|
||||||
|
productId?: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export enum CustomFieldEntityType {
|
|||||||
MACHINE = 'machine',
|
MACHINE = 'machine',
|
||||||
COMPOSANT = 'composant',
|
COMPOSANT = 'composant',
|
||||||
PIECE = 'piece',
|
PIECE = 'piece',
|
||||||
|
PRODUCT = 'product',
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CustomFieldEntityParamsDto {
|
export class CustomFieldEntityParamsDto {
|
||||||
@@ -76,6 +77,10 @@ export class CreateCustomFieldValueDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
pieceId?: string;
|
pieceId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
productId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateCustomFieldValueDto {
|
export class UpdateCustomFieldValueDto {
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ export class CreateDocumentDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
siteId?: string;
|
siteId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
productId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateDocumentDto {
|
export class UpdateDocumentDto {
|
||||||
@@ -57,4 +61,20 @@ export class UpdateDocumentDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
siteId?: string;
|
siteId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
machineId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
composantId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
pieceId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
productId?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ export class MachineComponentLinkPayloadDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
composantId?: string;
|
composantId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
productId?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
componentId?: string;
|
componentId?: string;
|
||||||
@@ -97,6 +101,10 @@ export class MachinePieceLinkPayloadDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
composantId?: string;
|
composantId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
productId?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
parentLinkId?: string;
|
parentLinkId?: string;
|
||||||
@@ -142,6 +150,59 @@ export class MachinePieceLinkPayloadDto {
|
|||||||
overrides?: Record<string, unknown>;
|
overrides?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class MachineProductLinkPayloadDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
id?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
linkId?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
requirementId: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
productId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
typeProductId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
parentLinkId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
parentComponentLinkId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
parentPieceLinkId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
parentRequirementId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
parentComponentRequirementId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
parentPieceRequirementId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
parentMachineComponentRequirementId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
parentMachinePieceRequirementId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class CreateMachineDto {
|
export class CreateMachineDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
name: string;
|
name: string;
|
||||||
@@ -177,6 +238,12 @@ export class CreateMachineDto {
|
|||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => MachinePieceLinkPayloadDto)
|
@Type(() => MachinePieceLinkPayloadDto)
|
||||||
pieceLinks?: MachinePieceLinkPayloadDto[];
|
pieceLinks?: MachinePieceLinkPayloadDto[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => MachineProductLinkPayloadDto)
|
||||||
|
productLinks?: MachineProductLinkPayloadDto[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateMachineDto {
|
export class UpdateMachineDto {
|
||||||
@@ -214,7 +281,14 @@ export class ReconfigureMachineDto {
|
|||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => MachinePieceLinkPayloadDto)
|
@Type(() => MachinePieceLinkPayloadDto)
|
||||||
pieceLinks?: MachinePieceLinkPayloadDto[];
|
pieceLinks?: MachinePieceLinkPayloadDto[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => MachineProductLinkPayloadDto)
|
||||||
|
productLinks?: MachineProductLinkPayloadDto[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MachineComponentLinkInput = MachineComponentLinkPayloadDto;
|
export type MachineComponentLinkInput = MachineComponentLinkPayloadDto;
|
||||||
export type MachinePieceLinkInput = MachinePieceLinkPayloadDto;
|
export type MachinePieceLinkInput = MachinePieceLinkPayloadDto;
|
||||||
|
export type MachineProductLinkInput = MachineProductLinkPayloadDto;
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ export class CreatePieceDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
typeMachinePieceRequirementId?: string;
|
typeMachinePieceRequirementId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
productId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdatePieceDto {
|
export class UpdatePieceDto {
|
||||||
@@ -60,4 +64,9 @@ export class UpdatePieceDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
typePieceId?: string;
|
typePieceId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@Transform(({ value }) => (value === '' ? null : value))
|
||||||
|
@IsString()
|
||||||
|
productId?: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
28
src/shared/dto/product.dto.ts
Normal file
28
src/shared/dto/product.dto.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { IsArray, IsNumber, IsOptional, IsString } from 'class-validator';
|
||||||
|
import { Transform } from 'class-transformer';
|
||||||
|
import { PartialType } from '@nestjs/mapped-types';
|
||||||
|
|
||||||
|
export class CreateProductDto {
|
||||||
|
@IsString()
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
reference?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@Transform(({ value }) => (value === '' ? null : value))
|
||||||
|
@IsNumber({}, { message: 'supplierPrice must be a valid number' })
|
||||||
|
supplierPrice?: number | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
typeProductId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
constructeurIds?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateProductDto extends PartialType(CreateProductDto) {}
|
||||||
@@ -122,6 +122,35 @@ export class TypeMachinePieceRequirementDto {
|
|||||||
orderIndex?: number;
|
orderIndex?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class TypeMachineProductRequirementDto {
|
||||||
|
@IsString()
|
||||||
|
typeProductId: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
label?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
minCount?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
maxCount?: number | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
required?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
allowNewModels?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
orderIndex?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export class CreateTypeMachineDto {
|
export class CreateTypeMachineDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
name: string;
|
name: string;
|
||||||
@@ -161,6 +190,12 @@ export class CreateTypeMachineDto {
|
|||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => TypeMachinePieceRequirementDto)
|
@Type(() => TypeMachinePieceRequirementDto)
|
||||||
pieceRequirements?: TypeMachinePieceRequirementDto[];
|
pieceRequirements?: TypeMachinePieceRequirementDto[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => TypeMachineProductRequirementDto)
|
||||||
|
productRequirements?: TypeMachineProductRequirementDto[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateTypeMachineDto {
|
export class UpdateTypeMachineDto {
|
||||||
@@ -203,6 +238,12 @@ export class UpdateTypeMachineDto {
|
|||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => TypeMachinePieceRequirementDto)
|
@Type(() => TypeMachinePieceRequirementDto)
|
||||||
pieceRequirements?: TypeMachinePieceRequirementDto[];
|
pieceRequirements?: TypeMachinePieceRequirementDto[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => TypeMachineProductRequirementDto)
|
||||||
|
productRequirements?: TypeMachineProductRequirementDto[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreateTypeComposantDto {
|
export class CreateTypeComposantDto {
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import { normalizeComponentModelStructure } from '../../component-models/structu
|
|||||||
import type {
|
import type {
|
||||||
ComponentModelStructure,
|
ComponentModelStructure,
|
||||||
PieceModelCustomField,
|
PieceModelCustomField,
|
||||||
|
PieceModelProduct,
|
||||||
PieceModelStructure,
|
PieceModelStructure,
|
||||||
|
ProductModelStructure,
|
||||||
} from '../types/inventory';
|
} from '../types/inventory';
|
||||||
|
|
||||||
export class ComponentModelStructureValidationError extends Error {
|
export class ComponentModelStructureValidationError extends Error {
|
||||||
@@ -28,6 +30,67 @@ function sanitizeOptionalString(value: unknown): string | undefined {
|
|||||||
return String(value);
|
return String(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateProducts(
|
||||||
|
products: ComponentModelStructure['products'],
|
||||||
|
): ComponentModelStructure['products'] {
|
||||||
|
return products.map((product, index) => {
|
||||||
|
if ('typeProductId' in product) {
|
||||||
|
const typeProductId = assertString(
|
||||||
|
product.typeProductId,
|
||||||
|
`products[${index}].typeProductId`,
|
||||||
|
).trim();
|
||||||
|
if (!typeProductId) {
|
||||||
|
throw new ComponentModelStructureValidationError(
|
||||||
|
`products[${index}].typeProductId ne peut pas être vide`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const payload: ComponentModelStructure['products'][number] = {
|
||||||
|
typeProductId,
|
||||||
|
role: sanitizeOptionalString(product.role),
|
||||||
|
};
|
||||||
|
if ('familyCode' in product && product.familyCode) {
|
||||||
|
const familyCode = assertString(
|
||||||
|
product.familyCode,
|
||||||
|
`products[${index}].familyCode`,
|
||||||
|
).trim();
|
||||||
|
if (familyCode) {
|
||||||
|
(payload as Record<string, unknown>).familyCode = familyCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ('reference' in product && product.reference) {
|
||||||
|
(payload as Record<string, unknown>).reference = sanitizeOptionalString(
|
||||||
|
product.reference,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ('typeProductLabel' in product && product.typeProductLabel) {
|
||||||
|
(payload as Record<string, unknown>).typeProductLabel =
|
||||||
|
sanitizeOptionalString(product.typeProductLabel);
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('familyCode' in product) {
|
||||||
|
const familyCode = assertString(
|
||||||
|
product.familyCode,
|
||||||
|
`products[${index}].familyCode`,
|
||||||
|
).trim();
|
||||||
|
if (!familyCode) {
|
||||||
|
throw new ComponentModelStructureValidationError(
|
||||||
|
`products[${index}].familyCode ne peut pas être vide`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
familyCode,
|
||||||
|
role: sanitizeOptionalString(product.role),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ComponentModelStructureValidationError(
|
||||||
|
`products[${index}] doit définir "familyCode" ou "typeProductId"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function validatePieces(
|
function validatePieces(
|
||||||
pieces: ComponentModelStructure['pieces'],
|
pieces: ComponentModelStructure['pieces'],
|
||||||
): ComponentModelStructure['pieces'] {
|
): ComponentModelStructure['pieces'] {
|
||||||
@@ -148,6 +211,7 @@ export const ComponentModelStructureSchema = {
|
|||||||
const normalized = normalizeComponentModelStructure(input);
|
const normalized = normalizeComponentModelStructure(input);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
products: validateProducts(normalized.products),
|
||||||
pieces: validatePieces(normalized.pieces),
|
pieces: validatePieces(normalized.pieces),
|
||||||
customFields: validateCustomFields(normalized.customFields),
|
customFields: validateCustomFields(normalized.customFields),
|
||||||
subcomponents: validateSubcomponents(normalized.subcomponents),
|
subcomponents: validateSubcomponents(normalized.subcomponents),
|
||||||
@@ -230,10 +294,57 @@ function normalizePieceModelCustomFields(
|
|||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizePieceModelProducts(products: unknown): PieceModelProduct[] {
|
||||||
|
if (!Array.isArray(products)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return products.map((entry, index) => {
|
||||||
|
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
||||||
|
throw new PieceModelStructureValidationError(
|
||||||
|
`products[${index}] doit être un objet`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = entry as Record<string, unknown>;
|
||||||
|
|
||||||
|
const rawTypeProductId =
|
||||||
|
typeof record.typeProductId === 'string'
|
||||||
|
? record.typeProductId
|
||||||
|
: typeof (record.typeProduct as { id?: unknown })?.id === 'string'
|
||||||
|
? (record.typeProduct as { id: string }).id
|
||||||
|
: undefined;
|
||||||
|
const typeProductId = rawTypeProductId ? rawTypeProductId.trim() : '';
|
||||||
|
|
||||||
|
const rawFamilyCode =
|
||||||
|
typeof record.familyCode === 'string'
|
||||||
|
? record.familyCode
|
||||||
|
: typeof (record.typeProduct as { code?: unknown })?.code === 'string'
|
||||||
|
? (record.typeProduct as { code: string }).code
|
||||||
|
: undefined;
|
||||||
|
const familyCode = rawFamilyCode ? rawFamilyCode.trim() : '';
|
||||||
|
|
||||||
|
const rawRole = typeof record.role === 'string' ? record.role.trim() : '';
|
||||||
|
const role = rawRole ? rawRole : undefined;
|
||||||
|
|
||||||
|
if (typeProductId) {
|
||||||
|
return role ? { typeProductId, role } : { typeProductId };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (familyCode) {
|
||||||
|
return role ? { familyCode, role } : { familyCode };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new PieceModelStructureValidationError(
|
||||||
|
`products[${index}] doit définir "familyCode" ou "typeProductId"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export const PieceModelStructureSchema = {
|
export const PieceModelStructureSchema = {
|
||||||
parse(input: unknown): PieceModelStructure {
|
parse(input: unknown): PieceModelStructure {
|
||||||
if (input === undefined || input === null) {
|
if (input === undefined || input === null) {
|
||||||
return { customFields: [] };
|
return { customFields: [], products: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof input !== 'object' || Array.isArray(input)) {
|
if (typeof input !== 'object' || Array.isArray(input)) {
|
||||||
@@ -250,6 +361,11 @@ export const PieceModelStructureSchema = {
|
|||||||
structure.customFields = customFields;
|
structure.customFields = customFields;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const products = normalizePieceModelProducts(record.products);
|
||||||
|
if (products.length > 0 || 'products' in record) {
|
||||||
|
structure.products = products;
|
||||||
|
}
|
||||||
|
|
||||||
const normalizedTypePiece = toStringOrNull(record.typePieceId);
|
const normalizedTypePiece = toStringOrNull(record.typePieceId);
|
||||||
if (normalizedTypePiece) {
|
if (normalizedTypePiece) {
|
||||||
structure.typePieceId = normalizedTypePiece;
|
structure.typePieceId = normalizedTypePiece;
|
||||||
@@ -260,3 +376,34 @@ export const PieceModelStructureSchema = {
|
|||||||
return structure;
|
return structure;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export class ProductModelStructureValidationError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'ProductModelStructureValidationError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProductModelStructureSchema = {
|
||||||
|
parse(input: unknown): ProductModelStructure {
|
||||||
|
if (input === undefined || input === null) {
|
||||||
|
return { customFields: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof input !== 'object' || Array.isArray(input)) {
|
||||||
|
throw new ProductModelStructureValidationError(
|
||||||
|
'La structure de produit doit être un objet JSON.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = input as Record<string, unknown>;
|
||||||
|
const structure: ProductModelStructure = { ...record };
|
||||||
|
const customFields = normalizePieceModelCustomFields(record.customFields);
|
||||||
|
|
||||||
|
if (customFields.length > 0 || 'customFields' in record) {
|
||||||
|
structure.customFields = customFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
return structure;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -16,6 +16,20 @@ export type ComponentModelStructure = {
|
|||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Familles de produits autorisées (ou identifiant de famille) — pas de quantité ici.
|
||||||
|
*/
|
||||||
|
products: Array<
|
||||||
|
| {
|
||||||
|
familyCode: string;
|
||||||
|
role?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
typeProductId: string;
|
||||||
|
role?: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Valeurs par défaut au niveau "modèle" (libres, mais clé obligatoire).
|
* Valeurs par défaut au niveau "modèle" (libres, mais clé obligatoire).
|
||||||
*/
|
*/
|
||||||
@@ -48,7 +62,25 @@ export type PieceModelCustomField = {
|
|||||||
options?: unknown;
|
options?: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PieceModelProduct =
|
||||||
|
| {
|
||||||
|
familyCode: string;
|
||||||
|
role?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
typeProductId: string;
|
||||||
|
role?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type PieceModelStructure = {
|
export type PieceModelStructure = {
|
||||||
customFields?: PieceModelCustomField[];
|
customFields?: PieceModelCustomField[];
|
||||||
|
products?: PieceModelProduct[];
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProductModelCustomField = PieceModelCustomField;
|
||||||
|
|
||||||
|
export type ProductModelStructure = {
|
||||||
|
customFields?: ProductModelCustomField[];
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { ConflictException, Injectable } from '@nestjs/common';
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
import { TypeMachinesRepository } from '../../common/repositories/type-machines.repository';
|
import { TypeMachinesRepository } from '../../common/repositories/type-machines.repository';
|
||||||
import {
|
import {
|
||||||
TYPE_MACHINE_DEFAULT_INCLUDE,
|
TYPE_MACHINE_DEFAULT_INCLUDE,
|
||||||
@@ -17,7 +18,12 @@ export class TypeMachineService {
|
|||||||
async create(dto: CreateTypeMachineDto) {
|
async create(dto: CreateTypeMachineDto) {
|
||||||
const data = TypeMachineMapper.toCreateInput(dto);
|
const data = TypeMachineMapper.toCreateInput(dto);
|
||||||
|
|
||||||
return this.repository.create(data, TYPE_MACHINE_DEFAULT_INCLUDE);
|
try {
|
||||||
|
return await this.repository.create(data, TYPE_MACHINE_DEFAULT_INCLUDE);
|
||||||
|
} catch (error) {
|
||||||
|
this.handlePrismaError(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAll() {
|
async findAll() {
|
||||||
@@ -53,7 +59,24 @@ export class TypeMachineService {
|
|||||||
await this.repository.createPieceRequirements(id, requirements);
|
await this.repository.createPieceRequirements(id, requirements);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.repository.update(id, updateData, TYPE_MACHINE_DEFAULT_INCLUDE);
|
if (dto.productRequirements !== undefined) {
|
||||||
|
await this.repository.deleteProductRequirements(id);
|
||||||
|
const requirements = TypeMachineMapper.mapProductRequirementInputs(
|
||||||
|
dto.productRequirements,
|
||||||
|
);
|
||||||
|
await this.repository.createProductRequirements(id, requirements);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await this.repository.update(
|
||||||
|
id,
|
||||||
|
updateData,
|
||||||
|
TYPE_MACHINE_DEFAULT_INCLUDE,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.handlePrismaError(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(id: string) {
|
async remove(id: string) {
|
||||||
@@ -69,4 +92,19 @@ export class TypeMachineService {
|
|||||||
await this.repository.deleteCustomFields(id);
|
await this.repository.deleteCustomFields(id);
|
||||||
return this.repository.delete(id);
|
return this.repository.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handlePrismaError(error: unknown): never {
|
||||||
|
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
|
if (
|
||||||
|
error.code === 'P2002' &&
|
||||||
|
Array.isArray(error.meta?.target) &&
|
||||||
|
error.meta.target.includes('name')
|
||||||
|
) {
|
||||||
|
throw new ConflictException(
|
||||||
|
'Nom déjà utilisé pour un type de machine.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user