solved merge conflicts
This commit is contained in:
@@ -49,13 +49,13 @@ export class BrandService {
|
||||
action: 'CREATE',
|
||||
resource: 'Brand',
|
||||
resourceId: record.id,
|
||||
userId: userContext.userId || 'system',
|
||||
userId: context.userId || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
// Notify tenant users
|
||||
if (userContext.tenantId) {
|
||||
NotificationService.notifyTenant(userContext.tenantId, userContext.userId, {
|
||||
if (context.tenantId) {
|
||||
NotificationService.notifyTenant(context.tenantId, context.userId, {
|
||||
variant: 'brand',
|
||||
action: 'created',
|
||||
title: 'New brand added',
|
||||
@@ -89,13 +89,13 @@ export class BrandService {
|
||||
action: 'UPDATE',
|
||||
resource: 'Brand',
|
||||
resourceId: id,
|
||||
userId: userContext.userId || 'system',
|
||||
userId: context.userId || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
// Notify tenant users
|
||||
if (userContext.tenantId) {
|
||||
NotificationService.notifyTenant(userContext.tenantId, userContext.userId, {
|
||||
if (context.tenantId) {
|
||||
NotificationService.notifyTenant(context.tenantId, context.userId, {
|
||||
variant: 'brand',
|
||||
action: 'updated',
|
||||
title: 'Brand updated',
|
||||
@@ -129,12 +129,12 @@ export class BrandService {
|
||||
action: 'DELETE',
|
||||
resource: 'Brand',
|
||||
resourceId: id,
|
||||
userId: userContext.userId || userContext.id || 'system'
|
||||
userId: context.userId || context.id || 'system'
|
||||
});
|
||||
|
||||
// Notify tenant users
|
||||
if (userContext.tenantId) {
|
||||
NotificationService.notifyTenant(userContext.tenantId, userContext.userId, {
|
||||
if (context.tenantId) {
|
||||
NotificationService.notifyTenant(context.tenantId, context.userId, {
|
||||
variant: 'brand',
|
||||
action: 'deleted',
|
||||
title: 'Brand deleted',
|
||||
@@ -162,12 +162,12 @@ export class BrandService {
|
||||
action: 'ARCHIVE',
|
||||
resource: 'Brand',
|
||||
resourceId: id,
|
||||
userId: userContext.userId || userContext.id || 'system'
|
||||
userId: context.userId || context.id || 'system'
|
||||
});
|
||||
|
||||
// Notify tenant users
|
||||
if (userContext.tenantId) {
|
||||
NotificationService.notifyTenant(userContext.tenantId, userContext.userId, {
|
||||
if (context.tenantId) {
|
||||
NotificationService.notifyTenant(context.tenantId, context.userId, {
|
||||
variant: 'brand',
|
||||
action: 'deleted',
|
||||
title: 'Brand deleted',
|
||||
|
||||
@@ -40,6 +40,7 @@ export class CatalogController {
|
||||
channelCount: raw.channelCount ?? 0,
|
||||
attributeGroups: raw.attributeGroups ?? 0,
|
||||
canDelete: raw.canDelete ?? true,
|
||||
completeness: raw.completeness ?? 100,
|
||||
lastUpdated: raw.updated_at || raw.updatedAt,
|
||||
createdBy: raw.created_by || 'system'
|
||||
};
|
||||
|
||||
@@ -24,11 +24,11 @@ export class Catalog extends Model {
|
||||
as: 'variantAxes'
|
||||
});
|
||||
|
||||
// M:N with AssetType for required assets
|
||||
Catalog.belongsToMany(models.AssetType, {
|
||||
// M:N with AssetFamily for required assets
|
||||
Catalog.belongsToMany(models.AssetFamily, {
|
||||
through: models.FamilyAssetRequirement,
|
||||
foreignKey: 'family_id',
|
||||
otherKey: 'asset_type_id',
|
||||
otherKey: 'asset_family_id',
|
||||
as: 'assetRequirements'
|
||||
});
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export class CatalogRepository {
|
||||
]
|
||||
},
|
||||
{
|
||||
model: models.AssetType,
|
||||
model: models.AssetFamily,
|
||||
as: 'assetRequirements',
|
||||
through: { attributes: [] }
|
||||
},
|
||||
@@ -106,7 +106,7 @@ export class CatalogRepository {
|
||||
]
|
||||
},
|
||||
{
|
||||
model: models.AssetType,
|
||||
model: models.AssetFamily,
|
||||
as: 'assetRequirements',
|
||||
through: { attributes: [] }
|
||||
},
|
||||
|
||||
@@ -3,7 +3,6 @@ import { models, sequelize } from '../../../shared/database/models.js';
|
||||
import { SocketService } from '../../../shared/services/socket.service.js';
|
||||
import { AuditService } from '../../../shared/services/audit.service.js';
|
||||
import { ApiError } from '../../../utils/helpers/ApiError.utils.js';
|
||||
import { ApiError } from '../../../utils/helpers/ApiError.utils.js';
|
||||
|
||||
export class CatalogService {
|
||||
async attachCounts(record, transaction) {
|
||||
@@ -66,6 +65,32 @@ export class CatalogService {
|
||||
record.setDataValue('attributeGroups', attributeGroupsCount);
|
||||
record.setDataValue('canDelete', productCount === 0);
|
||||
|
||||
// Calculate average completeness for the family
|
||||
let completeness = 100;
|
||||
if (productCount > 0) {
|
||||
const products = await models.Product.findAll({
|
||||
where: { family_id: id },
|
||||
attributes: ['id'],
|
||||
transaction
|
||||
});
|
||||
const productIds = products.map(p => p.id);
|
||||
const completenessRecords = await models.ProductCompleteness.findAll({
|
||||
where: {
|
||||
product_id: productIds,
|
||||
channel: 'default'
|
||||
},
|
||||
attributes: ['percentage'],
|
||||
transaction
|
||||
});
|
||||
if (completenessRecords.length > 0) {
|
||||
const totalPct = completenessRecords.reduce((sum, c) => sum + c.percentage, 0);
|
||||
completeness = Math.round(totalPct / completenessRecords.length);
|
||||
} else {
|
||||
completeness = 0;
|
||||
}
|
||||
}
|
||||
record.setDataValue('completeness', completeness);
|
||||
|
||||
// Load full Brand and Unit objects from completeness_rules IDs
|
||||
const rules = record.completeness_rules || {};
|
||||
let brandIds = rules.allowedBrands || [];
|
||||
@@ -226,26 +251,23 @@ export class CatalogService {
|
||||
variantAxes = uniqueAxes;
|
||||
}
|
||||
|
||||
// 5. Asset Requirement Validation & Deduplication
|
||||
// 5. Asset Family Validation & Deduplication
|
||||
let assetRequirements = [];
|
||||
if (data.assetRequirements && Array.isArray(data.assetRequirements)) {
|
||||
const uniqueAssetTypes = [...new Set(data.assetRequirements)];
|
||||
const existingAssetTypes = await models.AssetType.findAll({
|
||||
where: { id: uniqueAssetTypes },
|
||||
const uniqueAssetFamilies = [...new Set(data.assetRequirements)];
|
||||
const existingAssetFamilies = await models.AssetFamily.findAll({
|
||||
where: { id: uniqueAssetFamilies },
|
||||
transaction
|
||||
});
|
||||
if (existingAssetTypes.length !== uniqueAssetTypes.length) {
|
||||
throw new Error('One or more selected asset types do not exist');
|
||||
if (existingAssetFamilies.length !== uniqueAssetFamilies.length) {
|
||||
throw new Error('One or more selected asset families do not exist');
|
||||
}
|
||||
for (const at of existingAssetTypes) {
|
||||
if (at.status !== 'active') {
|
||||
throw new Error(`Asset type "${at.name}" is inactive`);
|
||||
}
|
||||
if (at.deleted_at || at.deletedAt) {
|
||||
throw new Error(`Asset type "${at.name}" has been deleted`);
|
||||
for (const af of existingAssetFamilies) {
|
||||
if (af.status !== 'active') {
|
||||
throw new Error(`Asset family "${af.name}" is inactive`);
|
||||
}
|
||||
}
|
||||
assetRequirements = uniqueAssetTypes;
|
||||
assetRequirements = uniqueAssetFamilies;
|
||||
}
|
||||
|
||||
// 6. Channel Validation & Deduplication
|
||||
@@ -337,7 +359,7 @@ export class CatalogService {
|
||||
for (const assetTypeId of assetRequirements) {
|
||||
await models.FamilyAssetRequirement.create({
|
||||
family_id: record.id,
|
||||
asset_type_id: assetTypeId
|
||||
asset_family_id: assetTypeId
|
||||
}, { transaction });
|
||||
}
|
||||
|
||||
@@ -362,7 +384,7 @@ export class CatalogService {
|
||||
action: 'CREATE',
|
||||
resource: 'Catalog',
|
||||
resourceId: record.id,
|
||||
userId: userContext.userId || 'system',
|
||||
userId: context.userId || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
@@ -477,26 +499,23 @@ export class CatalogService {
|
||||
variantAxes = uniqueAxes;
|
||||
}
|
||||
|
||||
// 5. Asset Requirement Validation & Deduplication
|
||||
// 5. Asset Family Validation & Deduplication
|
||||
let assetRequirements = null;
|
||||
if (data.assetRequirements && Array.isArray(data.assetRequirements)) {
|
||||
const uniqueAssetTypes = [...new Set(data.assetRequirements)];
|
||||
const existingAssetTypes = await models.AssetType.findAll({
|
||||
where: { id: uniqueAssetTypes },
|
||||
const uniqueAssetFamilies = [...new Set(data.assetRequirements)];
|
||||
const existingAssetFamilies = await models.AssetFamily.findAll({
|
||||
where: { id: uniqueAssetFamilies },
|
||||
transaction
|
||||
});
|
||||
if (existingAssetTypes.length !== uniqueAssetTypes.length) {
|
||||
throw new Error('One or more selected asset types do not exist');
|
||||
if (existingAssetFamilies.length !== uniqueAssetFamilies.length) {
|
||||
throw new Error('One or more selected asset families do not exist');
|
||||
}
|
||||
for (const at of existingAssetTypes) {
|
||||
if (at.status !== 'active') {
|
||||
throw new Error(`Asset type "${at.name}" is inactive`);
|
||||
}
|
||||
if (at.deleted_at || at.deletedAt) {
|
||||
throw new Error(`Asset type "${at.name}" has been deleted`);
|
||||
for (const af of existingAssetFamilies) {
|
||||
if (af.status !== 'active') {
|
||||
throw new Error(`Asset family "${af.name}" is inactive`);
|
||||
}
|
||||
}
|
||||
assetRequirements = uniqueAssetTypes;
|
||||
assetRequirements = uniqueAssetFamilies;
|
||||
}
|
||||
|
||||
// 6. Channel Validation & Deduplication
|
||||
@@ -601,7 +620,7 @@ export class CatalogService {
|
||||
for (const assetTypeId of assetRequirements) {
|
||||
await models.FamilyAssetRequirement.create({
|
||||
family_id: id,
|
||||
asset_type_id: assetTypeId
|
||||
asset_family_id: assetTypeId
|
||||
}, { transaction });
|
||||
}
|
||||
await AuditService.log({ action: 'ASSET_REQUIREMENTS_CHANGED', resource: 'Catalog', resourceId: id, userId: context.userId || 'system', details: assetRequirements });
|
||||
@@ -676,7 +695,7 @@ export class CatalogService {
|
||||
action: 'DELETE',
|
||||
resource: 'Catalog',
|
||||
resourceId: id,
|
||||
userId: userContext.userId || 'system'
|
||||
userId: context.userId || 'system'
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
@@ -11,7 +11,7 @@ export default (sequelize) => {
|
||||
allowNull: false,
|
||||
primaryKey: true
|
||||
},
|
||||
asset_type_id: {
|
||||
asset_family_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
primaryKey: true
|
||||
|
||||
@@ -199,8 +199,8 @@ export class CategorieService {
|
||||
});
|
||||
|
||||
// Notify tenant users
|
||||
if (userContext.tenantId) {
|
||||
NotificationService.notifyTenant(userContext.tenantId, userContext.userId, {
|
||||
if (context.tenantId) {
|
||||
NotificationService.notifyTenant(context.tenantId, context.userId, {
|
||||
variant: 'category',
|
||||
action: 'updated',
|
||||
title: 'Category updated',
|
||||
@@ -262,12 +262,12 @@ export class CategorieService {
|
||||
action: 'DELETE',
|
||||
resource: 'Categorie',
|
||||
resourceId: id,
|
||||
userId: userContext.userId || 'system'
|
||||
userId: context.userId || 'system'
|
||||
});
|
||||
|
||||
// Notify tenant users
|
||||
if (userContext.tenantId) {
|
||||
NotificationService.notifyTenant(userContext.tenantId, userContext.userId, {
|
||||
if (context.tenantId) {
|
||||
NotificationService.notifyTenant(context.tenantId, context.userId, {
|
||||
variant: 'category',
|
||||
action: 'deleted',
|
||||
title: 'Category deleted',
|
||||
@@ -303,12 +303,12 @@ export class CategorieService {
|
||||
action: 'ARCHIVE',
|
||||
resource: 'Categorie',
|
||||
resourceId: id,
|
||||
userId: userContext.userId || 'system'
|
||||
userId: context.userId || 'system'
|
||||
});
|
||||
|
||||
// Notify tenant users
|
||||
if (userContext.tenantId) {
|
||||
NotificationService.notifyTenant(userContext.tenantId, userContext.userId, {
|
||||
if (context.tenantId) {
|
||||
NotificationService.notifyTenant(context.tenantId, context.userId, {
|
||||
variant: 'category',
|
||||
action: 'deleted',
|
||||
title: 'Category deleted',
|
||||
@@ -342,7 +342,7 @@ export class CategorieService {
|
||||
action: 'RESTORE',
|
||||
resource: 'Categorie',
|
||||
resourceId: id,
|
||||
userId: userContext.userId || 'system'
|
||||
userId: context.userId || 'system'
|
||||
});
|
||||
|
||||
return restored;
|
||||
|
||||
@@ -61,8 +61,8 @@ export class AssetFamilyService {
|
||||
let dbReqIds = [];
|
||||
try {
|
||||
if (models.FamilyAssetRequirement) {
|
||||
const reqs = await models.FamilyAssetRequirement.findAll({ where: { family_id: familyData.id } });
|
||||
dbReqIds = reqs.map(r => r.asset_type_id).filter(Boolean);
|
||||
// FamilyAssetRequirement links Catalog to AssetFamily, so it does not contain asset_type_ids
|
||||
dbReqIds = [];
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
|
||||
@@ -149,7 +149,7 @@ export class AssetService {
|
||||
action: 'CREATE',
|
||||
resource: 'Asset',
|
||||
resourceId: record.id,
|
||||
userId: userContext.userId || 'system',
|
||||
userId: context.userId || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
@@ -212,7 +212,7 @@ export class AssetService {
|
||||
action: 'UPDATE',
|
||||
resource: 'Asset',
|
||||
resourceId: id,
|
||||
userId: userContext.userId || 'system',
|
||||
userId: context.userId || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
@@ -347,7 +347,7 @@ export class AssetService {
|
||||
action: 'DELETE',
|
||||
resource: 'Asset',
|
||||
resourceId: id,
|
||||
userId: userContext.userId || 'system'
|
||||
userId: context.userId || 'system'
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
@@ -30,7 +30,7 @@ export class CompletenessService {
|
||||
]
|
||||
},
|
||||
{
|
||||
model: models.AssetType,
|
||||
model: models.AssetFamily,
|
||||
as: 'assetRequirements'
|
||||
},
|
||||
{
|
||||
@@ -80,10 +80,36 @@ export class CompletenessService {
|
||||
}
|
||||
const allAttributes = Array.from(attributesMap.values());
|
||||
const requiredAttributes = allAttributes.filter(attr => attr.is_required);
|
||||
const requiredAssetTypes = family.assetRequirements || [];
|
||||
const requiredAssetFamilies = family.assetRequirements || [];
|
||||
const requiredChannels = family.channels || [];
|
||||
|
||||
const totalCount = requiredAttributes.length + requiredAssetTypes.length + requiredChannels.length;
|
||||
// Resolve required asset types dynamically across all assigned asset families
|
||||
const requiredAssetTypes = [];
|
||||
for (const assetFamily of requiredAssetFamilies) {
|
||||
let typeIds = [];
|
||||
const desc = assetFamily.description || '';
|
||||
if (desc.includes('__AT_IDS__:')) {
|
||||
const parts = desc.split('__AT_IDS__:');
|
||||
typeIds = (parts[1] || '').split(',').map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
if (typeIds.length > 0) {
|
||||
const types = await models.AssetType.findAll({
|
||||
where: {
|
||||
id: typeIds,
|
||||
is_required: true
|
||||
},
|
||||
transaction
|
||||
});
|
||||
requiredAssetTypes.push(...types);
|
||||
}
|
||||
}
|
||||
// Deduplicate required asset types
|
||||
const uniqueRequiredAssetTypesMap = new Map();
|
||||
requiredAssetTypes.forEach(at => uniqueRequiredAssetTypesMap.set(at.id, at));
|
||||
const uniqueRequiredAssetTypes = Array.from(uniqueRequiredAssetTypesMap.values());
|
||||
|
||||
const totalCount = requiredAttributes.length + uniqueRequiredAssetTypes.length + requiredChannels.length;
|
||||
let fulfilledCount = 0;
|
||||
|
||||
const missingAttributes = [];
|
||||
@@ -102,7 +128,7 @@ export class CompletenessService {
|
||||
|
||||
// 2. Validate required assets
|
||||
const assignedAssetTypeIds = (product.productAssets || []).map(pa => pa.asset?.assetType?.id).filter(Boolean);
|
||||
for (const assetType of requiredAssetTypes) {
|
||||
for (const assetType of uniqueRequiredAssetTypes) {
|
||||
if (assignedAssetTypeIds.includes(assetType.id)) {
|
||||
fulfilledCount++;
|
||||
} else {
|
||||
|
||||
@@ -27,7 +27,7 @@ export class CompletenessResolverService {
|
||||
const familyReqs = await models.FamilyAssetRequirement.findAll({
|
||||
where: { family_id: product.family_id }
|
||||
});
|
||||
const mappedIds = familyReqs.map(r => r.asset_type_id);
|
||||
const mappedIds = familyReqs.map(r => r.asset_family_id);
|
||||
|
||||
const assetFamilies = await models.AssetFamily.findAll({
|
||||
where: { id: mappedIds }
|
||||
@@ -46,8 +46,12 @@ export class CompletenessResolverService {
|
||||
|
||||
const assetFamiliesCompleteness = await Promise.all(
|
||||
assetFamilies.map(async (family) => {
|
||||
const afReqs = await models.FamilyAssetRequirement.findAll({ where: { family_id: family.id } });
|
||||
const typeIds = afReqs.map(r => r.asset_type_id);
|
||||
let typeIds = [];
|
||||
const desc = family.description || '';
|
||||
if (desc.includes('__AT_IDS__:')) {
|
||||
const parts = desc.split('__AT_IDS__:');
|
||||
typeIds = (parts[1] || '').split(',').map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
let types = [];
|
||||
if (typeIds.length > 0) {
|
||||
|
||||
@@ -406,13 +406,13 @@ export class ProductService {
|
||||
action: 'CREATE',
|
||||
resource: 'Product',
|
||||
resourceId: product.id,
|
||||
userId: userContext.userId || 'system',
|
||||
userId: context.userId || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
// Notify tenant users
|
||||
if (userContext.tenantId) {
|
||||
NotificationService.notifyTenant(userContext.tenantId, userContext.userId, {
|
||||
if (context.tenantId) {
|
||||
NotificationService.notifyTenant(context.tenantId, context.userId, {
|
||||
variant: 'product',
|
||||
action: 'created',
|
||||
title: 'New product created',
|
||||
@@ -568,13 +568,13 @@ export class ProductService {
|
||||
action: 'UPDATE',
|
||||
resource: 'Product',
|
||||
resourceId: id,
|
||||
userId: userContext.userId || 'system',
|
||||
userId: context.userId || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
// Notify tenant users
|
||||
if (userContext.tenantId) {
|
||||
NotificationService.notifyTenant(userContext.tenantId, userContext.userId, {
|
||||
if (context.tenantId) {
|
||||
NotificationService.notifyTenant(context.tenantId, context.userId, {
|
||||
variant: 'product',
|
||||
action: 'updated',
|
||||
title: 'Product updated',
|
||||
@@ -605,12 +605,12 @@ export class ProductService {
|
||||
action: 'DELETE',
|
||||
resource: 'Product',
|
||||
resourceId: id,
|
||||
userId: userContext.userId || userContext.id || 'system'
|
||||
userId: context.userId || context.id || 'system'
|
||||
});
|
||||
|
||||
// Notify tenant users
|
||||
if (userContext.tenantId) {
|
||||
NotificationService.notifyTenant(userContext.tenantId, userContext.userId, {
|
||||
if (context.tenantId) {
|
||||
NotificationService.notifyTenant(context.tenantId, context.userId, {
|
||||
variant: 'product',
|
||||
action: 'deleted',
|
||||
title: 'Product deleted',
|
||||
@@ -637,12 +637,12 @@ export class ProductService {
|
||||
action: 'ARCHIVE',
|
||||
resource: 'Product',
|
||||
resourceId: id,
|
||||
userId: userContext.userId || userContext.id || 'system'
|
||||
userId: context.userId || context.id || 'system'
|
||||
});
|
||||
|
||||
// Notify tenant users
|
||||
if (userContext.tenantId) {
|
||||
NotificationService.notifyTenant(userContext.tenantId, userContext.userId, {
|
||||
if (context.tenantId) {
|
||||
NotificationService.notifyTenant(context.tenantId, context.userId, {
|
||||
variant: 'product',
|
||||
action: 'deleted',
|
||||
title: 'Product deleted',
|
||||
|
||||
Reference in New Issue
Block a user