const prisma = require('../../config/prisma'); const ApiError = require('../../utils/ApiError'); const auditLog = require('../../utils/auditLog'); const { getPagination, isDropdownCall } = require('../../utils/pagination'); const { nextDocumentNumber } = require('../../utils/generateCode'); const { generatePdf } = require('../../utils/pdf/pdfGenerator'); const { generatePoHtml } = require('../../utils/pdf/templates/po.template'); const { formatDate } = require('../../utils/pdf/helpers/formatDate'); const { getCompanyForDocuments } = require('../settings/settings.service'); const { rowsToCsv } = require('../../utils/csv'); const { EDITABLE_STATUSES, SUBMITTABLE_STATUSES, APPROVABLE_STATUSES, CANCELLABLE_STATUSES, AMENDABLE_STATUSES, DELETABLE_STATUSES, } = require('./purchase-orders.constants'); const { computeLineAmounts, computeHeaderTotals, splitGst, toNum, } = require('./purchase-orders.calculations'); const repository = require('./purchase-orders.repository'); const { assertAnyLocation } = require('../../utils/locations'); const { sanitizeAttachment } = require('./po.attachments.service'); const locationSummarySelect = { id: true, code: true, name: true, type: true }; const poListInclude = { vendors: { select: { id: true, vendor_code: true, vendor_name: true, vendor_type: true } }, billing_location: { select: locationSummarySelect }, shipping_location: { select: locationSummarySelect }, users_purchase_orders_created_byTousers: { select: { id: true, full_name: true } }, }; const poDetailInclude = { ...poListInclude, payment_terms: { select: { id: true, code: true, name: true } }, delivery_terms: { select: { id: true, code: true, name: true } }, users_purchase_orders_updated_byTousers: { select: { id: true, full_name: true } }, purchase_orders: { select: { id: true, po_number: true, revision_no: true } }, purchase_order_items: { orderBy: { line_no: 'asc' }, include: { items: { select: { id: true, item_code: true, item_name: true, is_asset_item: true, hsn_code_id: true } }, uom: { select: { id: true, code: true, name: true } }, gst_rates: { select: { id: true, rate_pct: true, description: true } }, hsn_codes: { select: { id: true, code: true, description: true } }, }, }, po_approvals: { orderBy: { approval_level: 'asc' }, include: { roles: { select: { id: true, name: true } }, users: { select: { id: true, full_name: true } }, }, }, po_attachments: { orderBy: { created_at: 'desc' }, include: { users: { select: { id: true, full_name: true, employee_code: true } }, }, }, }; const poPdfInclude = { ...poDetailInclude, vendors: { select: { id: true, vendor_code: true, vendor_name: true, vendor_type: true, gstin: true, vendor_addresses: { where: { is_active: true }, orderBy: { id: 'asc' }, take: 5, select: { address_type: true, address_line1: true, address_line2: true, city: true, state: true, pincode: true, gstin: true, }, }, }, }, billing_location: { select: { id: true, code: true, name: true, type: true, gstin: true, address: true, city: true, state: true, pincode: true, }, }, shipping_location: { select: { id: true, code: true, name: true, type: true, gstin: true, address: true, city: true, state: true, pincode: true, }, }, }; const toDateOnly = (value) => { if (!value) return null; const date = new Date(value); return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())); }; const sanitizePo = (po) => { if (!po) return null; const { vendors, billing_location, shipping_location, payment_terms, delivery_terms, users_purchase_orders_created_byTousers, users_purchase_orders_updated_byTousers, purchase_orders, purchase_order_items, po_approvals, po_attachments, ...rest } = po; return { ...rest, vendor: vendors || null, billing: billing_location || null, shipping: shipping_location || null, payment_term: payment_terms || null, delivery_term: delivery_terms || null, created_by_user: users_purchase_orders_created_byTousers || null, updated_by_user: users_purchase_orders_updated_byTousers || null, parent_po: purchase_orders || null, items: (purchase_order_items || []).map((line) => ({ ...line, item: line.items || null, uom: line.uom || null, gst_rate: line.gst_rates || null, hsn_code: line.hsn_codes || null, items: undefined, gst_rates: undefined, hsn_codes: undefined, })), approvals: (po_approvals || []).map((row) => ({ ...row, approver_role: row.roles || null, approver_user: row.users || null, roles: undefined, users: undefined, })), attachments: (po_attachments || []).map(sanitizeAttachment), purchase_order_items: undefined, po_approvals: undefined, po_attachments: undefined, vendors: undefined, billing_location: undefined, shipping_location: undefined, payment_terms: undefined, delivery_terms: undefined, users_purchase_orders_created_byTousers: undefined, users_purchase_orders_updated_byTousers: undefined, purchase_orders: undefined, }; }; const assertStatus = (po, allowedStatuses, action) => { if (!allowedStatuses.includes(po.status)) { throw new ApiError(409, `Cannot ${action} PO in status ${po.status}`); } }; const SOFT_DELETE_TABLES = new Set([ 'vendors', 'locations', 'payment_terms', 'delivery_terms', 'items', 'uom', ]); const assertReference = async (table, id, label, { requireActive = true } = {}) => { if (!id) return null; const row = await prisma[table].findFirst({ where: { id: BigInt(id), ...(SOFT_DELETE_TABLES.has(table) ? { deleted_at: null } : {}), }, }); if (!row) throw new ApiError(422, `Invalid ${label}`); if (requireActive && row.is_active === false) throw new ApiError(422, `${label} is inactive`); return row; }; const loadGstRateMap = async (itemRows) => { const gstIds = [...new Set(itemRows.map((row) => row.gst_rate_id).filter(Boolean))]; if (!gstIds.length) return new Map(); const rates = await prisma.gst_rates.findMany({ where: { id: { in: gstIds.map((id) => BigInt(id)) }, is_active: true }, }); return new Map(rates.map((rate) => [rate.id.toString(), Number(rate.rate_pct)])); }; const validateAndBuildItems = async (items) => { const lineNos = items.map((row) => row.line_no); if (new Set(lineNos).size !== lineNos.length) { throw new ApiError(422, 'Duplicate line_no in items'); } const gstRateMap = await loadGstRateMap(items); const builtItems = []; for (const row of items) { const item = await assertReference('items', row.item_id, 'item_id'); await assertReference('uom', row.uom_id, 'uom_id'); if (row.hsn_code_id) await assertReference('hsn_codes', row.hsn_code_id, 'hsn_code_id', { requireActive: false }); let gstRatePct = 0; if (row.gst_rate_id) { const gst = await prisma.gst_rates.findFirst({ where: { id: BigInt(row.gst_rate_id), is_active: true }, }); if (!gst) throw new ApiError(422, 'Invalid gst_rate_id'); gstRatePct = Number(gst.rate_pct); } else if (item.gst_rate_id) { const gst = await prisma.gst_rates.findFirst({ where: { id: item.gst_rate_id, is_active: true }, }); gstRatePct = gst ? Number(gst.rate_pct) : 0; } else if (gstRateMap.has(String(row.gst_rate_id))) { gstRatePct = gstRateMap.get(String(row.gst_rate_id)); } const amounts = computeLineAmounts(row, gstRatePct); builtItems.push({ item_id: BigInt(row.item_id), line_no: row.line_no, ordered_qty: row.ordered_qty, uom_id: BigInt(row.uom_id), rate: row.rate, discount_pct: row.discount_pct ?? 0, discount_amount: amounts.discount_amount, gst_rate_id: row.gst_rate_id ? BigInt(row.gst_rate_id) : item.gst_rate_id || null, hsn_code_id: row.hsn_code_id ? BigInt(row.hsn_code_id) : item.hsn_code_id || null, taxable_amount: amounts.taxable_amount, tax_amount: amounts.tax_amount, line_total: amounts.line_total, remarks: row.remarks || null, }); } return builtItems; }; const normalizeState = (value) => value === null || value === undefined ? '' : String(value).trim().toLowerCase(); /** * Inter-state (IGST) when the place of supply (billing location state) differs * from the vendor's source of supply. When either state is unknown we default * to intra-state (CGST/SGST). */ const isInterStateSupply = (billingLocation, vendor) => { const placeOfSupply = normalizeState(billingLocation?.state); const vendorState = normalizeState(vendor?.source_of_supply); if (!placeOfSupply || !vendorState) return false; return placeOfSupply !== vendorState; }; const buildHeaderData = async (payload, builtItems, userId, { applyDefaultTerms = false } = {}) => { const vendor = await assertReference('vendors', payload.vendor_id, 'vendor_id'); const billing = await assertAnyLocation(payload.billing_id, 'billing_id'); await assertAnyLocation(payload.shipping_id, 'shipping_id'); if (payload.payment_term_id) await assertReference('payment_terms', payload.payment_term_id, 'payment_term_id'); if (payload.delivery_term_id) await assertReference('delivery_terms', payload.delivery_term_id, 'delivery_term_id'); const tdsApplicable = Boolean(payload.tds_applicable); const tdsSectionPct = tdsApplicable ? toNum(payload.tds_section_pct) : 0; const totals = computeHeaderTotals( builtItems, payload.discount_amount, payload.freight_charges, payload.other_charges, { tdsApplicable, tdsSectionPct, adjustment: payload.adjustment, } ); const gstSplit = splitGst(totals.tax_total, isInterStateSupply(billing, vendor)); let termsAndConditions = payload.terms_and_conditions !== undefined && payload.terms_and_conditions !== null ? String(payload.terms_and_conditions).trim() : ''; if (applyDefaultTerms && !termsAndConditions) { const { getDefaultNotesText } = require('../masters/terms-notes/terms-notes.service'); const defaultNotes = await getDefaultNotesText('PO'); if (defaultNotes) termsAndConditions = defaultNotes; } return { po_date: toDateOnly(payload.po_date), vendor_id: BigInt(payload.vendor_id), billing_id: BigInt(payload.billing_id), shipping_id: BigInt(payload.shipping_id), payment_term_id: payload.payment_term_id ? BigInt(payload.payment_term_id) : null, delivery_term_id: payload.delivery_term_id ? BigInt(payload.delivery_term_id) : null, expected_delivery_date: payload.expected_delivery_date ? toDateOnly(payload.expected_delivery_date) : null, terms_and_conditions: termsAndConditions || null, remarks: payload.remarks || null, tds_applicable: tdsApplicable, tds_section_pct: tdsApplicable ? tdsSectionPct : null, ...totals, ...gstSplit, updated_by: userId ? BigInt(userId) : null, }; }; const getPoOrThrow = async (id, { includeItems = false, forPdf = false } = {}) => { const include = forPdf ? poPdfInclude : includeItems ? poDetailInclude : poListInclude; const po = await prisma.purchase_orders.findFirst({ where: { id: BigInt(id), deleted_at: null }, include, }); if (!po) throw new ApiError(404, 'Purchase order not found'); return po; }; const humanizeLabel = (value) => { if (!value) return '-'; return String(value) .toLowerCase() .split('_') .filter(Boolean) .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(' '); }; const pickVendorAddress = (addresses = []) => { if (!addresses.length) return null; const preferred = addresses.find((row) => String(row.address_type || '').toUpperCase() === 'BILLING') || addresses.find((row) => String(row.address_type || '').toUpperCase() === 'SHIPPING') || addresses[0]; return preferred; }; const buildVendorAddressLine = (address) => { if (!address) return ''; return [address.address_line1, address.address_line2].filter(Boolean).join(', '); }; const buildPoPdfPayload = (po, company) => { const vendorAddress = pickVendorAddress(po.vendor?.vendor_addresses || []); return { company: { ...company, gstin: company.gstin || '', }, po: { po_number: po.po_number, po_date: po.po_date, expected_delivery_date: po.expected_delivery_date, status: humanizeLabel(po.status), vendor_type: humanizeLabel(po.vendor?.vendor_type), revision_no: po.revision_no ?? 0, payment_term: po.payment_term?.name || null, delivery_term: po.delivery_term?.name || null, terms_and_conditions: po.terms_and_conditions || null, remarks: po.remarks || null, }, vendor: { vendor_name: po.vendor?.vendor_name || '-', gstin: po.vendor?.gstin || vendorAddress?.gstin || '', address: buildVendorAddressLine(vendorAddress), city: vendorAddress?.city || '', state: vendorAddress?.state || '', pincode: vendorAddress?.pincode || '', }, bill_to: { name: po.billing?.name || '-', gstin: po.billing?.gstin || '', address: po.billing?.address || '', city: po.billing?.city || '', state: po.billing?.state || '', pincode: po.billing?.pincode || '', }, ship_to: { name: po.shipping?.name || '-', gstin: po.shipping?.gstin || '', address: po.shipping?.address || '', city: po.shipping?.city || '', state: po.shipping?.state || '', pincode: po.shipping?.pincode || '', }, items: (po.items || []).map((line) => { const orderedQty = toNum(line.ordered_qty); const rate = toNum(line.rate); return { line_no: line.line_no, item_name: line.item?.item_name || '-', hsn_code: line.hsn_code?.code || '-', remarks: line.remarks || null, uom: line.uom?.code || line.uom?.name || '-', ordered_qty: orderedQty, rate, discount_pct: toNum(line.discount_pct), gst_rate_pct: toNum(line.gst_rate?.rate_pct), amount: toNum(line.taxable_amount != null ? line.taxable_amount : orderedQty * rate), }; }), totals: { sub_total: toNum(po.sub_total), freight_charges: toNum(po.freight_charges), other_charges: toNum(po.other_charges), discount_amount: toNum(po.discount_amount), taxable_amount: toNum(po.taxable_amount), cgst: toNum(po.cgst), sgst: toNum(po.sgst), igst: toNum(po.igst), tax_total: toNum(po.tax_total), tds_amount: toNum(po.tds_amount), adjustment: toNum(po.adjustment), grand_total: toNum(po.grand_total), }, generated_at: `${formatDate(new Date(), { style: 'datetime' })} IST`, }; }; const hasReceipts = (po) => (po.purchase_order_items || []).some((line) => Number(line.received_qty) > 0); const createPurchaseOrder = async (payload, userId, requestId) => { const builtItems = await validateAndBuildItems(payload.items); const header = await buildHeaderData(payload, builtItems, userId, { applyDefaultTerms: true }); const poNumber = await nextDocumentNumber('PO'); header.po_number = poNumber; header.status = 'DRAFT'; header.created_by = userId ? BigInt(userId) : null; const itemsWithPo = builtItems.map((item) => ({ ...item })); const createdPo = await repository.createPurchaseOrderWithItems({ header, items: itemsWithPo }); const created = await getPoOrThrow(createdPo.id, { includeItems: true }); await auditLog({ tableName: 'purchase_orders', recordId: created.id, action: 'CREATE', oldValue: null, newValue: sanitizePo(created), userId, requestId, }); return sanitizePo(created); }; const buildPurchaseOrdersWhere = (query) => ({ deleted_at: null, ...(query.status ? { status: query.status } : {}), ...(query.vendor_id ? { vendor_id: BigInt(query.vendor_id) } : {}), ...(query.vendor_type ? { vendors: { vendor_type: query.vendor_type } } : {}), ...(query.billing_id ? { billing_id: BigInt(query.billing_id) } : {}), ...(query.shipping_id ? { shipping_id: BigInt(query.shipping_id) } : {}), ...(query.search ? { po_number: { contains: query.search, mode: 'insensitive' } } : {}), ...(query.date_from || query.date_to ? { po_date: { ...(query.date_from ? { gte: toDateOnly(query.date_from) } : {}), ...(query.date_to ? { lte: toDateOnly(query.date_to) } : {}), }, } : {}), }); const listPurchaseOrders = async (query) => { const where = buildPurchaseOrdersWhere(query); if (isDropdownCall(query)) { where.is_active = true; const rows = await prisma.purchase_orders.findMany({ where, include: poListInclude, orderBy: { created_at: 'desc' }, }); const data = rows.map(sanitizePo); 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.purchase_orders.findMany({ where, include: poListInclude, orderBy: { created_at: 'desc' }, skip, take: limit, }), prisma.purchase_orders.count({ where }), ]); return { data: rows.map(sanitizePo), meta: { page, limit, total } }; }; /** Approver inbox — only POs in PENDING_APPROVAL. */ const listPendingApprovalPurchaseOrders = async (query) => listPurchaseOrders({ ...query, status: 'PENDING_APPROVAL' }); const exportPurchaseOrders = async (query) => { const rows = await prisma.purchase_orders.findMany({ where: buildPurchaseOrdersWhere(query), include: poListInclude, orderBy: { created_at: 'desc' }, }); return rowsToCsv( [ { key: 'po_number', header: 'PO Number' }, { key: 'po_date', header: 'PO Date', type: 'date' }, { key: (row) => row.vendor?.vendor_type || '', header: 'Vendor Type' }, { key: 'status', header: 'Status' }, { key: 'revision_no', header: 'Revision' }, { key: (row) => row.vendor?.vendor_code || '', header: 'Vendor Code' }, { key: (row) => row.vendor?.vendor_name || '', header: 'Vendor Name' }, { key: (row) => row.billing?.name || '', header: 'Billing Location' }, { key: (row) => row.shipping?.name || '', header: 'Shipping Location' }, { key: 'expected_delivery_date', header: 'Expected Delivery', type: 'date' }, { key: 'sub_total', header: 'Sub Total' }, { key: 'tax_total', header: 'Tax Total' }, { key: 'cgst', header: 'CGST' }, { key: 'sgst', header: 'SGST' }, { key: 'igst', header: 'IGST' }, { key: 'grand_total', header: 'Grand Total' }, { key: (row) => row.created_by_user?.full_name || '', header: 'Created By' }, { key: 'created_at', header: 'Created At', type: 'datetime' }, ], rows.map(sanitizePo) ); }; const getPurchaseOrderById = async (id) => sanitizePo(await getPoOrThrow(id, { includeItems: true })); const updatePurchaseOrder = async (id, payload, userId, requestId) => { const existing = await getPoOrThrow(id, { includeItems: true }); assertStatus(existing, EDITABLE_STATUSES, 'update'); const merged = { po_date: payload.po_date ?? existing.po_date, vendor_id: payload.vendor_id ?? existing.vendor_id, billing_id: payload.billing_id ?? existing.billing_id, shipping_id: payload.shipping_id ?? existing.shipping_id, payment_term_id: payload.payment_term_id !== undefined ? payload.payment_term_id : existing.payment_term_id, delivery_term_id: payload.delivery_term_id !== undefined ? payload.delivery_term_id : existing.delivery_term_id, expected_delivery_date: payload.expected_delivery_date !== undefined ? payload.expected_delivery_date : existing.expected_delivery_date, discount_amount: payload.discount_amount ?? Number(existing.discount_amount), freight_charges: payload.freight_charges ?? Number(existing.freight_charges), other_charges: payload.other_charges ?? Number(existing.other_charges), tds_applicable: payload.tds_applicable !== undefined ? payload.tds_applicable : existing.tds_applicable, tds_section_pct: payload.tds_section_pct !== undefined ? payload.tds_section_pct : existing.tds_section_pct !== null && existing.tds_section_pct !== undefined ? Number(existing.tds_section_pct) : null, adjustment: payload.adjustment !== undefined ? payload.adjustment : Number(existing.adjustment ?? 0), terms_and_conditions: payload.terms_and_conditions !== undefined ? payload.terms_and_conditions : existing.terms_and_conditions, remarks: payload.remarks !== undefined ? payload.remarks : existing.remarks, items: payload.items, }; const sourceItems = payload.items || existing.purchase_order_items.map((line) => ({ item_id: line.item_id, line_no: line.line_no, ordered_qty: line.ordered_qty, uom_id: line.uom_id, rate: line.rate, discount_pct: line.discount_pct, discount_amount: line.discount_amount, gst_rate_id: line.gst_rate_id, hsn_code_id: line.hsn_code_id, remarks: line.remarks, })); const builtItems = await validateAndBuildItems(sourceItems); const header = await buildHeaderData(merged, builtItems, userId); if (payload.items) { await repository.replacePurchaseOrderItems( id, builtItems.map((item) => ({ ...item, po_id: BigInt(id) })) ); } const updated = await prisma.purchase_orders.update({ where: { id: BigInt(id) }, data: header, include: poDetailInclude, }); await auditLog({ tableName: 'purchase_orders', recordId: id, action: 'UPDATE', oldValue: sanitizePo(existing), newValue: sanitizePo(updated), userId, requestId, }); return sanitizePo(updated); }; const deletePurchaseOrder = async (id, userId, requestId) => { const existing = await getPoOrThrow(id, { includeItems: true }); assertStatus(existing, DELETABLE_STATUSES, 'delete'); if (hasReceipts(existing)) throw new ApiError(409, 'Cannot delete PO with received quantities'); const deleted = await prisma.purchase_orders.update({ where: { id: BigInt(id) }, data: { deleted_at: new Date(), updated_by: userId ? BigInt(userId) : null }, }); await auditLog({ tableName: 'purchase_orders', recordId: id, action: 'DELETE', oldValue: sanitizePo(existing), newValue: deleted, userId, requestId, }); }; const submitPurchaseOrder = async (id, payload, userId, requestId) => { const existing = await getPoOrThrow(id, { includeItems: true }); assertStatus(existing, SUBMITTABLE_STATUSES, 'submit'); if (!existing.purchase_order_items?.length) { throw new ApiError(422, 'PO must have at least one line item before submit'); } const updated = await prisma.$transaction(async (tx) => { await tx.po_approvals.deleteMany({ where: { po_id: BigInt(id) } }); await tx.po_approvals.create({ data: { po_id: BigInt(id), approval_level: 1, status: 'PENDING', }, }); return tx.purchase_orders.update({ where: { id: BigInt(id) }, data: { status: 'PENDING_APPROVAL', remarks: payload.remarks ?? existing.remarks, updated_by: userId ? BigInt(userId) : null, }, include: poDetailInclude, }); }); await auditLog({ tableName: 'purchase_orders', recordId: id, action: 'SUBMIT', oldValue: sanitizePo(existing), newValue: sanitizePo(updated), userId, requestId, }); return sanitizePo(updated); }; const approvePurchaseOrder = async (id, payload, userId, requestId) => { const existing = await getPoOrThrow(id, { includeItems: true }); assertStatus(existing, APPROVABLE_STATUSES, 'approve'); const pendingApproval = existing.po_approvals.find((row) => row.status === 'PENDING'); if (!pendingApproval) throw new ApiError(409, 'No pending approval step found'); const updated = await prisma.$transaction(async (tx) => { await tx.po_approvals.update({ where: { id: pendingApproval.id }, data: { status: 'APPROVED', remarks: payload.remarks || null, approver_user_id: userId ? BigInt(userId) : null, acted_at: new Date(), }, }); return tx.purchase_orders.update({ where: { id: BigInt(id) }, data: { status: 'APPROVED', updated_by: userId ? BigInt(userId) : null, }, include: poDetailInclude, }); }); await auditLog({ tableName: 'purchase_orders', recordId: id, action: 'APPROVE', oldValue: sanitizePo(existing), newValue: sanitizePo(updated), userId, requestId, }); return sanitizePo(updated); }; const rejectPurchaseOrder = async (id, payload, userId, requestId) => { const existing = await getPoOrThrow(id, { includeItems: true }); assertStatus(existing, APPROVABLE_STATUSES, 'reject'); const pendingApproval = existing.po_approvals.find((row) => row.status === 'PENDING'); if (!pendingApproval) throw new ApiError(409, 'No pending approval step found'); const updated = await prisma.$transaction(async (tx) => { await tx.po_approvals.update({ where: { id: pendingApproval.id }, data: { status: 'REJECTED', remarks: payload.remarks, approver_user_id: userId ? BigInt(userId) : null, acted_at: new Date(), }, }); return tx.purchase_orders.update({ where: { id: BigInt(id) }, data: { status: 'REJECTED', updated_by: userId ? BigInt(userId) : null, }, include: poDetailInclude, }); }); await auditLog({ tableName: 'purchase_orders', recordId: id, action: 'REJECT', oldValue: sanitizePo(existing), newValue: sanitizePo(updated), userId, requestId, }); return sanitizePo(updated); }; const cancelPurchaseOrder = async (id, payload, userId, requestId) => { const existing = await getPoOrThrow(id, { includeItems: true }); assertStatus(existing, CANCELLABLE_STATUSES, 'cancel'); if (hasReceipts(existing)) throw new ApiError(409, 'Cannot cancel PO with received quantities'); const updated = await prisma.purchase_orders.update({ where: { id: BigInt(id) }, data: { status: 'CANCELLED', remarks: payload.remarks ?? existing.remarks, updated_by: userId ? BigInt(userId) : null, }, include: poDetailInclude, }); await auditLog({ tableName: 'purchase_orders', recordId: id, action: 'CANCEL', oldValue: sanitizePo(existing), newValue: sanitizePo(updated), userId, requestId, }); return sanitizePo(updated); }; const amendPurchaseOrder = async (id, payload, userId, requestId) => { const existing = await getPoOrThrow(id, { includeItems: true }); assertStatus(existing, AMENDABLE_STATUSES, 'amend'); const sourceItems = payload.items ? payload.items : existing.purchase_order_items.map((line) => ({ item_id: line.item_id, line_no: line.line_no, ordered_qty: line.ordered_qty, uom_id: line.uom_id, rate: line.rate, discount_pct: line.discount_pct, discount_amount: line.discount_amount, gst_rate_id: line.gst_rate_id, hsn_code_id: line.hsn_code_id, remarks: line.remarks, })); const merged = { po_date: payload.po_date ?? existing.po_date, vendor_id: payload.vendor_id ?? existing.vendor_id, billing_id: payload.billing_id ?? existing.billing_id, shipping_id: payload.shipping_id ?? existing.shipping_id, payment_term_id: payload.payment_term_id !== undefined ? payload.payment_term_id : existing.payment_term_id, delivery_term_id: payload.delivery_term_id !== undefined ? payload.delivery_term_id : existing.delivery_term_id, expected_delivery_date: payload.expected_delivery_date !== undefined ? payload.expected_delivery_date : existing.expected_delivery_date, discount_amount: payload.discount_amount ?? Number(existing.discount_amount), freight_charges: payload.freight_charges ?? Number(existing.freight_charges), other_charges: payload.other_charges ?? Number(existing.other_charges), tds_applicable: payload.tds_applicable !== undefined ? payload.tds_applicable : existing.tds_applicable, tds_section_pct: payload.tds_section_pct !== undefined ? payload.tds_section_pct : existing.tds_section_pct !== null && existing.tds_section_pct !== undefined ? Number(existing.tds_section_pct) : null, adjustment: payload.adjustment !== undefined ? payload.adjustment : Number(existing.adjustment ?? 0), terms_and_conditions: payload.terms_and_conditions !== undefined ? payload.terms_and_conditions : existing.terms_and_conditions, remarks: payload.remarks !== undefined ? payload.remarks : existing.remarks, items: sourceItems, }; const builtItems = await validateAndBuildItems(sourceItems); const header = await buildHeaderData(merged, builtItems, userId); const amended = await repository.createAmendedPurchaseOrder({ header: { ...header, status: 'DRAFT', revision_no: existing.revision_no + 1, parent_po_id: BigInt(id), created_by: userId ? BigInt(userId) : null, }, items: builtItems, }); await prisma.purchase_orders.update({ where: { id: BigInt(id) }, data: { status: 'CLOSED', updated_by: userId ? BigInt(userId) : null, }, }); const created = await getPoOrThrow(amended.id, { includeItems: true }); await auditLog({ tableName: 'purchase_orders', recordId: id, action: 'AMEND', oldValue: sanitizePo(existing), newValue: sanitizePo(created), userId, requestId, }); return sanitizePo(created); }; const getPurchaseOrderPdf = async (id) => { const po = sanitizePo(await getPoOrThrow(id, { forPdf: true })); const company = await getCompanyForDocuments(); const html = generatePoHtml(buildPoPdfPayload(po, company)); const buffer = await generatePdf(html); return { filename: `${po.po_number.replace(/\//g, '-')}.pdf`, buffer, }; }; module.exports = { createPurchaseOrder, listPurchaseOrders, listPendingApprovalPurchaseOrders, exportPurchaseOrders, getPurchaseOrderById, updatePurchaseOrder, deletePurchaseOrder, submitPurchaseOrder, approvePurchaseOrder, rejectPurchaseOrder, cancelPurchaseOrder, amendPurchaseOrder, getPurchaseOrderPdf, };