# Orders-Print — Requirements

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

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

## Overview

`orders-print` is the standalone printable-receipt endpoint (`GET /orders/{order}/print`, `OrderController::printOrder`). It renders a single order as an 80mm thermal-printer receipt (`pages.pos-print`) — the store header, the order code/date, the customer block, the priced line items, discount and total, a thank-you footer, and bank details — with a browser **Print** button. It is read-only. 🟢 (`routes/web.php:73`, `OrderController.php:392-400`)

The same `pages.pos-print` view is rendered **directly** by `orders-crud`'s `store`/`update` when an order is finalised (`status === 'done'`); this route is the way to **re-print** an existing order later (linked from the order list with `?ref=orders`). 🟢 (`OrderController.php:162,334`; `orders.blade.php:74`)

The route is declared **before** `resource('/orders')`, so `/orders/{order}/print` is not captured by the resource `show` route, and it has no route name. 🟢 (`routes/web.php:73-75`)

## Responsibilities

- Load the order by id: `$order = Order::findOrFail($id)` — an unknown id returns a clean `404` (⚠ Reviewer 2026-09-22: corrected from a stale "`Order::find`, null-safe, not findOrFail" claim that contradicted the code and every other file in this unit; the previously-flagged null-deref/500 on an unknown id does **not** occur). 🟢 (`:396`)
- Set the page header to `In đơn hàng` ("Print order") and a single-item breadcrumb. 🟢 (`:395-398`)
- Render `pages.pos-print` with the `order`. 🟢 (`:399`)
- (In the view) render the 80mm receipt: store identity, `#QT78-{id}` code + `created_at` (`d-m-Y H:i`), optional customer block (name / phone / **current** points), one block per product line with a unit label and `qty × price = line total`, a discount line, a total line, footer + bank details, and a `noPrint` action bar (back link + `window.print()`). 🟢 (`resources/views/pages/pos-print.blade.php`)

## Business Rules

- **Any order is printable regardless of status.** There is no `draft`/`done` guard; a draft order can be printed just like a finalised one. **Confirmed intentional** (2026-09-25, `questions.md#question-13`). 🟢 (`:394-402`)
- **Human-facing code.** The receipt shows `$order->code` = `#QT78-{id}` (appended attribute). 🟢 (`pos-print.blade.php:43`; `Order.php:49-51`)
- **Customer block is conditional.** Rendered only when the order has a customer; it shows `fullname`, `phone`, and the customer's **current** loyalty points (`number_format(customer.points, 1)`) — i.e. the live balance at print time, not points at sale time. 🟡 (`pos-print.blade.php:45-49`)
- **Per-line unit label.** For each product line, the label is the `Unit::find(pivot.unit_id)->name` when a selling `unit_id` was captured, otherwise the product's base `unit` string. 🟢 (`pos-print.blade.php:56-60`)
- **Money shown at 0 decimals.** Line price/total, discount, and grand total render via `number_format(…, 0)` — no decimals on the receipt, even though amounts are stored with decimals. 🟡 (`pos-print.blade.php:64-81`)
- **Line total is `qty × pivot.price`.** Computed in the view from the captured sale-time pivot price, not re-priced. 🟢 (`pos-print.blade.php:65`)
- **Discount and total.** The receipt prints `- discount_amount` and the order `total` (`= subtotal − discount_amount`). 🟢 (`pos-print.blade.php:73,81`)
- **`?ref` controls the back button.** With `?ref=orders`, the "back" control is `history.back()` ("Trở lại"); otherwise it links to `/pos` ("Tạo đơn mới" — create a new order). 🟢 (`pos-print.blade.php:100-108`)
- **Print is client-side.** The receipt CSS hides `.noPrint` and chrome under `@media print`; the operator triggers printing via the `window.print()` button. 🟢 (`pos-print.blade.php:110-112,177-196`)

## Functional Requirements

| ID | Requirement | Priority | Acceptance criterion |
|----|-------------|----------|----------------------|
| RF-01 | Render an order as an 80mm receipt at `GET /orders/{id}/print` | Must | For a valid order id, the response is `200 text/html` showing the store header, `#QT78-{id}`, date, lines, discount, and total. 🟢 |
| RF-02 | Show the customer block (name / phone / current points) when the order has a customer | Should | A customer order prints the name, phone, and the customer's live points; a walk-in prints no customer block. 🟢 |
| RF-03 | Label each line with its selling unit (or base unit) | Should | A line sold in a converted unit shows that unit's name; a base-unit line shows the product's `unit`. 🟢 |
| RF-04 | Print `qty × price = line total`, a discount line, and the grand total (0 decimals) | Must | Each line shows `qty`, unit price, and `qty*price`; the footer shows `- discount_amount` and `total`, all `number_format(…,0)`. 🟢 |
| RF-05 | Provide a client-side Print button and a `?ref`-aware back link | Should | The `window.print()` button prints; `?ref=orders` yields a history-back link, otherwise a "new order" link to `/pos`. 🟢 |
| RF-06 | Require an authenticated admin session | Must | Anonymous request → `302` to `auth/login`. 🟢 (`routes/web.php:24-28`) |

## Non-Functional Requirements

| Type | Inferred requirement | Evidence in code | Confidence |
|------|----------------------|------------------|------------|
| Security | Admin authentication required (admin route group middleware `['web','admin']`) | `routes/web.php:24-28,73` | 🟢 |
| Usability | Receipt sized for an 80mm thermal printer (`max-width: 80mm`, print media collapses to 72mm and hides chrome) | `pos-print.blade.php:124-128,177-196` | 🟢 |
| Performance | Per-line `Unit::find(pivot.unit_id)` inside the print loop is an N+1 query (one lookup per line with a unit) | `pos-print.blade.php:57` | 🟡 |
| Reliability | ✅ Fixed 2026-09-21 — `printOrder` now uses `findOrFail` (was `find`, which returned `null` and made the view crash with a fatal error instead of a clean 404) | `OrderController.php:396` | 🟢 |
| Observability | None — no log/metric/trace on the print path | `OrderController.php:392-400` (absence) | 🔴 |

> Inferred from code. Validate with the operations team.

## Acceptance Criteria

```gherkin
Given an authenticated administrator and an existing order with a customer and items
When he accesses GET /orders/{id}/print
Then he receives HTTP 200 with the pages.pos-print receipt showing the store header, the code #QT78-{id}, the date, the customer block (name/phone/current points), each line with a unit label and qty × price = line total, the discount line, and the grand total

Given a walk-in order with no customer
When GET /orders/{id}/print is called
Then the receipt renders without the customer block

Given the re-print link from the order list (?ref=orders)
When the receipt is opened
Then the "back" control uses history.back() instead of linking to /pos

Given a non-existent order id
When GET /orders/{id}/print is called
Then it receives HTTP 404 (findOrFail) — fixed 2026-09-21 (was: Order::find returned null and the view fataled dereferencing $order)

Given a request with no authenticated admin session
When GET /orders/{id}/print is called
Then it receives HTTP 302 redirecting to auth/login
```

## Priority (MoSCoW)

| Requirement | MoSCoW | Justification |
|-------------|--------|---------------|
| Render the receipt (RF-01) | Must | The endpoint's whole purpose — a printable sales record |
| Line/discount/total printing (RF-04) | Must | The receipt is worthless without the priced lines and total |
| Customer block (RF-02) | Should | Adds loyalty/contact context when a customer is attached |
| Unit label per line (RF-03) | Should | Disambiguates multi-unit products on the receipt |
| Print button + `?ref` back link (RF-05) | Should | Operator ergonomics; printing itself is client-side |
| Admin authentication (RF-06) | Must | Enforced by the route group |

> Priority inferred from the endpoint's role as the receipt/re-print surface for orders finalised in `orders-crud`.

## Code Traceability

| File | Function / Class | Coverage |
|------|------------------|----------|
| `app/Http/Controllers/OrderController.php:394-402` | `OrderController::printOrder` | 🟢 |
| `routes/web.php:73` | `GET /orders/{order}/print` (declared before the resource) | 🟢 |
| `resources/views/pages/pos-print.blade.php` | The 80mm receipt template (shared with `orders-crud` done path) | 🟢 |
| `app/Models/Order.php:39-51` | `Order` (`products()` withPivot, `code` appended) | 🟢 |
| `app/Models/Unit.php` | `Unit::find(pivot.unit_id)->name` per-line label | 🟢 |
| `resources/views/pages/orders.blade.php:74` | Order-list re-print link (`?ref=orders`) | 🟢 |
