# Stock domain principles and architecture

**Phase:** Inventory **stock** domain architecture — **design and invariants**, not accounting valuation.  
**Companion:** [`inventory-domain-rules.md`](inventory-domain-rules.md) (catalog). **Governance:** [`stock-governance-rules.md`](stock-governance-rules.md). **Execution:** [`transaction-engine.md`](transaction-engine.md). **Reservations:** [`reservation-domain-principles.md`](reservation-domain-principles.md).

---

## Part 1 — Stock domain principles

### Stock is derived from movements

- **On-hand** and **available** quantities at a warehouse for an item are **not** authoritative facts stored in isolation. They are **derivable** from the sum of applied movement effects (plus reservation overlays where applicable).
- User-facing “current stock” should be read from **projection tables** (or materialized views) that are **maintained** by the movement application pipeline — never edited ad hoc to “fix numbers.”

### Movement ledger is source of truth

- The **append-only movement ledger** (immutable rows) records every quantity change intent that the business acknowledges: receipts, issues, transfers, adjustments, reservation holds, and releases.
- Corrections to mistakes are **new** ledger entries (e.g. reversing adjustment), not edits to old rows.

### Quantities are projections / cached state

- **Projection tables** (e.g. per `(tenant_id, warehouse_id, item_id)` balances) exist for **read performance** and **locking anchors**. They must be **rebuildable** from the ledger (full or incremental replay).
- Any mismatch between ledger replay and projection is a **data integrity incident**, not a cue to patch projection without a movement.

### Immutability principles

- **Never UPDATE or DELETE** historical movement rows after commit (except illegal GDPR-driven rare cases with legal process — out of normal ERP ops).
- Canceled business operations emit **compensating movements** with explicit `correlation_id` / `reverses_movement_id` linkage.

### Auditability requirements

- Every movement row carries: `tenant_id`, `warehouse_id` (and for transfers: source/destination semantics via type + paired lines or explicit columns), `item_id`, `quantity` (signed or typed direction), `movement_type`, `occurred_at` (business time), `recorded_at` (system time), `actor_user_id` / `actor_api_client_id`, `reference_type` + `reference_id` (nullable), `idempotency_key` (optional), `metadata` JSON (bounded size).
- Application and **activity_logs** (or dedicated stock audit stream) record high-level actions (who approved an adjustment).

---

## Part 2 — Stock ledger architecture

### Immutable movement records

| Concept | Rule |
|---------|------|
| Storage | Single table family, e.g. `inventory_stock_movements`, **append-only INSERT** |
| Identity | Monotonic `id` per tenant or global; `tenant_id` indexed first on every query |
| Correction | New row; optional `reverses_movement_id` FK to prior row |

### Tenant, warehouse, and item isolation

- **Mandatory:** `tenant_id` on every movement row; enforce with global scope + DB composite FKs to tenant-owned `inventory_items` / `inventory_warehouses`.
- **Warehouse isolation:** every quantity-affecting movement is scoped to a **warehouse** (and optionally `warehouse_section_id` when section-level stock is modeled).
- **Item isolation:** `item_id` always references catalog `inventory_items`; stock for **variants** is tracked at the **SKU (child item) level** unless a deliberate policy tracks only parents (document per tenant).

### Movement timestamps

- **`occurred_at`:** when the business event happened (backdated receipt allowed with policy).
- **`recorded_at`:** when the row was persisted (default `now()`).
- Reporting and sequencing use **`id` / `recorded_at`** for technical ordering; business disputes may use **`occurred_at`**.

### Actor traceability

- Persist **human** (`actor_user_id`) vs **system** (job name in `metadata`, or `actor_service` string) vs **integration** (`api_token_id` / client id).
- Never rely solely on session memory — batch jobs must stamp context.

### Movement types (examples)

| `movement_type` | Meaning |
|-----------------|--------|
| `stock_in` | Increase on-hand at warehouse (receipt, production output, etc.) |
| `stock_out` | Decrease on-hand (consumption, shipment, loss) |
| `transfer_out` | Decrease at source warehouse as part of an inter-warehouse transfer |
| `transfer_in` | Increase at destination warehouse for same transfer |
| `adjustment` | Controlled delta (cycle count, damage) — approval path in governance doc |
| `reservation` | Hold quantity against available (does not change on-hand until issue) |
| `reservation_release` | Release held quantity |

**Reservations** may be modeled either as **separate rows** in the same ledger with signed semantics, or as a **sibling table** that feeds the same projection updater — choose one model per implementation; either way, **do not** silently mutate history.

### Never update/delete movement history

- DB grants for application role: **INSERT only** on ledger table; **no UPDATE/DELETE** in production (migrations excepted).
- Admin “fixes” go through **governance** ([`stock-governance-rules.md`](stock-governance-rules.md)).

---

## Part 3 — Stock state projections

