Skip to main content

Place hierarchy & visibility

Places have no state machine — a place is either alive or soft-deleted, like any reference-data row. What does need explaining is how the place tree is stored and rebuilt, and how the same tree drives what each user is allowed to see across the rest of the platform.

Two representations of one tree

Every place carries both an adjacency link and a nested-set range:

  • ParentPlaceId — the source of truth. This is what you set on create and update.
  • LeftBower / RightBower — a derived nested-set encoding used for fast subtree queries ("this place and everything under it" becomes a single range comparison).

A subtree is every place whose bowers fall inside the ancestor's range — here, Aisle 1's subtree is everything with bowers between 2 and 5.

Bowers are regenerated tenant-wide

The server recomputes the nested set with [dbo].[RebuildNestedSets] on:

The rebuild renumbers the whole tenant's tree, not just the branch you touched. Practical consequence: do not cache LeftBower/RightBower across writes — any place mutation anywhere in the tenant can change every place's bowers. Treat bowers as a server-side query mechanism, and ParentPlaceId as the durable shape of the tree.

Exceptions to the rebuild

OperationRebuild behaviour
deleteplaceDoes not rebuild. The subtree is re-parented to the deleted node's parent, the deleted row's bowers are zeroed to 0/0, and the surviving bowers keep a gap until the next create or parent change closes it.
DeleteSystemPlaceDoes rebuild.
UpdatePlaceStructureClient-authoritative — the one endpoint where the bower values you send are persisted verbatim, with no rebuild. Only use it if you are deliberately supplying a complete, valid nested set.
No cycle guard — a bad parent move silently corrupts the tree

updateplace performs no existence or cycle validation on ParentPlaceId. A move that creates a cycle (e.g. re-parenting a place under its own descendant) returns HTTP 200 — and RebuildNestedSets silently drops the cycle members from the nested set, leaving their stale bowers colliding with the renumbered live tree. From that point, every bower-range consumer returns wrong data: subtree queries, visibility expansion, missing-item sweeps, and dashboards. The tree self-heals only when a valid re-parent triggers the next full rebuild. Until a server-side guard lands, validate parent moves client-side — never set ParentPlaceId to the place itself or any of its descendants. (Verified live 2026-07-04; a server-side cycle guard is under owner review.)

User place visibility

Users can be restricted to a subset of the place tree. A place-filtered user sees their granted places plus all descendants — the grant expands down the subtree via the same nested-set ranges described above.

The staleness window

Hierarchy changes and visibility grants re-seed the cache asynchronously via a Hangfire job. After a place move or a grant/revoke, a filtered user can briefly see a stale subtree — a just-granted place may not appear immediately, and a just-moved branch may still show under its old scope until the reload completes.

The same cache drives visibility of items, orders, and persons — not just place lists. A user who can't see a place also can't see the items located there or the orders scoped to it, which is why a corrupted tree (see the warning above) or a stale cache surfaces as "missing" records in completely unrelated list endpoints.