docs: add Tech Debt register and Inventory-on-Approval deep-dive

- New "Inventory on Approval" page: intended design (add ordered items to the
  delivery site stock at approval), the NEXT_PUBLIC_INVENTORY_ENABLED flag
  (UI-only gate), and how it actually behaves in prod -- dormant, because POs
  carry no siteId under the vessel-only cost-centre model and the write is not
  flag-gated. Includes remediation options.
- New "Tech Debt" register (Engineering section): TD-1 inventory-on-approval
  dormant path; TD-2 migrations not coupled to the build (caused the paymentDate
  P2022 prod incident).
- Sidebar links both pages; Inventory and Catalogue corrects the approval line
  and cross-links the new pages.
Hardik 2026-06-21 03:00:58 +05:30
parent 67cd529f84
commit c6bec19d85
4 changed files with 191 additions and 3 deletions

@ -72,9 +72,11 @@ surfaces are hidden; the vendor/product catalogue and cart remain available.
- **Sites** (`/admin/sites`) — ports/depots/offices that hold stock; geocoded
from pincode; vessels can be associated.
- **`ItemInventory`** — quantity per `(product, site)`. **Incremented at PO
approval** (not on close) for the ordered quantities, when the PO has a
`siteId`.
- **`ItemInventory`** — quantity per `(product, site)`. **Designed to be
incremented at PO approval** (not on close) for the ordered quantities, when
the PO has a `siteId`. ⚠️ In production this rarely fires — POs are raised
against a Vessel and carry no `siteId`, and the write isn't gated by the flag.
See [Inventory on Approval](Inventory-on-Approval) and [Tech Debt](Tech-Debt).
- **`ItemConsumption`** — daily draw-down per `(product, site, date)`, recorded
via the "Log Consumption" form on the site detail page (with `recordedBy`).

116
Inventory-on-Approval.md Normal file

@ -0,0 +1,116 @@
# 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](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`:
```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`:
```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](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](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.
## Related
- [Inventory and Catalogue](Inventory-and-Catalogue) — products, pricing, cart, sites.
- [PO Lifecycle](PO-Lifecycle) — where approval sits in the flow.
- [Tech Debt](Tech-Debt) — the open items captured above.

66
Tech-Debt.md Normal file

@ -0,0 +1,66 @@
# Tech Debt
A running register of known shortcuts, dormant code paths, and
design/implementation mismatches that we've consciously accepted for now. The
goal is visibility: each item records **what**, **why it matters**, and a
**suggested direction** — not a commitment to fix on any timeline.
> Add new items at the top of the "Open" list with a short, honest description.
> Move resolved items to "Resolved" with the commit/PR that closed them.
## Open
### TD-1 · Inventory-on-approval is dormant in production
**What.** Approving a PO is meant to add its ordered items to the delivery
site's stock (`ItemInventory`, keyed by `(productId, siteId)`). In production the
write almost never fires, and it isn't governed by the inventory feature flag.
**Why it matters.**
- POs are raised against a **Vessel** (cost centre), and PO forms set `vesselId`,
never `siteId`. The `if (po.siteId)` guard in `approvePo()` is therefore
almost always false → no inventory is written. Inventory screens render
(the flag defaults on) but stay empty.
- The `ItemInventory` upsert is **not gated by `NEXT_PUBLIC_INVENTORY_ENABLED`**
the flag only hides UI. So "inventory off" would still mutate data if a PO ever
did carry a `siteId`.
- Line items are counted only when they already have a `productId`, but products
are linked/created at **payment**, not approval — so hand-typed items would be
skipped even with a site.
- Root cause: inventory is modelled per **Site**, but POs are per **Vessel**, and
nothing maps a PO to a Site.
**Suggested direction.** Decide one: (a) give POs a Site (vessel→home-site link
or an explicit delivery-site field) and link products at approval; or (b) gate
the write on the flag / remove it and set `NEXT_PUBLIC_INVENTORY_ENABLED=false`
in prod to hide the empty screens. Full write-up:
[Inventory on Approval](Inventory-on-Approval).
**Touch points.** `app/(portal)/approvals/[id]/actions.ts` (the upsert),
`lib/feature-flags.ts`, `PurchaseOrder.siteId`, `payments/actions.ts`
(`syncProductCatalog`).
---
### TD-2 · Migrations are not coupled to the build
**What.** `pnpm build` runs `prisma generate` (TypeScript client) but **not**
`prisma migrate deploy`. Shipping code whose client expects a new column before
the DB has it throws `P2022 … column does not exist` at runtime.
**Why it matters.** This already caused a production incident (the `paymentDate`
column). The deploy workflow (`.forgejo/workflows/deploy.yml`) does run
`migrate deploy`, but **manual** deploys can skip it.
**Suggested direction.** Treat "apply migrations before the new build serves
traffic" as the invariant. Options: fold `prisma migrate deploy` into the
`start`/release script, or add a pre-deploy check that fails if migrations are
pending. Documented in the App `README.md`.
**Touch points.** `App/package.json` scripts, `.forgejo/workflows/deploy.yml`,
`App/README.md`.
## Resolved
_None yet._

@ -24,6 +24,7 @@
- [Purchase Orders](Purchase-Orders)
- [Vendors and GST Lookup](Vendors-and-GST-Lookup)
- [Inventory and Catalogue](Inventory-and-Catalogue)
- [Inventory on Approval](Inventory-on-Approval)
- [Notifications](Notifications)
- [File Storage](File-Storage)
- [Design System](Design-System)
@ -42,3 +43,6 @@
**Ops**
- [Deployment and Operations](Deployment-and-Operations)
- [Issue-to-Deploy Pipeline](Issue-to-Deploy-Pipeline)
**Engineering**
- [Tech Debt](Tech-Debt)