From 8de3afedb3df6db57b64763ba9fc0a47690fca41 Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Mon, 20 Jul 2026 11:55:39 +0530 Subject: [PATCH] GWM : locatin api parent id removed --- BACKEND_TASKS.md | 4 +- prisma/schema.prisma | 4 -- scripts/patch-locations-drop-parent-id.sql | 5 +++ src/docs/completed-routes.yaml | 10 +---- .../masters/locations/locations.service.js | 39 +------------------ .../masters/locations/locations.validation.js | 11 ------ src/utils/locations.js | 12 +----- 7 files changed, 12 insertions(+), 73 deletions(-) create mode 100644 scripts/patch-locations-drop-parent-id.sql diff --git a/BACKEND_TASKS.md b/BACKEND_TASKS.md index a8f0ed7..badf481 100644 --- a/BACKEND_TASKS.md +++ b/BACKEND_TASKS.md @@ -182,9 +182,11 @@ Each sub-master supports: `GET /` (list), `GET /:id`, `POST /`, `PUT /:id`, `DEL | [~] | Asset Subcategories | removed — use Item Subcategories | | [x] | Departments | `/masters/departments` | | [x] | Designations | `/masters/designations` | -| [x] | Locations | `/masters/locations` | Unified plants + warehouses (`type`: `plant` \| `warehouse`) | +| [x] | Locations | `/masters/locations` | Unified plants + warehouses (`type`: `plant` \| `warehouse`); no plant↔warehouse hierarchy | | [x] | Plants | `/masters/plants` | Alias — plant locations only | | [x] | Warehouses | `/masters/warehouses` | Alias — warehouse locations only | + +**DB patch:** `scripts/patch-locations-drop-parent-id.sql` — drops `locations.parent_id` (flat locations). | [x] | Document Series | `/masters/document-series` | **Masters total:** 13 modules × 5 endpoints (+ plants/warehouses aliases) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 18f1b07..6063a6e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -592,7 +592,6 @@ model locations { type String @db.VarChar(20) code String @db.VarChar(30) name String @db.VarChar(150) - parent_id BigInt? gstin String? @db.VarChar(15) address String? city String? @db.VarChar(100) @@ -610,8 +609,6 @@ model locations { transfers_to asset_transfers[] @relation("TransferToLocation") assets_location assets[] @relation("AssetLocation") grn_location grn[] @relation("GRNLocation") - parent locations? @relation("LocationHierarchy", fields: [parent_id], references: [id], onUpdate: NoAction) - children locations[] @relation("LocationHierarchy") po_billing purchase_orders[] @relation("POBillingLocation") po_shipping purchase_orders[] @relation("POShippingLocation") users_plant users[] @relation("UserPlantLocation") @@ -619,7 +616,6 @@ model locations { users_updated users? @relation("locations_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction, map: "fk_locations_updated_by") @@index([type], map: "idx_locations_type") - @@index([parent_id], map: "idx_locations_parent_id") } /// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info. diff --git a/scripts/patch-locations-drop-parent-id.sql b/scripts/patch-locations-drop-parent-id.sql new file mode 100644 index 0000000..1301bc5 --- /dev/null +++ b/scripts/patch-locations-drop-parent-id.sql @@ -0,0 +1,5 @@ +-- Remove location hierarchy (parent_id). Plants and warehouses are flat locations. + +ALTER TABLE locations DROP CONSTRAINT IF EXISTS locations_parent_id_fkey; +DROP INDEX IF EXISTS idx_locations_parent_id; +ALTER TABLE locations DROP COLUMN IF EXISTS parent_id; diff --git a/src/docs/completed-routes.yaml b/src/docs/completed-routes.yaml index d113ae7..9349cb1 100644 --- a/src/docs/completed-routes.yaml +++ b/src/docs/completed-routes.yaml @@ -358,7 +358,6 @@ components: type: { type: string, enum: [plant, warehouse], example: plant } code: { type: string, example: 'PLT01' } name: { type: string, example: 'Bharat Plant 1' } - parent_id: { type: integer, nullable: true, example: 1, description: Required when type is warehouse (plant id) } gstin: { type: string, example: '27ABCDE1234F1Z5' } address: { type: string, example: 'MIDC, Nashik' } city: { type: string, example: 'Nashik' } @@ -373,7 +372,6 @@ components: properties: code: { type: string, example: 'PLT01' } name: { type: string, example: 'Bharat Plant 1' } - parent_id: { type: integer, nullable: true, example: 1 } gstin: { type: string, example: '27ABCDE1234F1Z5' } address: { type: string, example: 'MIDC, Nashik' } city: { type: string, example: 'Nashik' } @@ -384,11 +382,10 @@ components: is_active: { type: boolean, example: true } WarehousesCreateBody: type: object - required: [code, name, plant_id] + required: [code, name] properties: code: { type: string, example: 'WH01' } name: { type: string, example: 'Main Warehouse' } - plant_id: { type: integer, example: 1 } location: { type: string, example: 'Ground Floor' } is_active: { type: boolean, example: true } WarehousesUpdateBody: @@ -397,7 +394,6 @@ components: properties: code: { type: string, example: 'WH01' } name: { type: string, example: 'Main Warehouse' } - plant_id: { type: integer, example: 1 } location: { type: string, example: 'Ground Floor' } is_active: { type: boolean, example: true } PlantsCreateBody: @@ -1527,10 +1523,6 @@ paths: - in: query name: type schema: { type: string, enum: [plant, warehouse] } - - in: query - name: parent_id - schema: { type: integer } - description: Filter warehouses by plant id responses: "200": description: List fetched diff --git a/src/modules/masters/locations/locations.service.js b/src/modules/masters/locations/locations.service.js index 3965b91..8c616cb 100644 --- a/src/modules/masters/locations/locations.service.js +++ b/src/modules/masters/locations/locations.service.js @@ -6,7 +6,6 @@ const { parseId } = require('../../../utils/parseId'); const { rowsToCsv } = require('../../../utils/csv'); const { LOCATION_TYPES, - assertPlant, toPlantResponse, toWarehouseResponse, toLocationResponse, @@ -31,7 +30,6 @@ const buildPlantData = (payload) => ({ pincode: normalizeString(payload.pincode), phone: normalizeString(payload.phone), is_active: payload.is_active, - parent_id: null, location: null, }); @@ -39,7 +37,6 @@ const buildWarehouseData = (payload) => ({ type: LOCATION_TYPES.WAREHOUSE, code: payload.code ? String(payload.code).trim().toUpperCase() : undefined, name: normalizeString(payload.name), - parent_id: payload.parent_id ?? payload.plant_id ?? null, location: normalizeString(payload.location), is_active: payload.is_active, gstin: null, @@ -50,8 +47,6 @@ const buildWarehouseData = (payload) => ({ phone: null, }); -const warehouseInclude = { parent: { select: { id: true, code: true, name: true } } }; - const assertUniqueCode = async (code, excludeId = null) => { if (!code) return; const existing = await prisma.locations.findFirst({ @@ -71,19 +66,10 @@ const createLocation = async (type, payload, userId, requestId) => { if (!data.code || !data.name) throw new ApiError(422, 'code and name are required'); await assertUniqueCode(data.code); - if (type === LOCATION_TYPES.WAREHOUSE) { - if (!data.parent_id) throw new ApiError(422, 'parent_id (plant) is required for warehouse'); - await assertPlant(data.parent_id, 'parent_id'); - data.parent_id = BigInt(data.parent_id); - } - data.created_by = userId ? BigInt(userId) : null; data.updated_by = userId ? BigInt(userId) : null; - const created = await prisma.locations.create({ - data, - include: type === LOCATION_TYPES.WAREHOUSE ? warehouseInclude : undefined, - }); + const created = await prisma.locations.create({ data }); const response = type === LOCATION_TYPES.PLANT ? toPlantResponse(created) : toWarehouseResponse(created); @@ -106,7 +92,6 @@ const buildLocationsWhere = (query, type = null) => { const where = { deleted_at: null, ...(effectiveType ? { type: effectiveType } : {}), - ...(query.parent_id ? { parent_id: BigInt(query.parent_id) } : {}), ...(query.is_active !== undefined ? { is_active: query.is_active } : {}), ...(query.search ? { @@ -123,9 +108,6 @@ const buildLocationsWhere = (query, type = null) => { const listLocations = async (query, type = null) => { const { where, effectiveType } = buildLocationsWhere(query, type); - const locationsInclude = - effectiveType === LOCATION_TYPES.WAREHOUSE || !effectiveType ? warehouseInclude : undefined; - const mapRow = (row) => { if (effectiveType === LOCATION_TYPES.PLANT) return toPlantResponse(row); if (effectiveType === LOCATION_TYPES.WAREHOUSE) return toWarehouseResponse(row); @@ -136,7 +118,6 @@ const listLocations = async (query, type = null) => { where.is_active = true; const rows = await prisma.locations.findMany({ where, - include: locationsInclude, orderBy: { created_at: 'desc' }, }); const data = rows.map(mapRow); @@ -148,7 +129,6 @@ const listLocations = async (query, type = null) => { const [rows, total] = await Promise.all([ prisma.locations.findMany({ where, - include: locationsInclude, orderBy: { created_at: 'desc' }, skip, take: limit, @@ -163,10 +143,6 @@ const exportLocations = async (query, type = null) => { const { where, effectiveType } = buildLocationsWhere(query, type); const rows = await prisma.locations.findMany({ where, - include: - effectiveType === LOCATION_TYPES.WAREHOUSE || !effectiveType - ? warehouseInclude - : undefined, orderBy: { created_at: 'desc' }, }); @@ -181,8 +157,6 @@ const exportLocations = async (query, type = null) => { ? [ { key: 'code', header: 'Code' }, { key: 'name', header: 'Name' }, - { key: (row) => row.parent?.code || row.plant?.code || '', header: 'Plant Code' }, - { key: (row) => row.parent?.name || row.plant?.name || '', header: 'Plant Name' }, { key: 'location', header: 'Location' }, { key: 'is_active', header: 'Is Active' }, { key: 'created_at', header: 'Created At', type: 'datetime' }, @@ -215,7 +189,6 @@ const getLocationById = async (id, type = null) => { deleted_at: null, ...(type ? { type } : {}), }, - include: warehouseInclude, }); if (!row) throw new ApiError(404, type ? `${type} not found` : 'Location not found'); @@ -228,7 +201,6 @@ 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 }, - include: warehouseInclude, }); if (!existing) throw new ApiError(404, `${type} not found`); @@ -240,18 +212,11 @@ const updateLocation = async (id, type, payload, userId, requestId) => { if (data.code) await assertUniqueCode(data.code, id); - if (type === LOCATION_TYPES.WAREHOUSE && data.parent_id !== undefined) { - if (!data.parent_id) throw new ApiError(422, 'parent_id (plant) is required for warehouse'); - await assertPlant(data.parent_id, 'parent_id'); - data.parent_id = BigInt(data.parent_id); - } - data.updated_by = userId ? BigInt(userId) : null; const updated = await prisma.locations.update({ where: { id: idBigInt }, data, - include: type === LOCATION_TYPES.WAREHOUSE ? warehouseInclude : undefined, }); const oldValue = @@ -276,7 +241,6 @@ const deleteLocation = async (id, type, userId, requestId) => { const idBigInt = parseId(id); const existing = await prisma.locations.findFirst({ where: { id: idBigInt, deleted_at: null, type }, - include: warehouseInclude, }); if (!existing) throw new ApiError(404, `${type} not found`); @@ -302,7 +266,6 @@ const deleteLocation = async (id, type, userId, requestId) => { const deleteLocationById = async (id, userId, requestId) => { const existing = await prisma.locations.findFirst({ where: { id: parseId(id), deleted_at: null }, - include: warehouseInclude, }); if (!existing) throw new ApiError(404, 'Location not found'); return deleteLocation(id, existing.type, userId, requestId); diff --git a/src/modules/masters/locations/locations.validation.js b/src/modules/masters/locations/locations.validation.js index 83f75ca..ef0b088 100644 --- a/src/modules/masters/locations/locations.validation.js +++ b/src/modules/masters/locations/locations.validation.js @@ -13,8 +13,6 @@ const locationFields = { pincode: Joi.string().max(10).allow(null, '').optional(), phone: Joi.string().max(15).allow(null, '').optional(), location: Joi.string().max(200).allow(null, '').optional(), - parent_id: Joi.number().integer().positive().optional(), - plant_id: Joi.number().integer().positive().optional(), is_active: Joi.boolean().optional(), }; @@ -45,7 +43,6 @@ const updatePlantSchema = Joi.object({ const createWarehouseSchema = Joi.object({ code: masterCode({ max: 30, required: true }), name: masterName({ max: 150, required: true }), - plant_id: Joi.number().integer().positive().required(), location: locationFields.location, is_active: Joi.boolean().default(true), }); @@ -53,7 +50,6 @@ const createWarehouseSchema = Joi.object({ const updateWarehouseSchema = Joi.object({ code: masterCode({ max: 30 }), name: masterName({ max: 150 }), - plant_id: Joi.number().integer().positive().optional(), location: locationFields.location, is_active: Joi.boolean().optional(), }).min(1); @@ -62,11 +58,6 @@ const createLocationSchema = Joi.object({ type: Joi.string().valid('plant', 'warehouse').required(), code: masterCode({ max: 30, required: true }), name: masterName({ max: 150, required: true }), - parent_id: Joi.when('type', { - is: 'warehouse', - then: Joi.number().integer().positive().required(), - otherwise: Joi.number().integer().positive().allow(null).optional(), - }), gstin: locationFields.gstin, address: locationFields.address, city: locationFields.city, @@ -80,7 +71,6 @@ const createLocationSchema = Joi.object({ const updateLocationSchema = Joi.object({ code: masterCode({ max: 30 }), name: masterName({ max: 150 }), - parent_id: Joi.number().integer().positive().optional(), gstin: locationFields.gstin, address: locationFields.address, city: locationFields.city, @@ -93,7 +83,6 @@ const updateLocationSchema = Joi.object({ const listLocationsQuerySchema = listQuerySchema.keys({ type: Joi.string().valid('plant', 'warehouse').optional(), - parent_id: Joi.number().integer().positive().optional(), }); const exportQuerySchema = toExportQuerySchema(listLocationsQuerySchema); diff --git a/src/utils/locations.js b/src/utils/locations.js index cc40f50..3aba383 100644 --- a/src/utils/locations.js +++ b/src/utils/locations.js @@ -15,9 +15,6 @@ const assertLocation = async (id, expectedType, label, { requireActive = true } type: expectedType, deleted_at: null, }, - include: expectedType === LOCATION_TYPES.WAREHOUSE - ? { parent: { select: { id: true, code: true, name: true } } } - : undefined, }); if (!row) throw new ApiError(422, `Invalid ${label}`); @@ -31,13 +28,12 @@ const assertPlant = (id, label = 'plant_id') => const assertWarehouse = (id, label = 'warehouse_id') => assertLocation(id, LOCATION_TYPES.WAREHOUSE, label); -/** Assert a location of any type (plant or warehouse). Returns the row (with parent). */ +/** Assert a location of any type (plant or warehouse). */ const assertAnyLocation = async (id, label, { requireActive = true } = {}) => { if (!id) return null; const row = await prisma.locations.findFirst({ where: { id: BigInt(id), deleted_at: null }, - include: { parent: { select: { id: true, code: true, name: true } } }, }); if (!row) throw new ApiError(422, `Invalid ${label}`); @@ -53,9 +49,7 @@ const warehouseListSelect = { id: true, code: true, name: true, - parent_id: true, location: true, - parent: { select: plantListSelect }, }; const toPlantResponse = (row) => ({ @@ -80,8 +74,6 @@ const toWarehouseResponse = (row) => ({ id: row.id, code: row.code, name: row.name, - plant_id: row.parent_id, - plant: row.parent || null, location: row.location, is_active: row.is_active, created_by: row.created_by, @@ -93,7 +85,7 @@ const toWarehouseResponse = (row) => ({ const toLocationResponse = (row) => { if (row.type === LOCATION_TYPES.PLANT) return { ...toPlantResponse(row), type: row.type }; - return { ...toWarehouseResponse(row), type: row.type, parent_id: row.parent_id }; + return { ...toWarehouseResponse(row), type: row.type }; }; module.exports = {