# TechTitans POS — Issued IPTV Invoices Say "TAX INVOICE", Not "PRO FORMA"

**Date:** 2026-11-12 · **Branch:** `techtitans/integration` · Local commit only (no push/deploy).
NH POS / legacy portal untouched; no legacy PHP executed; only safe offline (sandbox) dev payments; existing
invoice amounts, taxes, numbering and historical PDF bytes preserved; no historical document type changed.

## Problem

An IPTV renewal that was unpaid (or partially paid) rendered its document as **PRO FORMA INVOICE**. The user wants
a formally-issued renewal to read **INVOICE** (heading kept as the legally-standard **"TAX INVOICE"** — confirmed
with the user), with the payment state shown **separately** as the pill (PAYMENT PENDING / PARTIALLY PAID / PAID) —
pending payment alone must not demote an issued invoice to a pro forma.

## Root cause

Two things combined:

1. **`InvoiceService::documentTypeFor()` classified by payment only** — `payment_status === 'PAID' ? invoice :
   proforma`. So any not-fully-paid order produced a pro forma, regardless of whether it was a real issued sale.
2. **The IPTV billing pipeline left the order in `DRAFT`.** `OrderPipelineBilling::charge()` opened the order with
   `OrderService::createDraft()` (status `DRAFT`, no order number) and never advanced it, so even conceptually the
   renewal looked "preliminary". (This is why the old frozen invoices also showed `Order: (Draft)`.)

The PDF template was already correct — it drives the pill from `payment_status_snapshot` independently of the title —
so no template change was needed; only the classification was wrong.

## The fix (smallest safe change, following the established POS lifecycle)

1. **Finalize the renewal** — `OrderPipelineBilling::charge()` now advances the order `DRAFT → CONFIRMED` through the
   existing ADR-008 approval gate (`OrderService::transitionStatus`) once the billing transaction is committed. This
   is the "issue a proper invoice after the billing transaction is committed" step: it assigns a real order number
   and placed/confirmed timestamps. The transition only changes status/number — no kitchen routing, no stock move
   (the IPTV service product is non-stock), no notifications.
2. **Lifecycle-based classification** — `documentTypeFor()` now returns a tax **INVOICE** for any **committed** order
   (left `DRAFT`, i.e. CONFIRMED and beyond) **or** any **PAID** order, and reserves **PRO FORMA** only for a
   genuinely preliminary **unpaid DRAFT**. Payment status never demotes an issued invoice.
3. **One invoice number per order (no duplicates on settlement)** — `getOrIssue()` now draws a tax invoice's number
   **once** and keeps it: when an unpaid invoice is later settled (PENDING → PARTIALLY PAID → PAID) it **refreshes the
   same document in place** (`refreshDocument()`) — re-rendering the PDF and updating only the payment snapshot /
   amount paid / balance due, never the number, amounts or tax — instead of minting a second invoice. Pro formas keep
   their existing snapshot-per-state behaviour.

### Accounting safeguards honoured
- Amounts and taxes are frozen at issue; `refreshDocument()` recomputes **only** payment progress (paid / balance),
  never grand total / subtotal / tax.
- Invoice numbering and the INV-/PF- series rules are unchanged; a settlement never burns a new number.
- **Historical documents are never rewritten**: the ~40 pre-existing frozen `PF-` proformas stay exactly as issued
  (see the live UI list); the new behaviour applies to newly-issued renewals only.
- The IPTV paid-in-full / authorized-override grant policy is untouched (grants are payment-based, not status-based).

## Files changed

| File | Change |
|---|---|
| `app/Invoices/InvoiceService.php` | Lifecycle-based `documentTypeFor()`; `getOrIssue()` reuse-and-refresh for invoices; new `refreshDocument()`; updated class docblock. |
| `app/Iptv/Billing/OrderPipelineBilling.php` | Finalize the renewal order (`DRAFT → CONFIRMED`) before issuing the document. |
| `tests/Feature/Orders/InvoicePdfTest.php` | New: committed-unpaid → TAX INVOICE + PAYMENT PENDING; settlement refreshes the same number without duplicating. |
| `tests/Feature/Iptv/IptvBillingIntegrationTest.php` | Partial renewal now asserts `invoice` + `INV-` + PARTIALLY_PAID + single document; later-payment asserts one refreshed invoice (no duplicate). |

## Verification — real IPTV renewal workflow (Playwright, live dev app)

Logged into the real app as **techtitans / vmadmin2026**, drove the real renewal endpoints and opened invoices through
the real **View Invoice** route on IPTV account **IPTV-2026-0026**. Screenshots in
`docs/techtitans/screenshots/invoice-classification/`:

| Scenario | Order | Invoice | Heading | Pill | Amounts |
|---|---|---|---|---|---|
| **Pay Later** (authorized, unpaid) | ORD-20261112-0001 | **INV-2026-0026** | **TAX INVOICE** | PAYMENT PENDING | paid 0.00 / due 22.60 |
| **Later settlement** (same doc) | ORD-20261112-0001 | **INV-2026-0026** (same) | **TAX INVOICE** | PAID | paid 22.60 / due 0.00 |
| **Partial** | ORD-20261112-0002 | **INV-2026-0027** | **TAX INVOICE** | PARTIALLY PAID | paid 5.00 / due 17.60 |
| **Genuine draft** (not committed) | — | **PF-2026-0018** | PRO FORMA | PAYMENT PENDING | — |

Confirmed via DB + rendered PDFs:
- No document reads "PRO FORMA" for an issued renewal; all show **TAX INVOICE** with the uploaded TechTitans logo,
  the corrected ~20 mm margins, totals and footer.
- Settlement kept **exactly one** invoice per order (`invoices = 1`) — the pay-later invoice `INV-2026-0026` went
  PAYMENT PENDING → PAID in place; **no duplicate** number was drawn.
- A still-`DRAFT` order correctly remains a `PF-` pro forma — pro forma is still reserved for preliminary documents.
- **Free-credit extension** creates no order/invoice (nothing to classify) — unchanged.
- **Historical** `PF-2026-0008…0017` proformas are unchanged in the live invoice list.
- No duplicate orders/payments; sandbox cash only.

## Tests & static analysis

- `tests/Feature/Orders` + `tests/Feature/Iptv` → **279 passed**; full backend suite re-run green (see commit).
- Targeted: InvoicePdfTest / OrderInvoiceNotificationTest / IptvBillingIntegrationTest → all green incl. the new
  regression tests.
- **Pint**: passed. **PHPStan** (`app/Invoices`, `app/Iptv/Billing`): **No errors.**

## Commit (on `techtitans/integration`, no push)

- **Backend:** `0a8e61e` — the classification/finalization fix, tests, this report and the verification screenshots.
