# Branch Isolation Plan ## Summary Today `Branch` exists only inside HRM (`docs/12-BACKEND-HRM.md` C.1) — a bare org unit (`BranchId, Code, Name, Address?, Status`) referenced by `Department` and, optionally, `PayrollRun`. It has no relationship to inventory, procurement, or sales. `docs/00-CORE.md` §1 explicitly declares the system **single-tenant**, and none of the 42 Phase‑1 entities (`docs/10-BACKEND-PHASE1.md` Part C) or the Sales entities (`docs/14-BACKEND-SALES-API.md`) carry a `branch_id`. The ask: make **branches a real operational boundary**. Requested split — - **Stay global (shared, one copy for the whole company):** `Category`, `SubCategory`, `Brand`, `Uom`, `Item`. - **Everything else must be branch-isolated** — each branch sees and operates on its own rows: warehouses/bins, vendors, GRNs, purchase orders, requisitions/RFQs, stock (layers/ledger/transfers/adjustments/counts), sales invoices/slips, purchase invoices, customers. This doc is a **plan**, not a spec — same status as `docs/SALES_MODULE_PLAN.md` before its Phase 1 shipped. Nothing here is implemented yet. Once agreed, the entity-level detail moves into `10-BACKEND-PHASE1.md`/`14-BACKEND-SALES-API.md` in place (per `01-DOC-GUIDE.md` §5 — one source of truth, no forked copies) and this file is retired or reduced to a changelog pointer. --- ## Why the item catalog stays global `Item`, `Category`, `SubCategory`, `Brand`, `Uom` define **what a product is**, not where it is. Splitting the catalog per branch would mean the same SKU could mean different things in different branches, and every catalog edit (price, tax class, UOM conversion) would need replaying across branches. Keeping them global matches how `ITEM_REORDER` already works today: it is the join that ties a global item to a specific `warehouse_id` for reorder policy — the same pattern extends naturally to branches. `Vendor` and `Customer` are **not** in the "stays global" list per the request above, even though some ERPs share them company-wide; see Open Questions §7 for the trade-off. --- ## Anchor mechanism: Warehouse carries the branch Rather than stamping `branch_id` onto every one of the ~30 remaining entities, the plan anchors branch scoping at **`Warehouse`**, then lets everything that already references a warehouse inherit its branch transitively: ``` WAREHOUSE(warehouse_id PK, branch_id FK→BRANCH [NEW, required], code, name) ``` Entities that already carry `warehouse_id` (directly or via their lines) need **no new column** — their branch is `warehouse.branch_id`, one join away: - `ITEM_REORDER`, `GRN` (header), `PURCHASE_RETURN`, `STOCK_LAYER`, `STOCK_LEDGER`, `STOCK_TRANSFER` (src/dest — see §5 for the cross-branch-transfer question), `STOCK_ADJUSTMENT`, `STOCK_COUNT`. Entities that have **no warehouse at all today**, or only carry one at line-level while the header is branch-relevant on its own (numbering, listing, approval routing), get an **explicit `branch_id`**: - `REQUISITION`, `RFQ`, `PURCHASE_ORDER`, `VENDOR`, `CUSTOMER`, `SALES_INVOICE`, `SALES_SLIP`, `PURCHASE_INVOICE` (the AP entity introduced alongside GRN payments — see `Backend/ERPCore` `PurchaseInvoiceService`), `BIN` (via its warehouse — no new column needed, listed here only for completeness). `PO_LINE` currently carries its own `warehouse_id`, independent of anything on `PURCHASE_ORDER`. Once `Warehouse.branch_id` exists, a PO whose lines target warehouses in two different branches becomes a real modeling conflict — see Open Questions §1. --- ## Entity classification | Group (per `10-BACKEND-PHASE1.md` Part C) | Entity | Branch scope | |---|---|---| | C.1 Master Data | `Category`, `SubCategory`, `Brand`, `ItemType`, `Uom`, `Item` | **Global** | | C.1 Master Data | `ItemReorder` | Branch-scoped (via `warehouse_id`) | | C.1 Master Data | `Vendor` | Branch-scoped (**new** `branch_id`) | | C.1 Master Data | `Warehouse`, `Bin` | Branch-scoped (`Warehouse` gets the anchor `branch_id`; `Bin` inherits via `Warehouse`) | | C.1 Master Data | `ProductConfig` | Global (singleton feature-gate config; not a data record) | | C.2 Procurement | `Requisition`, `RFQ`, `VendorQuotation`, `PurchaseOrder`, `PurchaseReturn` | Branch-scoped (`Requisition`/`RFQ`/`PurchaseOrder` get **new** `branch_id`; `VendorQuotation` inherits via `RFQ`; `PurchaseReturn` inherits via `warehouse_id`) | | C.3 Goods Receipt | `GRN`, `GRN_LINE` | Branch-scoped (via `warehouse_id`) | | C.4 Batch/Serial | `Batch`, `Serial` | Branch-scoped (via the `Item`+`StockLayer` they're received into — see Open Questions §3) | | C.5 Stock Core | `StockLayer`, `StockLedger` | Branch-scoped (via `warehouse_id`) | | C.6 Stock Transactions | `StockTransfer`, `StockAdjustment`, `StockCount` (+ lines) | Branch-scoped (via `warehouse_id`) | | C.7 Cross-cutting | `User`, `AuditLog`, `JournalEntryStub` | **Global** (a user's *access* can be branch-restricted later via RBAC — see §6 — but the shadow records themselves aren't per-branch data) | | C.7 Cross-cutting | `NumberSequence` | See §5 — becomes branch-scoped if per-branch doc numbering is wanted | | C.7 Cross-cutting | `ReasonCode` | **Global** (a reason like "Damaged in transit" applies everywhere) | | Sales (`14-BACKEND-SALES-API.md`) | `Customer` | Branch-scoped (**new** `branch_id`) | | Sales | `SalesInvoice`, `SalesSlip`, bundle sales | Branch-scoped (via `warehouseId`, already present at header per `types/sales.ts` `SalesInvoiceSummary.warehouseId`) | | Purchase Invoice (AP) | `PurchaseInvoice` | Branch-scoped (**new** `branch_id`, or inherited from the linked `GRN`s — a purchase invoice consolidates GRNs per `PurchaseInvoiceService.LoadAndValidateGrnsAsync`, so it should reject GRNs from mixed branches) | | HRM (`12-BACKEND-HRM.md`) | `Branch` itself, `Department`, `Employee`, `PayrollRun` | Already branch-aware — **out of scope**, no change needed | --- ## What this doesn't solve by itself: access enforcement Adding `branch_id` columns and filtering queries by "current branch" makes the **data model** branch-isolated. It does **not**, on its own, stop a user from switching their client-side "current branch" to one they shouldn't see — `00-CORE.md` §1 states RBAC is deferred and `10-BACKEND-PHASE1.md` C.8 confirms the existing role system is **sidebar-visibility only**, not per-endpoint enforcement. Real isolation (user A physically cannot fetch branch B's GRNs even by guessing an ID) needs a per-request branch claim checked server-side, which is a bigger lift than this plan alone. Recommend treating this plan as **Phase 1 (data scoping + UI filtering)** and real cross-branch access control as a **Phase 2** that rides on top of whenever RBAC enforcement lands — flagging it here so it isn't silently assumed solved. --- ## Frontend impact (sketch — detail belongs in `20-FRONTEND.md` once agreed) - A "current branch" selector (similar in spirit to a warehouse picker) somewhere in the shell (`AppSidebar`/`Header`), persisted per session. - Every list/detail screen for a branch-scoped entity sends the selected branch as a filter; create/edit forms stamp it automatically rather than exposing a raw branch picker on every form. - Warehouse pickers throughout procurement/stock/sales screens narrow to the current branch's warehouses only, once `Warehouse.branch_id` exists. - Category/Brand/UOM/Item pickers and their settings screens are **unaffected** — they stay branch-agnostic everywhere they're used today. --- ## Migration phasing (suggested) 1. **Schema:** add `Branch` FK to `Warehouse` (backfill: create one default branch, assign all existing warehouses to it — zero-downtime for current single-branch users). 2. **Schema:** add `branch_id` to the header-only entities in §"Entity classification" that don't inherit via warehouse (`Vendor`, `Customer`, `Requisition`, `RFQ`, `PurchaseOrder`, `PurchaseInvoice`), backfilled to the same default branch. 3. **Backend:** thread "current branch" through the request (claim, header, or query param — decide in Open Questions §4) and add it as a filter in the relevant repositories/services. No cross-branch validation yet (data is scoped, not yet enforced). 4. **Frontend:** branch selector + filtering wired into the screens listed above. 5. **Validation:** add the cross-branch guards flagged in Open Questions (§1 PO lines, §3 batch/serial, §2 transfers) once the team has decided how to handle each. 6. **(Optional, later)** Per-branch document numbering (§5) and real endpoint-level access enforcement (§6), once RBAC ships. --- ## Open questions / decisions needed 1. **Purchase Order spanning branches.** `PoLine.warehouse_id` is independent of any future `PurchaseOrder.branch_id`. Decide: (a) constrain every PO to one branch — all lines' warehouses must belong to it (validated at write time), or (b) allow a PO to span branches and only branch-scope at the warehouse/stock level. (a) is simpler and matches "others must be branch-wise" more literally. 2. **Inter-branch stock transfers.** `StockTransfer` has `src_warehouse_id`/`dest_warehouse_id`. If warehouses in different branches can transfer to each other, `StockTransfer` needs to be visible to *both* branches (or modeled as leaving-branch/entering-branch documents) rather than owned by one. Decide whether cross-branch transfers are allowed at all in v1. 3. **Batch/Serial ownership.** `Batch`/`Serial` key off `item_id` only today, not `warehouse_id`. If the same batch number is received into two different branches' warehouses, is that the same `Batch` row (shared) or two independent ones? Affects FEFO/expiry queries. 4. **How "current branch" reaches the server.** A JWT claim (parallel to how `User`/`Role` are shadowed from AuthHex — `10-BACKEND-PHASE1.md` C.9), a custom request header, or a query parameter per call. A claim is the most tamper-resistant but needs AuthHex-side changes; a header/param is faster to ship but relies on the not-yet-built enforcement layer (§6) to not be spoofable. 5. **Document numbering.** `NumberSequence(sequence_id, doc_type, year, last_number)` is global today (one `GRN-2026-0001` sequence company-wide). Multi-branch ERPs commonly number per branch (`GRN-COL-2026-0001`). Decide if that's wanted now or deferred — it's an additive column (`branch_id` joins the sequence key) either way. 6. **Vendor/Customer sharing.** The request says "others must be branch-wise," which this plan takes literally for `Vendor`/`Customer`. Confirm that's intended — many ERPs keep vendors/customers company-wide (one AP/AR relationship, purchases/sales tagged by branch) rather than duplicating the vendor record per branch. If shared vendors/customers are actually wanted, move both back to the "Global" row and this becomes a much smaller change (no new FK on either). 7. **Reporting across branches.** Once data is isolated, does any role need a company-wide rollup (e.g. total stock value across all branches)? Not addressed here — flag if needed so the query layer isn't built branch-only from the start. --- *This plan follows `01-DOC-GUIDE.md`'s "one source of truth" rule: once decisions above are made, the actual column/entity changes are written into `10-BACKEND-PHASE1.md` Part C and `14-BACKEND-SALES-API.md` in place, and this file stops being duplicated content.*