# TechTitans POS — IPTV Two-Page Workflow

**Date:** 2026-11-11 · **Branch:** `techtitans/integration` · Local commits only (no push/deploy). NH POS and the legacy PHP portal untouched (read-only inspection only; no PHP executed). No real customer records modified.

Splits the single long "New IPTV Connection" form into **Page 1 — Create Connection** (customer + connection details, saved as a *pending-activation* record with no pricing/invoice/service) and **Page 2 — Subscription & Account Management** (package/pricing, credit months, payments, renewals, expiry, history) on the account's detail page.

---

## 1. Before / after workflow

**Before:** one page (`/iptv/new`) with sections A Customer · B Connection · C Subscription & Pricing (plan, expiry, monthly charge, box price, discount, promo) · D Notifications. "Create connection" immediately created an **active** subscription with a plan + expiry + an `activation` renewal row.

**After:**
- **Page 1 `/iptv/new`** — only **A Customer** + **B Connection Details**. "Create Connection" saves a bare connection in **`pending_activation`** status: no plan, no expiry, **no invoice, no payment, no granted service months**, then navigates to the account page.
- **Page 2 `/iptv/:id`** (the existing detail route) — top header (customer · MC ID · provider · MAC · status · **Edit Connection**), summary cards (monthly charge · expiry · outstanding · credit status), an **Activate** panel for pending connections (choose package → sets plan + expiry + optional first charge), and Credit Management (Free Credit / Pay Credit / Pay Now), Expiry management (Change Date By / Bring to Current Date with preview + reason), Monthly-charge change, Notification choices, and account-history tabs (Invoices / Payments / Renewals-Credits / Notes / Activity).

A saved-but-unactivated connection **never appears as an active subscription** in reporting or reminders (see §3).

## 2. Legacy feature mapping (old portal → new POS)

Documented read-only from `oldportal/admin/services/iptv_services.php`, `.../superadmin/viewiptv.php`, `.../vm-js/iptv_view3.js` and the existing audit docs in `backend/docs/techtitans/legacy-portal/`. **Legacy "credit" = service-months owed, NOT a stored cash balance**; real money always flowed through the shared POS sales tables (the `iptv_payments` table was dead code). The new system reuses POS Orders → Invoices → Payments — **no separate IPTV ledger**.

| Legacy action | Old behaviour (documented) | New-POS equivalent |
|---|---|---|
| **Free Credit** (0–12 mo) | Advances expiry by N months at **$0** (writes a $0 sales row); no money. | `grantFreeExtension(months 0–12)` — `free_extension` renewal row, advances expiry only, no order/payment. **Exists.** |
| **Pay Credit** (code 0–24; **brief/new: 0–18**) | Creates a **billable invoice** (debt: `paid=0, due=total`, +13% HST option) **and** advances expiry by the months. Not itself a payment. | Authorized **paid renewal/activation**: `apply_charge` + `authorize_unpaid` → creates a POS order+invoice (outstanding), grants the months. Amount = monthly × months, tax via the IPTV service product's tax class. **Reuses the billing pipeline.** |
| **Pay Now** (YES/NO) | YES → opens the generic POS payment screen to settle the just-created invoice; the IPTV code records no payment itself. | `pay_now_minor` on the renewal/activation (offline cash capture), or the later **Record payment** endpoint against the renewal's order. **Exists.** |
| **Change Date By (+/−)** | Manual expiry shift in **days** (not months); no money; audited to `ip_day_update_tbl`; **no reason required** in legacy. | `adjustDate(mode='by_days', ±days)` — `date_adjustment` row, audited. New UI adds a **required reason + before/after preview**. |
| **Bring to Current Date** | Hard-sets expiry to today; audited; **no guard** (can shorten future time). | `adjustDate(mode='to_today')`. New UI adds a **confirm + before/after preview** so paid time isn't destroyed silently. |

**Documented ambiguities (not reproduced blindly):** legacy Pay Credit range was 0–24 in code vs the brief's 0–18 (**we use 0–18 per the brief**, noting the discrepancy); legacy Free+Pay shared one form so a "free only" grant still wrote a $0 invoice (new system keeps free extensions non-financial — no order); legacy Pay-Now→payment association relied on session state (new system records the payment directly against the renewal's order); legacy date actions had no reason/guard (new UI adds both).

## 3. Schema changes (smallest safe additive)

