# Orders-CRUD — Contracts

> Produced by the Reversa **Writer** (phase: generation) · doc_level: `complete`
> Generated on 2026-09-21

**Confidence scale:** 🟢 CONFIRMED · 🟡 INFERRED · 🔴 GAP

External HTTP contract exposed by the `orders-crud` unit — the Laravel resource `resource('/orders','OrderController')` (route names `orders.index|store|show|update|destroy`, plus the empty `orders.create|edit`) under the admin group (`['web','admin']`, empty admin prefix). All routes require an authenticated admin session; an unauthenticated request gets `302 → auth/login`. `index`/`show` render HTML; `store`/`update` return a redirect (draft) or an HTML receipt (done); `destroy` returns JSON. The sibling routes `GET /orders/scan`, `GET /orders/{order}/print`, and `PUT /orders/{order}/note` are declared before the resource and are owned by `orders-scan`, `orders-print`, and `orders-note` respectively. 🟢 (`routes/web.php:72-75`, `OrderController.php`)

---

## GET `/orders` — order list 🟢 (`:21-47`)

- **Auth:** required; anonymous → `302 auth/login`. 🟢 (`routes/web.php:24-28`)
- **Request (query string):**

  | Field | Type | Required | Notes |
  |-------|------|----------|-------|
  | `q` | string | ❌ | Matches `id = q` OR `#QT78-{id} = q` OR customer `phone LIKE %q%`, grouped so `status` stays ANDed with the whole OR group. ✅ Fixed 2026-09-21 (was ungrouped). 🟢 (`:31-39`) |
  | `status` | enum `draft`\|`done` | ❌ | Exact filter. 🟢 (`:40-42`) |
  | `page` | integer | ❌ | `paginate(30)`. 🟢 (`:44`) |

- **Response — `200 text/html`** rendering `pages.orders` with `list: LengthAwarePaginator<Order>` (each row: `code`, `customer->fullname/phone`, `count`, `total`, `created_at`, `status`). 🟢 (`:46`, `orders.blade.php`)

---

## POST `/orders` — create order 🟢 (`:64-165`)

- **Auth:** required. **CSRF:** required (form POST). 🟢
- **Request (form body):**

  | Field | Type | Required | Rule / Notes |
  |-------|------|----------|--------------|
  | `items` | array | ✅ | `required|array`; empty → "Không thể tạo đơn hàng rỗng". 🟢 |
  | `items.*.code` | string | ✅ | `required|string`; product barcode/code (unknown codes silently skipped in totalling). 🟢 |
  | `items.*.qty` | numeric | ✅ | `required|numeric|min:1`. 🟢 |
  | `items.*.unit_id` | int/null | ❌ | `nullable`; resolved to a `ProductUnit` or nulled (base unit). 🟢 |
  | `customer.phone` | string | ❌ | `nullable`; resolves the buyer via `Customer::code(phone)`. 🟢 |
  | `notes` | string | ❌ | `nullable|string`. 🟢 |
  | `discount_amount` | numeric | ❌ | `nullable|numeric|min:0`; `round(…,1)`. 🟢 |
  | `debt_amount` | numeric | ❌ | `nullable|numeric|min:0`; `round(…,1)`; POS credit. 🟢 |
  | `status` | enum | ❌ | `nullable|in:draft,done`; defaults `draft`. 🟢 |

- **Side effects (on success):** persists `orders` + `order_product`; on `done` with a customer awards `earned_point` and stamps `points_awarded_at`; on `done` + customer + `debt_amount > 0` writes one `pos_debt` (`CustomerDebt::record`, note `"<code> (Tổng đơn: <total> ₫)"`). 🟢 (`:136-151`)
- **Responses:**
  - `draft` → `302 redirect(pos.index)` + toast "Lưu nháp thành công". 🟢 (`:153-156`)
  - `done` → `200 text/html` `pages.pos-print` (printable receipt). 🟢 (`:158-162`)
  - Validation failure → `422` (or redirect back with errors, web default). 🟢 (`:66-78`)
  - Debt-rule violation → `302 redirect(pos.index)` + error, nothing persisted. 🟢 (`:123-128`)
  - Save failure → `302 redirect(pos.index)` + "Tạo đơn hàng thất bại.". 🟢 (`:164`)

---

## GET `/orders/{id}` — order detail 🟢 (`:173-184`)

- **Auth:** required. **Response:** `200 text/html` `pages.orders-detail` with `item: Order`; unknown id → `404` (`findOrFail`). 🟢

---

## PUT/PATCH `/orders/{id}` — edit / finalise 🟢 (`:204-337`)

- **Auth:** required. **CSRF:** required. **Path:** order id.
- **Request (form body):** same fields as `POST`, except `items` is `required_without:create_now_mode` and per-item rules are `sometimes`. Additional:

  | Field | Type | Required | Rule / Notes |
  |-------|------|----------|--------------|
  | `create_now_mode` | bool/flag | ❌ | When set, rebuild items from the order's existing pivots and finalise; `items` may be omitted. 🟢 (`:223-240`) |

