erp_be/src/modules/masters/locations/locations.service.js

316 lines
10 KiB
JavaScript

const prisma = require('../../../config/prisma');
const ApiError = require('../../../utils/ApiError');
const auditLog = require('../../../utils/auditLog');
const { getPagination, isDropdownCall } = require('../../../utils/pagination');
const { parseId } = require('../../../utils/parseId');
const { rowsToCsv } = require('../../../utils/csv');
const {
LOCATION_TYPES,
toPlantResponse,
toWarehouseResponse,
toLocationResponse,
} = require('../../../utils/locations');
const { LOCATION_STATE_OPTIONS } = require('./locations.constants');
const TABLE_NAME = 'locations';
const normalizeString = (value) => {
if (value === null || value === undefined) return value;
return String(value).trim();
};
const buildPlantData = (payload) => ({
type: LOCATION_TYPES.PLANT,
code: payload.code ? String(payload.code).trim().toUpperCase() : undefined,
name: normalizeString(payload.name),
gstin: normalizeString(payload.gstin),
address: normalizeString(payload.address),
city: normalizeString(payload.city),
state: normalizeString(payload.state),
pincode: normalizeString(payload.pincode),
phone: normalizeString(payload.phone),
is_active: payload.is_active,
location: null,
});
const buildWarehouseData = (payload) => ({
type: LOCATION_TYPES.WAREHOUSE,
code: payload.code ? String(payload.code).trim().toUpperCase() : undefined,
name: normalizeString(payload.name),
location: normalizeString(payload.location),
is_active: payload.is_active,
gstin: null,
address: null,
city: null,
state: null,
pincode: null,
phone: null,
});
const assertUniqueCode = async (code, excludeId = null) => {
if (!code) return;
const existing = await prisma.locations.findFirst({
where: {
code,
deleted_at: null,
...(excludeId ? { id: { not: parseId(excludeId) } } : {}),
},
});
if (existing) throw new ApiError(409, 'Location code already exists');
};
const createLocation = async (type, payload, userId, requestId) => {
const data =
type === LOCATION_TYPES.PLANT ? buildPlantData(payload) : buildWarehouseData(payload);
if (!data.code || !data.name) throw new ApiError(422, 'code and name are required');
await assertUniqueCode(data.code);
data.created_by = userId ? BigInt(userId) : null;
data.updated_by = userId ? BigInt(userId) : null;
const created = await prisma.locations.create({ data });
const response =
type === LOCATION_TYPES.PLANT ? toPlantResponse(created) : toWarehouseResponse(created);
await auditLog({
tableName: TABLE_NAME,
recordId: created.id,
action: 'CREATE',
oldValue: null,
newValue: response,
userId,
requestId,
});
return response;
};
const buildLocationsWhere = (query, type = null) => {
const effectiveType = type || query.type || null;
const where = {
deleted_at: null,
...(effectiveType ? { type: effectiveType } : {}),
...(query.is_active !== undefined ? { is_active: query.is_active } : {}),
...(query.search
? {
OR: [
{ code: { contains: query.search, mode: 'insensitive' } },
{ name: { contains: query.search, mode: 'insensitive' } },
],
}
: {}),
};
return { where, effectiveType };
};
const listLocations = async (query, type = null) => {
const { where, effectiveType } = buildLocationsWhere(query, type);
const mapRow = (row) => {
if (effectiveType === LOCATION_TYPES.PLANT) return toPlantResponse(row);
if (effectiveType === LOCATION_TYPES.WAREHOUSE) return toWarehouseResponse(row);
return toLocationResponse(row);
};
if (isDropdownCall(query)) {
where.is_active = true;
const rows = await prisma.locations.findMany({
where,
orderBy: { created_at: 'desc' },
});
const data = rows.map(mapRow);
return { data, meta: { page: 1, limit: data.length, total: data.length, dropdown: true } };
}
const { page, limit, skip } = getPagination(query);
const [rows, total] = await Promise.all([
prisma.locations.findMany({
where,
orderBy: { created_at: 'desc' },
skip,
take: limit,
}),
prisma.locations.count({ where }),
]);
return { data: rows.map(mapRow), meta: { page, limit, total } };
};
const exportLocations = async (query, type = null) => {
const { where, effectiveType } = buildLocationsWhere(query, type);
const rows = await prisma.locations.findMany({
where,
orderBy: { created_at: 'desc' },
});
const mapRow = (row) => {
if (effectiveType === LOCATION_TYPES.PLANT) return toPlantResponse(row);
if (effectiveType === LOCATION_TYPES.WAREHOUSE) return toWarehouseResponse(row);
return toLocationResponse(row);
};
const columns =
effectiveType === LOCATION_TYPES.WAREHOUSE
? [
{ key: 'code', header: 'Code' },
{ key: 'name', header: 'Name' },
{ key: 'location', header: 'Location' },
{ key: 'is_active', header: 'Is Active' },
{ key: 'created_at', header: 'Created At', type: 'datetime' },
]
: effectiveType === LOCATION_TYPES.PLANT
? [
{ key: 'code', header: 'Code' },
{ key: 'name', header: 'Name' },
{ key: 'gstin', header: 'GSTIN' },
{ key: 'city', header: 'City' },
{ key: 'state', header: 'State' },
{ key: 'is_active', header: 'Is Active' },
{ key: 'created_at', header: 'Created At', type: 'datetime' },
]
: [
{ key: 'type', header: 'Type' },
{ key: 'code', header: 'Code' },
{ key: 'name', header: 'Name' },
{ key: 'is_active', header: 'Is Active' },
{ key: 'created_at', header: 'Created At', type: 'datetime' },
];
return rowsToCsv(columns, rows.map(mapRow));
};
const getLocationById = async (id, type = null) => {
const row = await prisma.locations.findFirst({
where: {
id: parseId(id),
deleted_at: null,
...(type ? { type } : {}),
},
});
if (!row) throw new ApiError(404, type ? `${type} not found` : 'Location not found');
if (type === LOCATION_TYPES.PLANT) return toPlantResponse(row);
if (type === LOCATION_TYPES.WAREHOUSE) return toWarehouseResponse(row);
return toLocationResponse(row);
};
const updateLocation = async (id, type, payload, userId, requestId) => {
const idBigInt = parseId(id);
const existing = await prisma.locations.findFirst({
where: { id: idBigInt, deleted_at: null, type },
});
if (!existing) throw new ApiError(404, `${type} not found`);
const data =
type === LOCATION_TYPES.PLANT ? buildPlantData(payload) : buildWarehouseData(payload);
Object.keys(data).forEach((key) => {
if (data[key] === undefined) delete data[key];
});
if (data.code) await assertUniqueCode(data.code, id);
data.updated_by = userId ? BigInt(userId) : null;
const updated = await prisma.locations.update({
where: { id: idBigInt },
data,
});
const oldValue =
type === LOCATION_TYPES.PLANT ? toPlantResponse(existing) : toWarehouseResponse(existing);
const newValue =
type === LOCATION_TYPES.PLANT ? toPlantResponse(updated) : toWarehouseResponse(updated);
await auditLog({
tableName: TABLE_NAME,
recordId: id,
action: 'UPDATE',
oldValue,
newValue,
userId,
requestId,
});
return newValue;
};
const deleteLocation = async (id, type, userId, requestId) => {
const idBigInt = parseId(id);
const existing = await prisma.locations.findFirst({
where: { id: idBigInt, deleted_at: null, type },
});
if (!existing) throw new ApiError(404, `${type} not found`);
await prisma.locations.update({
where: { id: idBigInt },
data: { deleted_at: new Date(), updated_by: userId ? BigInt(userId) : null },
});
const oldValue =
type === LOCATION_TYPES.PLANT ? toPlantResponse(existing) : toWarehouseResponse(existing);
await auditLog({
tableName: TABLE_NAME,
recordId: id,
action: 'DELETE',
oldValue,
newValue: { deleted_at: new Date() },
userId,
requestId,
});
};
const deleteLocationById = async (id, userId, requestId) => {
const existing = await prisma.locations.findFirst({
where: { id: parseId(id), deleted_at: null },
});
if (!existing) throw new ApiError(404, 'Location not found');
return deleteLocation(id, existing.type, userId, requestId);
};
const updateLocationById = async (id, payload, userId, requestId) => {
const existing = await prisma.locations.findFirst({
where: { id: parseId(id), deleted_at: null },
});
if (!existing) throw new ApiError(404, 'Location not found');
return updateLocation(id, existing.type, payload, userId, requestId);
};
module.exports = {
LOCATION_TYPES,
createPlant: (payload, userId, requestId) =>
createLocation(LOCATION_TYPES.PLANT, payload, userId, requestId),
listPlants: (query) => listLocations(query, LOCATION_TYPES.PLANT),
exportPlants: (query) => exportLocations(query, LOCATION_TYPES.PLANT),
getPlantById: (id) => getLocationById(id, LOCATION_TYPES.PLANT),
updatePlant: (id, payload, userId, requestId) =>
updateLocation(id, LOCATION_TYPES.PLANT, payload, userId, requestId),
deletePlant: (id, userId, requestId) =>
deleteLocation(id, LOCATION_TYPES.PLANT, userId, requestId),
createWarehouse: (payload, userId, requestId) =>
createLocation(LOCATION_TYPES.WAREHOUSE, payload, userId, requestId),
listWarehouses: (query) => listLocations(query, LOCATION_TYPES.WAREHOUSE),
exportWarehouses: (query) => exportLocations(query, LOCATION_TYPES.WAREHOUSE),
getWarehouseById: (id) => getLocationById(id, LOCATION_TYPES.WAREHOUSE),
updateWarehouse: (id, payload, userId, requestId) =>
updateLocation(id, LOCATION_TYPES.WAREHOUSE, payload, userId, requestId),
deleteWarehouse: (id, userId, requestId) =>
deleteLocation(id, LOCATION_TYPES.WAREHOUSE, userId, requestId),
createLocation: (payload, userId, requestId) => {
if (!payload.type) throw new ApiError(422, 'type is required');
if (![LOCATION_TYPES.PLANT, LOCATION_TYPES.WAREHOUSE].includes(payload.type)) {
throw new ApiError(422, 'type must be plant or warehouse');
}
return createLocation(payload.type, payload, userId, requestId);
},
listLocations: (query) => listLocations(query),
exportLocations: (query) => exportLocations(query),
getLocationById: (id) => getLocationById(id),
updateLocationById,
deleteLocationById,
listStateOptions: () => LOCATION_STATE_OPTIONS,
};