# Orders CRUD Specification

## Purpose

`orders-crud` is the back-office order lifecycle resource that exposes list, create, detail, edit/finalise, and delete operations for sales orders. It is the authoritative point where a POS cart submission becomes a durable financial record, applying loyalty point awards and customer debt postings as appropriate.

## Requirements

### Requirement: Authentication Required

The system SHALL require an authenticated admin session for every `/orders` route; an unauthenticated request SHALL receive an HTTP `302` redirect to `auth/login`. (Implemented in `routes/web.php:24-28`)

#### Scenario: Unauthenticated list request

- **GIVEN** a visitor with no authenticated admin session
- **WHEN** they send `GET /orders`
- **THEN** the system responds `302` redirecting to `auth/login`

#### Scenario: Unauthenticated mutation request

- **GIVEN** a visitor with no authenticated admin session
- **WHEN** they send `POST /orders`
- **THEN** the system responds `302` redirecting to `auth/login`

---

### Requirement: List Orders Paginated

The system SHALL respond to `GET /orders` with HTTP `200` HTML rendering up to 30 orders per page, ordered by `updated_at` descending, each row exposing the order code, customer name, customer phone, line count, total amount, creation date, and status. (Implemented in `app/Http/Controllers/OrderController.php:21-47`)

#### Scenario: Default order list

- **GIVEN** an authenticated admin and existing orders
- **WHEN** they send `GET /orders`
- **THEN** the system responds `200` with the order list, newest-activity-first, showing at most 30 rows per page

#### Scenario: Second page

- **GIVEN** more than 30 orders exist
- **WHEN** they send `GET /orders?page=2`
- **THEN** the system renders the second page of up to 30 results in the same order

---

### Requirement: Filter Orders by Status

The system SHALL restrict the order list to only records matching the submitted `status` value (`draft` or `done`) when the `status` query parameter is present. (Implemented in `app/Http/Controllers/OrderController.php:40-42`)

#### Scenario: Filter to done orders

- **GIVEN** an authenticated admin with both draft and done orders
- **WHEN** they send `GET /orders?status=done`
- **THEN** only finalised orders appear in the response

---

### Requirement: Search Orders by Query String

The system SHALL filter the order list to records whose `id` equals `q`, whose formatted code (`#QT78-{id}`) equals `q`, or whose customer phone contains `q` as a substring, when the `q` query parameter is present; the `status` filter SHALL remain effective across all three search branches and MUST NOT be bypassed by an id or code match. (Implemented in `app/Http/Controllers/OrderController.php:31-39`)

#### Scenario: Search by numeric id

- **GIVEN** an authenticated admin and an order with id `5`
- **WHEN** they send `GET /orders?q=5`
- **THEN** order `5` appears in the results

#### Scenario: Search by formatted code

- **GIVEN** an authenticated admin and an order with id `5`
- **WHEN** they send `GET /orders?q=%23QT78-5`
- **THEN** order `5` appears in the results

#### Scenario: Search by customer phone substring

- **GIVEN** an authenticated admin and orders for a customer with phone `0901234567`
- **WHEN** they send `GET /orders?q=0901`
- **THEN** that customer's orders appear in the results

#### Scenario: Status filter not bypassed by id match

- **GIVEN** an authenticated admin, a draft order with id `5`, and no done order with id `5`
- **WHEN** they send `GET /orders?q=5&status=done`
- **THEN** order `5` does not appear (the id match does not bypass the `status=done` constraint)

---

### Requirement: Create Order

The system SHALL accept `POST /orders` containing an `items` array, optional customer phone, optional notes, optional `discount_amount`, optional `debt_amount`, and optional `status`; it SHALL validate the payload, price each resolved line, compute totals and earned points, enforce debt rules, and persist the order together with its line pivots. (Implemented in `app/Http/Controllers/OrderController.php:64-165`)

#### Scenario: Valid cart submitted

- **GIVEN** an authenticated admin submits a cart with at least one resolvable product
- **WHEN** `POST /orders` is received
- **THEN** an order is persisted with computed `subtotal`, `total`, `earned_point`, and `count`; its line pivots carry `qty`, `price`, `unit_id`, and `conversion_qty`

---

### Requirement: Reject Empty Cart

The system SHALL reject `POST /orders` when `items` is absent or empty with a validation error and MUST NOT persist any record. (Implemented in `app/Http/Controllers/OrderController.php:66-78`)

#### Scenario: No items submitted

- **GIVEN** an authenticated admin
- **WHEN** they submit `POST /orders` with no `items`
- **THEN** the system returns a validation error "Không thể tạo đơn hàng rỗng" and no order is created

---

### Requirement: Default Order Status to Draft

The system SHALL assign `draft` as the order status when the `status` field is absent or null. (Implemented in `app/Http/Controllers/OrderController.php:86`)

#### Scenario: Status omitted

- **GIVEN** an authenticated admin submits a cart without a `status` field
- **WHEN** `POST /orders` is received with a valid payload
- **THEN** the persisted order has status `draft`

