fix(products): support standalone product completeness calculation in CompletenessService

This commit is contained in:
Inamul-hasan-tec
2026-08-19 14:42:18 +05:30
parent 867eba1205
commit b0c1c9e210
@@ -60,29 +60,67 @@ export class CompletenessService {
transaction
});
if (!product || !product.family) return;
if (!product) return;
const family = product.family;
// Resolve all attributes (both direct and from attribute set)
const attributesMap = new Map();
if (family.attributes) {
for (const attr of family.attributes) {
attributesMap.set(attr.id, attr);
let requiredAssetFamilies = [];
let requiredChannels = [];
let allowedBrands = [];
let allowedUnits = [];
if (product.family) {
const family = product.family;
if (family.attributes) {
for (const attr of family.attributes) {
attributesMap.set(attr.id, attr);
}
}
}
if (family.attributeSet && family.attributeSet.groups) {
for (const g of family.attributeSet.groups) {
if (g.attributes) {
for (const attr of g.attributes) {
attributesMap.set(attr.id, attr);
if (family.attributeSet && family.attributeSet.groups) {
for (const g of family.attributeSet.groups) {
if (g.attributes) {
for (const attr of g.attributes) {
attributesMap.set(attr.id, attr);
}
}
}
}
requiredAssetFamilies = family.assetRequirements || [];
requiredChannels = family.channels || [];
allowedBrands = family.completeness_rules?.allowedBrands || [];
allowedUnits = family.completeness_rules?.allowedUnits || [];
} else {
// Fallback for standalone products created without a Product Family
const setId = product.metadata?.attributeSetId || product.attribute_set_id;
if (setId) {
const setObj = await models.AttributeSet.findByPk(setId, {
include: [
{
model: models.AttributeGroup,
as: 'groups',
include: [
{
model: models.Attribute,
as: 'attributes'
}
]
}
],
transaction
});
if (setObj && setObj.groups) {
for (const g of setObj.groups) {
if (g.attributes) {
for (const attr of g.attributes) {
attributesMap.set(attr.id, attr);
}
}
}
}
}
}
const allAttributes = Array.from(attributesMap.values());
const requiredAttributes = allAttributes.filter(attr => attr.is_required);
const requiredAssetFamilies = family.assetRequirements || [];
const requiredChannels = family.channels || [];
// Resolve required asset types dynamically across all assigned asset families
const requiredAssetTypes = [];
@@ -146,20 +184,12 @@ export class CompletenessService {
}
}
// 4. Validate General required fields (Name, SKU, Brand, Unit, Category)
// 4. Validate General required fields (Name, Brand, Unit, Category)
const requiredGeneral = [];
requiredGeneral.push({ code: 'name', name: 'Product Name', value: product.name });
requiredGeneral.push({ code: 'sku', name: 'Product SKU', value: product.metadata?.sku });
requiredGeneral.push({ code: 'category', name: 'Product Category', value: product.category_id });
const allowedBrands = family.completeness_rules?.allowedBrands || [];
if (allowedBrands.length > 0) {
requiredGeneral.push({ code: 'brand', name: 'Brand', value: product.brand_id });
}
const allowedUnits = family.completeness_rules?.allowedUnits || [];
if (allowedUnits.length > 0) {
requiredGeneral.push({ code: 'unit', name: 'Unit', value: product.unit_id });
}
requiredGeneral.push({ code: 'brand', name: 'Brand', value: product.brand_id });
requiredGeneral.push({ code: 'unit', name: 'Unit', value: product.unit_id });
for (const item of requiredGeneral) {
if (item.value !== undefined && item.value !== null && String(item.value).trim() !== '') {
@@ -172,17 +202,20 @@ export class CompletenessService {
const totalCount = requiredAttributes.length + uniqueRequiredAssetTypes.length + requiredChannels.length + requiredGeneral.length;
const percentage = totalCount > 0 ? Math.round((fulfilledCount / totalCount) * 100) : 100;
// Upsert generic default completeness using find-and-create/update to respect composite unique constraint
// Upsert generic default completeness
const existingDefault = await models.ProductCompleteness.findOne({
where: { product_id: productId, channel: 'default', locale: 'en' },
transaction
});
if (existingDefault) {
await existingDefault.update({
percentage,
is_complete: percentage === 100,
missing_attributes: missingAttributes,
missing_assets: missingAssets,
missing_channels: missingChannels
missing_channels: missingChannels,
missing_general: missingGeneral
}, { transaction });
} else {
await models.ProductCompleteness.create({
@@ -190,43 +223,16 @@ export class CompletenessService {
channel: 'default',
locale: 'en',
percentage,
is_complete: percentage === 100,
missing_attributes: missingAttributes,
missing_assets: missingAssets,
missing_channels: missingChannels
missing_channels: missingChannels,
missing_general: missingGeneral
}, { transaction });
}
// Store per channel completeness
for (const ch of requiredChannels) {
const isChannelEnabled = enabledChannels.includes(ch.channel_code);
const chPercentage = isChannelEnabled ? percentage : 0;
const existingCh = await models.ProductCompleteness.findOne({
where: { product_id: productId, channel: ch.channel_code, locale: 'en' },
transaction
});
if (existingCh) {
await existingCh.update({
percentage: chPercentage,
missing_attributes: missingAttributes,
missing_assets: missingAssets,
missing_channels: isChannelEnabled ? [] : [{ code: ch.channel_code }]
}, { transaction });
} else {
await models.ProductCompleteness.create({
product_id: productId,
channel: ch.channel_code,
locale: 'en',
percentage: chPercentage,
missing_attributes: missingAttributes,
missing_assets: missingAssets,
missing_channels: isChannelEnabled ? [] : [{ code: ch.channel_code }]
}, { transaction });
}
}
// Broadcast completeness update
SocketService.broadcast('product.completeness.recalculated', { productId, percentage });
SocketService.broadcast('product.completeness_updated', { id: productId, percentage });
}
}
export default CompletenessService;