# Allocation safety rules

**Purpose:** Prevent **overselling** and **double allocation** at validation time.  
**Invariants:** [`reservation-invariants.md`](reservation-invariants.md). **Principles:** [`reservation-domain-principles.md`](reservation-domain-principles.md).

---

## Part 2 — Allocation safety rules

### Allocation validation (create / increase reservation)

- **Lock first:** acquire **same** projection row lock order as stock engine ([`transaction-engine.md`](transaction-engine.md) Part 6) before reading `on_hand` / `reserved` / `available`.
- **Check:** `requested_qty ≤ available` **after** evaluating all in-scope business rules (e.g. min ship qty, rounding to sellable UOM — out of scope here but must be **deterministic**).
- **Write:** append reservation ledger `create` (or `increase`), then update projection `reserved` / `available` in the **same DB transaction** as the lock scope.

### Overselling prevention

- **Never** approve allocation from a **stale read** of `available` (no read outside the locked transaction window).
- **ATP API** and **reservation create** must share the **same** invariant definition of `available` (R1).
- **Concurrent requests:** second request fails closed (`409` / domain error) or receives **partial** allocation only if product explicitly supports partial — default **fail closed** for enterprise safety.

### Partial allocation rules

- If business allows partial: return `allocated_qty < requested_qty` with **explicit** reason codes; **never** silently allocate zero without error when `requested_qty > 0`.
- **Rounding:** if pack size forces rounding down, **document** remainder handling (backorder vs reject).

### Warehouse allocation boundaries

- Reservations are **always** scoped to `(tenant_id, warehouse_id[, section_id])` — no cross-warehouse reservation without an explicit **transfer reservation model** (future).
- **Source** (`source_type`/`source_id`) must belong to the **same tenant** as warehouse and item.

### Allocation failure behavior

| Condition | Behavior |
|-----------|------------|
| Insufficient `available` | Reject; **no** partial projection update; optional suggest max allocatable |
| Lock timeout | Retryable error; **no** ledger row |
| Idempotency duplicate | Return original allocation outcome ([`transaction-engine.md`](transaction-engine.md) Part 5 pattern) |
| Catalog inactive | Reject (align with [`movement-validation-rules.md`](movement-validation-rules.md) spirit) |

**Allocation corruption creates invisible failures — fail closed by default.**