---

### Requirement: Unknown Product Codes Silently Skipped

The system SHALL omit any cart line whose product code does not resolve to a known product from totals, earned points, and persisted pivots, without returning an error. (Implemented in `app/Http/Controllers/OrderController.php:96,265`)

#### Scenario: Cart contains an unrecognised code

- **GIVEN** an authenticated admin submits a cart with one valid code and one unrecognised code
- **WHEN** `POST /orders` is received
- **THEN** the order is created with `count=1`, totals reflecting only the valid line, and only one pivot row; no error is raised for the unrecognised code

---

### Requirement: Walk-In Orders Allowed

The system SHALL permit order creation and finalisation without an attached customer; such orders earn no points and may carry no debt. (Implemented in `app/Http/Controllers/OrderController.php:134,308`)

#### Scenario: Done order without a customer

- **GIVEN** an authenticated admin submits a cart with `status=done` and no customer phone
- **WHEN** `POST /orders` is received
- **THEN** the order is persisted and finalised; no points or debt are posted

---

### Requirement: Debt Requires Customer

The system SHALL refuse any order submission where `debt_amount` is greater than zero but no customer phone is provided, redirecting without persisting any record. (Implemented in `app/Http/Controllers/OrderController.php:121-131,294-306`)

#### Scenario: Debt with no customer on create

- **GIVEN** an authenticated admin submits a cart with `debt_amount=100` and no customer phone
- **WHEN** `POST /orders` is received
- **THEN** the system redirects to `pos.index` with the error "Phải chọn khách hàng khi có tiền nợ." and no order is persisted

---

### Requirement: Debt Must Not Exceed Total

The system SHALL refuse any order submission where `debt_amount` exceeds the order total, redirecting without persisting any record. (Implemented in `app/Http/Controllers/OrderController.php:121-131,294-306`)

#### Scenario: Debt exceeds total on create

- **GIVEN** an authenticated admin submits a cart with a computed total of `200` and `debt_amount=300`
- **WHEN** `POST /orders` is received
- **THEN** the system redirects to `pos.index` with the error "Số tiền nợ không được lớn hơn tổng tiền hàng." and no order is persisted

---

### Requirement: Draft Order Redirects to POS

The system SHALL redirect to `pos.index` with a success toast after persisting a `draft` order. (Implemented in `app/Http/Controllers/OrderController.php:153-156,339-344`)

#### Scenario: Draft save response

- **GIVEN** an authenticated admin creates or updates an order with `status=draft`
- **WHEN** the order is successfully persisted
- **THEN** the system responds with a `302` redirect to `pos.index` accompanied by the toast "Lưu nháp thành công"

---

### Requirement: Done Order Returns Printable Receipt

The system SHALL respond with HTTP `200` HTML rendering the `pages.pos-print` receipt page after persisting a `done` order. (Implemented in `app/Http/Controllers/OrderController.php:158-162,345-349`)

#### Scenario: Done save response

- **GIVEN** an authenticated admin creates or updates an order with `status=done`
- **WHEN** the order is successfully persisted
- **THEN** the system responds `200` with the `pages.pos-print` receipt

---

### Requirement: Award Loyalty Points Exactly Once

The system SHALL increment the attached customer's points by the order's `earned_point` the first time an order is finalised to `done`, and SHALL NOT award points again on any subsequent finalisation of the same order. (Implemented in `app/Http/Controllers/OrderController.php:136-140,311-315`)

#### Scenario: Points awarded on first finalisation

- **GIVEN** a done order with an attached customer and `earned_point=10`
- **WHEN** the order is finalised to `done` for the first time
- **THEN** `customer.points` increases by `10` and `points_awarded_at` is recorded on the order

#### Scenario: Points not re-awarded on re-finalisation

- **GIVEN** a done order that already has `points_awarded_at` set
- **WHEN** the order is edited back to `draft` and re-finalised to `done`
- **THEN** `customer.points` is not incremented a second time

---

### Requirement: Post Customer Debt Exactly Once

The system SHALL post exactly one `pos_debt` ledger entry when an order is first finalised to `done` with an attached customer and a positive `debt_amount`; subsequent edits SHALL NOT re-post the debt and SHALL freeze `debt_amount` from further modification. (Implemented in `app/Http/Controllers/OrderController.php:149-151,321-323`; `app/Models/Order.php:65-67`)

#### Scenario: Debt posted on first finalisation

- **GIVEN** a done order with an attached customer and `debt_amount=500`
- **WHEN** the order is finalised to `done` for the first time
- **THEN** exactly one `pos_debt` row is recorded and the customer's `debt_total` increases by `500`

#### Scenario: Debt not re-posted on subsequent edit

- **GIVEN** an order already having a `pos_debt` row
- **WHEN** the order is edited and re-finalised
- **THEN** no additional `pos_debt` row is created and `debt_amount` on the order is unchanged

---

### Requirement: Show Order Detail

