From 7516354ce50a5631b93da2e7ba8045c4cf22a2b4 Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Wed, 22 Jul 2026 18:39:00 +0530 Subject: [PATCH] Reject reason added for PO --- BACKEND_TASKS.md | 2 +- prisma/schema.prisma | 1 + scripts/patch-po-reject-reason.sql | 4 ++++ src/docs/purchase-orders-routes.yaml | 6 ++++-- src/modules/purchase-orders/purchase-orders.service.js | 8 +++++++- .../purchase-orders/purchase-orders.validation.js | 9 +++++++-- 6 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 scripts/patch-po-reject-reason.sql diff --git a/BACKEND_TASKS.md b/BACKEND_TASKS.md index 33b66ab..e7b1243 100644 --- a/BACKEND_TASKS.md +++ b/BACKEND_TASKS.md @@ -239,7 +239,7 @@ Each sub-master supports: `GET /` (list), `GET /:id`, `POST /`, `PUT /:id`, `DEL | [x] | DELETE | `/purchase-orders/:id` | delete | Soft delete | | [x] | POST | `/purchase-orders/:id/submit` | edit | Submit for approval | | [x] | POST | `/purchase-orders/:id/approve` | approve | Approve PO | -| [x] | POST | `/purchase-orders/:id/reject` | approve | Reject PO | +| [x] | POST | `/purchase-orders/:id/reject` | approve | Reject PO (`reject_reason` required; stored on PO) | | [x] | POST | `/purchase-orders/:id/amend` | edit | Amend PO | | [x] | POST | `/purchase-orders/:id/cancel` | edit | Cancel PO | | [x] | GET | `/purchase-orders/:id/pdf` | view | PDF export | diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a4f68b0..6eb71b1 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -711,6 +711,7 @@ model purchase_orders { parent_po_id BigInt? terms_and_conditions String? remarks String? + reject_reason String? is_active Boolean @default(true) created_by BigInt? updated_by BigInt? diff --git a/scripts/patch-po-reject-reason.sql b/scripts/patch-po-reject-reason.sql new file mode 100644 index 0000000..b4187d1 --- /dev/null +++ b/scripts/patch-po-reject-reason.sql @@ -0,0 +1,4 @@ +-- Persist PO rejection reason on purchase_orders header + +ALTER TABLE purchase_orders + ADD COLUMN IF NOT EXISTS reject_reason TEXT; diff --git a/src/docs/purchase-orders-routes.yaml b/src/docs/purchase-orders-routes.yaml index fdc2b1d..829d9c6 100644 --- a/src/docs/purchase-orders-routes.yaml +++ b/src/docs/purchase-orders-routes.yaml @@ -67,9 +67,10 @@ components: remarks: { type: string, example: 'Approved for procurement' } PurchaseOrdersRejectBody: type: object - required: [remarks] + required: [reject_reason] properties: - remarks: { type: string, example: 'Rates not competitive' } + reject_reason: { type: string, example: 'Rates not competitive' } + remarks: { type: string, example: 'Rates not competitive', description: 'Legacy alias for reject_reason' } PoAttachmentResponse: type: object properties: @@ -247,6 +248,7 @@ paths: post: tags: [Purchase Orders] summary: Reject purchase order + description: Requires reject_reason. Stored on purchase_orders.reject_reason and cleared on resubmit/approve. parameters: - { name: id, in: path, required: true, schema: { type: string, example: '1' } } requestBody: diff --git a/src/modules/purchase-orders/purchase-orders.service.js b/src/modules/purchase-orders/purchase-orders.service.js index a387011..8cabf63 100644 --- a/src/modules/purchase-orders/purchase-orders.service.js +++ b/src/modules/purchase-orders/purchase-orders.service.js @@ -697,6 +697,7 @@ const submitPurchaseOrder = async (id, payload, userId, requestId) => { where: { id: BigInt(id) }, data: { status: 'PENDING_APPROVAL', + reject_reason: null, remarks: payload.remarks ?? existing.remarks, updated_by: userId ? BigInt(userId) : null, }, @@ -739,6 +740,7 @@ const approvePurchaseOrder = async (id, payload, userId, requestId) => { where: { id: BigInt(id) }, data: { status: 'APPROVED', + reject_reason: null, updated_by: userId ? BigInt(userId) : null, }, include: poDetailInclude, @@ -765,12 +767,15 @@ const rejectPurchaseOrder = async (id, payload, userId, requestId) => { const pendingApproval = existing.po_approvals.find((row) => row.status === 'PENDING'); if (!pendingApproval) throw new ApiError(409, 'No pending approval step found'); + const rejectReason = String(payload.reject_reason || payload.remarks || '').trim(); + if (!rejectReason) throw new ApiError(422, 'reject_reason is required'); + const updated = await prisma.$transaction(async (tx) => { await tx.po_approvals.update({ where: { id: pendingApproval.id }, data: { status: 'REJECTED', - remarks: payload.remarks, + remarks: rejectReason, approver_user_id: userId ? BigInt(userId) : null, acted_at: new Date(), }, @@ -780,6 +785,7 @@ const rejectPurchaseOrder = async (id, payload, userId, requestId) => { where: { id: BigInt(id) }, data: { status: 'REJECTED', + reject_reason: rejectReason, updated_by: userId ? BigInt(userId) : null, }, include: poDetailInclude, diff --git a/src/modules/purchase-orders/purchase-orders.validation.js b/src/modules/purchase-orders/purchase-orders.validation.js index 48a67aa..def3069 100644 --- a/src/modules/purchase-orders/purchase-orders.validation.js +++ b/src/modules/purchase-orders/purchase-orders.validation.js @@ -111,8 +111,13 @@ const workflowRemarksSchema = Joi.object({ }); const rejectPurchaseOrderSchema = Joi.object({ - remarks: Joi.string().trim().min(1).required(), -}); + reject_reason: Joi.string().trim().min(1).optional(), + remarks: Joi.string().trim().min(1).optional(), // legacy alias for reject_reason +}) + .or('reject_reason', 'remarks') + .messages({ + 'object.missing': 'reject_reason is required', + }); module.exports = { createPurchaseOrderSchema,