# POS Terminal Specification

## Purpose

The POS Terminal is the cashier point-of-sale screen and the application's effective landing page, serving an authenticated HTML terminal that hosts a client-side cart engine. It enables scanning products into a cart, selecting customers, managing order drafts, and submitting sales to the orders endpoint.

## Requirements

### Requirement: Landing Page Redirect

The system SHALL respond to `GET /` with a permanent `301` redirect to `/pos`. (Implemented in `routes/web.php:22`)

#### Scenario: Root path redirects to the terminal

- **GIVEN** any HTTP request to the root path `/`
- **WHEN** the request is received
- **THEN** the response has status `301` and a `Location` header of `/pos`

---

### Requirement: Authentication Guard

The system SHALL redirect unauthenticated requests for `GET /pos` to `auth/login` with a `302` response. (Implemented in `routes/web.php:24-28`)

#### Scenario: Anonymous request is rejected

- **GIVEN** a request without a valid admin session
- **WHEN** `GET /pos` is requested
- **THEN** the response is `302 Found` with `Location: auth/login`

---

### Requirement: Terminal Rendering

The system SHALL render the POS terminal page with `200 OK` for an authenticated admin requesting `GET /pos`. (Implemented in `app/Http/Controllers/PosController.php:18-687`)

#### Scenario: Authenticated request receives the terminal

- **GIVEN** a request carrying a valid admin session
- **WHEN** `GET /pos` is requested
- **THEN** the response is `200 OK` containing the POS terminal HTML with the injected cart engine

---

### Requirement: Collaborator URL Seeding

The system SHALL inject five collaborating endpoint URLs into the rendered terminal: the product scan URL (`pos/scan`), the customer scan URL (`customers/scan`), the draft picker URL (`orders/scan?status=draft`), the order submission base URL (`orders`), and the customer-detail link template using `#id#` as a client-side replacement placeholder. (Implemented in `app/Http/Controllers/PosController.php:25-29`)

#### Scenario: Terminal exposes all five collaborator URLs

- **GIVEN** an authenticated admin requests `GET /pos`
- **WHEN** the terminal page is rendered
- **THEN** the cart engine has access to all five collaborator URLs seeded by the server

---

### Requirement: Draft Preload via Query Parameter

The system SHALL, when `?id=<orderId>` is present, pre-load that order with its associated customer and products — each product's units replaced by a normalized units payload — and pass the result to the cart engine for rehydration at boot. (Implemented in `app/Http/Controllers/PosController.php:31-41,610`)

#### Scenario: Valid draft id pre-fills the terminal

- **GIVEN** order N exists
- **WHEN** an authenticated admin requests `GET /pos?id=N`
- **THEN** the terminal renders pre-filled with order N's customer, product lines, notes, and totals

---

### Requirement: Invalid Draft ID Fallback

The system SHALL open an empty terminal when the `?id=` query parameter refers to a missing or invalid order, returning `200 OK` rather than an error response. (Implemented in `app/Http/Controllers/PosController.php:31-41`)

#### Scenario: Nonexistent order id yields an empty terminal

- **GIVEN** no order exists with the requested id
- **WHEN** an authenticated admin requests `GET /pos?id=<nonexistent>`
- **THEN** the response is `200 OK` with an empty terminal, identical to a request without `?id=`

---

### Requirement: Units Payload Composition

The system SHALL compose each product's units payload with the base unit as the first entry (`unit_id` null, label from the product's `unit` string, `conversion_qty` 1) followed by each configured `ProductUnit`; the payload SHALL guarantee exactly one entry has `is_default` true, and SHALL set the base unit entry as the default when no `ProductUnit` carries the default flag. (Implemented in `app/Http/Controllers/PosController.php:786-810`)

#### Scenario: Product with configured conversion units yields a complete payload

- **GIVEN** a product has two `ProductUnit` records, one flagged as the default
- **WHEN** the units payload is composed for that product
- **THEN** the payload begins with the base unit entry (`unit_id` null) and contains exactly one entry where `is_default` is true

#### Scenario: Product with no conversion units defaults the base unit

- **GIVEN** a product has no `ProductUnit` records
- **WHEN** the units payload is composed for that product
- **THEN** the payload contains only the base unit entry and that entry has `is_default` true

---

### Requirement: Cart Line Keying and Deduplication

The cart engine SHALL key each line by the concatenation `code + '__' + (unit_id || 'base')`; scanning or selecting the same product in the same unit SHALL increment that line's quantity by one rather than inserting a new line, while the same product in a different unit SHALL produce a distinct line. (Implemented in `app/Http/Controllers/PosController.php:96-101,131`)