The system SHALL respond to `GET /orders/{id}` with HTTP `200` HTML rendering the order detail page for a known id, and HTTP `404` for an unknown id. (Implemented in `app/Http/Controllers/OrderController.php:173-184`)

#### Scenario: Valid order id

- **GIVEN** an authenticated admin and an order with id `42`
- **WHEN** they send `GET /orders/42`
- **THEN** the system responds `200` with the order detail page

#### Scenario: Unknown order id

- **GIVEN** an authenticated admin
- **WHEN** they send `GET /orders/99999` for a non-existent order
- **THEN** the system responds `404`

---

### Requirement: Enforce 24-Hour Edit Window

The system SHALL reject `PUT /orders/{id}` for a `done` order whose `updated_at` is more than 24 hours in the past, redirecting back with an error and leaving the order unchanged. (Implemented in `app/Http/Controllers/OrderController.php:242-243`; `app/Models/Order.php:53-55`)

#### Scenario: Edit refused after 24 hours

- **GIVEN** an authenticated admin and a done order with `updated_at` more than 24 hours ago
- **WHEN** `PUT /orders/{id}` is submitted
- **THEN** the system redirects back with the error "Không thể cập nhật đơn hàng hoàn thành quá 24h" and the order is not modified

#### Scenario: Edit accepted within 24 hours

- **GIVEN** an authenticated admin and a done order with `updated_at` less than 24 hours ago
- **WHEN** `PUT /orders/{id}` is submitted with a valid payload
- **THEN** the system processes the update and returns the appropriate draft or done response

---

### Requirement: Customer Immutable After Finalisation

The system SHALL ignore a customer phone submitted in `PUT /orders/{id}` for a non-draft order and SHALL retain the order's currently attached customer. (Implemented in `app/Http/Controllers/OrderController.php:245-249`)

#### Scenario: Customer phone ignored on done order edit

- **GIVEN** a done order attached to customer A
- **WHEN** `PUT /orders/{id}` is submitted with a phone number belonging to customer B
- **THEN** the order retains customer A; the submitted phone is disregarded

---

### Requirement: Finalise Draft via create_now_mode

The system SHALL accept `PUT /orders/{id}` with `create_now_mode` set, rebuilding the item list from the order's existing line pivots and re-running the totalling engine at current catalog prices, so that a stored draft can be finalised without resubmitting the cart; the `items` field MAY be omitted when `create_now_mode` is present. (Implemented in `app/Http/Controllers/OrderController.php:223-240`)

#### Scenario: Finalise stored draft without cart

- **GIVEN** an authenticated admin and a stored draft order with existing line pivots
- **WHEN** `PUT /orders/{id}` is submitted with `create_now_mode` set and `status=done`
- **THEN** the order is finalised using its stored pivots (re-priced at current catalog prices), totals are recomputed, and the receipt is rendered

---

### Requirement: Delete Order with Financial Reversal

The system SHALL permanently delete an order and atomically reverse all associated financial effects — subtracting awarded points from the customer (minimum zero), inserting a `debt_void` entry to reduce the customer's `debt_total`, and hard-deleting the order and its line pivots — then respond with HTTP `200` `application/json` containing a `status` boolean and a `message` string. For an unknown order id the system SHALL respond with `status: false` rather than HTTP `404`. (Implemented in `app/Http/Controllers/OrderController.php:362-383`)

#### Scenario: Delete order with points and debt

- **GIVEN** an authenticated admin and a done order that awarded points and posted a `pos_debt`
- **WHEN** `DELETE /orders/{id}` is sent
- **THEN** the awarded points are subtracted from the customer's balance (clamped at zero), a `debt_void` entry is inserted reducing `debt_total`, the order and its line pivots are permanently deleted, and the system responds `200` JSON `{"status":true,"message":"..."}`

#### Scenario: Delete walk-in order with no points or debt

- **GIVEN** an authenticated admin and a done order with no attached customer
- **WHEN** `DELETE /orders/{id}` is sent
- **THEN** the order and its line pivots are permanently deleted and the system responds `200` JSON `{"status":true,"message":"..."}`

#### Scenario: Delete unknown order id

- **GIVEN** an authenticated admin
- **WHEN** `DELETE /orders/99999` is sent for a non-existent order
- **THEN** the system responds `200` JSON `{"status":false,"message":"..."}` (not `404`)

---

### Requirement: Create and Edit Routes Are Empty Stubs

The system SHALL respond to `GET /orders/create` and `GET /orders/{id}/edit` with HTTP `200` and an empty body; neither route SHALL render a form or perform any action. (Implemented in `app/Http/Controllers/OrderController.php:53-56,192-195`)

#### Scenario: Create route returns empty

- **GIVEN** an authenticated admin
- **WHEN** they send `GET /orders/create`
- **THEN** the system responds `200` with an empty body

#### Scenario: Edit route returns empty

- **GIVEN** an authenticated admin
- **WHEN** they send `GET /orders/42/edit`
- **THEN** the system responds `200` with an empty body
