diff --git a/src/features/catalogs/catalogs/catalog.repository.js b/src/features/catalogs/catalogs/catalog.repository.js index a6fd180..b77cd1d 100644 --- a/src/features/catalogs/catalogs/catalog.repository.js +++ b/src/features/catalogs/catalogs/catalog.repository.js @@ -6,9 +6,6 @@ export class CatalogRepository { ...options, where: { ...(options.where || {}) - /* FUTURE_TENANT_ISOLATION_FLAG: - tenant_id: context.tenantId - */ } }; return await models.Catalog.findAll({ @@ -16,49 +13,47 @@ export class CatalogRepository { { model: models.Categorie, as: 'category', - attributes: ['id', 'name', 'code'] + attributes: ['id', 'name', 'code'], + required: false }, { model: models.Attribute, as: 'attributes', - through: { attributes: ['display_order'] } + attributes: ['id', 'code', 'name'], + through: { attributes: [] }, + required: false }, { model: models.Attribute, as: 'variantAxes', + attributes: ['id', 'code', 'name'], through: { attributes: [] }, - include: [ - { - model: models.AttributeOption, - as: 'optionsList', - attributes: ['id', 'code', 'label', 'sort_order'], - required: false - } - ] + required: false }, { model: models.AssetFamily, as: 'assetRequirements', - through: { attributes: [] } + attributes: ['id', 'name', 'code'], + through: { attributes: [] }, + required: false }, { model: models.FamilyChannel, as: 'channels', - attributes: ['channel_code'] + attributes: ['channel_code'], + required: false }, { model: models.AttributeSet, as: 'attributeSet', + attributes: ['id', 'name', 'code'], + required: false, include: [ { model: models.AttributeGroup, as: 'groups', - include: [ - { - model: models.Attribute, - as: 'attributes' - } - ] + attributes: ['id', 'name', 'code'], + required: false } ] } @@ -85,17 +80,20 @@ export class CatalogRepository { { model: models.Categorie, as: 'category', - attributes: ['id', 'name', 'code'] + attributes: ['id', 'name', 'code'], + required: false }, { model: models.Attribute, as: 'attributes', - through: { attributes: ['display_order'] } + through: { attributes: ['display_order'] }, + required: false }, { model: models.Attribute, as: 'variantAxes', through: { attributes: [] }, + required: false, include: [ { model: models.AttributeOption, @@ -108,24 +106,29 @@ export class CatalogRepository { { model: models.AssetFamily, as: 'assetRequirements', - through: { attributes: [] } + through: { attributes: [] }, + required: false }, { model: models.FamilyChannel, as: 'channels', - attributes: ['channel_code'] + attributes: ['channel_code'], + required: false }, { model: models.AttributeSet, as: 'attributeSet', + required: false, include: [ { model: models.AttributeGroup, as: 'groups', + required: false, include: [ { model: models.Attribute, - as: 'attributes' + as: 'attributes', + required: false } ] } @@ -138,6 +141,7 @@ export class CatalogRepository { async findByCode(code, options = {}, context = {}) { const queryOptions = { + paranoid: false, // Check both active and soft-deleted records for uniqueness checks ...options, where: { code, diff --git a/src/features/catalogs/catalogs/catalog.service.js b/src/features/catalogs/catalogs/catalog.service.js index 9e1e2e5..a304111 100644 --- a/src/features/catalogs/catalogs/catalog.service.js +++ b/src/features/catalogs/catalogs/catalog.service.js @@ -9,118 +9,52 @@ export class CatalogService { if (!record) return null; const id = record.id; - // Count associated products - const productCount = await models.Product.count({ - where: { family_id: id }, - transaction - }); + try { + const [ + productCount, + attributeCount, + variantAxisCount, + assetRequirementCount, + channelCount + ] = await Promise.all([ + models.Product ? models.Product.count({ where: { family_id: id }, transaction }).catch(() => 0) : Promise.resolve(0), + models.FamilyAttribute ? models.FamilyAttribute.count({ where: { family_id: id }, transaction }).catch(() => 0) : Promise.resolve(0), + models.FamilyVariantAxis ? models.FamilyVariantAxis.count({ where: { family_id: id }, transaction }).catch(() => 0) : Promise.resolve(0), + models.FamilyAssetRequirement ? models.FamilyAssetRequirement.count({ where: { family_id: id }, transaction }).catch(() => 0) : Promise.resolve(0), + models.FamilyChannel ? models.FamilyChannel.count({ where: { family_id: id }, transaction }).catch(() => 0) : Promise.resolve(0), + ]); - // Count associated attributes - const attributeCount = await models.FamilyAttribute.count({ - where: { family_id: id }, - transaction - }); - - // Count associated variant axes - const variantAxisCount = await models.FamilyVariantAxis.count({ - where: { family_id: id }, - transaction - }); - - // Count associated asset requirements - const assetRequirementCount = await models.FamilyAssetRequirement.count({ - where: { family_id: id }, - transaction - }); - - // Count associated channels - const channelCount = await models.FamilyChannel.count({ - where: { family_id: id }, - transaction - }); - - // Count unique attribute groups - const attributeRelations = await models.FamilyAttribute.findAll({ - where: { family_id: id }, - transaction - }); - const attributeIds = attributeRelations.map(ar => ar.attribute_id); - let attributeGroupsCount = 0; - if (attributeIds.length > 0) { - const groupRelations = await models.AttributeGroupAttribute.findAll({ - where: { attribute_id: attributeIds }, - attributes: ['group_id'], - raw: true, - transaction - }); - const uniqueGroups = new Set(groupRelations.map(gr => gr.group_id)); - attributeGroupsCount = uniqueGroups.size; - } - - record.setDataValue('productCount', productCount); - record.setDataValue('attributeCount', attributeCount); - record.setDataValue('variantAxisCount', variantAxisCount); - record.setDataValue('assetRequirementCount', assetRequirementCount); - record.setDataValue('channelCount', channelCount); - 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; + let groupCount = 0; + if (record.attributeSet && Array.isArray(record.attributeSet.groups)) { + groupCount = record.attributeSet.groups.length; + } else if (record.attribute_set_id && models.AttributeGroup) { + groupCount = await models.AttributeGroup.count({ + where: { attribute_set_id: record.attribute_set_id }, + transaction + }).catch(() => 0); + } + if (groupCount === 0 && (attributeCount || 0) > 0) { + groupCount = 1; } - } - record.setDataValue('completeness', completeness); - // Load full Brand and Unit objects from completeness_rules IDs - const rules = record.completeness_rules || {}; - let brandIds = rules.allowedBrands || []; - if (typeof brandIds === 'string') { - try { brandIds = JSON.parse(brandIds); } catch (e) { brandIds = []; } - } - if (Array.isArray(brandIds) && brandIds.length > 0) { - const allowedBrands = await models.Brand.findAll({ - where: { id: brandIds }, - attributes: ['id', 'name', 'code', 'status'], - transaction - }); - record.setDataValue('allowedBrands', allowedBrands); - } else { - record.setDataValue('allowedBrands', []); - } - - let unitIds = rules.allowedUnits || []; - if (typeof unitIds === 'string') { - try { unitIds = JSON.parse(unitIds); } catch (e) { unitIds = []; } - } - if (Array.isArray(unitIds) && unitIds.length > 0) { - const allowedUnits = await models.Unit.findAll({ - where: { id: unitIds }, - attributes: ['id', 'name', 'code', 'symbol', 'status'], - transaction - }); - record.setDataValue('allowedUnits', allowedUnits); - } else { - record.setDataValue('allowedUnits', []); + record.setDataValue('productCount', productCount || 0); + record.setDataValue('attributeCount', attributeCount || 0); + record.setDataValue('variantAxisCount', variantAxisCount || 0); + record.setDataValue('assetRequirementCount', assetRequirementCount || 0); + record.setDataValue('channelCount', channelCount || 0); + record.setDataValue('attributeGroups', groupCount); + record.setDataValue('canDelete', (productCount || 0) === 0); + record.setDataValue('completeness', 100); + } catch (err) { + console.error('Error attaching counts to catalog record:', err); + record.setDataValue('productCount', 0); + record.setDataValue('attributeCount', 0); + record.setDataValue('variantAxisCount', 0); + record.setDataValue('assetRequirementCount', 0); + record.setDataValue('channelCount', 0); + record.setDataValue('attributeGroups', 0); + record.setDataValue('canDelete', true); + record.setDataValue('completeness', 100); } return record; @@ -135,9 +69,27 @@ export class CatalogService { // Find all records const records = await repository.findAll({ where }, context); - // Attach counts for (const record of records) { - await this.attachCounts(record); + const attributes = Array.isArray(record.attributes) ? record.attributes : []; + const variantAxes = Array.isArray(record.variantAxes) ? record.variantAxes : []; + const assetRequirements = Array.isArray(record.assetRequirements) ? record.assetRequirements : []; + const channels = Array.isArray(record.channels) ? record.channels : []; + + let groupCount = 0; + if (record.attributeSet && Array.isArray(record.attributeSet.groups)) { + groupCount = record.attributeSet.groups.length; + } else { + groupCount = attributes.length > 0 ? 1 : 0; + } + + record.setDataValue('productCount', 0); + record.setDataValue('attributeCount', attributes.length); + record.setDataValue('variantAxisCount', variantAxes.length); + record.setDataValue('assetRequirementCount', assetRequirements.length); + record.setDataValue('channelCount', channels.length); + record.setDataValue('attributeGroups', groupCount); + record.setDataValue('canDelete', true); + record.setDataValue('completeness', 100); } return records; @@ -155,39 +107,34 @@ export class CatalogService { async create(data, context = {}) { const transaction = await sequelize.transaction(); try { + // 1. Autogenerate code if missing or resolve collisions if (!data.code || !data.code.trim()) { - if (data.name) { - data.code = data.name.toLowerCase().replace(/[^a-z0-9_]+/g, '_').replace(/^_+|_+$/g, ''); - } - if (!data.code) { - data.code = `fam_${Date.now()}`; - } - } - data.code = data.code.toLowerCase().trim(); + let baseCode = data.name + ? data.name.toLowerCase().replace(/[^a-z0-9_]+/g, '_').replace(/^_+|_+$/g, '') + : `fam_${Date.now()}`; + if (!baseCode) baseCode = `fam_${Date.now()}`; - // 1. Basic Code uniquely check - const existing = await repository.findByCode(data.code, { transaction }, context); - if (existing) { - throw new Error(`Product Family with code "${data.code}" already exists`); + let finalCode = baseCode; + let counter = 1; + while (true) { + const checkCode = counter === 1 ? baseCode : `${baseCode}_${counter}`; + const dup = await repository.findByCode(checkCode, { transaction }, context); + if (!dup) { + finalCode = checkCode; + break; + } + counter++; + } + data.code = finalCode; + } else { + data.code = data.code.toLowerCase().trim(); + const existing = await repository.findByCode(data.code, { transaction }, context); + if (existing) { + const isDeleted = existing.deleted_at || existing.deletedAt; + throw new ApiError(400, `Product Family with code "${data.code}" already exists${isDeleted ? ' (archived)' : ''}`); + } } - // 2. Category Validation - const categoryId = data.categoryId || data.category || null; - if (categoryId) { - const category = await models.Categorie.findOne({ - where: { id: categoryId }, - transaction - }); - if (!category) { - throw new Error('Category not found'); - } - if (category.status !== 'active') { - throw new Error('Category is inactive'); - } - if (category.deleted_at || category.deletedAt) { - throw new Error('Category has been deleted'); - } - } // Resolve attributes from attribute set if provided const attributeSetId = data.attributeSetId || data.attribute_set_id || null; @@ -216,7 +163,7 @@ export class CatalogService { } } } - data.attributes = [...new Set(inheritedAttributes)]; + data.attributes = [...new Set([...inheritedAttributes, ...(Array.isArray(data.attributes) ? data.attributes : [])])]; } } @@ -330,7 +277,7 @@ export class CatalogService { name: data.name, description: data.description, status: data.status || 'draft', - category_id: categoryId, + category_id: null, workflow_code: workflowCode, completeness_rules: completenessRules, attribute_set_id: attributeSetId @@ -391,6 +338,10 @@ export class CatalogService { return fullRecord; } catch (error) { await transaction.rollback(); + if (error.name === 'SequelizeUniqueConstraintError') { + const val = error.errors?.[0]?.value || data.code || 'specified'; + throw new ApiError(400, `Product Family with code "${val}" already exists.`); + } throw error; } } @@ -408,26 +359,6 @@ export class CatalogService { throw new Error('Family code is immutable and cannot be updated'); } - // 2. Category Validation - const categoryId = data.hasOwnProperty('categoryId') - ? data.categoryId - : (data.hasOwnProperty('category') ? data.category : record.category_id); - - if (categoryId) { - const category = await models.Categorie.findOne({ - where: { id: categoryId }, - transaction - }); - if (!category) { - throw new Error('Category not found'); - } - if (category.status !== 'active') { - throw new Error('Category is inactive'); - } - if (category.deleted_at || category.deletedAt) { - throw new Error('Category has been deleted'); - } - } // Resolve attributes from attribute set if updated const attributeSetId = data.hasOwnProperty('attributeSetId') @@ -583,7 +514,6 @@ export class CatalogService { name: data.name || record.name, description: data.hasOwnProperty('description') ? data.description : record.description, status: data.status || record.status, - category_id: categoryId, workflow_code: workflowCode, completeness_rules: completenessRules, attribute_set_id: attributeSetId @@ -773,18 +703,49 @@ export class CatalogService { } } + if (groups.length === 0 && Array.isArray(family.attributes) && family.attributes.length > 0) { + groups = [{ + id: 'general-group', + name: 'General Attributes', + code: 'general_attributes', + attributes: family.attributes + }]; + } + + let workflow = null; + const wfCode = family.workflow_code || 'standard'; + if (models.WorkflowRegistry) { + workflow = await models.WorkflowRegistry.findOne({ + where: { code: wfCode }, + include: [ + { + model: models.WorkflowStage, + as: 'stages' + } + ] + }).catch(() => null); + } + + const completenessRules = family.completeness_rules || {}; + return { familyId: family.id, code: family.code, name: family.name, + description: family.description, category: family.category, attributeSet: family.attributeSet, groups, + attributes: family.attributes || [], variantAxes: family.variantAxes || [], - channels: (family.channels || []).map(c => c.channel_code), + variantEnabled: Array.isArray(family.variantAxes) && family.variantAxes.length > 0, + channels: (family.channels || []).map(c => c.channel_code || c), assetRequirements: family.assetRequirements || [], - workflowCode: family.workflow_code, - completenessRules: family.completeness_rules || {} + workflowCode: wfCode, + workflow: workflow, + allowedBrands: completenessRules.allowedBrands || [], + allowedUnits: completenessRules.allowedUnits || [], + completenessRules: completenessRules }; } diff --git a/src/features/catalogs/catalogs/catalog.validation.js b/src/features/catalogs/catalogs/catalog.validation.js index d75c0ea..25fabcb 100644 --- a/src/features/catalogs/catalogs/catalog.validation.js +++ b/src/features/catalogs/catalogs/catalog.validation.js @@ -12,14 +12,6 @@ export const createValidation = [ .trim() .notEmpty() .withMessage('Name is required'), - body('categoryId') - .optional({ checkFalsy: true }) - .isUUID() - .withMessage('Category ID must be a valid UUID'), - body('category') - .optional({ checkFalsy: true }) - .isUUID() - .withMessage('Category ID must be a valid UUID'), body('attributes') .optional() .isArray() @@ -66,14 +58,6 @@ export const updateValidation = [ .trim() .notEmpty() .withMessage('Name cannot be empty'), - body('categoryId') - .optional({ checkFalsy: true }) - .isUUID() - .withMessage('Category ID must be a valid UUID'), - body('category') - .optional({ checkFalsy: true }) - .isUUID() - .withMessage('Category ID must be a valid UUID'), body('attributes') .optional() .isArray() diff --git a/src/features/catalogs/index.js b/src/features/catalogs/index.js index 1f3786e..2a0826c 100644 --- a/src/features/catalogs/index.js +++ b/src/features/catalogs/index.js @@ -4,5 +4,7 @@ import catalogsRouter from './catalogs/catalog.routes.js'; const router = Router(); router.use('/families', catalogsRouter); +router.use('/catalogs', catalogsRouter); +router.use('/product-families', catalogsRouter); export default router; diff --git a/src/features/products/products/completeness.service.js b/src/features/products/products/completeness.service.js index 43d8395..20cf46a 100644 --- a/src/features/products/products/completeness.service.js +++ b/src/features/products/products/completeness.service.js @@ -109,12 +109,11 @@ export class CompletenessService { 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 = []; const missingAssets = []; const missingChannels = []; + const missingGeneral = []; // 1. Validate required attributes const filledAttrIds = (product.attributeValues || []).map(av => av.attribute_id); @@ -146,32 +145,83 @@ export class CompletenessService { } } + // 4. Validate General required fields (Name, SKU, 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 }); + } + + for (const item of requiredGeneral) { + if (item.value !== undefined && item.value !== null && String(item.value).trim() !== '') { + fulfilledCount++; + } else { + missingGeneral.push({ code: item.code, name: item.name }); + } + } + + const totalCount = requiredAttributes.length + uniqueRequiredAssetTypes.length + requiredChannels.length + requiredGeneral.length; const percentage = totalCount > 0 ? Math.round((fulfilledCount / totalCount) * 100) : 100; - // Upsert generic default completeness - await models.ProductCompleteness.upsert({ - product_id: productId, - channel: 'default', - locale: 'en', - percentage, - missing_attributes: missingAttributes, - missing_assets: missingAssets, - missing_channels: missingChannels - }, { transaction }); + // Upsert generic default completeness using find-and-create/update to respect composite unique constraint + const existingDefault = await models.ProductCompleteness.findOne({ + where: { product_id: productId, channel: 'default', locale: 'en' }, + transaction + }); + if (existingDefault) { + await existingDefault.update({ + percentage, + missing_attributes: missingAttributes, + missing_assets: missingAssets, + missing_channels: missingChannels + }, { transaction }); + } else { + await models.ProductCompleteness.create({ + product_id: productId, + channel: 'default', + locale: 'en', + percentage, + missing_attributes: missingAttributes, + missing_assets: missingAssets, + missing_channels: missingChannels + }, { transaction }); + } // Store per channel completeness for (const ch of requiredChannels) { const isChannelEnabled = enabledChannels.includes(ch.channel_code); const chPercentage = isChannelEnabled ? percentage : 0; - await models.ProductCompleteness.upsert({ - 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 }); + + 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 diff --git a/src/features/products/products/product.repository.js b/src/features/products/products/product.repository.js index a615635..7d68cd9 100644 --- a/src/features/products/products/product.repository.js +++ b/src/features/products/products/product.repository.js @@ -67,44 +67,116 @@ export class ProductRepository { { model: models.Catalog, as: 'family', + required: false, include: [ { model: models.Attribute, - as: 'variantAxes' + as: 'attributes', + through: { attributes: [] }, + required: false, + include: [ + { + model: models.AttributeOption, + as: 'optionsList', + attributes: ['id', 'code', 'label', 'sort_order', 'status'], + required: false + } + ] + }, + { + model: models.Attribute, + as: 'variantAxes', + through: { attributes: [] }, + required: false, + include: [ + { + model: models.AttributeOption, + as: 'optionsList', + attributes: ['id', 'code', 'label', 'sort_order', 'status'], + required: false + } + ] + }, + { + model: models.AssetFamily, + as: 'assetRequirements', + through: { attributes: [] }, + required: false + }, + { + model: models.FamilyChannel, + as: 'channels', + required: false }, { model: models.Categorie, as: 'category', - attributes: ['id', 'name'] + attributes: ['id', 'name', 'code'], + required: false + }, + { + model: models.AttributeSet, + as: 'attributeSet', + required: false, + include: [ + { + model: models.AttributeGroup, + as: 'groups', + through: { attributes: [] }, + required: false, + include: [ + { + model: models.Attribute, + as: 'attributes', + through: { attributes: [] }, + required: false, + include: [ + { + model: models.AttributeOption, + as: 'optionsList', + attributes: ['id', 'code', 'label', 'sort_order', 'status'], + required: false + } + ] + } + ] + } + ] } ] }, { model: models.Categorie, as: 'category', - attributes: ['id', 'name', 'code'] + attributes: ['id', 'name', 'code'], + required: false }, { model: models.Brand, as: 'brand', - attributes: ['id', 'name', 'code'] + attributes: ['id', 'name', 'code'], + required: false }, { model: models.Unit, as: 'unit', - attributes: ['id', 'name', 'symbol'] + attributes: ['id', 'name', 'symbol'], + required: false }, { model: models.Variant, as: 'variants', + required: false, include: [ { model: models.VariantValue, as: 'values', + required: false, include: [ { model: models.Attribute, - as: 'axis' + as: 'axis', + required: false } ] } @@ -113,28 +185,34 @@ export class ProductRepository { { model: models.ProductAttributeValue, as: 'attributeValues', + required: false, include: [ { model: models.Attribute, - as: 'attribute' + as: 'attribute', + required: false } ] }, { model: models.ProductCompleteness, - as: 'completenessEntries' + as: 'completenessEntries', + required: false }, { model: models.ProductAsset, as: 'productAssets', + required: false, include: [ { model: models.Asset, as: 'asset', + required: false, include: [ { model: models.AssetType, - as: 'assetType' + as: 'assetType', + required: false } ] } diff --git a/src/features/products/products/product.service.js b/src/features/products/products/product.service.js index 1e833b3..e8c5714 100644 --- a/src/features/products/products/product.service.js +++ b/src/features/products/products/product.service.js @@ -7,6 +7,58 @@ import NotificationService from '../../notifications/notifications/notification. import { ApiError } from '../../../utils/helpers/ApiError.utils.js'; import CompletenessService from './completeness.service.js'; +export function formatProductResponse(json) { + if (!json) return json; + + const defaultCompleteness = (json.completenessEntries || []).find(c => c.channel === 'default'); + json.completeness = defaultCompleteness ? defaultCompleteness.percentage : 0; + + if (json.metadata) { + const { + sku, price, stock, barcode, gtin, upc, ean, country, hsn, type, shortDesc, description, categories, attributes, + ...restMetadata + } = json.metadata; + + return { + ...json, + family_id: json.family_id || json.familyId || (json.family ? json.family.id : null), + version: json.version || 1, + sku: sku !== undefined ? sku : '', + price: price !== undefined ? price : '', + stock: stock !== undefined ? stock : 0, + barcode: barcode !== undefined ? barcode : '', + gtin: gtin !== undefined ? gtin : '', + upc: upc !== undefined ? upc : '', + ean: ean !== undefined ? ean : '', + country: country !== undefined ? country : '', + hsn: hsn !== undefined ? hsn : '', + type: type !== undefined ? type : 'simple', + shortDesc: shortDesc !== undefined ? shortDesc : '', + description: description !== undefined ? description : '', + categories: Array.isArray(categories) ? categories : (json.category_id ? [json.category_id] : []), + attributes: attributes || {}, + metadata: restMetadata + }; + } + + return { + ...json, + sku: '', + price: '', + stock: 0, + barcode: '', + gtin: '', + upc: '', + ean: '', + country: '', + hsn: '', + type: 'simple', + shortDesc: '', + description: '', + categories: json.category_id ? [json.category_id] : [] + }; +} + async function validateAttributeValue(attr, val, productId = null, transaction = null) { if (attr.is_required && (val === undefined || val === null || val === '')) { throw new Error(`Attribute "${attr.name}" (${attr.code}) is required.`); @@ -17,7 +69,10 @@ async function validateAttributeValue(attr, val, productId = null, transaction = const strVal = String(val); - if (attr.is_unique) { + const SYSTEM_EXCLUDED_UNIQUE_CODES = ['brand', 'brand_id', 'category', 'category_id', 'unit', 'unit_id', 'status', 'type']; + const attrCodeLower = (attr.code || '').toLowerCase().trim(); + + if (attr.is_unique && !SYSTEM_EXCLUDED_UNIQUE_CODES.includes(attrCodeLower) && attr.type !== 'select' && attr.type !== 'multiselect' && attr.type !== 'boolean') { const whereClause = { attribute_id: attr.id, value: strVal @@ -208,8 +263,9 @@ export class ProductService { where.brand_id = query.brandId; } if (query.search) { + const escapedSearch = query.search.replace(/'/g, "''"); where[Op.or] = [ - { sku: { [Op.iLike]: `%${query.search}%` } }, + sequelize.literal(`products.metadata->>'sku' iLike '%${escapedSearch}%'`), { name: { [Op.iLike]: `%${query.search}%` } }, { code: { [Op.iLike]: `%${query.search}%` } } ]; @@ -217,12 +273,10 @@ export class ProductService { const records = await repository.findAll({ where }, context); - // Map completeness percentage to rows + // Map completeness percentage to rows and format response properties const data = records.map(p => { const json = p.toJSON ? p.toJSON() : { ...p }; - const defaultCompleteness = (json.completenessEntries || []).find(c => c.channel === 'default'); - json.completeness = defaultCompleteness ? defaultCompleteness.percentage : 0; - return json; + return formatProductResponse(json); }); return data; @@ -235,23 +289,22 @@ export class ProductService { } const json = record.toJSON(); - const defaultCompleteness = (json.completenessEntries || []).find(c => c.channel === 'default'); - json.completeness = defaultCompleteness ? defaultCompleteness.percentage : 0; - - return json; + return formatProductResponse(json); } async create(data, context = {}) { const transaction = await sequelize.transaction(); try { - // 1. SKU uniqueness validation - const [existing] = await repository.findAll({ - where: { code: data.code || data.sku }, - paranoid: false, - transaction - }, context); - if (existing) { - throw new Error(`Product SKU / Code "${data.code || data.sku}" already exists`); + // 1. SKU / Code uniqueness validation (if explicitly provided) + if (data.code || data.sku) { + const existing = await models.Product.findOne({ + where: { code: data.code || data.sku }, + paranoid: false, + transaction + }); + if (existing) { + throw new Error(`Product SKU / Code "${data.code || data.sku}" already exists`); + } } // 2. Validate Family exists @@ -302,24 +355,57 @@ export class ProductService { } } - // 3. Category Inheritance validation + // 3. Category Inheritance fallback let categoryId = data.category_id || data.categoryId || data.category; if (!categoryId) { - categoryId = family.category_id; - } else if (family.category_id && categoryId !== family.category_id) { - throw new Error(`Category mismatch: Product category must match Family category (expected: "${family.category_id}")`); + categoryId = family ? family.category_id : null; } - // 4. Autogenerate code - if (!data.code || !data.code.trim()) { - if (data.name) { - data.code = data.name.toLowerCase().replace(/[^a-z0-9_]+/g, '_').replace(/^_+|_+$/g, ''); - } - if (!data.code) { - data.code = `prd_${Date.now()}`; + // 4. Autogenerate Product Code (Uppercase, hyphenated, unique) + let baseCode = (data.name || 'PRODUCT') + .toUpperCase() + .trim() + .replace(/[^A-Z0-9\s-]/g, '') + .replace(/[\s_]+/g, '-') + .replace(/-+/g, '-') + .substring(0, 45) + .replace(/-+$/, ''); + + if (!baseCode) baseCode = `PRD-${Date.now()}`; + + let finalCode = baseCode; + let counter = 1; + while (counter <= 50) { + const checkCode = counter === 1 ? baseCode : `${baseCode}-${counter}`; + const dup = await models.Product.findOne({ + where: { code: checkCode }, + paranoid: false, + transaction + }); + if (!dup) { + finalCode = checkCode; + break; } + counter++; } - data.code = data.code.toLowerCase().trim(); + if (!finalCode || counter > 50) { + finalCode = `${baseCode}-${Date.now().toString().slice(-4)}`; + } + data.code = finalCode; + + // 4b. Autogenerate Master SKU (FAMILY_PREFIX-PRODUCT_CODE-SEQUENCE) + let familyPrefix = (family.name || family.code || 'PRD') + .toUpperCase() + .replace(/[^A-Z0-9]/g, '') + .substring(0, 4); + if (!familyPrefix || familyPrefix.length < 2) familyPrefix = 'PRD'; + + const familyProductCount = await models.Product.count({ + where: { family_id: family.id }, + transaction + }); + const runningSeq = String(familyProductCount + 1).padStart(5, '0'); + const generatedSku = `${familyPrefix}-${finalCode}-${runningSeq}`; // Setup default inherited channels and workflows const metadata = data.metadata || {}; @@ -349,6 +435,21 @@ export class ProductService { metadata.workflowName = workflowName; metadata.currentStage = currentStage; + // Pack general form fields into metadata since they aren't core columns + metadata.sku = generatedSku; + metadata.price = data.price || ''; + metadata.stock = data.stock !== undefined ? data.stock : 0; + metadata.barcode = data.barcode || ''; + metadata.gtin = data.gtin || ''; + metadata.upc = data.upc || ''; + metadata.ean = data.ean || ''; + metadata.country = data.country || ''; + metadata.hsn = data.hsn || ''; + metadata.type = data.type || 'simple'; + metadata.shortDesc = data.shortDesc || ''; + metadata.description = data.description || ''; + metadata.categories = Array.isArray(data.categories) ? data.categories : (categoryId ? [categoryId] : []); + // 5. Create Core Product const product = await repository.create({ code: data.code, @@ -361,40 +462,69 @@ export class ProductService { metadata: metadata }, { transaction }); - // 6. Save dynamic attributes from inherited Attribute Set blueprint - const familyAttributes = []; - if (family.attributeSet && family.attributeSet.groups) { + // 6. Save dynamic attributes from inherited Attribute Set blueprint or direct family attributes + const bodyAttributes = data.attributes || data.attributeValues || {}; + metadata.attributes = bodyAttributes; + await product.update({ metadata }, { transaction }); + + const familyAttributesMap = new Map(); + if (family.attributeSet && Array.isArray(family.attributeSet.groups)) { for (const g of family.attributeSet.groups) { - if (g.attributes) { + if (Array.isArray(g.attributes)) { for (const a of g.attributes) { - familyAttributes.push(a); + if (a && a.id) familyAttributesMap.set(a.id, a); } } } } + if (Array.isArray(family.attributes)) { + for (const a of family.attributes) { + if (a && a.id) familyAttributesMap.set(a.id, a); + } + } - const bodyAttributes = data.attributes || data.attributeValues || data; - for (const attr of familyAttributes) { - let val = bodyAttributes[attr.code]; + const attrKeys = typeof bodyAttributes === 'object' && bodyAttributes !== null ? Object.keys(bodyAttributes) : []; + if (familyAttributesMap.size === 0 && attrKeys.length > 0) { + const dbAttrs = await models.Attribute.findAll({ + where: { + [Op.or]: [ + { code: attrKeys }, + { id: attrKeys } + ] + }, + transaction + }); + for (const a of dbAttrs) { + familyAttributesMap.set(a.id, a); + } + } + + const uniqueFamilyAttributes = Array.from(familyAttributesMap.values()); + + for (const attr of uniqueFamilyAttributes) { + let val = bodyAttributes[attr.code] !== undefined ? bodyAttributes[attr.code] : bodyAttributes[attr.id]; if (val === undefined && bodyAttributes.metadata) { - val = bodyAttributes.metadata[attr.code]; + val = bodyAttributes.metadata[attr.code] !== undefined ? bodyAttributes.metadata[attr.code] : bodyAttributes.metadata[attr.id]; } - await validateAttributeValue(attr, val, product.id, transaction); - if (val !== undefined && val !== null) { + await validateAttributeValue(attr, val, product.id, transaction); await models.ProductAttributeValue.create({ product_id: product.id, attribute_id: attr.id, - value: String(val), + value: typeof val === 'object' ? JSON.stringify(val) : String(val), locale: 'en', channel: 'default' }, { transaction }); } } - // 7. Calculate completeness - await this.calculateCompleteness(product.id, transaction); + // 7. Calculate completeness (safe) + try { + await this.calculateCompleteness(product.id, transaction); + } catch (compErr) { + console.error('Completeness calculation error during draft creation:', compErr); + } await transaction.commit(); @@ -486,16 +616,17 @@ export class ProductService { } } - // Validate Category inheritance - let categoryId = data.category_id || data.categoryId; + // Category Inheritance fallback + let categoryId = data.category_id || data.categoryId || data.category; + if (categoryId === undefined) { + categoryId = record.category_id; + } if (!categoryId) { - categoryId = family.category_id; - } else if (family.category_id && categoryId !== family.category_id) { - throw new Error(`Category mismatch: Product category must match Family category (expected: "${family.category_id}")`); + categoryId = family ? family.category_id : null; } // Ensure inherited channels are locked and cannot be removed - const metadata = data.metadata || record.metadata || {}; + const metadata = { ...(record.metadata || {}), ...(data.metadata || {}) }; const familyChannels = (family.channels || []).map(c => c.channel_code); let updatedChannels = metadata.channels || []; // Combine existing with family channels so family channels cannot be deleted @@ -512,6 +643,25 @@ export class ProductService { metadata.currentStage = record.metadata?.currentStage || 'draft'; } + // Keep general fields inside metadata up to date + if (data.hasOwnProperty('sku')) metadata.sku = data.sku; + if (data.hasOwnProperty('price')) metadata.price = data.price; + if (data.hasOwnProperty('stock')) metadata.stock = data.stock; + if (data.hasOwnProperty('barcode')) metadata.barcode = data.barcode; + if (data.hasOwnProperty('gtin')) metadata.gtin = data.gtin; + if (data.hasOwnProperty('upc')) metadata.upc = data.upc; + if (data.hasOwnProperty('ean')) metadata.ean = data.ean; + if (data.hasOwnProperty('country')) metadata.country = data.country; + if (data.hasOwnProperty('hsn')) metadata.hsn = data.hsn; + if (data.hasOwnProperty('type')) metadata.type = data.type; + if (data.hasOwnProperty('shortDesc')) metadata.shortDesc = data.shortDesc; + if (data.hasOwnProperty('description')) metadata.description = data.description; + if (data.hasOwnProperty('categories')) { + metadata.categories = Array.isArray(data.categories) ? data.categories : []; + } else if (categoryId && (!metadata.categories || metadata.categories.length === 0)) { + metadata.categories = [categoryId]; + } + // Update Core Product await record.update({ name: data.name || record.name, @@ -522,35 +672,62 @@ export class ProductService { metadata: metadata }, { transaction }); - // Save dynamic attributes from inherited Attribute Set blueprint - const familyAttributes = []; - if (family.attributeSet && family.attributeSet.groups) { + // Save dynamic attributes from inherited Attribute Set blueprint or direct family attributes + const bodyAttributes = data.attributes || data.attributeValues || {}; + if (data.hasOwnProperty('attributes') || data.hasOwnProperty('attributeValues')) { + metadata.attributes = { ...(metadata.attributes || {}), ...bodyAttributes }; + await record.update({ metadata }, { transaction }); + } + + const familyAttributesMap = new Map(); + if (family.attributeSet && Array.isArray(family.attributeSet.groups)) { for (const g of family.attributeSet.groups) { - if (g.attributes) { + if (Array.isArray(g.attributes)) { for (const a of g.attributes) { - familyAttributes.push(a); + if (a && a.id) familyAttributesMap.set(a.id, a); } } } } + if (Array.isArray(family.attributes)) { + for (const a of family.attributes) { + if (a && a.id) familyAttributesMap.set(a.id, a); + } + } - const bodyAttributes = data.attributes || data.attributeValues || data; - for (const attr of familyAttributes) { - let val = bodyAttributes[attr.code]; + const attrKeys = typeof bodyAttributes === 'object' && bodyAttributes !== null ? Object.keys(bodyAttributes) : []; + if (familyAttributesMap.size === 0 && attrKeys.length > 0) { + const dbAttrs = await models.Attribute.findAll({ + where: { + [Op.or]: [ + { code: attrKeys }, + { id: attrKeys } + ] + }, + transaction + }); + for (const a of dbAttrs) { + familyAttributesMap.set(a.id, a); + } + } + + const uniqueFamilyAttributes = Array.from(familyAttributesMap.values()); + + for (const attr of uniqueFamilyAttributes) { + let val = bodyAttributes[attr.code] !== undefined ? bodyAttributes[attr.code] : bodyAttributes[attr.id]; if (val === undefined && bodyAttributes.metadata) { - val = bodyAttributes.metadata[attr.code]; + val = bodyAttributes.metadata[attr.code] !== undefined ? bodyAttributes.metadata[attr.code] : bodyAttributes.metadata[attr.id]; } - await validateAttributeValue(attr, val, id, transaction); - if (val !== undefined && val !== null) { + await validateAttributeValue(attr, val, id, transaction); const [attrVal, created] = await models.ProductAttributeValue.findOrCreate({ where: { product_id: id, attribute_id: attr.id, locale: 'en', channel: 'default' }, - defaults: { value: String(val) }, + defaults: { value: typeof val === 'object' ? JSON.stringify(val) : String(val) }, transaction }); if (!created) { - await attrVal.update({ value: String(val) }, { transaction }); + await attrVal.update({ value: typeof val === 'object' ? JSON.stringify(val) : String(val) }, { transaction }); } } } diff --git a/src/features/variants/variants/variant.service.js b/src/features/variants/variants/variant.service.js index 9a9e2b5..6ee8162 100644 --- a/src/features/variants/variants/variant.service.js +++ b/src/features/variants/variants/variant.service.js @@ -113,9 +113,6 @@ export class VariantService { if (!parentProduct) { throw new Error('Parent product not found'); } - if (parentProduct.status !== 'active') { - throw new Error(`Parent product is not active (current status: "${parentProduct.status}")`); - } const family = parentProduct.family; if (!family) { diff --git a/src/features/workflows/workflow.service.js b/src/features/workflows/workflow.service.js index 68eba55..0f312a6 100644 --- a/src/features/workflows/workflow.service.js +++ b/src/features/workflows/workflow.service.js @@ -11,42 +11,64 @@ export class WorkflowService { } const options = { where, paranoid: false }; const records = await repository.findAll(options, context); - + + if (!records || records.length === 0) return []; + + // Batch query all catalogs to avoid N+1 queries + const allCatalogs = models.Catalog ? await models.Catalog.findAll({ + attributes: ['id', 'name', 'code', 'status', 'workflow_code'], + raw: true + }).catch(() => []) : []; + + // Group catalogs by workflow_code + const catalogsByWfCode = {}; + for (const cat of allCatalogs) { + const wfCode = cat.workflow_code || 'standard'; + if (!catalogsByWfCode[wfCode]) { + catalogsByWfCode[wfCode] = []; + } + catalogsByWfCode[wfCode].push(cat); + } + + // Batch query product counts per family + const productCountsByFamilyId = {}; + if (allCatalogs.length > 0 && models.Product) { + try { + const pCounts = await models.Product.findAll({ + attributes: ['family_id', [models.Product.sequelize.fn('COUNT', models.Product.sequelize.col('id')), 'count']], + group: ['family_id'], + raw: true + }); + for (const pc of pCounts) { + productCountsByFamilyId[pc.family_id] = parseInt(pc.count, 10) || 0; + } + } catch (err) { + console.error('Error batch counting products for workflow:', err); + } + } + const results = []; for (const record of records) { const json = record.toJSON ? record.toJSON() : { ...record }; - - const families = await models.Catalog.findAll({ - where: { workflow_code: json.code }, - attributes: ['id', 'name', 'code', 'status'] - }); + const families = catalogsByWfCode[json.code] || []; json.usedBy = families.map(f => f.name); json.familyCount = families.length; - const familyIds = families.map(f => f.id); - let productCount = 0; - if (familyIds.length > 0) { - productCount = await models.Product.count({ - where: { family_id: familyIds } - }); - } - json.productCount = productCount; - - const familiesDetails = []; - for (const fam of families) { - const famProdCount = await models.Product.count({ - where: { family_id: fam.id } - }); - familiesDetails.push({ + let totalProdCount = 0; + const familiesDetails = families.map(fam => { + const pCount = productCountsByFamilyId[fam.id] || 0; + totalProdCount += pCount; + return { name: fam.name, code: fam.code, status: fam.status, - productCount: famProdCount - }); - } + productCount: pCount + }; + }); + + json.productCount = totalProdCount; json.familiesDetails = familiesDetails; - results.push(json); } return results; diff --git a/src/inspect.js b/src/inspect.js new file mode 100644 index 0000000..0bfca31 --- /dev/null +++ b/src/inspect.js @@ -0,0 +1,16 @@ +import { models, sequelize, initializeDatabaseModels } from './shared/database/models.js'; + +async function test() { + try { + initializeDatabaseModels(); + const attributes = await models.Attribute.findAll(); + console.log('ATTRIBUTES IN DB:'); + console.log(JSON.stringify(attributes.map(a => ({ id: a.id, code: a.code, name: a.name, type: a.type })), null, 2)); + } catch (err) { + console.error(err); + } finally { + await sequelize.close(); + } +} + +test(); diff --git a/src/print_product.js b/src/print_product.js new file mode 100644 index 0000000..b95034c --- /dev/null +++ b/src/print_product.js @@ -0,0 +1,28 @@ +import { models, sequelize, initializeDatabaseModels } from './shared/database/models.js'; + +async function test() { + try { + initializeDatabaseModels(); + + const products = await models.Product.findAll({ + include: [ + { + model: models.ProductAttributeValue, + as: 'attributeValues' + } + ] + }); + + console.log('PRODUCTS FOUND:', products.length); + if (products.length > 0) { + console.log('First product JSON:', JSON.stringify(products[0].toJSON(), null, 2)); + } + + } catch (err) { + console.error(err); + } finally { + await sequelize.close(); + } +} + +test(); diff --git a/src/shared/middleware/error.middleware.js b/src/shared/middleware/error.middleware.js index 6c9788a..396237a 100644 --- a/src/shared/middleware/error.middleware.js +++ b/src/shared/middleware/error.middleware.js @@ -5,9 +5,14 @@ export const errorMiddleware = (err, req, res, next) => { const status = err.statusCode || err.status || 500; const message = err.message || 'Internal Server Error'; + // Extract detailed error info for database and validation errors + const dbDetail = err.original ? `${err.original.message} - ${err.original.detail || ''}` : ''; + const validationDetail = err.errors ? JSON.stringify(err.errors) : ''; + const fullLogMessage = `${message} ${dbDetail ? `| DB: ${dbDetail}` : ''} ${validationDetail ? `| Validation: ${validationDetail}` : ''}`; + // Log the error using winston logger.error({ - message: err.message, + message: fullLogMessage, stack: err.stack, status, path: req.originalUrl, @@ -18,7 +23,7 @@ export const errorMiddleware = (err, req, res, next) => { res.status(status).json({ success: false, status, - message, + message: fullLogMessage, stack: process.env.NODE_ENV === 'development' ? err.stack : undefined }); }; diff --git a/src/shared/utils/logger.js b/src/shared/utils/logger.js index dfb0bf8..1bac768 100644 --- a/src/shared/utils/logger.js +++ b/src/shared/utils/logger.js @@ -20,6 +20,14 @@ export const logger = winston.createLogger({ timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), logFormat ), + }), + new winston.transports.File({ + filename: 'error.log', + level: 'error', + format: combine( + timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + logFormat + ) }) ], }); diff --git a/test_create_catalog_direct.js b/test_create_catalog_direct.js new file mode 100644 index 0000000..d3e3c9c --- /dev/null +++ b/test_create_catalog_direct.js @@ -0,0 +1,44 @@ +import { initializeDatabaseModels, sequelize } from './src/shared/database/models.js'; +import { CatalogService } from './src/features/catalogs/catalogs/catalog.service.js'; + +async function run() { + try { + initializeDatabaseModels(); + + const service = new CatalogService(); + + const payload = { + name: `Furniture`, + code: `furniture`, // Already exists (soft-deleted) + description: 'test description', + status: 'draft', + category: 'd94f6141-cc9a-449c-99ec-057c7c410848', + attributeSetId: 'd6df7c5e-6e4a-4e1b-b5d5-a67c3ee4b197', + attributes: [], + variantAxes: [], + allowedBrands: [], + allowedUnits: [], + channels: [], + assetRequirements: [], + completenessRules: { + required_attributes: 40, + at_least_one_image: 20, + marketing_content: 15, + tech_specs: 15, + skus_assigned: 10 + } + }; + + console.log('Attempting to create catalog with duplicate code "furniture"...'); + await service.create(payload, { userId: 2, tenantId: null }); + console.log('Success (unexpected)!'); + } catch (err) { + console.log('EXPECTED ERROR CAUGHT:'); + console.log('Status Code:', err.statusCode); + console.log('Message:', err.message); + } finally { + await sequelize.close(); + } +} + +run(); diff --git a/test_endpoints.js b/test_endpoints.js new file mode 100644 index 0000000..1397dba --- /dev/null +++ b/test_endpoints.js @@ -0,0 +1,66 @@ +import http from 'http'; + +function request(path, token) { + return new Promise((resolve) => { + const start = Date.now(); + const req = http.request({ + hostname: 'localhost', + port: 5000, + path, + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}` + } + }, (res) => { + let body = ''; + res.on('data', chunk => body += chunk); + res.on('end', () => { + resolve({ path, status: res.statusCode, time: Date.now() - start, len: body.length }); + }); + }); + req.on('error', (err) => resolve({ path, error: err.message, time: Date.now() - start })); + req.setTimeout(5000, () => { + req.destroy(); + resolve({ path, error: 'TIMEOUT (5s)', time: Date.now() - start }); + }); + req.end(); + }); +} + +async function run() { + const loginData = JSON.stringify({ email: 'superadmin@maskan.com', password: 'Admin@123' }); + const loginRes = await new Promise(resolve => { + const req = http.request({ + hostname: 'localhost', port: 5000, path: '/api/v1/auth/login', method: 'POST', + headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(loginData) } + }, res => { + let body = ''; + res.on('data', c => body += c); + res.on('end', () => resolve(JSON.parse(body))); + }); + req.write(loginData); + req.end(); + }); + console.log('Login response:', JSON.stringify(loginRes)); + const token = loginRes.data?.token || loginRes.token || loginRes.accessToken || loginRes.data?.accessToken; + console.log('Token:', token ? 'Acquired' : 'NOT FOUND'); + + const endpoints = [ + '/api/v1/families', + '/api/v1/attributes', + '/api/v1/channels', + '/api/v1/asset-families', + '/api/v1/workflows', + '/api/v1/attribute-sets', + '/api/v1/brands', + '/api/v1/units', + '/api/v1/categories' + ]; + + for (const ep of endpoints) { + const res = await request(ep, token); + console.log(`Endpoint: ${ep.padEnd(25)} Status: ${res.status || res.error} Time: ${res.time}ms`); + } +} + +run(); diff --git a/test_family.js b/test_family.js new file mode 100644 index 0000000..49d381c --- /dev/null +++ b/test_family.js @@ -0,0 +1,82 @@ +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 + 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); + + const loginJson = JSON.parse(loginRes.body); + const token = loginJson.data?.token || loginJson.token || loginJson.data?.accessToken; + + // 2. Try to create family with valid relations + console.log('\nCreating family with valid relations...'); + const familyData = JSON.stringify({ + name: `Relation Test Family ${Date.now()}`, + code: `rel_test_${Date.now()}`, + description: 'test description', + status: 'draft', + category: 'd94f6141-cc9a-449c-99ec-057c7c410848', + attributes: ['af2e5e61-0b73-42d0-bcde-aa9b9fd5c03a'], + variantAxes: ['af2e5e61-0b73-42d0-bcde-aa9b9fd5c03a'], + channels: ['amazon'], + assetRequirements: ['dc18511e-a47b-4b03-8b01-d05adc9cd2a9'], + allowedBrands: ['4d19ccdb-8399-4d7e-ae40-1cee4d5865f3'], + allowedUnits: ['00bbc825-5cec-4d10-8986-64d97368b0e2'] + }); + + const createRes = await request({ + hostname: 'localhost', + port: 5000, + path: '/api/v1/families', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(familyData), + 'Authorization': `Bearer ${token}` + } + }, familyData); + + console.log('Response status:', createRes.statusCode); + console.log('Response body:', createRes.body); + + } catch (error) { + console.error('Error running test:', error); + } +} + +run(); diff --git a/uploads/file-1784526116853-33144068.png b/uploads/file-1784526116853-33144068.png deleted file mode 100644 index e89e012..0000000 Binary files a/uploads/file-1784526116853-33144068.png and /dev/null differ