# TechTitans POS — IPTV Billing & Renewal Integration

**Date:** 2026-11-11 · **Branch:** `techtitans/integration` (backend + frontend) · Local commits only, no push/deploy. NH POS and the legacy PHP portal untouched. No live payment-provider charges.

## Summary

IPTV renewal billing is now **integrated with the existing POS Order → Invoice → Payment stack and verified end-to-end**. Step 1 inspection found the core pipeline was already implemented (`OrderPipelineBilling`, grant policy, renewal↔order/invoice linkage, most of the detail UI) but shipped **disabled** behind a fail-safe no-op (`DeferredBilling`), and blocked from real multi-tenant use by a single **global** service-product config. This work **completed and enabled** it — per-tenant service-product provisioning, the invoice/payment-viewing + outstanding-balance surface, and safe enablement — reusing the existing financial services, **not a parallel ledger**.

## What already existed (reused, not rebuilt)
- `App\Iptv\Billing\OrderPipelineBilling` → `OrderService::createDraft` → `addLine` → `OfflinePaymentService::record` → `InvoiceService::getOrIssue` → `PaymentLedger`.
- `IptvSubscriptionService::doRenew` — builds the charge, enforces the grant policy, writes the immutable renewal row (`order_id`/`invoice_id`/`billing_status`), advances `paid_through_date` only when granted.
- Distinct non-financial ops: promotional free months (`grantFreeExtension`), manual expiry correction (`adjustDate`), monthly-charge change.
- Detail-page Renew / free-months / charge / date-correction UIs; the authoritative tax quote (`IptvPricingService` + POS tax engine).

## What was implemented to complete the integration

### Backend
- **`app/Iptv/Billing/IptvBillingProvisioner.php` (new)** — resolves, per tenant, a reusable **non-stock "IPTV Service" product + default variant**, idempotent by the stable SKU **`IPTV-SERVICE`** (second call returns the same ids; no duplicate). Created as `product_kind = service` (inventory tracking none), active, under a dedicated `iptv-services` category, stamped at the subscription's location to satisfy the same-location product→category FK, within the correct `TenantContext`. Replaces the broken single-global-config approach.
- **`app/Iptv/Billing/OrderPipelineBilling.php`** — now resolves the service product/variant via the provisioner for the subscription's tenant (the global `iptv.billing.service_product_id`/`service_variant_id` remain an optional override when both are set); removed the hard "unconfigured" throw.
- **`app/Iptv/IptvSubscriptionService.php`** — added `billingOverview()`, `recordRenewalPayment()`, `orderFinancials()` (reads the existing `PaymentLedger`/order/`Invoice`/`CustomerFinancialService` — no new ledger).
- **Endpoints** (`routes/api.php` + `IptvSubscriptionController`):
  - `GET iptv/subscriptions/{id}/billing` (`iptv.view`) → `{ billing: { renewals[], orders[{order_id, grand_total_minor, amount_paid_minor, outstanding_minor, payment_status, invoice_id, invoice_number, invoice_document_type}], customer_financials[] } }`.
  - `POST iptv/subscriptions/{id}/payment` (`iptv.renew`, idempotent via `X-Request-Id`) → records a later offline payment against a renewal's order (partial allowed, overpayment guarded), re-derives `billing_status`, re-issues the invoice; **never** changes expiry/grant.
  - `show()` now eager-loads `subcontractor` (completeness).
- **Safe enablement**: `IPTV_BILLING_ENABLED` stays **false by default** (fail-safe); set `true` in local `.env` (gitignored) for the dev app; **`phpunit.xml` pins it `false force="true"`** so the suite never creates orders/payments implicitly (the integration tests flip it on per-test).
- **Invoice PDF** reuses the existing `GET /api/v1/orders/{order}/invoice` (`invoices.view`) — the UI links by `order_id`.

### Frontend (`src/pages/iptv/IptvDetailPage.tsx`, `src/lib/api/iptv.ts`)
- `iptvApi.billing()`, `iptvApi.recordRenewalPayment()`, `iptvApi.invoiceBlobUrl()` (authed-blob, mirrors the repairs attachment pattern) + types.
- Billing tab: an **Outstanding balance** card (per-currency customer totals); the renewal history augmented with **Charge / Paid / Outstanding / payment status** (joined by `order_id`), a **View invoice** action (opens the PDF), and a **Record payment** action (when outstanding > 0) via a **confirm dialog** (amount prefilled to outstanding, partial allowed, method select, inline 422 handling). All existing actions preserved.

## Financial transaction mapping

| IPTV operation | Permission | Financial effect |
|---|---|---|
| **Paid renewal** (`apply_charge`) | `iptv.renew` | SYSTEM `Order` (`order_source=SYSTEM`) → one priced line on the IPTV service product (tax from its tax class) → optional offline-cash `Payment` → `Invoice` (proforma while unpaid, **tax invoice once PAID**). Renewal row links `order_id`/`invoice_id`/`billing_status`. |
| **Later payment** | `iptv.renew` | `OfflinePaymentService::record` against the renewal's order; outstanding re-derived from `PaymentLedger`; invoice re-issued. No expiry change. |
| **Promotional free months** | `iptv.credit_grant` | Expiry only. **No order/invoice/payment.** `free_extension` row, `order_id = null`. |
| **Manual expiry correction** | `iptv.adjust_date` | Expiry only. No financial record. |
| **Monthly charge change** | `iptv.manage` | Config only; affects future charges. |

