Skip to content

Places contain places and stock and space by any field — and #31's document flows, which never reached main - #32

Merged
goetchstone merged 2 commits into
mainfrom
feat/stock-places-tree
Oct 16, 2026
Merged

goetchstone merged 2 commits into
mainfrom
feat/stock-places-tree

Conversation

@goetchstone

@goetchstone goetchstone commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Summary

PR 3 of the stock-ledger plan (docs/domains/inventory.md → "Delivery"): phases 12–14, places and space.

This PR also brings PR 2 to main. #31 merged into its stacked base, feat/stock-places-and-journal, 27 seconds after #30 merged that branch into main, so #31's work never reached main. This branch is main plus that same commit (its tree is identical to #31's merge, a4129e1), plus this PR's. One PR and one CI run instead of two. The first commit was already reviewed as #31; the second is new.

  • 12. Places contain places. StockLocation.parentId nests places as deep as a store wants: North Barn › Floor 2 › Stock Sofas › Shelf 3. A small store stays flat. A new GROUP type (a building, a floor) holds only places, and the ledger refuses stock into one. One set of rules in lib/inventory/placeTree.ts, used by the routes and the preset (rule 42):

    • a parent is an active place at the same store;
    • a place is never inside itself, however deep the loop;
    • a place holding stock, or one a store lands stock in, never becomes a group, and no landing place can point at a group;
    • a place with active places inside it stays active, and one with any places inside it is not deleted.

    Warehouse › Locations shows each store's places as a tree, with an "Inside" picker. Square footage can now be entered for any place. Before, it was sent only for floor places, so saving any other place wiped its footage.

  • The migration (20261018120000_places_contain_places) turns the free-text building/floor columns into GROUP places and nests every place inside them. area was an index that each place's code already carried, so it is dropped. The migration refuses to finish if any place would lose its building or floor.

  • 13. Stock and space (Reports › Stock and Space). Look inside every store, a store, or any place, and group by the places inside it, place type, department, category, type, vendor or collection.

    • Each unit counts toward its nearest measured place, and that place's footage is split by unit share.
    • Empty measured places show as "Empty space"; units with no measured place above them show as "Unmeasured units".
    • A measured place inside another measured place is flagged, because that floor would count twice.
  • 14. The stock-locations preset says where a place is with inside. It refuses the retired building/floor/area fields rather than dropping them: dropping them would move every place of an old file to the top of its store. An export always applies back unchanged.

What changed

  • Migration:
    • adds StockLocation.parentId (FK ON DELETE RESTRICT);
    • creates the GROUP places BLDG-<name> and FLR-[<building>-]<n>, parented to their building;
    • parents every place;
    • drops building, floor and area.
  • Lib:
    • new: inventory/placeTree.ts (descendantIds, ancestry, placePath, treeOrder, placeParentProblem, placeTypeProblem, placeDeactivateProblem) and reports/stockBy.ts;
    • locationTypes.ts: GROUP and holdsStock;
    • ledger.ts: assertPlaceAtStore refuses a group;
    • landingPlaces.ts: landingAllows, so "any type" means any type that holds stock, shared by the store route and the screen;
    • config: presetSchema (inside, loop and unknown-code checks, retired fields refused), presetSerialize, dbConfigState (exports inside only for an active parent), applyPreset (parents set after every listed place exists; refuses making a group of a place holding stock).
  • Routes:
    • the stock-location create and update routes validate the parent, the type change and deactivation, and delete is refused while places are inside;
    • the store routes and the lean picker send parentId;
    • new: /api/reports/stock-by.
  • Screens:
    • Warehouse › Locations: the tree view, plus "Inside" and square footage for every type in the stock-location dialog;
    • new: Reports › Stock and Space (card and page permission reporting.read).
  • Docs:
    • inventory.md: "Places contain places"; phases 12–14 ticked; place labels moved to phase 15, where the scanner reads them;
    • config-presets.md: inside and the retired fields;
    • reporting.md: Stock and Space, plus the Oversold row it was missing.

