# TechTitans POS — Customer CRM Phase C7 Sales Contribution Charts

**Date:** 2026-11-12 · **Branch:** `techtitans/integration` (backend + frontend) · Local commits only (no push/deploy).
**Builds on:** C4 (`CustomerFinancialService::overview()`) + C5/C6 layout. This adds three donut charts (Sales/IPTV/Repairs) showing this customer's contribution to total TechTitans revenue per service, **additively** on the existing finance-overview response. **No billing/CRM-schema change, no DB tables added, no data migrated, no NH POS / legacy PHP touched, nothing pushed.** All existing CRM functionality, calculations, APIs and permissions preserved.

## 1. Backend revenue definition (the one consistent basis)
**Eligible / realized sales** for a category =
`Σ over orders of max(0, grand_total_minor − refunded_minor)` where the order is
- **placed** (`order_number IS NOT NULL` → drafts/preliminary excluded),
- **not a void** (`status NOT IN ('CANCELLED','REJECTED')`),
- in the **primary currency**, within the caller's **orders.view location scope**.

`refunded_minor` = Σ `refunded_amount_minor` over **real-mode** payments (`production`/`offline`) on that order. Discounts are already reflected in `grand_total_minor`. **Outstanding is never used as sales.** The *same* definition is applied to the customer (numerator) and the store (denominator), so the percentage is apples-to-apples.

This is intentionally **distinct** from the panel's top figures (Total billed / paid / outstanding) — "billed" includes everything non-void; "realized sales" excludes drafts and nets refunds. Both are shown, kept logically separate (task §5).

## 2. Financial data sources
- **Sales (retail):** `orders` with `order_source != 'SYSTEM'` (POS/retail). Blue `#0284C7`.
- **IPTV:** `orders` with `order_source = 'SYSTEM'` — the IPTV renewal pipeline bills through the **same** Order→Invoice→Payment ledger (`app/Iptv/Billing/OrderPipelineBilling.php`), so IPTV is never double-counted as retail and vice-versa. Green `#059669`.
- **Repairs:** **no billing in R1** (the `repair_tickets` migration states billing is deliberately out of scope). Reported **unavailable** (`available:false`, null figures) — never ticket counts or invented revenue. Purple `#7C3AED`.
- `refunds` from the `payments` ledger; voids = cancelled/rejected orders.

## 3. Customer / store percentage formula
```
percentage = store_total_minor > 0
           ? round(customer_total_minor / store_total_minor * 100, 2)
           : null            // zero denominator → "No data" (no artificial 0% / 100%)
```
A category with `available:false` (repairs) shows an **N/A** state. The chart arc length = `percentage/100 × circumference`; 100% fills the ring, 0% draws no arc.

## 4. API (additive)
`GET /customers/{id}/finance-overview` (unchanged route, payments.view-gated, orders bounded to orders.view scope) — the response **preserves every existing field** and adds:
```json
"sales_contribution": {
  "currency": "CAD",
  "period": "all_time",
  "basis": "Realized sales — placed, non-void orders in this currency; refunds netted; drafts & outstanding excluded.",
  "categories": {
    "sales":   { "customer_total_minor": 0,    "store_total_minor": 0,    "percentage": null, "available": true },
    "iptv":    { "customer_total_minor": 9605, "store_total_minor": 9605, "percentage": 100,  "available": true },
    "repairs": { "customer_total_minor": null, "store_total_minor": null, "percentage": null, "available": false }
  }
}
```
Two bounded `GROUP BY` queries (customer + store) over the orders ledger with a single `refunded` sub-aggregate join — **no N+1**, no duplicated payments/revenue. Tenant isolation via explicit `tenant_id`; location scope reused from the existing finance mechanism.

