diff --git a/.env.local b/.env.local index 127a7a5..ad9d794 100644 --- a/.env.local +++ b/.env.local @@ -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. diff --git a/src/features/brands/brands/brand.service.js b/src/features/brands/brands/brand.service.js index 7d03e06..c52f860 100644 --- a/src/features/brands/brands/brand.service.js +++ b/src/features/brands/brands/brand.service.js @@ -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', diff --git a/src/features/catalogs/catalogs/catalog.controller.js b/src/features/catalogs/catalogs/catalog.controller.js index 9865cf9..81f7b6f 100644 --- a/src/features/catalogs/catalogs/catalog.controller.js +++ b/src/features/catalogs/catalogs/catalog.controller.js @@ -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' }; diff --git a/src/features/catalogs/catalogs/catalog.model.js b/src/features/catalogs/catalogs/catalog.model.js index f667436..ec051f2 100644 --- a/src/features/catalogs/catalogs/catalog.model.js +++ b/src/features/catalogs/catalogs/catalog.model.js @@ -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' }); diff --git a/src/features/catalogs/catalogs/catalog.repository.js b/src/features/catalogs/catalogs/catalog.repository.js index 1bb99b6..a6fd180 100644 --- a/src/features/catalogs/catalogs/catalog.repository.js +++ b/src/features/catalogs/catalogs/catalog.repository.js @@ -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: [] } }, diff --git a/src/features/catalogs/catalogs/catalog.service.js b/src/features/catalogs/catalogs/catalog.service.js index ab53e0c..9e1e2e5 100644 --- a/src/features/catalogs/catalogs/catalog.service.js +++ b/src/features/catalogs/catalogs/catalog.service.js @@ -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; diff --git a/src/features/catalogs/catalogs/familyAssetRequirement.model.js b/src/features/catalogs/catalogs/familyAssetRequirement.model.js index fe0a35f..635b10d 100644 --- a/src/features/catalogs/catalogs/familyAssetRequirement.model.js +++ b/src/features/catalogs/catalogs/familyAssetRequirement.model.js @@ -11,7 +11,7 @@ export default (sequelize) => { allowNull: false, primaryKey: true }, - asset_type_id: { + asset_family_id: { type: DataTypes.UUID, allowNull: false, primaryKey: true diff --git a/src/features/categories/categories/categorie.service.js b/src/features/categories/categories/categorie.service.js index f72e213..e2194b3 100644 --- a/src/features/categories/categories/categorie.service.js +++ b/src/features/categories/categories/categorie.service.js @@ -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; diff --git a/src/features/media/assetFamilies/assetFamily.service.js b/src/features/media/assetFamilies/assetFamily.service.js index 42294fa..95cbb3e 100644 --- a/src/features/media/assetFamilies/assetFamily.service.js +++ b/src/features/media/assetFamilies/assetFamily.service.js @@ -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 diff --git a/src/features/media/assets/asset.service.js b/src/features/media/assets/asset.service.js index 484fd47..d4170ac 100644 --- a/src/features/media/assets/asset.service.js +++ b/src/features/media/assets/asset.service.js @@ -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; diff --git a/src/features/products/products/completeness.service.js b/src/features/products/products/completeness.service.js index 0044b73..43d8395 100644 --- a/src/features/products/products/completeness.service.js +++ b/src/features/products/products/completeness.service.js @@ -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 { diff --git a/src/features/products/products/completenessResolver.service.js b/src/features/products/products/completenessResolver.service.js index fe7f1d5..69446e5 100644 --- a/src/features/products/products/completenessResolver.service.js +++ b/src/features/products/products/completenessResolver.service.js @@ -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) { diff --git a/src/features/products/products/product.service.js b/src/features/products/products/product.service.js index c3348a8..1e833b3 100644 --- a/src/features/products/products/product.service.js +++ b/src/features/products/products/product.service.js @@ -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', diff --git a/test_api_attribute.js b/test_api_attribute.js new file mode 100644 index 0000000..8b0783a --- /dev/null +++ b/test_api_attribute.js @@ -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(); diff --git a/test_create_attribute.js b/test_create_attribute.js new file mode 100644 index 0000000..443c5c8 --- /dev/null +++ b/test_create_attribute.js @@ -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); diff --git a/uploads/file-1784526116853-33144068.png b/uploads/file-1784526116853-33144068.png new file mode 100644 index 0000000..e89e012 Binary files /dev/null and b/uploads/file-1784526116853-33144068.png differ