# TechTitans POS — Integration Regression Stabilization

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

Goal: fix the three known regression-test issues from consolidation and verify a clean integration baseline, without changing business functionality. A fourth, pre-existing blocker (a broken production build) surfaced during validation and was also fixed to achieve a green baseline.

---

## 1. Backend — permission-row contamination (FIXED)

**Symptom:** 9 tests (`Authorization\PermissionResolverTest`, `PermissionMiddlewareTest`) failed with `UniqueConstraintViolationException` only when the `Menu`/`AI` suites ran before `Authorization` in one `php artisan test` invocation; all passed in isolation.

**Root cause (test isolation, not product code):** `tests/Feature/Menu/VariantSkuConcurrencyTest.php` deliberately does **not** use `RefreshDatabase` (its child worker processes must see committed rows). Its `tenantUserWith()` helper calls `$this->seed(PermissionSeeder::class)`, which — with no wrapping RefreshDatabase transaction — **commits the entire global `permissions` catalogue**. The test's `tearDown` deleted only tenant-scoped rows (`WHERE tenant_id = …`), but `permissions` is tenant-global (no `tenant_id`), so those rows survived. Later `RefreshDatabase` tests assume a fresh-migration (empty `permissions`) baseline and `INSERT` catalogue keys directly, colliding on the unique `key` index.

**Fix:** in `VariantSkuConcurrencyTest::tearDown`, after the existing tenant-scoped cleanup and inside its `FOREIGN_KEY_CHECKS=0` guard, added `DB::table('permissions')->delete();` to restore the empty-catalogue baseline. This is a true source fix (the leaking test now self-isolates) and does **not** weaken authorization coverage — the Authorization tests still create their own catalogue keys and assert real unique-constraint behaviour; they simply start from a clean table. **Files:** `tests/Feature/Menu/VariantSkuConcurrencyTest.php` (1 file).

## 2. Frontend — stale `ProductsListPage` tests (FIXED)

**Symptom:** 5 `ProductsListPage.test.tsx` tests failed (even in isolation) with `Unable to find role="heading" /^add product$/i`.

**Root cause:** "Add product" now routes to the **Smart Product Creator** landing (`/menu/products/add`, heading "Add a product") offering four creation methods (Take photo/Upload, Describe with AI, Scan/enter code, **Manual entry**). The full-page editor (with its "Add product" heading + "What are you adding?" type grid) only mounts **after** choosing **Manual entry**. The tests clicked "Add product" and immediately looked for the editor, missing the new chooser hop.

**Fix:** inserted a single navigation step — `await user.click(await screen.findByRole('button', { name: /manual entry/i }))` — after the "Add product" click in each of the 5 tests. Every original assertion (integer minor-units POST = 1250, 422-stays-on-page, Cancel-returns-to-list, unsaved-changes discard prompt, duplicate-name warning) is preserved verbatim. The Product Creator UI was **not** reverted or weakened. Manual **and** AI-assisted entry remain covered (`SmartAddProductPage.test.tsx`, `ProductFormDrawer.*` still green). **Result:** 67/67 pass.

## 3. Frontend — `NewRepairPhotoFirst` flakiness (FIXED)

**Symptom:** "creates exactly one ticket even if Create is double-clicked" passed in isolation but intermittently failed under parallel load with `expected 2 to be 1` — a genuine double-submit.

**Root cause:** the Create button's only guard is `disabled={disabled || loading}`. `submit()` clears its `saving` flag in a `finally` that runs **before** React Router's navigation commits. Under CPU contention the request chain can drain and re-enable the button while the page is still mounted, so a fast second click fires a second POST. The test raced request-settle vs. route-unmount rather than exercising the guard.

**Fix (test-only, no timeout inflation):** the double-click test now returns a **gated (pending) promise** for the ticket POST, so the first request stays genuinely in flight across both clicks — the button stays disabled (the real production guard), the second click is a deterministic no-op, then the gate releases and the flow navigates. Asserts exactly one POST. No component code changed; no blanket timeout bump. **Result:** 4/4 across 5 consecutive runs, and green in the full parallel run.

## 4. Frontend — broken production build (pre-existing; FIXED to unblock the baseline)

Validation step "Production build" revealed `npm run build` (`tsc -b && vite build`) was **already failing** on `iptv-i2`/`integration` — the repo's day-to-day `tsc --noEmit` check missed it, but `tsc -b` (what the build runs) caught three pre-existing type errors in `src/pages/iptv/*`. A green build is required for a clean baseline, so these were fixed with minimal, type-safe changes:

