# Orders-CRUD — Implementation Tasks

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

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

## Prerequisites

- [ ] `Order` model with `$fillable = ['notes','debt_amount']`, `$appends = ['code','is_editable','debt_locked']`, `$status = ['draft','done']`, `$prefix_id = '#QT78-'`, `$limit_hours_editable = 24`, and relations `customer()`/`products()` (`order_product` withPivot `qty,price,unit_id,conversion_qty`)/`debts()`. **No SoftDeletes.** 🟢 (`app/Models/Order.php:12-67`)
- [ ] `Product` model with `code()` scope, `units()` relation, and `getPriceByCustomerType($type,$unitId)` (owned by `products-pricing`). 🟢 (`products-pricing`)
- [ ] `Customer` model with `code()` (phone) scope, `points`, `type`, and `reversePointsForOrder(Order)`. 🟢 (`customers-crud`, `Customer.php:97-112`)
- [ ] `CustomerDebt::record()` and `voidForOrder()` available (owned by `customers-debt-actions`). 🟢 (`CustomerDebt.php:38-64,83-144`)
- [ ] `orders` and `order_product` tables per the data dictionary (`order_product.order_id` cascade; `customer_debts.order_id` set null). 🟢 (`data-dictionary.md:200-237,174`)
- [ ] Admin route group `['web','admin']` + CSRF; the three pre-resource `/orders/*` routes (`scan`, `print`, `note`) registered **before** the resource. 🟢 (`routes/web.php:72-75`)

## Tasks

> Each task references the legacy file the behaviour was extracted from.

- [ ] T-01, Register `resource('/orders','OrderController')` in the admin group, **after** `/orders/scan`, `/orders/{order}/print`, and `PUT /orders/{order}/note`.
  - Legacy origin: `routes/web.php:72-75`
  - Done when: `index/store/show/update/destroy` resolve; `create`/`edit` are reachable but empty; the three named routes are not shadowed.
  - Confidence: 🟢

- [x] T-02, Implement `index`: `Order::with('customer')->orderBy('updated_at','desc')`, optional `q` (id / `#QT78-{id}` / customer phone, grouped closure) and `status` filter, `paginate(30)`, render `pages.orders`.
  - Legacy origin: `OrderController.php:21-47`
  - Done when: rows show code/customer/count/total/status newest-first; `?status=` narrows; `?q=` searches id/code/phone; pagination preserves the query string.
  - Confidence: 🟢 — ✅ Fixed 2026-09-21 (`q` block wrapped in a grouped closure; was ungrouped, letting an id/code match bypass `status`).

- [ ] T-03, Implement the **totalling engine** (shared helper): iterate `items`, resolve each `Product::with('units')->code(code)->first()` (skip unknown), resolve `unit_id`→`ProductUnit` (null fallback), price via `getPriceByCustomerType`, accumulate `subtotal += round(price,1)*qty`, `earned_point += round(reward_point,1)*qty`, `count++` per line, and build pivot rows `{qty,price,unit_id,conversion_qty}`.
  - Legacy origin: `OrderController.php:92-117,261-288`
  - Done when: totals/points/count and pivot payloads match the engine for a mixed cart (known + unknown codes, base + converted units).
  - Confidence: 🟢

- [ ] T-04, Implement `store`: validate the payload (empty-cart message "Không thể tạo đơn hàng rỗng"), run the engine, `total = subtotal - discount_amount`, `paid = total`, apply the debt guard, associate customer, award points on `done`, save + attach pivots, record `pos_debt` on a done debt-bearing order, then route draft→POS / done→`pages.pos-print`.
  - Legacy origin: `OrderController.php:64-165`
  - Done when: a valid done order persists with correct totals/pivots, awards points once, posts one `pos_debt`, and renders the receipt; a draft toasts + redirects to POS.
  - Confidence: 🟢

- [ ] T-05, Enforce the debt guard: `debt_amount > 0` requires a customer (else redirect + "Phải chọn khách hàng khi có tiền nợ."); `debt_amount > total` → redirect + "Số tiền nợ không được lớn hơn tổng tiền hàng."; nothing persisted on either.
  - Legacy origin: `OrderController.php:121-131,294-306`
  - Done when: both violations short-circuit before persistence with the exact messages.
  - Confidence: 🟢

- [ ] T-06, Implement the **award-once** points rule: on `done` with a customer, `customer.points += earned_point` and stamp `points_awarded_at`; in `update` only when `points_awarded_at` is null.
  - Legacy origin: `OrderController.php:136-140,311-315`
  - Done when: finalising twice (done→draft→done) credits points exactly once.
  - Confidence: 🟢

- [ ] T-07, Implement the **post-once** debt rule via `debt_locked`: record `pos_debt` only when done + customer + `debt_amount > 0` and no prior `pos_debt` row; freeze `debt_amount` on edit when `debt_locked`.
  - Legacy origin: `OrderController.php:149-151,296,321-323`; `Order.php:65-67`
  - Done when: a debt-bearing order posts one ledger row; a subsequent edit neither re-posts nor changes `debt_amount`.
  - Confidence: 🟢

- [ ] T-08, Implement `show`: `Order::findOrFail($id)` → `pages.orders-detail` (404 on unknown).
  - Legacy origin: `OrderController.php:173-184`
  - Done when: a valid id renders detail; an unknown id returns 404.
  - Confidence: 🟢

