1 Inventory on Approval
Hardik edited this page 2026-06-21 03:00:58 +05:30

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 the if (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 Sites admin 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 ItemInventory upsert in approvePo() (section 1) does not check INVENTORY_ENABLED. It runs purely on the po.siteId condition. 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:

  1. POs don't carry a siteId. The cost centre is a Vessel — PO create/edit/import forms set vesselId, never siteId (siteId remains an optional legacy column on PurchaseOrder). The if (siteId) guard is therefore almost always false, so nothing is written.
  2. Even with a siteId, line items may be skipped. Items are counted only when they already have a productId. Products are auto-linked/created at payment (syncProductCatalog in payments/actions.ts), so line items typed by hand have no productId at 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 the siteId precondition 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.siteId is 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 ItemInventory upsert honour INVENTORY_ENABLED so "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=false in prod to hide the empty screens, and delete/flag-gate the dormant write.