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

@@ -21,6 +21,18 @@ const PIECE_WITH_RELATIONS_INCLUDE = {
customField: true,
},
},
product: {
include: {
typeProduct: true,
constructeurs: true,
customFieldValues: {
include: {
customField: true,
},
},
documents: true,
},
},
machineLinks: {
include: {
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 };
}
async create(createPieceDto: CreatePieceDto) {
try {
const { data, constructeurIds } = await this.buildCreateInput(
createPieceDto,
const { data, constructeurIds } =
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({
where: { id: created.id },
where: { id: pieceId },
include: PIECE_WITH_RELATIONS_INCLUDE,
});
if (refreshed && syncedConstructeurIds.length > 0) {
(refreshed as typeof refreshed & { constructeurIds?: string[] }).constructeurIds =
[...syncedConstructeurIds];
(
refreshed as typeof refreshed & { constructeurIds?: string[] }
).constructeurIds = [...syncedConstructeurIds];
}
return refreshed;
@@ -134,9 +166,8 @@ export class PiecesService {
const constructeurIds = this.normalizeConstructeurIds(
updatePieceDto.constructeurIds,
);
resolvedConstructeurIds = await this.resolveExistingConstructeurIds(
constructeurIds,
);
resolvedConstructeurIds =
await this.resolveExistingConstructeurIds(constructeurIds);
}
if (updatePieceDto.typePieceId !== undefined) {
@@ -145,6 +176,16 @@ export class PiecesService {
: { 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;
try {
await this.prisma.$transaction(async (tx) => {
@@ -166,6 +207,7 @@ export class PiecesService {
await this.applyPieceSkeleton({
pieceId: updated.id,
typePiece: updated.typePiece as PieceTypeWithSkeleton | null,
product: updated.product,
prisma: tx,
});
});
@@ -176,8 +218,9 @@ export class PiecesService {
});
if (refreshed && syncedConstructeurIds) {
(refreshed as typeof refreshed & { constructeurIds?: string[] }).constructeurIds =
[...syncedConstructeurIds];
(
refreshed as typeof refreshed & { constructeurIds?: string[] }
).constructeurIds = [...syncedConstructeurIds];
}
return refreshed;
@@ -247,10 +290,15 @@ export class PiecesService {
private async applyPieceSkeleton({
pieceId,
typePiece,
product,
prisma,
}: {
pieceId: string;
typePiece: PieceTypeWithSkeleton | null;
product: {
typeProductId: string | null;
typeProduct?: { code: string | null } | null;
} | null;
prisma: Prisma.TransactionClient | PrismaService;
}) {
if (!typePiece?.id) {
@@ -267,6 +315,13 @@ export class PiecesService {
}
const customFields = skeleton.customFields ?? [];
const productRequirements: PieceProductRequirement[] = Array.isArray(
skeleton.products,
)
? skeleton.products.filter(
(entry): entry is PieceProductRequirement => !!entry,
)
: [];
await this.ensurePieceCustomFieldDefinitions(
prisma,
@@ -279,6 +334,99 @@ export class PiecesService {
typePiece.id,
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 dun 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[] {
@@ -529,3 +677,7 @@ type PieceTypeWithSkeleton = Prisma.ModelTypeGetPayload<{
type PieceCustomFieldEntry = NonNullable<
PieceModelStructure['customFields']
>[number];
type PieceProductRequirement = NonNullable<
PieceModelStructure['products']
>[number];