# Orders Print Design

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

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

## Data Model

`orders-print` is read-only. It touches four models/tables:

| Model | Table | Fields read | Owner unit |
|-------|-------|-------------|------------|
| `Order` | `orders` | `id`, `code` (appended), `created_at`, `total`, `discount_amount` | `orders-crud` |
| `order_product` (pivot) | `order_product` | `qty`, `price`, `unit_id`, `conversion_qty` | `orders-crud` |
| `Customer` | `customers` | `fullname`, `phone`, `points` (live balance) | `customers-crud` |
| `Unit` | `units` | `name` | `units` |

### ERD (read paths only)

```mermaid
erDiagram
    orders {
        int id PK
        string code "appended: #QT78-{id}"
        datetime created_at
        decimal total
        decimal discount_amount
        int customer_id FK
    }
    order_product {
        int order_id FK
        int product_id FK
        decimal qty
        decimal price
        int unit_id FK
        decimal conversion_qty
    }
    customers {
        int id PK
        string fullname
        string phone
        decimal points
    }
    units {
        int id PK
        string name
    }
    orders ||--o{ order_product : "products()"
    orders }o--o| customers : "customer()"
    order_product }o--o| units : "Unit::find(unit_id)"
```

`Order::products()` is a `belongsToMany` with `withPivot('qty','price','unit_id','conversion_qty')`. The `code` attribute (`#QT78-{id}`) is an appended accessor, not a stored column. 🟢 (`app/Models/Order.php:39-51`)

No schema is introduced by this unit; it projects over existing tables. 🟢

## Internal Flows

### Main request flow

```mermaid
sequenceDiagram
    participant Browser
    participant Middleware as Admin Middleware<br/>[web, admin]
    participant Controller as OrderController
    participant Model as Order (Eloquent)
    participant View as pages.pos-print

    Browser->>Middleware: GET /orders/{id}/print
    alt unauthenticated
        Middleware-->>Browser: 302 → auth/login
    else authenticated admin
        Middleware->>Controller: printOrder($id)
        Controller->>Model: Order::findOrFail($id)
        alt id not found
            Model-->>Controller: ModelNotFoundException
            Controller-->>Browser: 404
        else found
            Model-->>Controller: $order (with products pivot, customer)
            Controller->>Controller: set $header = "In đơn hàng", $breadcrumb
            Controller->>View: view('pages.pos-print', compact('order'))
            View-->>Browser: 200 text/html — receipt
        end
    end
```

🟢 (`OrderController.php:394-402`, `routes/web.php:24-28,73`)

### Receipt render flow (inside the view)

1. Store identity block (logo / store name / address). 🟢
2. Title "Hoá đơn"; `Số HĐ: {order->code}`; `Ngày: {created_at d-m-Y H:i}`. 🟢 (`pos-print.blade.php:43`)
3. `@if($order->customer)` → customer block: `fullname`, `phone`, `number_format(points, 1)` (live balance). 🟢 (`pos-print.blade.php:45-49`)
4. `@foreach($order->products …)` — per-line resolution:
   - If `pivot.unit_id` is set → `Unit::find(pivot.unit_id)->name ?? ''`; else product's base `unit` string. 🟢 (`pos-print.blade.php:56-60`)
   - Print `qty`, `number_format(pivot.price, 0)`, `number_format(qty * pivot.price, 0)`. 🟢 (`pos-print.blade.php:64-67`)
5. Discount row: `- {number_format(discount_amount, 0)}`. 🟢 (`pos-print.blade.php:73`)
6. Total row: `{number_format(total, 0)}`. 🟢 (`pos-print.blade.php:81`)
7. Footer + bank details. 🟢
8. `.noPrint` action bar:
   - `?ref=orders` → `history.back()` ("Trở lại"). 🟢 (`pos-print.blade.php:100`)
   - Otherwise → `<a href="/pos">Tạo đơn mới</a>`. 🟢 (`pos-print.blade.php:108`)
   - Print button: `onclick="window.print()"`. 🟢 (`pos-print.blade.php:110`)

### Shared view — three render sites

```mermaid
flowchart LR
    A["orders-crud store\n(status → done)"] -->|view pages.pos-print| V["pages.pos-print\n(80mm receipt)"]
    B["orders-crud update\n(status → done)"] -->|view pages.pos-print| V
    C["orders-print\nGET /orders/{id}/print"] -->|view pages.pos-print| V
```

`?ref=orders` distinguishes the re-print path (C) from the done-sale paths (A/B), controlling the back-navigation target. 🟢 (`pos-print.blade.php:100`)

## Technical Decisions

| Decision | Rationale / Evidence | Confidence |
|----------|----------------------|------------|
| Single shared receipt template (`pages.pos-print`) for sale and re-print paths | `store`/`update`/`printOrder` all render the same view; `?ref` handles the navigation difference | 🟢 (`:162,334,401`) |
| Route registered **before** `resource('/orders')` | Prevents the resource `show` route from capturing `/orders/{order}/print`; no route name assigned | 🟢 (`routes/web.php:73-75`) |
| `Order::findOrFail($id)` — ✅ Fixed 2026-09-21 | Was `Order::find`, which returned `null` and caused a fatal null-dereference in the view; now returns a clean `404` on miss, consistent with `show` | 🟢 (`:396`) |
| `?ref` query param switches back-navigation | `@if(request('ref') === 'orders')` selects `history.back()` vs. `/pos` link; no server state needed | 🟢 (`pos-print.blade.php:100`) |
| Client-side printing via `window.print()` + print CSS | `@media print` hides `.noPrint` chrome and sizes the receipt to ~72–80mm; no server-side PDF generation | 🟢 (`pos-print.blade.php:110,177-196`) |
| No status guard — any order is printable | Draft orders can be printed like done ones; confirmed intentional 2026-09-25 (`questions.md#question-13`) | 🟢 (`:394-402`) |
| Live customer points on the receipt | `customer.points` is the **current** balance at render time, not snapshotted at sale time; matches the label "hiện tại"; snapshotting deferred (needs schema migration + relabeling) | 🟡 (`pos-print.blade.php:48`) |
| Money displayed at 0 decimals | `number_format(…, 0)` throughout; amounts stored with decimals but receipts round to whole ₫ units | 🟡 (`pos-print.blade.php:64-81`) |
| No per-record authorization | Any authenticated admin can print any order | 🟡 (ADR-0009) |

## Notes

### N+1 unit lookups

`Unit::find(pivot.unit_id)` executes one query **per line** that has a `unit_id`. For orders with many lines this is an unbounded N+1. The fix is to eager-load units for the pivot ids before entering the loop (or join them in the products query). 🟡 (`pos-print.blade.php:57`)

### Live points — open decision

The "Điểm thưởng hiện tại" label explicitly says "current points", which matches the live-read behaviour. Snapshotting the sale-time balance would require: a new `orders` column, a migration, a fallback for pre-existing rows, **and** relabeling the field. Deferred 2026-09-25 (`questions.md#question-13` part 2). 🟡

### No observability

`printOrder` emits no log entry, metric, or trace span. A slow N+1 print produces no structured signal. The absence is confirmed, not accidental. 🔴 (`OrderController.php:394-402`)

### Controller scope

`printOrder` is request-scoped: it holds only `$order`, `$header`, and `$breadcrumb` locals. No writes, no session mutation, no side effects. 🟢
