# Repair — Voice/Text-First Smart Intake

**Date:** 2026-11-07 · **Branch:** `techtitans/repair-r1` · Local commits only (no push). Preserves the R1 repair module + manual wizard (the manual flow is byte-for-byte unchanged behind "Continue manually").

## Objective delivered
Staff can create a repair draft from ONE natural-language instruction (typed or spoken). The app extracts a structured draft, matches the customer + device against the existing CRM/catalogues, fills the existing 3-step wizard, and shows the Review step for explicit approval. Nothing is created automatically.

## Root architecture changes
- **One instruction → one draft, filling the existing wizard** — no second repair form. The smart panel maps the draft into the SAME page state the manual wizard binds (`customer`, `deviceMode`/`newDevice`/`selectedDeviceId`, `intakeNotes`, `deviceCondition`, `accessories`, `repairTypeId`, `priorityId`, `technicianId`, `promisedAt`). Creation still happens only from Step 3's existing idempotent `createTicket` + create-then-upload.
- **Extraction extended (backend)** to a richer intake draft; **matching added (frontend)** as pure, testable rules over the existing customer/device/employee/catalogue APIs.

## API endpoints (reused/extended — no new AI service)
- `POST repairs/intake/analyze-text` — EXTENDED draft shape, now also returns `customer_name`, `customer_phone`, `customer_email`, `repair_type`, `technician`, `promised_date`, `accessories[]` (plus the prior device/fault fields). Accepts `context: { device_categories, brands, repair_types, technicians }` hints. `provider` echoes the driver.
- `POST repairs/intake/transcribe` — voice→text (config-gated; `none`→503).
- `GET repairs/intake/capabilities` — now reports `{ configured, driver, required_env }` per feature so the UI can honestly show mock vs real vs not-configured.
- Reused: `customers` search, `customers/{id}/devices`, employees list, `repairs/config/{device-categories,types,priorities}`, `repairs/brands`, `repairs/tickets` (create). Reuses the existing `App\AI\Repair` abstraction + OpenAI adapters (`RepairDraftExtractor` Mock/Null/Openai). No duplicate AI service.

## Backend files changed
- `app/AI/Repair/Contracts/RepairDraftExtractor.php` — extended return shape + rules (capture stated customer/technician/type/date/accessories; NEVER fabricate imei/serial/model/diagnosis/price/warranty or a date).
- `app/AI/Repair/Mock/MockRepairDraftExtractor.php` — deterministic extraction of the new fields (person-verb-anchored name, phone/email regex, technician "assign to NAME", ISO-only date, accessory keywords); brand no longer grabs the customer name.
- `app/AI/Repair/Openai/{OpenAiRepairDraftExtractor,RepairDraftShape}.php` — prompt + coercion for the new fields; `promised_date` coerced to ISO-or-null.
- `tests/Feature/Repairs/RepairIntakeAiTest.php` — new fields covered; `technician` is now a legitimate stated-only field (removed from the forbidden list; `model`/dates asserted never fabricated).

## AI provider status (honest)
- **draft** default `mock` — a deterministic development stub, clearly badged **"Development AI (mock) — review every field"** in the UI; NEVER presented as real analysis.
- **transcription** + **vision** default `none` → fail closed (503) with the exact env vars; the voice UI offers transcription only when configured and never claims success otherwise.
- **Real OpenAI** path is implemented for all three but was **not exercised** (no API key on this machine). Enable with `AI_REPAIR_{DRAFT,VISION,TRANSCRIPTION}_DRIVER=openai` + the matching server-side `*_API_KEY`.

## Customer / device / technician matching behaviour (frontend, `src/lib/repairs/intakeMatching.ts`)
- **Customer:** unique exact normalized-**phone** → preselect; else unique exact **email** → preselect; else unique exact **name** → preselect; multiple/weak/no match → a **confirmation dialog** (Select candidate / Create new [prefilled, explicit] / Search manually). Never auto-selects on a weak name; never auto-creates.
- **Device:** serial match → else exact brand+model (category-constrained) → select the saved device; otherwise register-new mode prefilled from the draft. **No category/brand auto-create** from AI guesses; staff keep/correct or use Add New. Customer-owned devices never touch inventory.
- **Technician:** unique exact name → assign; unknown/ambiguous → Unassigned with a visible notice.
- **Type/priority:** matched to configured ids (key/label, active only). **Model & promised_date never fabricated.**

## Frontend files changed
- `src/components/repairs/SmartIntakePanel.tsx` (top card: instruction textarea + reused voice capture + Prepare/Continue-manually + honest provider badge), `src/components/repairs/CustomerMatchDialog.tsx`, `src/lib/repairs/intakeMatching.ts` (+ test).
- `src/pages/repairs/NewRepairPage.tsx` (mount panel above the wizard; `prepareWithAi`/`applyDraftToWizard`/`resolveCustomerFromDraft`; extended Review with existing/new tags + uncertain/missing). `src/lib/api/repairs.ts` (extended types + `analyzeText(text, context)`). `src/pages/customers/CustomerFormDrawer.tsx` (optional prefill). `repairs.css`.

## Tests
- Backend: `php artisan test --filter=RepairIntakeAi` → **11 pass**; full repair suite **56 pass (203 assertions)**; Pint + PHPStan clean.
- Frontend: `src/pages/repairs src/components/repairs src/lib/repairs` → **63 tests / 10 files pass**; `tsc` clean; lint 0 errors; `build` succeeds. Covers text extraction→fill, phone/email preselect, ambiguous→dialog (no auto-select/create), existing-device match, new-device draft, technician map + unknown→Unassigned, unknown-model stays empty, Continue-manually bypass, provider=none disables Prepare honestly, mock labelled, no auto-ticket.

## Browser verification (live `techpos`, draft driver = mock) — `docs/techtitans/screenshots/repair-smart-intake/`
- `01-smart-panel` — the Smart Intake card atop New Repair with the mock badge.
- `02-after-prepare` — one instruction → **customer preselected by phone** (Repair Test Customer), device prefilled (category=phone, brand=Apple, model empty — not fabricated), "Matched customer by phone" notice.
- `03-step2-problem` / `04-review` — wizard prefilled; Review shows Existing-customer + New-device tags, reported fault + requested service, **Priority High**, accessories "charger", technician Unassigned.
- `05-created-detail` — **Create Repair → ticket 201** (explicit approval; a real repair was created from the text-first flow).
- `05-no-match-dialog` — unknown customer → **"No customer match"** dialog (Create new / Search manually; no auto-create) + honest "couldn't match technician 'John' — Unassigned" notice.
- `06-mobile-smart-panel` — responsive on a 390px viewport.

## Remaining limitations
- The **mock** extractor is deterministic/naive (e.g. it never extracts a device model, so text-only device matching to a saved device won't fire — staff enter the model); it is clearly labelled as a development stub. Real OpenAI extraction is wired but unverified (no key).
- Voice transcription + vision are not configured on this machine (fail closed, honest) — the recording UI works; transcription/analysis require a provider.

## Commits (local, no push)
Backend + frontend commit hashes on `techtitans/repair-r1` are recorded with this change (see the repo log). Nothing pushed or deployed; the R1 module, manual wizard, and the permissive historical-migration API contract are untouched.