#### Scenario: Repeating a product in the same unit increments quantity

- **GIVEN** the cart has a line for product X in its base unit with quantity 1
- **WHEN** product X is scanned again in the base unit
- **THEN** the cart has one line for X with quantity 2

#### Scenario: Same product in two different units yields two lines

- **GIVEN** product X has a base unit and a "box" conversion unit
- **WHEN** product X is added in the base unit and then added in the "box" unit
- **THEN** the cart contains two distinct lines for product X

---

### Requirement: Per-Line Price Refresh

The cart engine SHALL re-price each affected line via `GET /products/get-price` after every add, quantity edit, unit change, or customer change, and SHALL compute the line total as `qty × round(unitPrice, 1)`. (Implemented in `app/Http/Controllers/PosController.php:57-72`)

#### Scenario: Adding a product applies its fetched price

- **GIVEN** the terminal is open with no items
- **WHEN** a product is added to the cart
- **THEN** the line total equals the product's quantity multiplied by the rounded unit price returned by the pricing endpoint

---

### Requirement: Unit Change with Line Merge

The cart engine SHALL, when the unit of a cart line is changed, relabel that line to the new unit and re-price it while preserving its quantity; if a line for the same product in the target unit already exists, the engine SHALL add the source line's quantity directly into the existing line, re-price the merged result, and remove the source line. (Implemented in `app/Http/Controllers/PosController.php:354-407`)

#### Scenario: Unit change with no collision relabels and re-prices

- **GIVEN** the cart has product X in the base unit with quantity 3 and no "box" line for X
- **WHEN** the base unit line's unit is changed to "box"
- **THEN** the cart has one line for X in the "box" unit with quantity 3, re-priced for "box"

#### Scenario: Unit change into an existing line merges quantities

- **GIVEN** the cart has product X in the base unit with quantity 2 and a "box" line for X with quantity 1
- **WHEN** the base unit line's unit is changed to "box"
- **THEN** the cart has a single line for X in the "box" unit with quantity 3, re-priced for "box"

---

### Requirement: Customer Selection and Full-Cart Re-pricing

The cart engine SHALL re-price every existing cart line for the selected customer's type immediately upon customer change, and SHALL display that customer's outstanding debt hint when `debt_total` is greater than zero. (Implemented in `app/Http/Controllers/PosController.php:489-511`)

#### Scenario: Selecting a customer re-prices all lines

- **GIVEN** the cart has items priced at retail rates
- **WHEN** a wholesale customer is selected
- **THEN** every cart line is re-priced for the wholesale customer's type

---

### Requirement: Quick-Add Customer

The cart engine SHALL allow creating a new customer via an inline modal that posts to the `customers` endpoint; on success, the newly created customer SHALL be selected and their information filled into the terminal. (Implemented in `app/Http/Controllers/PosController.php:619-650`)

#### Scenario: Quick-add creates and selects the new customer

- **GIVEN** the quick-add modal is submitted with valid customer data
- **WHEN** the `customers` endpoint responds with success
- **THEN** the new customer is selected in the terminal and their information is displayed

---

### Requirement: Running Totals Maintenance

The cart engine SHALL maintain `subtotal` as the sum of all rounded line totals, `total` as `subtotal − discount`, and SHALL keep `count`, `discount`, and `debt` synchronized; all totals SHALL update on every cart mutation, discount change, or debt change. (Implemented in `app/Http/Controllers/PosController.php:168-189,336-345`)

#### Scenario: Totals reflect the current cart state

- **GIVEN** the cart has two lines with totals 100 and 50, and a discount of 20
- **WHEN** totals are computed
- **THEN** `subtotal` is 150, `total` is 130, and `count` is 2

---

### Requirement: Debt Input Locking

The cart engine SHALL clear and disable the debt input when no customer is selected; it SHALL disable the debt input without clearing it when the loaded order has `debt_locked` set. (Implemented in `app/Http/Controllers/PosController.php:415-422,549-554`)

#### Scenario: Clearing the customer disables and empties the debt input

- **GIVEN** a customer is selected and the debt input is enabled
- **WHEN** the customer selection is removed
- **THEN** the debt input is disabled and its value is empty

#### Scenario: A debt-locked order disables the debt input

- **GIVEN** an order with `debt_locked` true is loaded into the terminal
- **WHEN** the terminal rehydrates from that order
- **THEN** the debt input is disabled

---

### Requirement: Debt Validation on Submission

