# TechTitans POS — IPTV Invoice Classification + One "View Invoice" Action

**Date:** 2026-11-12 · **Branch:** `techtitans/integration` · Local commit only (no push/deploy).
NH POS / legacy portal untouched; sandbox (offline) dev data only. **No invoice amounts, taxes, payment ledger,
numbering, idempotency, or any stored/historical PDF bytes were changed.**

## Root cause of the PRO FORMA classification (already corrected)

IPTV renewals were classified PRO FORMA because `InvoiceService::documentTypeFor()` keyed off payment only
(`PAID → invoice, else proforma`) **and** `OrderPipelineBilling` left the renewal order in **DRAFT** when the invoice
was generated — so an unpaid renewal looked "preliminary". The fix shipped in commit **`0a8e61e`**:

- `OrderPipelineBilling::charge()` finalizes the order **DRAFT → CONFIRMED** through the ADR-008 approval gate once the
  billing transaction commits (it draws an order number; no kitchen/stock/notification side effects).
- `documentTypeFor()` is **lifecycle-based**: a **committed** (confirmed/placed) **or paid** order is a tax
  **INVOICE**; PRO FORMA is reserved for a genuinely preliminary **unpaid DRAFT**. Payment status never demotes an
  issued invoice — it is shown separately as the pill.
- `getOrIssue()` keeps one invoice number per order and refreshes it in place on settlement (no duplicate numbering).

This task **verified** that behaviour live and added the single-button viewing experience below. (Numbering, tax,
idempotency and audit history are all retained; original amounts/payment records are untouched.)

### Verified classification (live, real pipeline)
A fresh **Pay-Later** IPTV renewal issued **INV-2026-0029** — heading **TAX INVOICE** (a formally-issued invoice,
*not* PRO FORMA), with **PAYMENT PENDING** shown separately, the TechTitans logo, aligned columns, and no PICKUP
(digital service). Pay Now → PAID, Partial → PARTIALLY PAID, later settlement → same invoice refreshed to PAID, and a
free-credit extension creates **no** invoice — all covered by `IptvBillingIntegrationTest` + `InvoicePdfTest`.

> **Heading note:** issued invoices read **"TAX INVOICE"** — the legally-standard heading, explicitly chosen earlier by
> the user, which unambiguously *is* an invoice (never "PRO FORMA"). If the plain word **"INVOICE"** is preferred, it is
> a one-line change in `resources/views/invoices/pdf.blade.php` (`__('Tax Invoice')` → `__('Invoice')`); say the word.

## One "View Invoice" action (replacing View Original + Branded Copy)

The two confusing buttons are gone. Each IPTV invoice row now has:

| Action | Who | Route | Returns |
|---|---|---|---|
| **View invoice** (primary) | `invoices.view` | `GET /orders/{order}/invoice` | the professional document — the **stored branded PDF** when already branded, or a **branded reprint** (clearly banner-marked) for a legacy pre-branding document |
| **Original** (admin/manager, secondary) | `invoices.send` | `GET /orders/{order}/invoice/original` | the **exact original stored bytes**, never branded/reprinted (audit/legal) |

### Backend
- `InvoiceService::viewDocument(Invoice): {pdf, filename, reprint}` — serves the stored bytes when the document is
  already branded (carries the logo image, or the tenant has no logo to add); otherwise a `brandedReprint` (legacy
  logo-bearing tenant whose stored PDF has no logo). **No mutation** either way.
- `OrderInvoiceController::download` is now the smart **View Invoice**; new `original` streams the raw stored bytes
  behind `invoices.send` (403 without). Both audit (`order.invoice.viewed` / `order.invoice.original_viewed`),
  `Cache-Control: no-store`, and are location-scoped + tenant-isolated (404 cross-tenant).
- The previous `/invoice/branded-copy` route is removed (its behaviour is folded into the smart View Invoice).

### Frontend (IPTV Account → Invoices)
One prominent **View invoice** button per row; an admin-only **Original** (lock icon) appears only with
`invoices.send`. An explainer states the branded copy never changes the original or any amounts.

## Historical documents (preserved)

- **Original PDFs + classifications are untouched.** PF-2026-0015 is still a frozen `proforma` with its exact bytes;
  the admin **Original** streams them verbatim (no logo — the legacy layout as issued).
