fix(masters): update applyTenantScope to include global tenant_id null master records, reject duplicate code conflicts with HTTP 409, and preserve master data across session cycles

This commit is contained in:
Inamul-hasan-tec
2026-08-12 16:42:51 +05:30
parent 16ca868c5c
commit a36880c27b
3 changed files with 96 additions and 21 deletions
+41 -8
View File
@@ -4,7 +4,7 @@ import { SocketService } from '../../../shared/services/socket.service.js';
import { AuditService } from '../../../shared/services/audit.service.js';
import NotificationService from '../../notifications/notifications/notification.service.js';
import { ApiError } from '../../../utils/helpers/ApiError.utils.js';
import { generateUniqueCode } from '../../../utils/helpers/code.utils.js';
import { Op } from 'sequelize';
export class BrandService {
async getAll(query = {}, context = {}) {
@@ -24,8 +24,28 @@ export class BrandService {
}
async create(data, context = {}) {
const baseCode = data.code || data.name || 'brand';
data.code = await generateUniqueCode(models.Brand, baseCode, 'code');
const rawCode = data.code || data.name || 'brand';
const code = rawCode.toLowerCase().trim().replace(/[^a-z0-9_]/g, '_');
const tenantId = (context.userType !== 'platform' && context.tenantId) ? context.tenantId : (data.tenant_id || null);
// Reject duplicate codes within tenant or global scope
const existing = await models.Brand.findOne({
where: {
code,
[Op.or]: [
{ tenant_id: tenantId },
{ tenant_id: null }
]
}
});
if (existing) {
throw new ApiError(409, `Brand with code "${code}" already exists in this workspace`);
}
data.code = code;
data.tenant_id = tenantId;
const record = await repository.create(data, {}, context);
@@ -63,10 +83,23 @@ export class BrandService {
}
if (data.code && data.code !== record.code) {
const [existing] = await repository.findAll({ where: { code: data.code } }, context);
const code = data.code.toLowerCase().trim().replace(/[^a-z0-9_]/g, '_');
const tenantId = context.tenantId || record.tenant_id || null;
const existing = await models.Brand.findOne({
where: {
code,
id: { [Op.ne]: id },
[Op.or]: [
{ tenant_id: tenantId },
{ tenant_id: null }
]
}
});
if (existing) {
throw new Error(`Brand with code "${data.code}" already exists`);
throw new ApiError(409, `Brand with code "${code}" already exists in this workspace`);
}
data.code = code;
}
const updatedRecord = await repository.update(id, data, {}, context);
@@ -105,7 +138,7 @@ export class BrandService {
// Check product linkage
const productCount = await models.Product.count({ where: { brand_id: id } });
if (productCount > 0) {
throw new Error('Cannot delete Brand because it is used by one or more products');
throw new ApiError(400, 'Cannot delete Brand because it is used by one or more products');
}
// Hard delete
@@ -138,7 +171,7 @@ export class BrandService {
async archive(id, context = {}) {
const record = await repository.findById(id, {}, context);
if (!record) {
throw new Error('Brand not found');
throw new ApiError(404, 'Brand not found');
}
// Soft delete / Archive
@@ -171,7 +204,7 @@ export class BrandService {
async restore(id, context = {}) {
const record = await repository.restore(id, {}, context);
if (!record) {
throw new Error('Brand not found');
throw new ApiError(404, 'Brand not found');
}
SocketService.broadcast('brand:restored', record);
+45 -11
View File
@@ -2,7 +2,8 @@ import repository from './unit.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';
import { ApiError } from '../../../utils/helpers/ApiError.utils.js';
import { Op } from 'sequelize';
export class UnitService {
async getAll(query = {}, context = {}) {
@@ -16,14 +17,34 @@ export class UnitService {
async getById(id, context = {}) {
const record = await repository.findById(id, {}, context);
if (!record) {
throw new Error('Unit not found');
throw new ApiError(404, 'Unit not found');
}
return record;
}
async create(data, context = {}) {
const baseCode = data.code || data.name || 'unit';
data.code = await generateUniqueCode(models.Unit, baseCode, 'code');
const rawCode = data.code || data.name || 'unit';
const code = rawCode.toLowerCase().trim().replace(/[^a-z0-9_]/g, '_');
const tenantId = (context.userType !== 'platform' && context.tenantId) ? context.tenantId : (data.tenant_id || null);
// Reject duplicate codes within tenant or global scope
const existing = await models.Unit.findOne({
where: {
code,
[Op.or]: [
{ tenant_id: tenantId },
{ tenant_id: null }
]
}
});
if (existing) {
throw new ApiError(409, `Unit with code "${code}" already exists in this workspace`);
}
data.code = code;
data.tenant_id = tenantId;
const record = await repository.create(data, {}, context);
@@ -43,14 +64,27 @@ export class UnitService {
async update(id, data, context = {}) {
const record = await repository.findById(id, {}, context);
if (!record) {
throw new Error('Unit not found');
throw new ApiError(404, 'Unit not found');
}
if (data.code && data.code !== record.code) {
const [existing] = await repository.findAll({ where: { code: data.code } }, context);
const code = data.code.toLowerCase().trim().replace(/[^a-z0-9_]/g, '_');
const tenantId = context.tenantId || record.tenant_id || null;
const existing = await models.Unit.findOne({
where: {
code,
id: { [Op.ne]: id },
[Op.or]: [
{ tenant_id: tenantId },
{ tenant_id: null }
]
}
});
if (existing) {
throw new Error(`Unit with code "${data.code}" already exists`);
throw new ApiError(409, `Unit with code "${code}" already exists in this workspace`);
}
data.code = code;
}
const updatedRecord = await repository.update(id, data, {}, context);
@@ -71,13 +105,13 @@ export class UnitService {
async delete(id, context = {}) {
const record = await repository.findById(id, {}, context);
if (!record) {
throw new Error('Unit not found');
throw new ApiError(404, 'Unit not found');
}
// Check product linkage
const productCount = await models.Product.count({ where: { unit_id: id } });
if (productCount > 0) {
throw new Error('Cannot delete Unit because it is used by one or more products');
throw new ApiError(400, 'Cannot delete Unit because it is used by one or more products');
}
// Hard delete
@@ -98,7 +132,7 @@ export class UnitService {
async archive(id, context = {}) {
const record = await repository.findById(id, {}, context);
if (!record) {
throw new Error('Unit not found');
throw new ApiError(404, 'Unit not found');
}
// Soft delete / Archive
@@ -119,7 +153,7 @@ export class UnitService {
async restore(id, context = {}) {
const record = await repository.restore(id, {}, context);
if (!record) {
throw new Error('Unit not found');
throw new ApiError(404, 'Unit not found');
}
SocketService.broadcast('unit:restored', record);
+10 -2
View File
@@ -1,19 +1,27 @@
import { Op } from 'sequelize';
export const formatResponse = (success, data, message = '') => {
return { success, data, message };
};
/**
* Applies tenant_id scoping to a Sequelize where clause if a tenant context exists.
* Platform users (userType === 'platform' or no tenantId) bypass tenant filtering.
* Non-platform users view both tenant-specific records (tenant_id = context.tenantId)
* and global baseline master records (tenant_id IS NULL).
* Platform users (userType === 'platform') bypass tenant filtering.
* @param {Object} [where={}] - Existing Sequelize where clause
* @param {Object} [context={}] - Request context containing tenantId and userType
* @returns {Object} Scoped where clause
*/
export const applyTenantScope = (where = {}, context = {}) => {
if (context && context.tenantId && (context.userType !== 'platform' || context.isImpersonating)) {
if (where.tenant_id !== undefined) return where;
return {
...where,
tenant_id: context.tenantId
[Op.or]: [
{ tenant_id: context.tenantId },
{ tenant_id: null }
]
};
}
return where;