Phase 4 PO Flow Follow-up — Findings¶
Status: closed (2026-05-17) — all six post-merge follow-ups shipped
Tracks the implementation of the Phase 4 PO Flow Plan (design locked 2026-05-15). All seven design PRs merged 2026-05-15 → 16; four staging-deploy fixes (#101, #107, #108, #110) ironed out the Medusa-runtime gotchas; full smoke green on CT 105 covering D1–D8. Six post-merge follow-ups closed out 2026-05-17 via #127 + #129 (see Follow-up status below). PR #130 added import-picker E2E coverage, aligned dashboard React to react-dom, and documented the Cloudflare Access static-asset bypass required for client hydration on staging.
What shipped¶
| PR | Title | Surface | GitHub |
|---|---|---|---|
| PR-1 | po_date + expected_delivery_date + overdue chip (D6) |
Server migration + DTOs + dashboard form + list-view chip | #94 |
| PR-2 | Explicit quantity_received counter on import_batch_item |
Server migration + model field | #100 (PR #95 was cascade-closed when PR-1 merged with --delete-branch — same workaround as the Phase 4 wire-up cascade) |
| PR-3 | Variant picker + stock preview + landed-cost preview service | Combobox + 2 server endpoints + service | #96 |
| PR-4 | import_batch_receipt table + receiveBatchItem workflow + status enum (D2/D3/D4/D5/D7/D8) |
Migration + model + workflow + 2 endpoints + DTOs | #97 |
| PR-5 | Dashboard partial-receive panel + receipt history + D7 overage toggle | Per-line UI + history table + server actions | #98 |
| PR-6 | Receive workflow integration tests | 3 new test cases on admin-dashboard-imports.spec.ts |
#99 |
| PR-7 | This findings doc | Docs hub | this PR |
Stack¶
All seven PRs are linearly stacked. Merge order must be 1 → 7. After PR-1 merges, the rest will auto-retarget; downstream branches may need a git rebase --onto main <old-base-sha> to drop the squashed commit ([reference_medusa_subscriber_wiring]'s sibling gotcha applies — see Phase 4 Wire-up Findings closing notes).
Staging-deploy bugs (4)¶
Deploying the merged stack to CT 105 surfaced four bugs the local PR-by-PR work hadn't caught. All shipped as one-line follow-up PRs:
| PR | Title | Symptom | Root cause |
|---|---|---|---|
| #101 | Fix partially_received enum migration |
ALTER TYPE … ADD VALUE failed with type does not exist |
Medusa DML enums render as text columns + CHECK constraint, not Postgres enum types. Fix: drop + recreate the check constraint with the new value included. |
| #107 | Rename receive step to avoid . in id |
Every receive POST returned 500 with TransactionOrchestrator.canContinue reading .id of undefined |
Step name "receive-batch-item.run" contains a dot. The orchestrator parses parent step IDs by splitting on ., so the dot made the parent lookup fail. Earlier multi-step iterations used dotted names like "receive-batch-item.validate" and crashed identically — the bug was the dot, not the composition. One-character fix: . → -. |
| #108 | Add raw_effective_cost_at_receipt_sgd column |
Receipt insert failed: column raw_effective_cost_at_receipt_sgd does not exist |
Medusa model.bigNumber() requires a paired raw_<field> JSONB column for the high-precision representation. The PR-4 migration only created the numeric column. Pattern matches batch_allocation.raw_cost_per_unit_at_allocation and batch_adjustment.raw_cost_delta_per_unit. |
| #110 | Set applied_at on auto-overship adjustment (closes #109) |
D7 force=true returned 422 with Value for BatchAdjustment.applied_at is required |
The auto-create payload for the overship batch_adjustment row didn't set applied_at. The manual adjustment routes already do — this auto-path now matches. |
Smoke results (2026-05-16, CT 105)¶
End-to-end against the deployed Medusa, with all four staging fixes merged:
| Check | Result |
|---|---|
D1 — po_date + expected_delivery_date round-trip |
✅ |
| D2/D3/D4 — partial → arrived auto-transition | ✅ partially_received → arrived |
| D5 — receipt rows with cost snapshot | ✅ 3 rows in history |
D7 — over-receive without force rejected with 422 + helpful message |
✅ |
D7 — force=true creates overage adjustment and records receipt |
✅ |
| D8 — consolidation reject | ✅ (covered by integration test in #99) |
| Receipt history endpoint returns chronological list | ✅ |
Deviations from the locked design¶
D-1 — quantity_received was added fresh, not "dropped + recreated"¶
The locked plan (D2) described the column as a generated column that PR-2 would drop and recreate as plain. In reality the wire-up never landed quantity_received at all — it landed received_into_inventory_at (single timestamp) instead. PR-2 adds the column fresh; backfill comes from the timestamp (non-null → quantity_ordered, null → 0). The data-model §4.2 description was reconciled to the explicit-counter implementation in the same closeout pass.
D-2 — Status enum migration is single-step¶
The plan flagged Risk R-B (ALTER TYPE in a single transaction is tricky). Medusa's migration runner commits each addSql() sequentially, so the single migration in PR-4 works. No two-transaction split needed.
D-3 — OVERDUE_PENDING_STATUSES narrowed in PR-1, widened in PR-4¶
PR-1 ships the D6 overdue chip with status set {ordered, paid, in_transit} — partially_received doesn't exist until PR-4. PR-4 widens the set to include partially_received. Net effect after the stack merges: chip behaves per the locked design.
D-4 — Variant-link inventory write is null-safe¶
The receive workflow's step 4 (incrementInventoryStep) returns null inventory_level_id if the line has no linked product_variant or the variant has no inventory_item. The receipt + qty counter + status transition still fire. This deviates from a strict reading of the design (which assumed every line has a linked variant) but matches the wire-up's existing behavior — line items without variant links are common during the migration period.
D-5 — Receipt history endpoint is per-item, not per-batch¶
The plan didn't specify a receipts-listing endpoint. PR-4 added GET /admin/dashboard/imports/:id/items/:itemId/receipts (per-item, for the dashboard history table). If a per-batch endpoint is needed later, it's a one-line server-side addition.
Smoke (proven via curl, pending in-browser pass)¶
The smoke above is the curl/API path. The dashboard pass (PR-1, PR-3, PR-5 UI surfaces) is the next operator-facing milestone:
- UI smoke (PR-1) — open
/imports/new, today's date pre-fillsPO date; setExpected delivery = 2026-05-01(past); save; list view showsOverdue: N dayschip - UI smoke (PR-3) — open
/imports/new, type a known SKU substring, picker shows match with stocked qty; pick variant; set qty + invoice; landed-cost preview updates as you type - UI smoke (PR-5) — open a PO in
in_transit; receive panel renders per-line; submit qty=3 → status →Partially received, history table shows the receipt; submit qty=remaining → status →Arrived - D7 UI smoke — submit qty > expected → overage toggle appears; tick + submit → succeeds +
batch_adjustment(reason=quantity_correction)recorded
Follow-up status¶
All six post-merge follow-ups closed out 2026-05-17 by #127 (bundled items 1, 2, 5, 6) and #129 (item 3 + the link-resolution bug it surfaced). Item 4 turned out to be already-handled docs drift; reconciled in this closeout.
| # | Follow-up | Resolution |
|---|---|---|
| 1 | Adjustments in the detail response — PartialReceiveSection treated qtyDeltaByItem as {} because the detail endpoint didn't ship adjustments. |
Closed by #127. ImportBatchDetail now includes adjustments: BatchAdjustmentItem[]; the dashboard's PO page builds qtyDeltaByItem from it (running counter reflects shortfall / overship correctly). |
| 2 | defineLink(import_batch_receipt ↔ inventory_level) — promote inventory_level_id to a Module Link. |
Re-scoped + closed by #127. A true defineLink isn't buildable — Medusa's inventory module joinerConfig only exposes InventoryItem and ReservationItem as linkables, not InventoryLevel (@medusajs/inventory/dist/joiner-config.js). The receipt row's existing text column is now actually populated by the workflow's step-4b backfill once the inventory increment succeeds. |
| 3 | Variant-link + inventory-write integration fixture — tests exercised the null-variant branch only. | Closed by #129. New "variant-linked receive" test seeds product + variant + inventory_item + stock_location and asserts inventory_level.stocked_quantity increments correctly across both create-new and update-existing branches. Surfaced + fixed issue #128 in the same PR (workflow's linkSvc.list returned empty for valid link rows — replaced with query.graph traversal). Smoke on CT 105 confirmed end-to-end: stocked_quantity went 100 → 103 → 104 across two receives. |
| 4 | data-model.md update for §4.2 quantity_received. |
Already done by an earlier pass — §4.2 line 152 reads "Explicit counter maintained by receiveBatchItem; starts at 0 and increments on physical receipt events." Stale follow-up; the deviation paragraph above has been reconciled. |
| 5 | Wire-up ReceiveSection component dead code — receive-section.tsx + receive-form.tsx no longer rendered. |
Closed by #127. Both files deleted; PartialReceiveSection / PartialReceiveForm are the live components. |
| 6 | Legacy /imports/:id/receive endpoint deprecation. |
Closed by #127. Endpoint stays mounted but now emits a [deprecated] warn log + Deprecation: true + Link: <…/items/:itemId/receive>; rel="successor-version" response headers. Removal slated once the warn log shows zero hits for two release cycles. |
Post-closeout hardening¶
PR #130 closed the staging-only import-picker hydration regression follow-up:
apps/dashboardnow pinsreactto19.2.6to matchreact-dom./imports/newhas Playwright coverage for variant search, result selection, and the line-items dropdown clipping regression.apps/dashboard/README.mddocuments the required Cloudflare Access Bypass application for/_next/static/*,/_next/image*, and/favicon.ico. Without that bypass, Access redirects module/script asset requests toexzen.cloudflareaccess.com, SSR HTML renders, and client-only widgets such as the variant picker never hydrate.
Risks ratified at code-complete¶
| Risk | Status |
|---|---|
| R-A: in-flight POs at migration time | Mitigated — staging has known state (smoke-validated batch from wire-up) |
| R-B: enum migration split | Single-step works in Medusa's migration runner; no split needed |
| R-C: overship cost recompute makes per-unit cheaper | Confirmed correct-but-surprising; PR-6 test doesn't assert recompute math (worker-mode subscriber fires async; would need polling). Captured for staging smoke. |
Links¶
- Phase 4 PO Flow Plan — design lock (D1–D8)
- Phase 4 Wire-up Findings — predecessor closeout
- Data Model — §4.2 DCA tables (needs update per follow-up #4)
- tcg-platform issue #93 — tracking issue (closed 2026-05-16 with this findings doc as the close comment)
- Design PR stack: #94 · #100 · #96 · #97 · #98 · #99
- Staging-deploy fixes: #101 · #107 · #108 · #110