fix(products): support standalone product completeness calculation in CompletenessService
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user