Trace / Verification

  • Preflight on a restored deployment database (read-only; numbers only): 60 places; 42 have a building, across 3 buildings; 54 have a floor; every area number also appears in the place's code.
  • The migration:
    • on a fresh copy of that database, taken through the earlier migrations and this one, then dropped: 13 GROUP places (8 floors inside a building), 54 of 60 places parented, stock unchanged at 19,317 rows and 43,146 units;
    • on a fresh scratch database built by npm run setup -- --scale=ci (all migrations, then the seed): prisma migrate diff --exit-code printed No difference detected.
  • Mutation checks, each restored afterwards:
    • no loop check in placeParentProblem: stockPlaceTree 1/7 failed;
    • the ledger takes stock into a group: 1/7;
    • any place may become a group: stockPlaceTree 2/7, stockLocationsPreset 1/13;
    • space counted only at the unit's own place: splitSpaceBy 2/6, stockBy 4/6;
    • retired fields dropped silently: stockLocationsPreset (unit) 1/13;
    • the preset never sets parents: 5/13;
    • a landing place may be a group: stockPlaceTree 1/7;
    • the export names an inactive parent: stockLocationsPreset 1/13;
    • deactivating a place with places inside: stockPlaceTree 1/7.
  • In the browser, on that seeded scratch database:
    • Warehouse › Locations: a GROUP place "North Barn" (5,000 sq ft) was created, and Back Stock was put inside it. The tree shows Back Stock indented under the barn, and its 1,800 sq ft survived the save.
    • This is where I found that the Returns-place picker offered the group. Returns allows "any" type, which now included GROUP; fixed with landingAllows, and after a reload no landing-place picker offers it.
    • Reports › Stock and Space, every store by place: Central Distribution Warehouse 560 units / 25,500 sq ft; Millbrook 275 / 11,000; Wintergreen 312 / 7,500; Empty space 5,000 (the barn); Unmeasured units 8 (in transit). The double-count warning names Back Stock inside North Barn.
    • The same store by department splits Millbrook's floor across eight departments. ?by=colour answers 400 with the list of fields.
    • No console errors on either screen, apart from one hot-reload error caught mid-edit, which cleared on reload.
  • Callers:
    • building/floor/area have no readers left in src/, prisma/seed or the shipped config/presets.
    • The landing-place type check had two readers, the store route (landingPlaceProblem) and LocationsView; both now call landingAllows.
    • placeTypeProblem is called from the stock-location update route and applyPreset; placeParentProblem from both stock-location write routes.

Scan / gate state

npm run check:local on the branch:

