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, };