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:
Matthieu
2025-11-05 15:34:42 +01:00
parent e81f71e3e7
commit 6cf2b566ce
38 changed files with 2601 additions and 120 deletions

View File

@@ -9,8 +9,10 @@ async function deleteExistingData() {
await prisma.machinePieceLink.deleteMany();
await prisma.machine.deleteMany();
await prisma.customFieldValue.deleteMany();
await prisma.product.deleteMany();
await prisma.composant.deleteMany();
await prisma.piece.deleteMany();
await prisma.typeMachineProductRequirement.deleteMany();
await prisma.modelType.deleteMany({
where: {
@@ -22,6 +24,7 @@ async function deleteExistingData() {
'cooling-module',
'structural-frame',
'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() {
console.log('Nettoyage des données existantes…');
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 dapprovisionnement (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 dapprovisionnement (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 dapprovisionnement (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…');
const coolingComponentFields: {
name: string;
@@ -509,6 +698,15 @@ async function main() {
} as Prisma.InputJsonValue,
});
await prisma.composant.update({
where: { id: coolingModule.id },
data: {
product: {
connect: { id: coolingProduct.id },
},
},
});
const structuralFrame = await createComponent({
name: 'Châssis structurel XC-800',
reference: 'FRAME-XC800',