## 5. Frontend
- **`CustomerSalesContributionChart`** (new, self-contained **SVG** donuts — no new charting dependency): three categories with coloured icon tile + title, a customer-share arc (category colour) over a grey "other customers" ring, a centre label (`%` / `No data` / `N/A`), and a legend showing *This customer* vs *Store total* amounts (currency-formatted). "All time · CAD" basis chip with a tooltip (task §7).
- **Financial overview panel** restructured per §5: **Top** Total billed/paid/outstanding · **Middle** the three donuts · **Below** a compact **Service · Customer · TechTitans · Share** breakdown table (Sales/IPTV/Repairs-N/A). The old two-bar contribution chart was removed to avoid duplication.
- Three across on desktop, reflowing to one per row on mobile; percentages always readable; no legend overlap.

## 6. Correctness guarantees (task §7)
IPTV is **not** classified as retail and **not** double-counted (`order_source` split); repair estimates are **not** treated as sales (R1 has none → N/A); **outstanding is never** used as sales; currencies are **not** mixed (primary-currency only, multi-currency flagged); **no percentage** is shown when the store total is unknown/zero (→ "No data"); and a single-customer dev DB correctly yields a real 100% **only** when that customer is genuinely the whole category (verified in tests), while empty categories show "No data" instead of a misleading 100%.

## 7. Testing

### Backend (`php artisan test --filter=Customer`) → **112 passed**
New `CustomerFinancialTest` cases:
1. **Sales + IPTV aggregation** split by category with **refund netting** and **draft + void exclusion** (customer net 1400/store 2000 → 70%; IPTV 2000/3000 → 66.67%).
2. **Repairs unavailable** (null figures, `available:false`).
3. **Zero store sales** → percentage `null` ("No data"), not 0/100.
4. **One customer → 100%** (sole POS order).
5. **Multiple customers** contributing different percentages (via the "other customer" orders).
6. **Refund & void handling** (refund reduces net; cancelled excluded; draft excluded).
7. **Tenant isolation** (another tenant's 9000 sale never inflates this store total).
8. **Financial-permission enforcement** (finance-overview 403 without payments.view).
9. **Location scope** — same `orderScope` mechanism as the already-tested financial summary/activity (`activity respects order location scope`).
- **Pint** clean; **PHPStan** clean on the service.

### Frontend → **40 customers + full iptv tests passed** (65 in the customers+iptv run)
New `CustomerSalesContributionChart.test.tsx`: three charts render with correct titles/percentages (20% / 100%); **approved colours** on the arcs (`#0284C7`, `#059669`; repairs draws no arc); currency-formatted customer + store amounts; **N/A** repair state; **No-data** (not 0%) when the store total is zero. `CustomerProfilePage.test.tsx` updated for the donuts. `tsc -b`, `eslint`, `npm run build` clean.

### Live Playwright (real dev UI, customer "Dinesh")
- **Three donuts** render — titles Sales / IPTV / Repairs; the only arc drawn is **IPTV green `#059669`** at **100%**; Sales shows **"No data"** (no realized POS sales → not a fake 0%); Repairs shows **"N/A"**. Percentages reconcile with the backend (Dinesh is the sole realized-IPTV customer in CAD; IPTV customer CA$96.05 = store CA$96.05 → 100%).
- Existing features intact; **no console errors**; **no horizontal overflow** at **1920 / 1440 / 768 / 390** and zoom-emulated **125 % / 150 % / 200 %**.
- Screenshots: `screenshots/customer-crm-c7/c7-after-{desktop,tablet,mobile}.png`.

## 8. Unavailable-data limitations
- **Repairs:** no R1 billing → permanently **N/A** until repair estimates/payments land (then the same donut lights up automatically from the ledger).
- **Realized vs billed:** the donuts use realized sales (drafts excluded, refunds netted), which can be **lower** than the panel's "Total billed" — by design, and clearly separated.
- **Multi-currency:** charts show the primary currency only; a per-currency breakdown is a future increment.

## 9. Commits (local only — no push)
- **Backend:** this commit — `sales_contribution` in `CustomerFinancialService::overview()` + tests + this report + screenshots.
- **Frontend:** `5cf94c6` — `CustomerSalesContributionChart` donuts + finance-panel layout + types + tests.