- [ ] T-09, Implement `update` (`$id, Request` arg order): validate (`items` `required_without:create_now_mode`), `findOrFail`, capture `debtLocked`, handle `create_now_mode` (rebuild items from pivots), enforce `is_editable` (24h window), pick the customer source by status, re-run the engine, detach + re-attach pivots, apply/freeze debt, award points once, save, record `pos_debt` when applicable, route draft→POS / done→receipt.
  - Legacy origin: `OrderController.php:204-337`
  - Done when: an editable order recomputes correctly; a done order older than 24h is refused with "Không thể cập nhật đơn hàng hoàn thành quá 24h"; `create_now_mode` finalises a stored draft.
  - Confidence: 🟢

- [ ] T-10, Implement `destroy`: `Order::find($id)`, then inside `DB::transaction` call `Customer::reversePointsForOrder`, `CustomerDebt::voidForOrder`, and `order->delete()` (hard); return `{status,message}` JSON using `trans('admin.delete_succeeded'|'delete_failed')`.
  - Legacy origin: `OrderController.php:362-383`
  - Done when: deleting a done order reverses points (clamped ≥ 0), inserts `debt_void`, hard-deletes order + pivots, and returns `{status:true}`; an unknown id returns `{status:false}` (not 404).
  - Confidence: 🟢

- [ ] T-11, Render `pages.orders` (list): status `<select>` + `q` box (JS redirect), columns code/customer/phone/count/`number_format(total) ₫`/created_at/status, row links to `orders.show` and `orders/{id}/print?ref=orders`, empty state "Không có dữ liệu", pagination appending the query.
  - Legacy origin: `resources/views/pages/orders.blade.php`
  - Done when: the table, filters, links, empty state, and paging behave as legacy.
  - Confidence: 🟢

- [ ] T-12, Leave `create`/`edit` as no-ops (legacy stubs) — order creation/editing goes through the POS terminal, not these forms.
  - Legacy origin: `OrderController.php:53-56,192-195`
  - Done when: neither verb is wired to a form; both are documented as intentionally empty.
  - Confidence: 🟢

- [x] T-13, Wrap `store`/`update` in a `DB::transaction` so points/order/pivots/`pos_debt` persist atomically; in `update`, run the debt guard before any pivot mutation.
  - Legacy origin: `OrderController.php:135-160,301-337`
  - Done when: a forced failure mid-write rolls back all of order, pivots, points, and debt; a debt-guard rejection in `update` leaves pivots untouched.
  - Confidence: 🟢 — ✅ Fixed 2026-09-25 (`questions.md#question-11`). Verified: the 5-test `OrderControllerTest` suite still passes; a direct `php artisan tinker` reproduction of a debt-guard-rejected `update()` call shows pivots and `updated_at` unchanged (guard now runs before mutation).

- [ ] T-14, (Improvement, not in legacy) Add observability — structured logs/metrics on create, finalise, debt post, and delete.
  - Legacy origin: `OrderController.php` (absence)
  - Done when: each financial mutation emits a traceable event.
  - Confidence: 🔴

## Test Tasks

- [ ] TT-01, Create happy path: valid done order → correct `subtotal`/`total`/`earned_point`/`count`, pivots with `qty`/`price`/`unit_id`/`conversion_qty`, points credited, `pages.pos-print` rendered (see `requirements.md`, Acceptance Criteria).
- [ ] TT-02, Empty cart: `POST /orders` with no items → validation error "Không thể tạo đơn hàng rỗng"; no order persisted.
- [ ] TT-03, Debt guards: debt with no customer, and debt > total → each redirects with the exact error and persists nothing.
- [ ] TT-04, Points idempotency: done→draft→done credits `earned_point` exactly once.
- [ ] TT-05, Debt idempotency: a done debt-bearing order posts one `pos_debt`; re-edit does not re-post and keeps `debt_amount` frozen.
- [ ] TT-06, Edit window: a done order with `updated_at` > 24h ago → `update` refused with the exact message.
- [ ] TT-07, `create_now_mode`: finalising a stored draft rebuilds items from pivots and posts points/debt as a done order.
- [ ] TT-08, Delete reversal: deleting a done order with points + `pos_debt` reverses points (≥ 0), inserts `debt_void`, hard-deletes order + pivots (cascade), and returns `{status:true}`.
- [ ] TT-09, Unknown code skipped: a cart line with a non-existent code contributes nothing to totals/points/pivots.
- [ ] TT-10, List filters: `?status=done` narrows; `?q=#QT78-{id}` and `?q={id}` surface the order; regression guard for the status/`q` interaction (see Pending Gaps).
- [ ] TT-11, Auth: anonymous request to any `/orders` verb → `302` to `auth/login`.
- [ ] TT-12, Atomicity regression (T-13, fixed 2026-09-25): `PUT /orders/{id}` with a `debt_amount` exceeding total is rejected by the guard, and the order's `order_product` pivots and `updated_at` are byte-identical to before the request (no detach/attach occurred).

## Data Migration Tasks (if applicable)

- None. The unit operates on the existing `orders` / `order_product` tables and writes to `customers`/`customer_debts` owned by other units. No schema change is introduced here. 🟢 (`data-dictionary.md:200-237`)

## Suggested Order

1. T-01 → T-03 (routes + totalling engine) form the shared foundation.
2. T-04 → T-07 build `store` and its money invariants; T-08 (`show`) is independent.
3. T-09 (`update`) reuses the engine (T-03) and the guards (T-05→T-07).
4. T-10 (`destroy`) depends on the `customers-debt-actions` reversal helpers.
5. T-11/T-12 (views/stubs), then T-14 (observability) last.

## Pending Gaps (🔴)

- **No observability (T-14):** the system's highest-value financial mutations emit no signal.
