Inventory on Approval
How site inventory is (meant to be) updated when a PO is approved, the feature flag that governs the inventory surfaces, and how it actually behaves in production today — which diverges from the intended design.
TL;DR — The design is "approving a PO adds its ordered items to the delivery site's stock immediately." In production this path is effectively dormant: POs no longer carry a
siteId(cost centre is a Vessel), so theif (po.siteId)guard in the approve action is almost never true, and the write isn't gated by the inventory feature flag anyway. See Tech Debt for the open items.
1. What the design is meant to do
When a Manager approves a PO, the ordered line items should be added to the delivery site's on-hand stock right away — so inventory reflects orders-in-flight rather than waiting for delivery/closure.
- Inventory lives in
ItemInventory, keyed by(productId, siteId). - On approval, for each line item the ordered quantity is incremented onto that product's stock at the PO's site.
- This replaced an earlier design where inventory was only updated at receipt confirmation (close). The intent of moving it to approval was to surface committed demand sooner on the site dashboards.
Implementation lives in approvePo() —
app/(portal)/approvals/[id]/actions.ts:
// after the PO is set to MGR_APPROVED …
const siteId = po.siteId ?? null;
if (siteId) {
for (const li of po.lineItems) {
if (!li.productId) continue; // unlinked items are skipped
await db.itemInventory.upsert({
where: { productId_siteId: { productId: li.productId, siteId } },
update: { quantity: { increment: Number(li.quantity) } },
create: { productId: li.productId, siteId, quantity: Number(li.quantity) },
});
}
revalidatePath(`/admin/sites/${siteId}`);
}
2. The feature flag
NEXT_PUBLIC_INVENTORY_ENABLED — read once in lib/feature-flags.ts:
export const INVENTORY_ENABLED =
process.env.NEXT_PUBLIC_INVENTORY_ENABLED !== "false";
- On unless explicitly
"false". Unset ⇒ enabled. - The flag only controls UI visibility of the inventory tracking surfaces:
the
Sitesadmin area, per-site stock tables, the consumption charts/form, and the Sites sidebar link. The vendor list, product catalogue, and cart used for PO creation are always available, regardless of the flag. - The flag does not gate the data write. The
ItemInventoryupsert inapprovePo()(section 1) does not checkINVENTORY_ENABLED. It runs purely on thepo.siteIdcondition. So "inventory disabled" hides the screens but does not stop the approval-time mutation.
3. How it functions in production right now
Production does not set NEXT_PUBLIC_INVENTORY_ENABLED, so
INVENTORY_ENABLED is true and the inventory screens are visible. Despite
that, the auto-increment is effectively a no-op:
- POs don't carry a
siteId. The cost centre is a Vessel — PO create/edit/import forms setvesselId, neversiteId(siteIdremains an optional legacy column onPurchaseOrder). Theif (siteId)guard is therefore almost always false, so nothing is written. - Even with a
siteId, line items may be skipped. Items are counted only when they already have aproductId. Products are auto-linked/created at payment (syncProductCataloginpayments/actions.ts), so line items typed by hand have noproductIdat approval and are skipped.
Net effect in prod: the Sites / inventory pages render (flag is on) but stay empty, because the on-approve write rarely — in practice, never — fires for a normally-created PO. Inventory is visible but unpopulated.
The slightly different statement in Inventory and Catalogue ("incremented at PO approval … when the PO has a
siteId") is technically correct but reads as if the path is live; in the current vessel-only model thesiteIdprecondition is the reason it isn't.
4. Why it diverges (root cause)
Inventory is modelled per Site, but POs are now raised against a Vessel (the Vessel-or-Site cost-centre model was collapsed to Vessel-only). Nothing in the current flow maps a PO to a Site, so the per-site write has no site to target.
5. Options to make it match the design (or retire it)
Pick one direction and apply consistently — tracked in Tech Debt:
- Give POs a site. Derive a Site from the chosen Vessel (e.g. a vessel→home-site
link) or add an explicit "delivery site" field, so
po.siteIdis populated. - Link products at approval. If inventory-on-approve is to mean anything, ensure line items are product-linked (or create products) before the upsert, rather than only at payment.
- Gate the write on the flag. Make the
ItemInventoryupsert honourINVENTORY_ENABLEDso "off" has no data side effects — or remove the approval-time write entirely if the feature is being parked. - Retire it. If inventory tracking isn't a near-term priority, set
NEXT_PUBLIC_INVENTORY_ENABLED=falsein prod to hide the empty screens, and delete/flag-gate the dormant write.
Related
- Inventory and Catalogue — products, pricing, cart, sites.
- PO Lifecycle — where approval sits in the flow.
- Tech Debt — the open items captured above.
Pelagia Portal (PPMS)
Overview
Build & Run
System
Product
- Feature Catalogue
- Pages and Navigation
- Workflows
- Purchase Orders
- Vendors and GST Lookup
- Inventory and Catalogue
- Inventory on Approval
- Notifications
- File Storage
- Design System
Planned
Quality
Ops
Engineering
Pelagia Portal (PPMS) — internal purchase-order management. Self-hosted on pms1, live at pms.pelagiamarine.com. This wiki tracks the shipped product; authoritative sources are the repo code, App/CLAUDE.md, Docs/, and CHANGELOG.md.