# TechTitans POS — Simple Quote / Invoice / Receipt

**Date:** 2026-11-12 · **Branch:** `techtitans/integration` · Local commit only (no push/deploy).
Reuses the existing POS billing + payment + tax engine — **no** parallel financial tables and **no** module-specific
invoice system. No invoice amounts, taxes, payment ledger, numbering or any stored/historical PDF bytes were changed.

## The everyday experience (three documents, no clutter)

| Stage | Action | Document |
|---|---|---|
| Before the sale | **Print quote** (IPTV → Credit & Renewal) | **QUOTE** — estimate; records nothing |
| Sale committed | **Print invoice** (IPTV → Invoices) | **INVOICE** (tax invoice); payment status shown separately |
| Payment received | **Print receipt** / **80mm** (IPTV → Payments) | **RECEIPT** — only the amount captured |

### Removed from the staff interface
`View Original`, `Branded Copy`, `Branded Reprint`, reprint banners and the lengthy explanations are gone. The
invoice PDF no longer renders any reprint banner. The exact original stored document is retained for **authorized
audit only** — a small admin-only icon (behind `invoices.send`) on the Invoices row, not a customer-facing control.
Verified live: the Invoices tab contains no "Branded copy", "View original" or "reprint" text.

## 1. Quote (pre-sale)

`POST /iptv/quote/print` (iptv.view + location) renders a **QUOTE** via the new shared `App\Documents\QuoteService`
using the **same authoritative** `IptvPricingService` computation as the live preview (extracted into
`IptvSubscriptionController::computeQuote`). It creates **no order, invoice or payment** (asserted by test). The PDF
shows the customer, item, quantity, discount, **estimated** tax (POS tax engine) and estimated total, an **ESTIMATE**
badge, and a clear note: *"a quote / estimate only — not a tax invoice and no payment is due."* "Print quote" appears
in Credit & Renewal whenever paid months are selected.

## 2. Invoice (issued sale)

Unchanged engine: a committed IPTV renewal issues a tax **INVOICE** (never PRO FORMA for merely being unpaid — the
lifecycle fix in `0a8e61e` finalizes the order and classifies by lifecycle; an unpaid draft stays a genuine pro
forma). Heading **"TAX INVOICE"**, with payment status shown **separately** as the pill (PAYMENT PENDING / PARTIALLY
PAID / PAID). Uploaded logo, navy/teal template, ~20 mm margins, aligned columns, no PICKUP for digital IPTV. The
single **Print invoice** action streams the stored branded PDF, or — for a legacy pre-branding document — a clean
branded render (no banner, normal filename). *(Heading reads "TAX INVOICE", the legally-standard invoice heading you
chose earlier; if you want the bare word "INVOICE", it's a one-line Blade change — say the word.)*

## 3. Receipt (payment received)

New shared `App\Documents\ReceiptService` → `GET /orders/{order}/receipt` (invoices.view). Available **only once real
money is captured** (422 otherwise — "no receipt for an unpaid transaction", verified). **No logo.** Shows business
name/address/phone/email, date & time, receipt reference, linked invoice number, customer, items, **tax disclosure**
(per-rate breakdown — receipts are not assumed tax-free), amount paid, and **remaining balance for a partial
payment** (acknowledging only the amount received), payment method(s), and a thank-you. Two printable formats:
**A4** (browser-printable) and **`?format=thermal`** (compact **80 mm** roll). "Print receipt" + "80mm" appear on the
Payments tab only for rows with a recorded payment.

## Taxes & GST/HST

All three documents disclose sales tax via the POS tax engine (no separate React math). When a tax is charged/estimated
but **no GST/HST registration number is configured**, the document shows a **configuration warning** ("Add it in
Business Settings → Operations") — it never invents a number. (TechTitans currently has none configured, so the
warning renders — see screenshots.)

## Shared services (apply throughout TechTitans)

`InvoiceService` (invoice), `ReceiptService` (receipt) and `QuoteService` (quote) are shared POS document services that
reuse `BusinessIdentity` (extended with `forLocation()` for pre-sale quotes), `PaymentLedger` and the tax engine — so
retail and repair transactions can adopt the same Receipt/Quote renderers without module-specific templates. This
increment wires them into the IPTV surfaces; the renderers are module-agnostic.

## Historical documents

Preserved: no stored PDF deleted/overwritten/relabelled; a genuine historical PRO FORMA keeps its bytes and
classification (the admin "original" streams it verbatim). New invoices carry no disclaimers/reprint banners.

## Verification

- **Backend** `tests/Feature/Orders` + `tests/Feature/Iptv` → **44 passed**, incl.:
  `test_receipt_is_blocked_until_payment_then_prints_a4_and_thermal`,
  `test_receipt_requires_invoices_view_and_is_tenant_isolated`,
  `test_quote_print_returns_a_pdf_estimate_and_creates_nothing`,
  plus the single-Print-Invoice smart-view + admin-original tests.
- **Frontend** `IptvPages.test.tsx` → **25 passed** (single Print invoice; admin Original gated by invoices.send).
- **Pint** clean; **PHPStan** (`app/Documents`, `app/Invoices`, `app/Http/.../Orders`) **No errors** (one pre-existing
  IPTV-controller `renewals` typing error, in untouched code, is unrelated); `tsc -b`, `eslint`, `npm run build` clean.
- **Live Playwright** (real staff buttons): **Print quote** → `POST /iptv/quote/print` 200; **Print invoice** →
  `GET /orders/{id}/invoice` 200 (no Branded copy / View original / reprint on the tab); **Print receipt** →
  `GET /orders/{id}/receipt?format=a4` 200; the **unpaid** rows (CA$0.00 paid) show **no** Print receipt. 0 console
  errors. Rendered documents inspected: `screenshots/quote-invoice-receipt/`
  (`quote-estimate.png`, `receipt-a4-partial.png`, `receipt-80mm-thermal.png`,
  `ui-payments-print-receipt-none-when-unpaid.png`, `ui-invoices-single-print-invoice.png`).

## Scope note

Delivered end-to-end for **IPTV** (the surface in the screenshots) with module-agnostic shared services. Retail and
repair already share the invoice template; wiring their Quote/Receipt buttons is a mechanical follow-up using the same
`QuoteService` / `ReceiptService`. No real payments were processed.

## Files changed

Backend: `app/Documents/ReceiptService.php`, `app/Documents/QuoteService.php`,
`resources/views/documents/{receipt,quote}.blade.php`, `resources/views/invoices/pdf.blade.php` (banner removed),
`app/Invoices/InvoiceService.php` (viewDocument cleanup), `app/Invoices/BusinessIdentity.php` (`forLocation`),
`app/Http/Controllers/Api/V1/Orders/OrderInvoiceController.php` (receipt), `app/Http/Controllers/Api/V1/Iptv/IptvSubscriptionController.php` (quotePrint/computeQuote), `routes/api.php`, tests.
Frontend: `src/lib/api/client.ts` (postBlob), `src/lib/api/iptv.ts` (receipt/quote/original helpers),
`src/pages/iptv/IptvDetailPage.tsx` (Print invoice / receipt / quote; admin Original), `IptvPages.test.tsx`.

## Commits (no push)

- **Backend:** `6a131ba` — shared Quote/Receipt services + routes + de-clutter + tests + this report + screenshots.
- **Frontend:** `c6042e1` — Print quote / invoice / receipt actions + postBlob.