Step Result
1 validate ✓ (0 errors; warnings 935, unchanged from #31)
2 test:coverage (all unit + all integration, merged) ✓: statements 69.84%, branches 58.28%, functions 75.09%, lines 71.09% (#31: 69.45 / 57.89 / 74.72 / 70.74)
3 Sonar not run: no SONAR_TOKEN locally
4 markdownlint ✓
5 Semgrep ✓ 0 findings
6 OSV skipped (no lockfile change)

The run covered the final code: nothing changed after it except the rebase onto main, whose resulting tree is byte-identical to the tree that was checked. CI has never run on the first commit (#31's work): the workflows run only on PRs into main, and #31 targeted a feature branch. This PR is its first CI run.

Not in this PR, and why

  • Place labels (L-<id>) move to phase 15 (PR 4), with the scanner that reads them.
  • Space is split by unit share. Splitting by product footprint waits until enough products carry dimensions (rule 41); the choice is noted in stockBy.ts and reporting.md.

Test plan

  • CI: Lint/Typecheck/Format/Test, integration, "Migrations match the schema", setup/seed/smoke, Semgrep, osv-scanner
  • After merge and the local rebuild: Warehouse › Locations at a local instance with buildings and floors shows the buildings and floors as groups with their places inside; Reports › Stock and Space for one store

🤖 Generated with Claude Code

goetchstone and others added 2 commits October 16, 2026 10:19
…nt, transfers go through transit, receipts and returns land in the right place, one pull list, scoped allocation, and an Oversold report

Ledger plan PR 2 of 4 (phases 5-11; docs/domains/inventory.md, "Document
flows"). Stacked on #30.

- Landing places per store (receiving, returns, quarantine, transit, pickup),
  the store an order also draws from, and a move mode, set in Warehouse >
  Locations and validated to be places of the right type at that store.
- 5: the POS stock import plans one physical target per place from the whole
  file (aliases summed, zero rows honoured, dropped products zeroed at covered
  places) and sets free stock to physical minus committed; a second run writes
  nothing. The catch-all store is AppSettings.unmatchedStockStoreLocationId
  (Settings > Configuration), replacing a store found by the name "Warehouse"
  (rule 61); unset, unmatched rows are reported, not written (rule 63).
  DECISIONS #18.
- 6: shipped units wait in the source store's transit place; receipts can be
  partial; cancel returns units to the exact places they left from the journal;
  an order's transfer carries its commitment; quick vs tracked move modes.
- 7: damaged/wrong goods go to quarantine; a PO line for an order arrives
  committed to it; zero-quantity lines leave the PO status count (rule 39).
- 8: owner direction -- a return never holds up the customer (DECISIONS #19).
  RECEIVED puts the piece in the store's returns place; a per-store returned-
  stock queue puts it back or writes it off; the journal reads that recorded
  disposition. Return.quantity is fractional. The New Return screen sent
  orderId to a route that reads salesOrderId, so every return from it failed.
- 9: one pull-list builder for run and order pick lists, active lines only
  (rule 33), from the order's own committed stock.
- 10: allocation draws back stock in reach (own store, then its fulfilment
  store) before floor models; take-with takes its own store's floor piece
  first; order resync is a per-product delta so commitments in transit stay.
- 11: one open exception per order and product, closed automatically once
  covered; Reports > Oversold with suggested fixes and CSV; dashboard counts.

Part rolls parse as fractions on the transfer, receive and return forms. The
returns status route takes sales.return (warehouse staff work the queue); the
inventory-exceptions page takes inventory.adjust like its API.

Tests: importSync (unit), allocationScope (unit), and stockImportSync,
stockTransfer, stockReceive, stockReturns, pullList, allocationScope,
oversoldReport integration suites; existing suites updated. Seven mutations
(alias summing, delta resync, committed receipts, active-lines filter, draw
order, disposition claim, cancel legs) each fail their test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cation.parentId nests buildings, floors and bays, a GROUP holds only places, and Reports › Stock and Space splits each measured floor over the units in it

Ledger plan phases 12-14 (docs/domains/inventory.md).

- Migration 20261018120000_places_contain_places turns the free-text
  building/floor columns into GROUP places, parents every place, drops
  building/floor/area, and refuses to finish if a place would lose its
  building or floor.
- lib/inventory/placeTree.ts holds the tree's rules for every writer
  (rule 42): same-store active parent, no loops, no group made of a place
  holding stock or landing it, no deactivating a place with active places
  inside, no delete with places inside. The ledger refuses stock into a
  group; a landing place never points at one (landingAllows).
- Warehouse > Locations shows the tree, with "Inside" and square footage
  for every type (it was sent only for floor places, so saving any other
  place wiped it).
- lib/reports/stockBy.ts and /app/reports/stock-by: units and space inside
  every store, a store or a place, by place, place type or product field.
- The stock-locations preset says `inside`, refuses the retired fields
  instead of dropping them, and exports `inside` only for an active parent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@goetchstone
goetchstone merged commit 3e5ef65 into main Oct 16, 2026
12 checks passed
@goetchstone
goetchstone deleted the feat/stock-places-tree branch October 17, 2026 16:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant