# Orders-Print — Technical Design

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

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

## Interface

Single HTML GET endpoint, admin group (`['web','admin']`, empty admin prefix), no route name. 🟢 (`routes/web.php:73`)

| Method | Path | Input | Output | Status codes |
|--------|------|-------|--------|--------------|
| GET | `/orders/{order}/print` | path `order` id; `ref?: string` (query) | `text/html` — `pages.pos-print` | 200, 302 (unauthenticated → `auth/login`), 404 on unknown id |

Controller symbol:

| Symbol | Signature | Return | Note |
|--------|-----------|--------|------|
| `OrderController::printOrder` | `($id)` | view `pages.pos-print` | Loads via `Order::findOrFail($id)`; sets `$header`/`$breadcrumb`; renders. 🟢 (`:394-402`) — ✅ Fixed 2026-09-21 (was `find`, a null-dereference fatal error on unknown id). |

View-supplied variable:

| View variable | Type | Meaning |
|---------------|------|---------|
| `order` | `Order` | The order to print, with its `products` (and pivots) and optional `customer`. Unknown id → `404` (`findOrFail`). 🟢 (`:396`) |

## Main Flow 🟢 (`:394-402`)

1. `GET /orders/{order}/print` reaches `printOrder($id)` via the admin group (route registered before `resource('/orders')` so it is not shadowed). 🟢 (`routes/web.php:73`)
2. `$order = Order::findOrFail($id)` — 404 on miss. 🟢 (`:396`) — ✅ Fixed 2026-09-21 (was `find`, returning `null` and crashing the view).
3. Set `$this->header = __('In đơn hàng')` and a one-item breadcrumb. 🟢 (`:397-400`)
4. `return $this->view('pages.pos-print', compact('order'))`. 🟢 (`:401`)
5. The view renders the receipt: store identity block; title "Hoá đơn"; `Số HĐ: {order->code}`, `Ngày: {created_at d-m-Y H:i}`; a customer block (`fullname`/`phone`/`number_format(points,1)`) if `order->customer`; a `@foreach($order->products …)` body where each line resolves its unit label (`Unit::find(pivot.unit_id)->name ?? ''` when `pivot.unit_id`, else `item->unit`) and prints `qty`, `number_format(pivot.price,0)`, and `number_format(qty*price,0)`; a discount row `- {discount_amount}`; a total row `{total}`; a footer + bank details; and a `.noPrint` bar with a `?ref`-aware back link and a `window.print()` button. 🟢 (`pos-print.blade.php:32-114`)

## Alternative Flows

- **Walk-in order (no customer):** the `@if($order->customer)` block is skipped; the rest prints normally. 🟢 (`pos-print.blade.php:45-49`)
- **Line with no `unit_id`:** the label falls back to the product's base `unit` string. 🟢 (`pos-print.blade.php:57`)
- **`?ref=orders`:** back control is `history.back()`; otherwise a link to `/pos`. 🟢 (`pos-print.blade.php:100-108`)
- **Unknown id:** `findOrFail` raises `ModelNotFoundException` → clean `404`. ✅ Fixed 2026-09-21 (was `find`, returning `null` and crashing the view via `->code`/`->created_at`/`->products`). 🟢 (`:396`)
- **Unauthenticated:** admin group middleware → `302 auth/login`. 🟢 (`routes/web.php:24-28`)

## The shared receipt view

`pages.pos-print` is rendered from **three** places, all producing the same receipt:

- `orders-crud` `store` on a finalised (`done`) create → shows the receipt straight after the sale. 🟢 (`OrderController.php:162`)
- `orders-crud` `update` on a finalised (`done`) edit → re-shows the receipt after finalisation. 🟢 (`OrderController.php:334`)
- this unit's `printOrder` → an on-demand re-print reached from the order list. 🟢 (`:401`)

The `?ref` query param is how the view distinguishes "just sold, offer a new order" from "re-printing from the list, go back". 🟢 (`pos-print.blade.php:100`)

## Dependencies

- **`Order` model** — the subject; supplies `code`, `created_at`, `total`, `discount_amount`, the `customer`, and `products` with pivots. 🟢 (`app/Models/Order.php`)
- **`Unit` model** — per-line label via `Unit::find(pivot.unit_id)->name`. 🟢 (`units` unit; `pos-print.blade.php:57`)
- **`Customer` model** — the optional customer block (live `points`). 🟢 (`customers-crud`)
- **`admin::index` layout + partials** (`partials.error`, `admin::partials.exception`, `admin::partials.toastr`) — chrome around the receipt. 🟢 (`pos-print.blade.php:1-19`)
- **`orders-crud`** — produces the orders being printed and renders this same view directly on the done path. 🟢

## Identified Design Decisions

| Decision | Evidence in code | Confidence |
|----------|------------------|------------|
| One receipt template shared by the sale path and the re-print route | `store`/`update`/`printOrder` all `->view('pages.pos-print')` | 🟢 (`:162,334,401`) |
| `findOrFail` on the print path (consistent with `show`) | `$order = Order::findOrFail($id)` | 🟢 (`:396`) — ✅ Fixed 2026-09-21 (was null-safe `find`) |
| `?ref` query param switches the back-navigation target | `@if(request('ref') === 'orders') … @else … @endif` | 🟢 (`pos-print.blade.php:100`) |
| Client-side printing via `window.print()`; print CSS hides chrome | `onclick="window.print()"`, `@media print { .noPrint { display:none } }` | 🟢 (`pos-print.blade.php:110,177-196`) |
| Receipt money at 0 decimals | `number_format(…, 0)` throughout the body | 🟡 (`pos-print.blade.php:64-81`) |

## Internal State

None. `printOrder` holds only request-scoped `$order`/`$header`/`$breadcrumb` locals and performs no writes. The customer points shown are read live at render time. 🟢 (`:394-402`)

## Observability

None. The action emits no log, metric, or trace; a slow N+1 unit-lookup produces no structured signal. 🔴 (`OrderController.php:394-402`, absence)

## Risks and Gaps

- ✅ **Unknown id — Fixed 2026-09-21.** `printOrder` now uses `findOrFail`, matching `show`; a missing order returns a clean `404` instead of the prior fatal null-dereference in the view.
- 🟡 **N+1 unit lookups in the print loop.** `Unit::find($item->pivot->unit_id)` runs once per line with a unit; eager-loading the units (or joining) would remove the per-line query. (`pos-print.blade.php:57`)
- 🟢 **Confirmed intentional (2026-09-25): no status guard.** A `draft` order can be printed as a receipt just like a `done` one — kept as-is. `questions.md#question-13` (part 1) closed. (`:392-400`)
- 🟡 **Live points on a historical receipt — deferred (2026-09-25).** The customer block prints the customer's **current** points, which may differ from the balance at sale time — a re-printed old receipt shows today's points, matching the label's own "hiện tại" ("current") wording. Snapshotting the sale-time balance was considered but deferred: `points_awarded_at`'s existing once-only guard means a snapshot would need to be written at that same single point (the `done ↔ draft` toggle doesn't complicate this), but adopting it would require a new `orders` column, a migration, deciding the fallback for pre-existing orders with no snapshot, **and** relabeling "Điểm thưởng hiện tại" (since it would no longer be showing the current balance) — a bigger change than a simple fix. `questions.md#question-13` (part 2) left open for a future decision.
- 🟡 **0-decimal money display.** Receipts round to whole units via `number_format(…,0)` while amounts are stored with decimals — a display convention worth confirming against the domain's currency (₫). (`pos-print.blade.php:64-81`)
- 🟡 **No per-record authorization.** Any authenticated admin can print any order. (ADR-0009)
- 🔴 **No observability** on the print path (see above).