### Purpose

- Serve dashboards, ATP (available-to-promise), picking, and API reads without scanning the full ledger.
- Act as **lock targets** for concurrency control (see Part 6).

### Suggested projection shape (warehouse + item)

Per `(tenant_id, warehouse_id, item_id)` (optionally `warehouse_section_id`):

| Field | Meaning |
|-------|--------|
| `on_hand_qty` | Sum of movements affecting on-hand |
| `reserved_qty` | Sum of open reservations |
| `available_qty` | `on_hand_qty - reserved_qty` (stored **or** computed on read — if stored, must be kept consistent in the same transaction as movement apply) |

### Recalculation support

- **Full rebuild:** batch job replays all movements ordered by `(recorded_at, id)` and recomputes projections — used after corruption suspicion or new projection column.
- **Incremental:** each movement application transaction updates one projection row (or few) via deterministic rules.
- **Version column** (`row_version` integer) on projection rows supports optimistic concurrency detection.

### Projections are not source of truth

- If projection and ledger disagree after replay, **ledger wins**; projection is repaired.
- Do not “type UPDATE” on `on_hand_qty` without a corresponding movement in the same **serializable** business transaction (except controlled rebuild jobs).

---

## Part 4 — Reservation system

### Goals

- **Reserve** stock without issuing it (pending sales order, transfer preparation, approval hold).
- **Release** explicitly when order cancels or timeout fires.
- **Expiration-ready:** `expires_at` nullable; sweeper job releases or escalates.

### Reference tracking

- `reference_type` + `reference_id` (polymorphic) tie reservation to order line, transfer draft, etc.
- **`external_ref`** string optional for partner idempotency.

### Lifecycle

- States: `active` → `released` | `expired` | `converted` (converted = consumed into `stock_out` / transfer when fulfilled).
- **Double-spend prevention:** releasing twice must be idempotent or rejected with clear error.

### Examples

- **Pending sale order:** reservation on ATP check; release on cancel; convert to `stock_out` on pick confirm.
- **Transfer preparation:** reserve at source until pick complete; then `transfer_out` + `transfer_in` pair (or four lines with in-transit bucket — future enhancement).
- **Approval workflow hold:** reservation with `expires_at` until approval or timeout.

---

## Part 5 — Transfer architecture

### Traceability

- Every transfer has a **header** `inventory_stock_transfers` (id, tenant_id, from_warehouse_id, to_warehouse_id, state, created_by, …) and **lines** (item_id, quantity, optional section ids).
- Ledger rows reference `transfer_id` + `transfer_line_id` in `metadata` or dedicated nullable FK columns.

### Source and destination

- **Explicit** from/to warehouse on header; lines may not cross warehouses without a new transfer.
- **Partial transfer:** lines support partial quantities; header state `partially_shipped` / `completed` per line aggregates.

### Transfer states (example)

`draft` → `released` (reservations placed) → `in_transit` (optional) → `received` → `closed` / `cancelled` (with compensating movements).

### Ledger mapping

- **Minimum:** `transfer_out` at source, `transfer_in` at destination, **same** `correlation_id` for pairing.
- **Audits:** never delete header; cancel = new movements + state flip.

---

## Part 6 — Concurrency and consistency

### Prevent overselling and negative stock races

- **Apply path:** single DB transaction: (1) validate or lock projection row, (2) insert ledger row(s), (3) update projection.
- **Business rules:** reject if `available_qty` (or `on_hand_qty` for non-reserved paths) would go negative unless tenant policy explicitly allows **negative stock** (document as exception).

### Transactional movement application

- One **StockMovementService::apply(MovementIntent $dto)`** entry point per business operation — no partial applies visible to other sessions (isolation level **repeatable read** or **serializable** for hot rows, per DB capability).

### Locking strategy

| Strategy | When | Trade-off |
|----------|------|-----------|
| **Pessimistic** (`SELECT … FOR UPDATE` on projection row for `(tenant, warehouse, item)`) | High contention SKUs, transfers, reservations | Safer; risk of deadlocks if lock order not standardized |
| **Optimistic** (`row_version` check on UPDATE projection) | Low contention catalogs | Retries on conflict; still need ledger insert deduped |

**Recommendation:** **Pessimistic** on projection row for reservation and issue paths; **optimistic** optional for bulk low-value SKUs. **Always** define a **global lock ordering** (e.g. tenant_id → warehouse_id → item_id) to reduce deadlocks.

### Optimistic vs pessimistic summary

- **Pessimistic** = simpler mental model for finance-adjacent stock; preferred default for enterprise ERP.
- **Optimistic** = throughput for sparse updates; must combine with **idempotency keys** on movement insert to avoid duplicate lines on retry.

---

**Self-validation (Part 9):** see [`stock-governance-rules.md`](stock-governance-rules.md#part-9-self-validation).