`iptv_subscriptions` previously required `expiry_date` + `start_date` NOT NULL and a 3-value status CHECK, so a connection could not be saved without a plan/expiry. Minimal additive change (no dummy data):

1. **`expiry_date` + `start_date` → nullable** (a pending connection has neither until activation).
2. **Status CHECK** `chk_iptv_subscriptions_status` extended to `IN ('active','suspended','cancelled','pending_activation')`; `IptvSubscription::STATUSES` gains `pending_activation`.
3. **No other schema change** — `plan_id`, `monthly_charge_minor`, `paid_through_date` were already nullable; the `plan` composite FK already tolerates null; the renewals schema and all unique constraints/MC-ID guard are unchanged.

**Why pending rows stay out of "active" reporting with no query rewrites:** the dashboard `active`/`expiring_soon`/`expired`/`active_by_provider`, `remindersDue`, and `processExpiryReminders` all already filter `status='active'`, so a `pending_activation` row is excluded automatically. The only change is the dashboard: `pending_activation` is excluded from the unfiltered `total` and surfaced as its own `pending` count.

## 4. Pending → Active (the one new behaviour)

- **Create (Page 1):** when `status='pending_activation'`, `doCreate` skips the plan/expiry requirement, leaves `expiry_date` null, does not stamp `paid_through_date`, and does **not** write an `activation` renewal row — but still assigns the MC id safely (locked, tenant-unique), saves encrypted credentials, and audits `iptv.subscription_created`. No order/invoice/payment/service.
- **Activate (Page 2):** new `POST /iptv/subscriptions/{id}/activate` (`iptv.renew`, idempotent). Choose a package (plan) → compute expiry from today via the calendar-safe `RenewalCalculator`; optionally `apply_charge` (monthly × months, discount + tax through the existing quote/billing pipeline) with `pay_now`/`authorize_unpaid`. **Grant policy:** the connection flips to `active` (plan + expiry + paid-through set) only on a free/no-charge activation, a paid-in-full charge, or an authorized override — an invoice alone never activates. Writes the immutable `activation` renewal row; audits `iptv.subscription_activated`. 422 if already active.

## 5. Implementation

### Backend (`backend/`)
- **Migration** `..._allow_pending_activation_iptv_subscriptions.php` — `expiry_date` + `start_date` → nullable; `status` widened `varchar(16)→varchar(24)` (the value `pending_activation` is 18 chars); status CHECK re-created `IN ('active','suspended','cancelled','pending_activation')`. Reversible; applied additively to `techpos`.
- `IptvSubscription` — `STATUSES` + `isPendingActivation()`.
- `IptvSubscriptionService` — pending branch in `doCreate` (no plan/expiry/paid-through/activation-row when `status='pending_activation'`); new `activate()`/`doActivate()`/`resolveActivationDuration()`; dashboard `pending` count + `total` excludes pending.
- `IptvException` — `planRequiredForActivation()`, `alreadyActivated()`.
- `IptvSubscriptionController@activate` + `ActivateIptvSubscriptionRequest`; `routes/api.php` `POST iptv/subscriptions/{subscription}/activate` (`permission.any:iptv.renew`, `resolve.location`, `reject.tenant_id`, idempotent via `X-Request-Id`).
- `ListIptvSubscriptionsRequest` — `pending_activation` added to the `expiry_state` enum.
- **Quote tax-preview fix** — the IPTV `quote` now resolves the IPTV **service product's** tax class (the authoritative source the order line uses) when billing is enabled and no tax class is supplied, so the on-screen preview tax/total matches the invoice that will be created.

### Frontend (`frontend/`)
- **Page 1** (`NewIptvPage.tsx`) — reduced to **A Customer + B Connection details** (+ the New-customer / Add-provider / Add-subcontractor drawers). Removed the Subscription & Pricing and Notifications sections and all their state/effects. "Create connection" posts `status:'pending_activation'` with no plan/expiry/pricing/notifications, then navigates to `/iptv/:id`. Subtitle marks it "Step 1 of 2" (no wizard).
- **Page 2** (`IptvDetailPage.tsx`) — identity header + **Edit Connection** drawer (`updateSubscription`), **summary cards** (charge / expiry / outstanding / status), **Activation panel** (pending only: package + discount + optional billing → `activate`), **Package & pricing** (renew + Add-package drawer + charge + box price), **Credit management** (Free Credit 0–12 → `grantFreeExtension`; Pay Credit 0–18 → authorized paid renewal; Pay Now / Record payment), **Expiry management** (Change Date By / Bring to Current Date with before→after preview + required reason), and **five account-history tabs** (Invoices / Payments / Renewals-Credits / Notes / Activity), plus a **Notifications** panel. Every money/date action is a distinct, permission-gated ConfirmDialog.
- `iptv.ts` — `iptvApi.activate`, `pending_activation` status/expiry-state, `pending` dashboard field, extended `UpdateIptvSubscriptionBody` (connection fields the existing PATCH already validates).