- **Guards:** `is_editable` (draft, or done ≤ 24h of `updated_at`) — else `302 back` + "Không thể cập nhật đơn hàng hoàn thành quá 24h"; customer immutable unless the order is `draft`; debt frozen when `debt_locked`; points awarded only if `!points_awarded_at`. 🟢 (`:242-316`)
- **Responses:** `draft` → `302 redirect(pos.index)` + toast; `done` → `200` `pages.pos-print`; unknown id → `404`; validation/guard failures → `422`/`302 back`. 🟢 (`:325-336`)

---

## DELETE `/orders/{id}` — delete with reversal 🟢 (`:362-383`)

- **Auth:** required. **Path:** order id.
- **Behaviour:** inside a `DB::transaction`, reverse awarded points (`reversePointsForOrder`, clamped ≥ 0), void `pos_debt` (`voidForOrder`, insert `debt_void`, reduce `debt_total` by `min(entry, balance)`), then hard-delete the order (`order_product` cascades, `customer_debts.order_id` → null). 🟢 (`:366-370`)
- **Response — `200 application/json`:**

  | Field | Type | Meaning |
  |-------|------|---------|
  | `status` | bool | `true` on success, `false` otherwise (incl. unknown id — `find` returns null, **no 404**). 🟢 |
  | `message` | string | `trans('admin.delete_succeeded')` / `trans('admin.delete_failed')`. 🟢 |

---

## GET `/orders/create`, `/orders/{id}/edit` — empty stubs 🟢 (`:53-56,192-195`)

- Both actions are `//` no-ops (return empty). Order creation/editing is driven by the POS terminal, not these resource forms. Documented for completeness; do not reimplement as forms. 🟢

---

## Consumed contracts (owned by other units)

`orders-crud` reads/writes several other units' data models but calls **no external service**. 🟢

| Reads / calls / writes | Owner unit | Purpose |
|------------------------|------------|---------|
| `Product::code()`, `getPriceByCustomerType($type,$unitId)` | `products-pricing` / `products-catalog` | resolve and price each cart line |
| `Customer::code(phone)`, `customer.type`, `customer.points` (write) | `customers-crud` | resolve the buyer, read tier, credit points |
| `Customer::reversePointsForOrder(Order)` | `customers-crud` / `customers-loyalty` | reverse points on delete |
| `CustomerDebt::record('pos_debt', …)` | `customers-debt-actions` | post the sale's credit balance |
| `CustomerDebt::voidForOrder(Order)` | `customers-debt-actions` | void the debt on delete |
| `pages.pos-print` receipt view | shared with `orders-print` | render the printable receipt |

---

## Producer/consumer relationships

| This unit is… | Counterparty | Contract |
|---------------|--------------|----------|
| **Consumer** | `pos-terminal` / `pos-scan` | the browser cart produces the `store`/`update` payloads. 🟢 |
| **Consumer** | `products-pricing` | line prices come from `getPriceByCustomerType`. 🟢 |
| **Producer** | `customers-*` | writes `customer.points` (loyalty) and `customer_debts` (`pos_debt`), which those units also mutate. 🟢 |
| **Producer** | `orders-print` | a `done` `store`/`update` renders `pages.pos-print` directly; the standalone print route re-renders it. 🟢 |
| **Producer** | `dashboard` / `customers-statistics` | orders feed the KPIs and the nightly `customer_order_summary`. 🟢 |

---

## Cross-cutting contract notes

- **Verb surface:** `index/store/show/update/destroy` live; `create/edit` empty. 🟢 (`OrderController.php`)
- **Content types:** `index`/`show` → HTML; `store`/`update` → redirect or HTML receipt; `destroy` → JSON. Not content-negotiated. 🟢
- **Idempotency:** points post once (`points_awarded_at`); debt posts once (`debt_locked`) — both survive a `done → draft → done` cycle. 🟢 (`:311`, `Order.php:65-67`)
- **Atomicity:** `destroy`, `store`, and `update` are all transactional. ✅ Fixed 2026-09-25 (`questions.md#question-11`) — `store`/`update` now wrap points/order-save/pivot-mutation/`pos_debt`-record in a `DB::transaction`; `update`'s debt guard also now runs before any pivot mutation (was after, leaving line items mutated on a routine guard rejection). 🟢 (`:135-160,301-337`)
- **Search grouping:** the `q` closure is grouped, so `status` stays ANDed with the id/code/phone OR — ✅ Fixed 2026-09-21 (was ungrouped). 🟢 (`:31-39`)
- **Deletion:** hard (no SoftDeletes); a deleted order never reappears in any list or history. 🟢 (`data-dictionary.md:203`)
- **Authorization:** authentication only; any admin may act on any order. 🟡 (ADR-0009)
- **No observability:** the financial write paths emit no telemetry. 🔴 (`OrderController.php`, absence)