The cart engine SHALL block submission with the message "Phải chọn khách hàng khi có tiền nợ." when the debt amount is greater than zero, the debt input is enabled, and no customer is selected; it SHALL block submission with the message "Số tiền nợ không được lớn hơn tổng tiền hàng." when the debt amount exceeds `subtotal − discount`; a zero debt amount or a disabled debt input SHALL not trigger either block. (Implemented in `app/Http/Controllers/PosController.php:308-334`)

#### Scenario: Submission blocked when positive debt has no customer

- **GIVEN** the debt input is enabled, contains a positive value, and no customer is selected
- **WHEN** the submit button is clicked
- **THEN** submission is blocked and the message "Phải chọn khách hàng khi có tiền nợ." is displayed

#### Scenario: Submission blocked when debt exceeds the order total

- **GIVEN** a customer is selected, the debt input is enabled, and `subtotal − discount` is 200
- **WHEN** the debt input contains 250 and the submit button is clicked
- **THEN** submission is blocked and the message "Số tiền nợ không được lớn hơn tổng tiền hàng." is displayed

#### Scenario: Zero debt submits without a debt-related block

- **GIVEN** the debt amount is 0 and all other validations pass
- **WHEN** the submit button is clicked
- **THEN** the form is submitted without a debt-related error

---

### Requirement: Draft Rehydration and Update Mode

The cart engine SHALL, when a draft order is loaded via server preload or the draft picker, populate the terminal with the order's customer, product lines, notes, and totals; it SHALL append `?id=<orderId>` to the browser URL and repoint the form to `PUT /orders/{id}` via method override; loading an order with status `done` SHALL additionally lock the customer search field. (Implemented in `app/Http/Controllers/PosController.php:519-564,610-617`)

#### Scenario: Loading a draft fills the terminal and switches to update mode

- **GIVEN** order N exists as a draft
- **WHEN** the draft is loaded via `GET /pos?id=N` or the draft picker
- **THEN** the terminal displays order N's customer, lines, notes, and totals, and submitting the form issues `PUT /orders/N`

#### Scenario: Loading a done order locks the customer search

- **GIVEN** order N has status `done`
- **WHEN** order N is loaded into the terminal
- **THEN** the customer search field is not interactive

---

### Requirement: Status-Driven Form Submission

The cart engine SHALL, on a submit button click, write that button's `data-status` value (`draft` or `done`) into the form's hidden `status` input and submit `#pos_form` to the order endpoint — `POST /orders` for a new sale or `PUT /orders/{id}` for a loaded draft. (Implemented in `app/Http/Controllers/PosController.php:308-334,332-333,561-562`)

#### Scenario: Save-draft button submits with status draft

- **GIVEN** the terminal has items and no draft is loaded
- **WHEN** the save-draft button is clicked and all validations pass
- **THEN** the form is submitted as `POST /orders` with `status` set to `draft`

#### Scenario: Finalize button submits with status done

- **GIVEN** the terminal has items and no draft is loaded
- **WHEN** the finalize button is clicked and all validations pass
- **THEN** the form is submitted as `POST /orders` with `status` set to `done`

---

### Requirement: Unsaved-Work Guard and Scanner Hotkey

The cart engine SHALL arm a `beforeunload` prompt when the terminal contains unsubmitted cart items, warning the operator before navigation away from the page; it SHALL bind the `F2` key to refocus the scanner input. (Implemented in `app/Http/Controllers/PosController.php:652-665`)

#### Scenario: Navigating away with items triggers a confirmation

- **GIVEN** the cart contains at least one product line
- **WHEN** the operator attempts to navigate away from the terminal
- **THEN** the browser displays an unsaved-work confirmation prompt

#### Scenario: F2 refocuses the scanner

- **GIVEN** the terminal is open and focus is elsewhere on the page
- **WHEN** the operator presses F2
- **THEN** focus moves to the scanner input field

---

### Requirement: Restricted HTTP Surface

The system SHALL expose only the `GET /pos` index action for the POS resource; resource verbs `POST`, `PUT`, `PATCH`, and `DELETE` on `/pos`, and the actions `create`, `store`, `show`, `edit`, `update`, and `destroy`, SHALL NOT be operational endpoints of this unit. (Implemented in `app/Http/Controllers/PosController.php:694-753`, `routes/web.php:81`)

#### Scenario: Only GET /pos is served

- **GIVEN** the POS resource is registered
- **WHEN** any HTTP verb other than `GET` is sent to `/pos`
- **THEN** the request is not handled as a functional POS terminal action
