solved merge conflicts

This commit is contained in:
m7338ohd-debug
2026-07-20 11:41:16 +05:30
parent 44814a42a2
commit 4eb59e888e
16 changed files with 261 additions and 86 deletions
+4 -4
View File
@@ -4,11 +4,11 @@ CORS_ORIGIN=http://localhost:5173
JWT_SECRET=supersecretjwtkeythatislongandsecure
JWT_EXPIRES_IN=7d
DB_HOST=127.0.0.1
DB_HOST=106.51.105.22
DB_PORT=5432
DB_NAME=Pim_Maskan
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=pc_local
DB_USER=pc_user
DB_PASSWORD="#TpW@%a&b$[zm"
DB_DIALECT=postgres
# IMPORTANT: Gmail SMTP requires an App Password, NOT your regular password.
+12 -12
View File
@@ -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
+3 -3
View File
@@ -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',
+90
View File
@@ -0,0 +1,90 @@
import http from 'http';
function request(options, postData) {
return new Promise((resolve, reject) => {
const req = http.request(options, (res) => {
let body = '';
res.on('data', (chunk) => body += chunk);
res.on('end', () => {
resolve({
statusCode: res.statusCode,
headers: res.headers,
body: body
});
});
});
req.on('error', reject);
if (postData) {
req.write(postData);
}
req.end();
});
}
async function run() {
try {
// 1. Login
console.log('Logging in...');
const loginData = JSON.stringify({
email: 'superadmin@maskan.com',
password: 'Admin@123'
});
const loginRes = await request({
hostname: 'localhost',
port: 5000,
path: '/api/v1/auth/login',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(loginData)
}
}, loginData);
console.log('Login Response Status:', loginRes.statusCode);
console.log('Login Response Body:', loginRes.body);
const loginJson = JSON.parse(loginRes.body);
// Let's inspect where the token is
const token = loginJson.data?.token || loginJson.token || loginJson.data?.accessToken;
console.log('Using Token:', token);
// 2. Try to create attribute
console.log('\nCreating attribute...');
const attributeData = JSON.stringify({
name: `API Test Attribute ${Date.now()}`,
code: `api_attr_${Date.now()}`,
type: 'text',
status: 'draft',
isUnique: false,
isRequired: false,
isVariantEligible: false,
isSearchable: false,
isFilterable: false,
isLocalizable: false,
isChannelSpecific: false,
minLength: 0,
maxLength: 255
});
const createRes = await request({
hostname: 'localhost',
port: 5000,
path: '/api/v1/attributes',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(attributeData),
'Authorization': `Bearer ${token}`
}
}, attributeData);
console.log('Create Response Status:', createRes.statusCode);
console.log('Create Response Body:', createRes.body);
} catch (error) {
console.error('Error running test:', error);
}
}
run();
+35
View File
@@ -0,0 +1,35 @@
import sequelize, { connectDatabase } from './src/shared/database/connection.js';
import { models } from './src/shared/database/models.js';
import AttributeService from './src/features/attributes/attributes/attribute.service.js';
async function test() {
await connectDatabase();
const payload = {
name: `Test Attribute ${Date.now()}`,
code: `test_attr_${Date.now()}`,
type: 'text',
status: 'draft',
isUnique: false,
isRequired: false,
isVariantEligible: false,
isSearchable: false,
isFilterable: false,
isLocalizable: false,
isChannelSpecific: false,
minLength: 0,
maxLength: 255
};
try {
console.log('\nAttempting to create attribute with payload:', JSON.stringify(payload, null, 2));
const result = await AttributeService.create(payload);
console.log('SUCCESS! Created attribute:', result.id);
} catch (e) {
console.error('FAILED to create attribute:', e);
}
await sequelize.close();
}
test().catch(console.error);
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB