# TechTitans POS — IPTV Package Creation & Discount Support

**Branch:** `techtitans/iptv-i2`. Enhances the one-page New IPTV Connection form with inline package
creation, per-purchase discounts, promotional free service, and an authoritative provisional price summary.
Reuses the existing IPTV + POS architecture (plan APIs, tax engine, renewal/date rules, idempotent create).
**Not pushed.**

## Task 1 — Add New Package (inline)

A `+ Add new package` action in Subscription & Pricing opens a compact modal (name, duration value + unit,
selling price, optional acquisition cost, active) that **reuses `POST iptv/plans`** (`iptvApi.createPlan`)
— no separate package store. On success the package is appended, **auto-selected**, and price + proposed
expiry re-default to it. Duplicate/validation errors surface inline. Shared package creation is restricted
to **`iptv.configure`** (the action is only shown to holders). Tax class is not in this inline modal (it is
set on the full Plans page) — noted as a limitation.

## Task 2 — Discounts (per-purchase, authoritative)

Discount controls (No discount / Fixed amount / Percentage) + optional reason, shown only with the new
**`iptv.discount`** permission. The discount applies to the **current purchase's package price only** —
never to the shared plan price. Encoding: fixed = minor units, percent = basis points. Validation lives in
the backend `IptvPricingService`: no negatives, percent ≤ 100%, fixed capped at the package, totals never
negative. **Tax is never computed in React** — see Task 4.

## Task 3 — Promotional free service (not money)

Optional free days/months at creation, shown only with **`iptv.credit_grant`**. It is recorded as a
**`free_extension` history row** (explicit reason, actor, date, audit) that advances the service expiry but
**never** `paid_through_date` — promotional service is explicitly NOT a payment or financial credit. It
reuses the existing calendar-safe renewal date math. Unauthorized promos are rejected (403); the whole
create is idempotent, preventing duplicates.

## Task 4 — Price summary (provisional)

`POST iptv/quote` (new, `iptv.view`, `resolve.location`) returns the authoritative breakdown — package
price, discount, box price, subtotal, tax (from the plan's POS `TaxClass` rate_bps + per-location override),
final total — using `IptvPricingService`. The frontend displays it verbatim. A quote **creates no order or
invoice and charges no one**; the UI labels it "Provisional quote — this does not create an invoice or
charge the customer." (I2 billing through Order → Invoice → Payment remains config-gated; a quote is not a
sale.)

## Task 5 — Persistence & renewal compatibility

Inspected I2 first; the only additive schema is on the **history** table (never a new ledger):
`iptv_subscription_renewals` gains `discount_type`, `discount_minor`, `tax_minor`, `discount_reason`. The
initial activation records the discount + authoritative tax while `price_minor` keeps the **gross** package
price — so the shared plan price is never overwritten, and the discount is fully auditable. **Future
renewal pricing uses the plan/monthly charge without the initial discount** (the discount is one-time, on
the activation row only). Promotions persist as `free_extension` rows. Nothing is stored only in React.

## Task 6 — UI

Everything stays on the one page: compact two-column grids, the add-package + confirm as modals (no extra
scrolling), the discount/promo controls and price-summary card inside Subscription & Pricing, and the
**confirm summary now includes the selected package, discount, promotional free, and the provisional
total**.

## Files changed

- Backend: `app/Iptv/IptvPricingService.php` (new), `app/Iptv/IptvSubscriptionService.php` (discount on
  activation + creation promo), `app/Http/Controllers/Api/V1/Iptv/IptvSubscriptionController.php` (quote +
  discount/promo gating), `app/Http/Requests/Api/V1/Iptv/StoreIptvSubscriptionRequest.php`,
  `app/Iptv/IptvResource.php`, `app/Iptv/Exceptions/IptvException.php`,
  `app/Authorization/PermissionCatalog.php` + `RoleTemplates.php`, `app/Models/IptvSubscriptionRenewal.php`,
  `routes/api.php`, migrations `2026_11_25_100001/100002`, tests
  (`tests/Unit/Iptv/IptvPricingServiceTest.php`, `tests/Feature/Iptv/IptvOperationsTest.php`,
  `tests/Support/ProvisionsIptv.php`, `PermissionCatalogTest.php`).
- Frontend: `pages/iptv/NewIptvPage.tsx` (add-package modal + discount/promo + quote summary + confirm),
  `pages/iptv/iptv.css`, `lib/api/iptv.ts` (quote + discount/promo body), `lib/auth/usePermissions.ts`,
  `pages/iptv/IptvPages.test.tsx`, `e2e/iptv-packages-discounts.spec.ts` (new).

## Schema / API changes

- Migration: 4 additive columns on `iptv_subscription_renewals` + `iptv.discount` backfill. No new table.
- API: `POST iptv/quote` (provisional); `POST iptv/subscriptions` accepts `discount_type/discount_value/
  discount_reason` (needs `iptv.discount`) and `promo_unit/promo_value/promo_reason` (needs
  `iptv.credit_grant`).

## Tests

- **Backend** `IptvPricingServiceTest` (6) — no-discount, fixed, percent (bps), fixed cap, >100% rejected,
  tax-on-net. `IptvOperationsTest` (+4) — quote breakdown creates nothing; fixed discount persists on the
  activation + **plan price preserved**; discount requires `iptv.discount` (403); promo extends expiry (not
  paid-through) + requires `iptv.credit_grant`. Full IPTV + catalog suite **47 passed**.
- **Frontend** `IptvPages.test.tsx` (8) incl. "applies a percentage discount" (quote call + create payload
  `discount_value` in bps) and the one-page create/confirm/no-accidental-submit flow. `tsc` ✓, `eslint` ✓.
- **Browser** (Playwright) `e2e/iptv-packages-discounts.spec.ts` — **passed**: add-package modal → auto-
  selected package, a 10% discount with the authoritative summary (**$20 package − $2 = $18 provisional
  total**), a promotional free month, the confirm summary, and create. Screenshots in
  `frontend/test-results/iptv-packages/`: `add-package-modal.png`, `form-discount.png`, `confirm.png`.

## Known limitations

- Inline package modal omits tax class (set on the Plans config page); the box price is not independently
  taxed in the quote (service net is taxed) — can be refined.
- A quote is provisional only; it does not create an order/invoice. Real billing through Order → Invoice →
  Payment stays config-gated (I2 boundary) and is not claimed as a sale.
- Discounts/promos are applied at creation; discounting a later renewal is a future increment.

## Commit hashes (branch `techtitans/iptv-i2`, not pushed)

- Backend: **d1e43be** (packages/discounts/quote) + the commit carrying this report.
- Frontend: see the latest `techtitans/iptv-i2` frontend commit.