- **View invoice** on a legacy document returns a **branded reprint** — clearly banner-marked "BRANDED REPRINT …",
  retaining the original number, document type, issue date and **recorded** payment status. Historical PRO FORMA is
  **not** silently converted into a tax invoice.
- Three distinct documents remain cleanly separable: **newly issued invoice** (stored branded), **branded historical
  reprint** (View invoice on a legacy doc), and the **exact original** (admin Original).

## Testing (Task 6)

- **Backend** `tests/Feature/Orders` → **31 passed**:
  - `InvoicePdfTest::test_view_document_reprints_a_legacy_document_but_serves_stored_when_already_branded` — View
    Invoice reprints a legacy doc (branded, logo) and serves the exact stored bytes for an already-branded one.
  - `InvoicePdfTest::test_branded_reprint_preserves_the_original_immutable_document` — original bytes/sha unchanged, no
    new invoice.
  - `OrderInvoiceNotificationTest` — `/original` streams the exact stored doc for admins, requires `invoices.send`
    (403 without `send` even with `view`), tenant-isolated (404); the lifecycle Pay/Partial/Settlement tests still pass.
- **Frontend** `IptvPages.test.tsx` → **25 passed**: single **View invoice** hits `/invoice`; admin **Original** hits
  `/invoice/original`; the Original action is **hidden** without `invoices.send`.
- **Pint** + **PHPStan** (`app/Invoices`, `app/Http/Controllers/Api/V1/Orders`) clean; `tsc -b`, `eslint`,
  `npm run build` clean.

## Live verification (Playwright, real routes) — screenshots in `screenshots/invoice-classification-viewing/`

| # | Workflow | Result |
|---|---|---|
| 1-4 | Renewal Pay Now / Pay Later / Partial / later settlement | TAX INVOICE + PAID / PAYMENT PENDING / PARTIALLY PAID / refreshed-to-PAID (one number) |
| 5 | Free credit | no invoice created |
| 6 | New invoice with logo | `new-INVOICE-INV-2026-0029-payment-pending.png` — TAX INVOICE, TechTitans logo, PAYMENT PENDING |
| 7 | Historical PRO FORMA | `historical-admin-original-legacy.png` — PF-2026-0015 exact legacy bytes (no logo), unchanged |
| 8 | One View Invoice button | `ui-one-view-invoice-plus-admin-original.png` — single **View invoice** + admin **Original** |
| 9 | Admin original access | `/invoice/original` → `proforma-PF-2026-0015.pdf` (880 KB, no logo); hidden/403 without `invoices.send` |
| 10 | Alignment & margins | branded invoice/reprint: aligned 6-column table, ~20 mm margins, footer |
| 11 | No duplicate invoices/payments | viewing/reprinting creates none; original row/sha unchanged |
| 12 | Isolation & authorization | cross-tenant 404; `invoices.view` for View, `invoices.send` for Original |

View Invoice on PF-2026-0015 → `reprint-PF-2026-0015.pdf` (892 KB, **logo**, PRO FORMA preserved, reprint banner,
no PICKUP) = `historical-view-invoice-branded-reprint.png`. The real buttons fired `GET /invoice` and
`GET /invoice/original` (both 200); 0 console errors.

## Files changed

| File | Change |
|---|---|
| `app/Invoices/InvoiceService.php` | `viewDocument` (smart stored-vs-reprint). |
| `app/Http/Controllers/Api/V1/Orders/OrderInvoiceController.php` | `download` → smart View Invoice; new admin `original`; removed `brandedCopy`. |
| `routes/api.php` | `/invoice` (view) + `/invoice/original` (invoices.send); removed `/invoice/branded-copy`. |
| `frontend/src/lib/api/iptv.ts` | `invoiceOriginalBlobUrl` (replaces branded helper). |
| `frontend/src/pages/iptv/IptvDetailPage.tsx` | one **View invoice** + admin **Original**; explainer. |
| tests | backend smart-view/original tests; frontend single-button + admin tests. |

## Commits (no push)

- **Backend:** `ea046d7` — smart View Invoice + admin original + tests + this report + screenshots.
- **Frontend:** `15e5cd1` — one View Invoice button + admin Original + API + tests.
