# Inventory Locations Design

**Date:** 2026-07-05
**Slug:** `inventory-locations`

## Scope

Task B11 adds inventory location management at `/inventory/locations` so invoice-enabled tenants can maintain the storage locations used by stock receipts, stock counts, and stock reporting.

Rationale (2026-07-05 B14): location management now uses a dedicated `inventory:manage` boundary so storage-configuration access stays narrower than invoice operations.

## Recommended Approach

**Approach 1: extend the existing inventory module with tenant-scoped location CRUD and a single-default invariant**

| Dimension | Assessment |
|-----------|------------|
| Robustness | Reuses the existing `stock_locations` table and its unique default index instead of inventing a parallel settings model. |
| Long-term | Keeps location metadata and the default-location rule in one place, which future stock flows can consume directly. |
| Scalability | Tenant-scoped CRUD is cheap; location counts are small and fit direct request/response flows. |
| Performance | Single-table reads/writes with one sibling reset when the default changes. |
| Reversibility | Two-way door; UI/API can grow later without schema churn because the table already carries `is_active` and `is_default`. |

**Weakness:** deleting locations must stay conservative because stock history can later attach to them more broadly.

**Recommended: Approach 1** — it fits the current inventory architecture, keeps the invariant in the canonical table, and avoids coupling default-location behavior to `tenant_settings`.

## Contract

### App routes

- `/inventory` remains the stock-list page.
- `/inventory/locations` renders the location-management page within the same inventory module.

### API routes

Mounted under `/api/inventory/locations` and guarded by authenticated user session, `invoices` module enabled, and inventory permissions:

- `GET /api/inventory/locations`
  - requires `inventory:manage`
  - returns `{ locations: InventoryLocation[] }`
- `POST /api/inventory/locations`
  - requires `inventory:manage`
  - body: `{ name: string; code?: string | null; isDefault?: boolean; isActive?: boolean }`
- `PATCH /api/inventory/locations/:id`
  - requires `inventory:manage`
  - body: `{ name?: string; code?: string | null; isDefault?: boolean; isActive?: boolean }`
- `DELETE /api/inventory/locations/:id`
  - requires `inventory:manage`
  - deletion is rejected for the tenant default location with HTTP `409`

### Data shape

`InventoryLocation`:

```ts
type InventoryLocation = {
  id: string
  name: string
  code: string | null
  isDefault: boolean
  isActive: boolean
}
```

### Default-location rules

- Exactly one default location exists per tenant after any successful create or update.
- Creating a location with `isDefault=true` clears `isDefault` on sibling rows in the same tenant before marking the target row default.
- Updating `isDefault=true` behaves the same way.
- Creating a non-default location does not disturb the current default.
- Deleting the current default location is rejected.
- The seeded `Main` / `MAIN` location remains the initial default for tenants until a user changes it.

### Validation and uniqueness

- `name` is required, trimmed, and limited to the existing UI-safe short-text range.
- `code` is optional, trimmed, nullable, and uniqueness is tenant-scoped using the existing DB constraint.
- Empty-string `code` is normalized to `null`.

### UI behavior

- Inventory page adds a secondary navigation between `Stock` and `Locations`.
- The `Locations` navigation affordance and page itself are available only to users with `inventory:manage`.
- Locations page shows:
  - title and supporting copy
  - summary cards for total locations, active locations, inactive locations, and current default location
  - locations table with name, code, status, and default marker
  - create dialog
  - edit dialog
  - actions to set default, activate/deactivate, and delete non-default rows
- Empty state heading: `No locations yet.`
- API failures render a shared danger alert.

## Test Strategy

- Query-layer tests for create/update default switching and default-delete rejection.
- API route tests for list/create/update/delete permissions and default invariant handling.
- App route test for `/inventory/locations` registration and render of location-management content.
