merge: pull latest changes from origin/mahir_backend and resolve code generation conflicts

This commit is contained in:
Inamul-hasan-tec
2026-08-11 18:10:44 +05:30
20 changed files with 126 additions and 143 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
PORT=5002
NODE_ENV=local
CORS_ORIGIN=http://localhost:5173
CORS_ORIGIN=http://localhost:5173,http://localhost:5174
JWT_SECRET=supersecretjwtkeythatislongandsecure
JWT_REFRESH_SECRET=supersecretrefreshjwtkeythatislongandsecure
JWT_EXPIRES_IN=1d
+1 -1
View File
@@ -17,7 +17,7 @@ app.use(helmet({
crossOriginResourcePolicy: { policy: 'cross-origin' },
}));
app.use(cors({
origin: process.env.CORS_ORIGIN || '*',
origin: process.env.CORS_ORIGIN ? (process.env.CORS_ORIGIN.includes(',') ? process.env.CORS_ORIGIN.split(',') : process.env.CORS_ORIGIN) : '*',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization', 'Accept', 'X-Requested-With']
}));
+3
View File
@@ -270,6 +270,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -1219,6 +1220,7 @@
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
"integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@@ -2425,6 +2427,7 @@
"resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz",
"integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==",
"license": "MIT",
"peer": true,
"dependencies": {
"pg-connection-string": "^2.14.0",
"pg-pool": "^3.14.0",
@@ -24,8 +24,8 @@ export class AttributeGroupService {
async create(data, context = {}) {
const transaction = await sequelize.transaction();
try {
data.code = await generateUniqueCode(models.AttributeGroup, data.code || data.name, 'code', transaction);
const baseCode = data.code || data.name || 'group';
data.code = await generateUniqueCode(models.AttributeGroup, baseCode, 'code', transaction);
const record = await models.AttributeGroup.create(data, { transaction });
@@ -24,8 +24,8 @@ export class AttributeSetService {
async create(data, context = {}) {
const transaction = await sequelize.transaction();
try {
data.code = await generateUniqueCode(models.AttributeSet, data.code || data.name, 'code', transaction);
const baseCode = data.code || data.name || 'set';
data.code = await generateUniqueCode(models.AttributeSet, baseCode, 'code', transaction);
const record = await models.AttributeSet.create(data, { transaction });
@@ -5,6 +5,7 @@ import { AuditService } from '../../../shared/services/audit.service.js';
import { ApiError } from '../../../utils/helpers/ApiError.utils.js';
import { generateUniqueCode } from '../../../utils/helpers/code.utils.js';
import { Op } from 'sequelize';
import { generateUniqueCode } from '../../../utils/helpers/code.utils.js';
export class AttributeService {
@@ -133,9 +134,8 @@ export class AttributeService {
async create(data, context = {}) {
const transaction = await sequelize.transaction();
try {
data.code = await generateUniqueCode(models.Attribute, data.code || data.name, 'code', transaction, { paranoid: false });
// Automatically assign display order if not provided
const baseCode = data.code || data.name || 'attribute';
data.code = await generateUniqueCode(models.Attribute, baseCode, 'code', transaction, { paranoid: false });
if (data.display_order === undefined || data.display_order === null) {
const maxOrder = await models.Attribute.max('display_order', { transaction }) || 0;
data.display_order = maxOrder + 1;
@@ -295,44 +295,44 @@ export class AttributeService {
}
// Usage Check: Groups mapping (count only active groups)
const groupCount = await models.AttributeGroup.count({
const groupCount = await models.AttributeGroup.count({
include: [{
model: models.Attribute,
as: 'attributes',
where: { id: id },
required: true
}],
transaction
transaction
});
// Usage Check: Product Families mapping (count only active catalogs)
const familyCount = await models.Catalog.count({
const familyCount = await models.Catalog.count({
include: [{
model: models.Attribute,
as: 'attributes',
where: { id: id },
required: true
}],
transaction
transaction
});
// Usage Check: Variant axes mapping (count only active catalogs)
const axisCount = await models.Catalog.count({
const axisCount = await models.Catalog.count({
include: [{
model: models.Attribute,
as: 'variantAxes',
where: { id: id },
required: true
}],
transaction
transaction
});
// Usage Check: Variant Values
let valCount = 0;
if (models.VariantValue) {
valCount = await models.VariantValue.count({
valCount = await models.VariantValue.count({
where: { axis_id: id },
transaction
transaction
});
}
@@ -354,7 +354,7 @@ export class AttributeService {
const oldValues = record.toJSON();
// Change status to Archived, save updated_by and deleted_by context
await record.update({
await record.update({
status: 'archived',
updated_by: context.userId || null,
deleted_by: context.userId || null
@@ -396,19 +396,19 @@ export class AttributeService {
async restore(id, context = {}) {
const transaction = await sequelize.transaction();
try {
const record = await models.Attribute.findByPk(id, {
const record = await models.Attribute.findByPk(id, {
paranoid: false,
transaction
transaction
});
if (!record) {
throw new Error('Attribute not found');
}
await record.restore({ transaction });
await record.update({
status: 'active',
await record.update({
status: 'active',
deleted_by: null,
updated_by: context.userId || null
updated_by: context.userId || null
}, { transaction });
await transaction.commit();
+2 -2
View File
@@ -24,8 +24,8 @@ export class BrandService {
}
async create(data, context = {}) {
data.code = await generateUniqueCode(models.Brand, data.code || data.name);
const baseCode = data.code || data.name || 'brand';
data.code = await generateUniqueCode(models.Brand, baseCode, 'code');
const record = await repository.create(data, {}, context);
+2 -2
View File
@@ -22,8 +22,8 @@ export class UnitService {
}
async create(data, context = {}) {
data.code = await generateUniqueCode(models.Unit, data.code || data.name);
const baseCode = data.code || data.name || 'unit';
data.code = await generateUniqueCode(models.Unit, baseCode, 'code');
const record = await repository.create(data, {}, context);
+1 -2
View File
@@ -2,10 +2,9 @@ import { body, param } from 'express-validator';
export const createValidation = [
body('code')
.optional({ checkFalsy: true })
.isString()
.trim()
.notEmpty()
.withMessage('Code is required')
.matches(/^[a-z0-9_]+$/)
.withMessage('Code must be lowercase alphanumeric and underscores only'),
body('name')
@@ -4,7 +4,10 @@ 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 { generateUniqueCode } from '../../../utils/helpers/code.utils.js';
<<<<<<< HEAD
=======
>>>>>>> origin/mahir_backend
export class CatalogService {
async attachCounts(record, transaction) {
@@ -125,10 +128,8 @@ export class CatalogService {
async create(data, context = {}) {
const transaction = await sequelize.transaction();
try {
// 1. Autogenerate unique code
data.code = await generateUniqueCode(models.ProductFamily, data.code || data.name, 'code', transaction, { paranoid: false });
// Resolve attributes from attribute set if provided
const baseCode = data.code || data.name || 'family';
data.code = await generateUniqueCode(models.ProductFamily, baseCode, 'code', transaction, { paranoid: false });
const attributeSetId = data.attributeSetId || data.attribute_set_id || null;
if (attributeSetId) {
const attributeSet = await models.AttributeSet.findByPk(attributeSetId, {
@@ -6,6 +6,7 @@ import NotificationService from '../../notifications/notifications/notification.
import { ApiError } from '../../../utils/helpers/ApiError.utils.js';
import { generateUniqueCode } from '../../../utils/helpers/code.utils.js';
import { Op } from 'sequelize';
import { generateUniqueCode } from '../../../utils/helpers/code.utils.js';
export class CategorieService {
async getAll(query = {}, context = {}) {
@@ -19,7 +20,7 @@ export class CategorieService {
async getById(id, context = {}) {
const record = await repository.findById(id, {}, context);
if (!record) {
throw new ApiError(404, 404, 'Category not found');
throw new ApiError(404, 'Category not found');
}
return record;
}
@@ -27,7 +28,8 @@ export class CategorieService {
async create(data, context = {}) {
const transaction = await sequelize.transaction();
try {
data.code = await generateUniqueCode(models.Categorie, data.code || data.name, 'code', transaction);
const baseCode = data.code || data.name || 'category';
data.code = await generateUniqueCode(models.Categorie, baseCode, 'code', transaction);
let level = 0;
@@ -2,7 +2,10 @@ import repository from './channelType.repository.js';
import { models } from '../../../shared/database/models.js';
import { SocketService } from '../../../shared/services/socket.service.js';
import { AuditService } from '../../../shared/services/audit.service.js';
<<<<<<< HEAD
import { ApiError } from '../../../utils/helpers/ApiError.utils.js';
=======
>>>>>>> origin/mahir_backend
import { generateUniqueCode } from '../../../utils/helpers/code.utils.js';
export class ChannelTypeService {
@@ -23,7 +26,8 @@ export class ChannelTypeService {
}
async create(data, userContext = {}) {
data.code = await generateUniqueCode(models.ChannelType, data.code || data.name);
const baseCode = data.code || data.name || 'channel_type';
data.code = await generateUniqueCode(models.ChannelType, baseCode, 'code');
const record = await repository.create(data);
@@ -3,6 +3,7 @@ import { models } 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 { generateUniqueCode } from '../../../utils/helpers/code.utils.js';
export class ChannelService {
async getAll(query = {}) {
@@ -19,6 +20,9 @@ export class ChannelService {
}
async create(data, userContext = {}) {
const baseCode = data.code || data.name || 'channel';
data.code = await generateUniqueCode(models.Channel, baseCode, 'code');
const record = await repository.create(data);
// Broadcast event
@@ -3,6 +3,7 @@ import { models } 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 { generateUniqueCode } from '../../../utils/helpers/code.utils.js';
export class AssetFamilyService {
encodeDescription(text = '', assetTypeIds = []) {
@@ -100,11 +101,8 @@ export class AssetFamilyService {
async create(data, userContext = {}) {
const { assetTypes, assetTypeIds, description, ...familyData } = data;
if (!familyData.code || !familyData.code.trim()) {
familyData.code = familyData.name
? familyData.name.toLowerCase().replace(/[^a-z0-9]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '')
: `family_${Date.now()}`;
}
const baseCode = familyData.code || familyData.name || 'asset_family';
familyData.code = await generateUniqueCode(models.AssetFamily, baseCode, 'code');
const inputTypeIds = (assetTypes || assetTypeIds || []).map(item => typeof item === 'object' ? (item.assetTypeId || item.id) : item).filter(Boolean);
familyData.description = this.encodeDescription(description, inputTypeIds);
@@ -3,6 +3,7 @@ import { models } 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 { generateUniqueCode } from '../../../utils/helpers/code.utils.js';
export class AssetTypeService {
async getAll(query = {}) {
@@ -22,27 +23,8 @@ export class AssetTypeService {
}
async create(data, userContext = {}) {
let rawCode = data.code ? data.code.trim() : '';
let isAutoGenerated = !rawCode;
if (!rawCode) {
rawCode = data.name ? data.name.toLowerCase().replace(/[^a-z0-9]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '') : `ast_type_${Date.now()}`;
isAutoGenerated = true;
}
let finalCode = rawCode;
let existing = await models.AssetType.findOne({ where: { code: finalCode } });
if (existing) {
if (isAutoGenerated) {
finalCode = `${rawCode}_${Date.now().toString().slice(-4)}`;
} else {
const err = new Error(`Asset Type code "${rawCode}" is already in use. Please enter a different code.`);
err.statusCode = 400;
throw err;
}
}
data.code = finalCode;
const baseCode = data.code || data.name || 'asset_type';
data.code = await generateUniqueCode(models.AssetType, baseCode, 'code');
const payload = {
name: data.name,
@@ -7,6 +7,7 @@ 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;
@@ -15,7 +16,7 @@ export function formatProductResponse(json) {
if (json.metadata) {
const {
sku, price, stock, barcode, gtin, upc, ean, country, hsn, type, shortDesc, description, categories, attributes,
sku, price, stock, barcode, gtin, upc, ean, country, hsn, type, description, categories, attributes,
...restMetadata
} = json.metadata;
@@ -33,7 +34,6 @@ export function formatProductResponse(json) {
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 || {},
@@ -53,7 +53,6 @@ export function formatProductResponse(json) {
country: '',
hsn: '',
type: 'simple',
shortDesc: '',
description: '',
categories: json.category_id ? [json.category_id] : []
};
@@ -295,51 +294,43 @@ export class ProductService {
async create(data, context = {}) {
const transaction = await sequelize.transaction();
try {
// 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
// 2. Validate Family exists if provided
const familyId = data.family_id || data.familyId;
let family = null;
if (familyId) {
family = await models.Catalog.findByPk(familyId, {
transaction,
include: [
{
model: models.FamilyChannel,
as: 'channels'
},
{
model: models.AttributeSet,
as: 'attributeSet',
include: [
{
model: models.AttributeGroup,
as: 'groups',
include: [
{
model: models.Attribute,
as: 'attributes'
}
]
}
]
}
]
});
if (existing) {
throw new Error(`Product SKU / Code "${data.code || data.sku}" already exists`);
if (!family) {
throw new Error('Product Family (Catalog) must exist if specified');
}
}
// 2. Validate Family exists
const family = await models.Catalog.findByPk(data.family_id || data.familyId, {
transaction,
include: [
{
model: models.FamilyChannel,
as: 'channels'
},
{
model: models.AttributeSet,
as: 'attributeSet',
include: [
{
model: models.AttributeGroup,
as: 'groups',
include: [
{
model: models.Attribute,
as: 'attributes'
}
]
}
]
}
]
});
if (!family) {
throw new Error('Product Family (Catalog) is required and must exist');
}
// Validate Brand is allowed by Family
const brandId = data.brand_id || data.brandId || data.brand;
if (brandId) {
if (brandId && family) {
const allowedBrands = family.completeness_rules?.allowedBrands || [];
if (allowedBrands.length > 0 && !allowedBrands.includes(brandId)) {
throw new Error('Selected brand is not allowed for this product family');
@@ -348,7 +339,7 @@ export class ProductService {
// Validate Unit is allowed by Family
const unitId = data.unit_id || data.unitId || data.unit;
if (unitId) {
if (unitId && family) {
const allowedUnits = family.completeness_rules?.allowedUnits || [];
if (allowedUnits.length > 0 && !allowedUnits.includes(unitId)) {
throw new Error('Selected unit is not allowed for this product family');
@@ -394,27 +385,38 @@ export class ProductService {
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';
let familyPrefix = 'PRD';
if (family) {
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');
let runningSeq = '00001';
if (family) {
const familyProductCount = await models.Product.count({
where: { family_id: family.id },
transaction
});
runningSeq = String(familyProductCount + 1).padStart(5, '0');
} else {
const totalProductCount = await models.Product.count({
transaction
});
runningSeq = String(totalProductCount + 1).padStart(5, '0');
}
const generatedSku = `${familyPrefix}-${finalCode}-${runningSeq}`;
// Setup default inherited channels and workflows
const metadata = data.metadata || {};
if (!metadata.channels) {
metadata.channels = (family.channels || []).map(c => c.channel_code);
metadata.channels = family ? (family.channels || []).map(c => c.channel_code) : [];
}
// Fetch workflow details dynamically
const workflowCode = family.workflow_code || 'standard';
const workflowCode = family ? (family.workflow_code || 'standard') : 'standard';
let workflowName = 'Standard Approval';
let currentStage = 'draft';
@@ -446,7 +448,6 @@ export class ProductService {
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] : []);
@@ -455,7 +456,7 @@ export class ProductService {
code: data.code,
name: data.name,
status: data.status || 'draft',
family_id: family.id,
family_id: family ? family.id : null,
category_id: categoryId,
brand_id: data.brand_id || data.brandId || data.brand,
unit_id: data.unit_id || data.unitId || data.unit,
@@ -468,7 +469,7 @@ export class ProductService {
await product.update({ metadata }, { transaction });
const familyAttributesMap = new Map();
if (family.attributeSet && Array.isArray(family.attributeSet.groups)) {
if (family && family.attributeSet && Array.isArray(family.attributeSet.groups)) {
for (const g of family.attributeSet.groups) {
if (Array.isArray(g.attributes)) {
for (const a of g.attributes) {
@@ -654,7 +655,6 @@ export class ProductService {
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 : [];
@@ -8,20 +8,22 @@ export const createValidation = [
.trim()
.withMessage('Name must be a string'),
body('family_id')
.notEmpty()
.withMessage('Product Family (family_id) is required')
.optional({ checkFalsy: true })
.isUUID()
.withMessage('Product Family (family_id) must be a valid UUID'),
body('category_id')
.optional({ checkFalsy: true })
.notEmpty()
.withMessage('Category (category_id) is required')
.isUUID()
.withMessage('Category must be a valid UUID'),
body('brand_id')
.optional({ checkFalsy: true })
.notEmpty()
.withMessage('Brand (brand_id) is required')
.isUUID()
.withMessage('Brand must be a valid UUID'),
body('unit_id')
.optional({ checkFalsy: true })
.notEmpty()
.withMessage('Unit (unit_id) is required')
.isUUID()
.withMessage('Unit must be a valid UUID'),
body('code')
+3 -15
View File
@@ -2,6 +2,7 @@ import repository from './workflow.repository.js';
import { models } from '../../shared/database/models.js';
import { SocketService } from '../../shared/services/socket.service.js';
import { AuditService } from '../../shared/services/audit.service.js';
import { generateUniqueCode } from '../../utils/helpers/code.utils.js';
export class WorkflowService {
async getAll(query = {}, context = {}) {
@@ -116,21 +117,8 @@ export class WorkflowService {
}
async create(data, context = {}) {
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 = `wfk_${Date.now()}`;
}
}
data.code = data.code.toLowerCase().trim();
// Check duplicate code
const existing = await models.WorkflowRegistry.findOne({ where: { code: data.code } });
if (existing) {
throw new Error(`Workflow with code "${data.code}" already exists`);
}
const baseCode = data.code || data.name || 'workflow';
data.code = await generateUniqueCode(models.WorkflowRegistry, baseCode, 'code');
const record = await repository.create({
code: data.code,
+1 -1
View File
@@ -9,7 +9,7 @@ class SocketServiceClass {
init(server) {
this.io = new Server(server, {
cors: {
origin: process.env.CORS_ORIGIN || '*',
origin: process.env.CORS_ORIGIN ? (process.env.CORS_ORIGIN.includes(',') ? process.env.CORS_ORIGIN.split(',') : process.env.CORS_ORIGIN) : '*',
methods: ['GET', 'POST']
}
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB