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

@@ -2,7 +2,9 @@ import { normalizeComponentModelStructure } from '../../component-models/structu
import type {
ComponentModelStructure,
PieceModelCustomField,
PieceModelProduct,
PieceModelStructure,
ProductModelStructure,
} from '../types/inventory';
export class ComponentModelStructureValidationError extends Error {
@@ -28,6 +30,67 @@ function sanitizeOptionalString(value: unknown): string | undefined {
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(
pieces: ComponentModelStructure['pieces'],
): ComponentModelStructure['pieces'] {
@@ -148,6 +211,7 @@ export const ComponentModelStructureSchema = {
const normalized = normalizeComponentModelStructure(input);
return {
products: validateProducts(normalized.products),
pieces: validatePieces(normalized.pieces),
customFields: validateCustomFields(normalized.customFields),
subcomponents: validateSubcomponents(normalized.subcomponents),
@@ -230,10 +294,57 @@ function normalizePieceModelCustomFields(
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 = {
parse(input: unknown): PieceModelStructure {
if (input === undefined || input === null) {
return { customFields: [] };
return { customFields: [], products: [] };
}
if (typeof input !== 'object' || Array.isArray(input)) {
@@ -250,6 +361,11 @@ export const PieceModelStructureSchema = {
structure.customFields = customFields;
}
const products = normalizePieceModelProducts(record.products);
if (products.length > 0 || 'products' in record) {
structure.products = products;
}
const normalizedTypePiece = toStringOrNull(record.typePieceId);
if (normalizedTypePiece) {
structure.typePieceId = normalizedTypePiece;
@@ -260,3 +376,34 @@ export const PieceModelStructureSchema = {
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;
},
};