# Module 2 — Branches & Branch Settings

> **KB version:** 2.0 · **Code baseline:** `b71c156` · **Last reviewed:** 2026-10-26
> **Epic:** EP-TBD · **Status:** Reviewed
> **Pending PO review:** none
> **Related flags:** TBD (requires As-Built Spec cross-reference, out of scope this pass) · **Open decisions:** TBD (requires As-Built Spec OD register, out of scope this pass)

---

## Module Overview
- **Purpose:** Model physical restaurant locations and their device/receipt configuration.
- **Business Goal:** Multi-branch operation with per-branch taxation, invoicing identity, order scoping, day-closing, and delivery-outlet mapping.
- **Main Actors:** Admins (see branch list), cashiers (operate within their branch), platforms (target a branch by slug).
- **Dependencies:** Users (`users.branch_id`), Orders (global `BranchScope`), Noon outlets, Branch settings.
- **Entry Points:**
  - `GET /v1/dashboard/branches/list` (⚠ unauthenticated — outside the auth group)
  - `GET /v1/pos/branches/listWithUsers` (guest key)
  - `GET /v1/pos/branch-settings/` and `PUT /v1/pos/branch-settings/update` (cashier/chief)

## Business Workflow
1. Branches are provisioned via seeders/DB (no admin CRUD API exists).
2. A staff user is assigned to a branch; on login, the branch rides along on every request.
3. A **global query scope** (`BranchScope`) automatically filters `Order` and `DailySalesReport` queries to the requesting user's branch — a cashier can never see another branch's orders.
4. Each branch stores its own receipt hardware config (printer IP, scanner threshold, receipt language) via the POS branch-settings screen, which also returns the global invoice-header settings so the POS can render receipts.

## Business Rules
- Branch fields and their business meaning:
  - `name` — internal name; `display_name` — the name printed on invoices (falls back to `name` if null).
  - `trn` — UAE Tax Registration Number printed on invoices.
  - `is_taxable` (default **1**) — master switch for the 5% VAT math on all orders of the branch (see Module 4).
  - `franchise_company_name` — legal entity name printed on invoices (nullable).
  - `address` — printed on invoices.
  - `mapper_slug` — the identifier external platforms use to target the branch when submitting orders.
  - `sort_order` — display order of branches in dashboard lists.
  - `noon_outlet_id` — links a branch to exactly one Noon outlet (unique, nullable). **A branch without this link cannot dispatch delivery orders to Noon.**
- Branch settings are one row per branch, upserted (`updateOrCreate`) — updating always overwrites all three fields (`printer_ip`, `scanner_threshold`, `receipt_lang`), even with nulls.
- The branch-settings "show" bundles global dashboard settings (`logo`, `company_name`, `invoice_header_slogan`, `invoice_footer_slogan`, `display_trn`) + the branch + its settings — this is the POS boot payload for receipt rendering.

## Hidden Rules
- **Branch scoping only applies when the request carries a branch** (i.e. authenticated staff requests). Console jobs, webhooks, and the unauthenticated test endpoints see ALL branches' orders.
- `UpdateBranchSetingRequest` has its validation rules commented out → **branch settings accept any input, including nulls that erase existing values.**
- The dashboard branch list endpoint was outside the auth middleware group (anyone could enumerate branches) — **fixed 2026-07-07**: it now sits inside the admin/superadmin auth group.
- A branch's "taxability" is fixed per branch, not per order or per item.
- 🚩 **SPRINT 1 · 09-08-2026 (2026-08-10):** Mobile orders are now pinned to the branch named by the request's `branch_slug`, not the branch of the platform's related user — `PlatformAuthMiddleware` resolves that branch, merges it into the request, and re-registers `BranchScope` pinned to it. **This fixed a real bug:** order numbering for mobile orders previously drew from the related user's branch (or from *every* branch at once when that user had none), producing wrong or duplicated order numbers on mobile orders (see Module 5, A8).
- 🚩 **SPRINT 1 · 09-08-2026:** `BranchScope` now treats `admin`/`superadmin` as branch-unscoped (head office is cross-branch by design — this was implicit before but is now an explicit rule in the scope itself), and an explicitly-passed branch id overrides the scope for any user. The POS pre-login picker (`listWithUsers`) now lists **branch staff only** — admins/superadmins are excluded from that list.

## Decision Tree
```
ON ANY Order/DSR QUERY:
IF request has 'branch' (set by auth middleware)
THEN add WHERE branch_id = user's branch
ELSE no branch filter (all branches visible)

ON BRANCH SETTINGS UPDATE:
Always upsert row for user's branch with the 3 provided values (no validation)
```

## Database Understanding
- `branches` — the physical locations; the anchor for tax policy, invoice identity, order numbering scope-of-day, and Noon dispatch.
- `branch_settings` — 1:1 device/receipt config; exists separately so operational hardware config doesn't pollute the branch's business identity.
- `noon_outlets` — Noon's own outlet registry mirrored locally (codes, bilingual names, geo, serviceability flags); a branch points at one outlet (`branches.noon_outlet_id`).

## State Machine
None (branches have no status field). "Branch open/closed" is *derived* in dashboard charts from the latest order's `closed_at` (see Module 7).

## APIs
| Endpoint | Purpose | Input | Output | Validation | Errors | Side effects |
|---|---|---|---|---|---|---|
| `GET /v1/dashboard/branches/list` | Branch dropdowns in dashboard | token | id + name, ordered by `sort_order` | admin/superadmin auth (since 2026-07-07) | 401/403/500 | none |
| `GET /v1/pos/branches/listWithUsers` | POS login screen | guest key | branches with their users | guest key | 401 | none |
| `GET /v1/pos/branch-settings/` | POS boot config | token | header settings + branch + settings | role cashier/chief | 401/403 | none |
| `PUT /v1/pos/branch-settings/update` | Save device config | printer_ip, scanner_threshold, receipt_lang | saved row | **none** | 500 | upsert row |

## Edge Cases
- Branch with `display_name = null` → invoice shows internal `name`.
- Deleting a `noon_outlet` sets `branches.noon_outlet_id` handling via FK `set null` on the *old* (pre-refactor) column; the current FK is declared on `branches` side without cascade rules — orphan risk.
- `BranchSettingController::update` error path calls `BranchSettingResource::collection($e->getMessage())` — passing a string to a resource collection → a *second* exception masks the first (error responses from this endpoint are unreliable).

## Assumptions
- Branch provisioning is an engineering task (seeder/SQL), intentionally not exposed to admins.
- Exactly one Noon outlet per branch and vice versa.

## TODOs
- ~~Add auth to the dashboard branch list~~ — done (2026-07-07).
- Restore validation on branch settings update.
- Provide admin CRUD for branches (currently impossible without DB access).

## Open Questions
- Is `scanner_threshold` a barcode scanner sensitivity? (Name only; nothing consumes it server-side.)
- Should `is_taxable=0` branches print TRN? (Currently they can, since TRN display is a global setting.)

---