## 6. API changes
- **New:** `POST /api/v1/iptv/subscriptions/{id}/activate` (`iptv.renew`). Body: `plan_id?`, `duration_unit?`+`duration_value?`, `monthly_charge_minor?`, `apply_charge?`, `pay_now_minor?`, `authorize_unpaid?`, `discount_type?`/`discount_value?`/`discount_reason?`, `method?`. 422 if already active.
- **Extended:** `GET iptv/dashboard` adds `pending`; create accepts `status:'pending_activation'`; `POST iptv/quote` returns authoritative service-product tax when billing is on. No other endpoint signatures changed; no new financial ledger (reuses Orders/Invoices/Payments).

## 7. Tests
- **Backend:** `php artisan test tests/Feature/Iptv` → green (incl. the new `IptvPendingActivationTest`: pending create writes no plan/expiry/activation-row/order; excluded from active/expiring/reminders, counted in `pending`, out of `total`; activate free → active + expiry + activation row; activate paid-in-full → order+invoice+payment + tax; authorized-unpaid → active + outstanding; partial-no-authorize → stays pending; already-active → 422; idempotency; isolation) plus the quote-tax test. Pint clean; PHPStan no new errors.
- **Frontend:** `vitest run src/pages/iptv src/components/ui` → **70 passed**; `tsc -b`, eslint, `npm run build` all clean. Covers Page-1 pending create (no plan/pricing in body), Page-2 activation POST, summary cards + 5 tabs on active, expiry reason-gating + preview, Pay-Credit = authorized renew, Free Credit, Edit-Connection PATCH.

## 8. Browser evidence (live dev, billing enabled) — `docs/techtitans/screenshots/iptv-two-page/`
Live E2E **16/16 PASS**, 0 console errors. Page 1 shows only A+B (`01`); create → **Pending activation**, expiry —, no invoice/payment (`02`); **activate free** → Active + expiry set + activation row (`03`); **activate paid** → invoice INV-2026-0006 **$16.95 (15.00 + 13% tax) PAID** (`04`); summary cards (`05`); **Free Credit** +1mo no money; **Pay Credit** 2mo → receivable + expiry +2mo (`06`); record payment lowers outstanding; **expiry** +10d with before→after preview, submit blocked until a reason is entered (`07`); monthly charge change; **Edit Connection** updates MAC (`08`); all **5 tabs** render (`09`); an **existing** active sub renders with no activation panel (`10`); mobile 390 + 150% zoom have 0px overflow (`11`,`12`).

## 9. Existing-subscription compatibility
Active/expired/suspended/cancelled subscriptions are unaffected (the pending panel only shows for `pending_activation`); existing invoices/payments/renewals render in the new tabs; the detail URL `/iptv/:id` is unchanged; no account records were duplicated. Reporting/reminders still gate on `status='active'`, so pending connections never surface as active.

## 10. Limitations
- **Per-renewal discounts**: discounts apply on the **activation** path; the plain `renew` body carries no discount field, so the Package-&-pricing renewal shows a tax/total preview without a discount input (activation + Pay-Credit cover the discounted paths). Documented, not misleading.
- **Legacy Pay Credit range** was 0–24 in the old code; we use **0–18** per the brief.
- **Tax class**: renewal/activation tax is governed by the IPTV service product's tax class (13% on the dev tenant); a tenant with no taxable class bills at its default rate until configured.
- Notifications are recorded as eligibility only — **no messages are sent in development**.

## 11. Local commits (separate per repo, on `techtitans/integration`, no push)
- **Backend:** `966a0cb` — migration, model, service (pending + activate), exception, controller + activate request, routes, list request, quote tax fix, tests, this report + screenshots.
- **Frontend:** `f62b5fd` — Page 1 (`NewIptvPage`), Page 2 (`IptvDetailPage`), `iptv.css`, `iptv.ts` types/binding, IPTV page tests.
