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:
@@ -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 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[] {
|
||||
@@ -529,3 +677,7 @@ type PieceTypeWithSkeleton = Prisma.ModelTypeGetPayload<{
|
||||
type PieceCustomFieldEntry = NonNullable<
|
||||
PieceModelStructure['customFields']
|
||||
>[number];
|
||||
|
||||
type PieceProductRequirement = NonNullable<
|
||||
PieceModelStructure['products']
|
||||
>[number];
|
||||
|
||||
Reference in New Issue
Block a user