- **`src/components/ui/AppCard.tsx`** — `AppCard` is a controlled-props component that **did not forward** `role`/`tabIndex`/`onClick`/`onKeyDown`. The IPTV dashboard passed these to make its "Expiring soon"/"Expired" tiles navigate, but they were silently dropped — so **the tiles were never actually clickable** (a real latent bug, not just a type error). Added the four optional interaction props and forwarded them to the underlying element. Backward-compatible (all optional; existing cards pass none → unchanged). This restores the tiles' intended navigation — **confirmed in-browser** (see §6).
- **`src/lib/api/iptv.ts`** — added the `subcontractor?: IptvSubcontractor` relation to `IptvSubscription`. The backend `IptvResource` already returns this nested object when loaded (`IptvResource.php:150`), and `IptvDetailPage` reads `sub.subcontractor?.name`; the type just omitted it.
- **`src/pages/iptv/IptvPages.test.tsx`** — fixed a test-only `null`→`Record` cast (`(posted as Record<string, unknown> | null)?.account_identifier`).

**Result:** `tsc -b` clean; `npm run build` succeeds (`✓ built`, dist emitted).

## 5. Validation

### Backend (`php artisan test`, Xdebug off, DB `techtitans_pos_test`)
- Regression combo `Menu + AI + Authorization` → **1100 passed, 0 failed** (re-confirmed independently on the fixed tree).
- Original failing 8-suite combo `Repairs Iptv Menu Customers AI Authorization Roles Workforce` → **1535 passed, 0 failed**.
- Full suite in bounded batches, all **0 failures**: Batch A (attendance/audit/auth) 533 · Batch B (channels/checkout/install/inventory/kds/locations/mail/orders) 535 · Batch C (payments/pin/platform/printers/scheduling/settings/shipping/stations/storefront/tax/tenancy/timeclock) 655 · Batch D (Unit + ExampleTest) 76.
- `pint --dirty` → passed. (PHPStan: `phpstan.neon` scans `app/` only; the test-file change is out of lint scope and adds no `app/` findings.)

### Frontend (from `frontend/`)
- `ProductsListPage.test.tsx` → **67/67**. `NewRepairPhotoFirst.test.tsx` → **4/4 ×5 runs** + green in the full run.
- Full suite `vitest run` → **1782 passed**, 5 pre-existing load-induced flakes (see §7).
- `tsc -b` → clean. `eslint` (changed files) → 0 errors. `npm run build` → **succeeds**.

### Browser smoke (dev UI, branch `techtitans/integration`)
- IPTV dashboard tiles now navigate: "Expiring soon" → `/iptv?state=expiring_soon`, "Expired" → `/iptv?state=expired` (the AppCard fix, confirmed live).
- `/repairs/dashboard`, `/menu/products`, a customer detail → all render primary content with **0 console/page errors**.
- Screenshots in `docs/techtitans/screenshots/stabilization/`. No records created.

## 6. Safety / restrictions honoured

- **DB protection verified:** `Tests\TestCase` runs `TestDatabaseGuard::assertApproved` before any migration — requires `APP_ENV=testing`, `mysql`, and a name matching `^techtitans_[a-z0-9_]+$_test`; `phpunit.xml` forces `DB_DATABASE=techtitans_pos_test` + `TECHTITANS_ALLOWED_DATABASES=techtitans_pos_test` (`force="true"`). `techpos` and NH DBs can never be the target; the concurrency test writes only to the forced test connection. **No dev-DB wipe.**
- **No real OpenAI from tests:** `phpunit.xml` pins `AI_PRODUCT_*`/`AI_REPAIR_*` drivers to `mock`/`none`; the fixes don't change that.
- No NH POS or `oldportal` changes. No customer/IPTV/repair records modified. No push/deploy.

## 7. Remaining issues (pre-existing, NOT introduced here)

1. **Frontend load-induced flakiness** — in the full parallel `vitest run`, ~5 tests fail per run from a *shifting* set (seen: `AppShell`, `EmployeeProfilePage`, `ProductDetailPage`, `AttendanceDevicesPage`, and on other runs `EmployeeDetailPage`, `TimeClockPage`, `CategoryDetailPage`, `DashboardPage`). **All pass in isolation** (verified: the 4 files from this run → 103/103). This is environmental CPU-contention flakiness on this Windows/WAMP box, in files unrelated to this task. **Recommended follow-up:** cap vitest worker concurrency (e.g. `poolOptions.forks.maxForks`) for local runs, or stabilise the specific async waits per file. Not fixed here (out of scope; no single defect).
2. **Backend PHPStan + test files** — `phpstan.neon` intentionally scans `app/` only (no Larastan), so running it against a test file yields false positives on Eloquent magic properties. No action needed.

## 8. Local commits (separate per repo, on `techtitans/integration`)

- **Backend:** `44c4286` — `tests/Feature/Menu/VariantSkuConcurrencyTest.php` + this report.
- **Frontend:** `a78bf90` — `AppCard.tsx`, `lib/api/iptv.ts`, `IptvPages.test.tsx`, `ProductsListPage.test.tsx`, `NewRepairPhotoFirst.test.tsx`.

**Baseline status: clean** — the three known issues are fixed, the production build is green, backend suites pass, and the only remaining frontend failures are pre-existing environmental flakes that pass in isolation.
