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:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user