**Financial customer "credit":** the POS has **no stored store-credit/account-balance ledger** — "outstanding" is always derived from unpaid orders (`CustomerFinancialService`). We reuse that derivation and did **not** introduce a credit ledger. Promotional "free credit" is the non-financial free-months extension above.

## Payment & activation rules (verified)

**Service is granted (expiry + paid-through advance) only when the renewal is PAID IN FULL, or an explicit `authorize_unpaid` override (`iptv.renew`) is supplied. An invoice existing never grants service.** Partial / pending / unpaid renewals record the charge + invoice but leave expiry unchanged unless authorized. Payment states are represented by the existing order `payment_status` (PENDING / PARTIALLY_PAID / PAID / FAILED / VOIDED / REFUNDED) and offline `verification_status`; cash captures immediately, e-transfer/bank stay pending until verified. Duplicate orders/invoices/renewals are prevented by `RequestIdempotency` (`X-Request-Id`) and `InvoiceService::getOrIssue` reuse.

## Tests

- **Backend** (Xdebug off): `php artisan test tests/Feature/Iptv` → **45 passed (279 assertions)**, incl. 9 new billing tests: paid-in-full (order+line+**tax**+captured payment+tax invoice; expiry+paid-through advance), partial (PARTIALLY_PAID, proforma, not granted), authorized override (granted + invoiced + full outstanding), `GET .../billing` shape, `POST .../payment` pays down → paid + re-issue, free-extension & date-adjustment create **no** order/payment, renew idempotency (one order per request id), tenant/location isolation, provisioner idempotency. Pre-existing deferred-path renewal tests still green (billing pinned off for the suite). Pint clean; PHPStan clean on new code.
- **Frontend**: `vitest run src/pages/iptv src/lib` → **267 passed**; `IptvPages.test.tsx` 14/14 (billing overview, record-payment body, view-invoice fetch). `tsc -b` clean, eslint 0, `npm run build` succeeds.

## Browser evidence (live dev, `techtitans`, billing enabled) — `docs/techtitans/screenshots/iptv-billing/`

Subscription `IPTV-2026-0011`, Monthly plan $15.00, IPTV tax class 13%. All PASS:
1. **Paid-in-full** — $15.00 + **$1.95 tax = $16.95**, paid $16.95, outstanding $0, PAID; expiry 2026-12-10 → **2027-01-10** (`step1`).
2. **View invoice** — `GET /orders/{id}/invoice` → 200 `application/pdf`, 880 KB (`step2`).
3. **Partial (not granted)** — pay $10 of $16.95 → outstanding $6.95, PARTIALLY_PAID; expiry **unchanged** (`step3`).
4. **Record later payment** — pay remaining $6.95 → outstanding $0, PAID; outstanding card → $0 (`step4`).
5. **Authorized override** — pay $0, authorize ON → expiry **advances** to 2027-02-10 despite outstanding $16.95 (PENDING) (`step5`).
6. **Free month** — expiry → 2027-03-10; `free_extension` row, `order_id = null`, orders count unchanged (`step6`).
7. **Outstanding balance card** — reflects CA$16.95 after the unpaid authorized renewal (`step7`).
Sanity: Repairs dashboard + Products list load without error. Only offline-cash payments used; nothing wiped (strictly additive). 0 page errors.

## Known limitations

- **Renewal tax is governed by the IPTV service product's tax class** (the order engine resolves line tax from the product). For the dev tenant a 13% "IPTV Sales Tax" class was attached; a tenant with no taxable class falls back to its default rate (0 until configured). The create-form quote uses the plan's tax class — keep the plan tax classes aligned with the service product for quote↔charge parity.
- **No IPTV `order_type`** in the restaurant enum — renewal orders use `order_type=PICKUP` with `order_source=SYSTEM` (the discriminator). SYSTEM orders stay DRAFT (no `order_number`), so the UI links invoices by `order_id`.
- `customer_financials` on the billing view is scoped by the caller's `iptv.view` location scope (consistent with the IPTV view permission), not a separate `orders.view` gate.
- The box price (`unit_price_minor`, one-time at activation) is not independently taxed in the quote; recurring monthly renewals are the billed path.
- No stored customer credit ledger (by design — reuse the derived outstanding).

## Local commits (separate per repo, on `techtitans/integration`, no push)

- **Backend:** `004ff0f` — `IptvBillingProvisioner` (new), `OrderPipelineBilling`, `IptvSubscriptionService`, `IptvResource`, `IptvSubscriptionController`, `routes/api.php`, `phpunit.xml`, `tests/Feature/Iptv/IptvBillingIntegrationTest.php` (new), this report + screenshots.
- **Frontend:** `5f3b5d3` — `lib/api/iptv.ts`, `pages/iptv/IptvDetailPage.tsx`, `pages/iptv/IptvPages.test.tsx`.
