From 79fbfec61f870a674d295d5a9fc22e817de922a2 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Wed, 20 May 2026 21:22:01 -0400 Subject: [PATCH] docs(fusion_repairs): add design spec Comprehensive 4-phase design for fusion_repairs Odoo 19 module covering three intake surfaces (backend wizard, sales rep portal, public client portal), AI self-check with strict medical safety guardrails, weekend on-call paging, repairs pricelist automation, Poynt payment collection, and maintenance lifecycle with client self-booking. 53 features across phases 1-4; reuses existing fusion_tasks technician model and fusion_authorizer_portal sales rep scaffolding. Includes Appendices A-D with seed AI system prompt + JSON schema, 15 upsell rules, voicemail scripts, and 30 deterministic self-check rules across 7 medical equipment categories. Co-authored-by: Cursor --- .../specs/2026-05-20-fusion-repairs-design.md | 1351 +++++++++++++++++ 1 file changed, 1351 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-20-fusion-repairs-design.md diff --git a/docs/superpowers/specs/2026-05-20-fusion-repairs-design.md b/docs/superpowers/specs/2026-05-20-fusion-repairs-design.md new file mode 100644 index 00000000..dd709a35 --- /dev/null +++ b/docs/superpowers/specs/2026-05-20-fusion-repairs-design.md @@ -0,0 +1,1351 @@ +# fusion_repairs — Module Design Spec + +**Date:** 2026-05-20 +**Module:** `fusion_repairs` (new) +**Owner:** Gurpreet +**Status:** Approved (ready for implementation plan) +**Scope:** Four-phase build (~8-12 weeks); three intake surfaces; 53 features +**Sister modules:** `fusion_repair_compliance`, `fusion_repair_plans`, `fusion_repair_shop`, `fusion_repair_analytics` (Phase 4, optional split) + +## Summary + +Build `fusion_repairs` as an Odoo 19 addon that extends the standard Repairs app (`repair.order`) with: + +- **Three intake surfaces** sharing one service layer (`fusion.repair.intake.service`): + 1. Backend wizard for CS on phone calls + 2. Sales rep mobile portal (`/my/repair/new`) for reps on the road + 3. Public AI-assisted client portal (`/repair`) with self-check + upsell engine — voicemail-ready +- **Field service dispatch** via existing `fusion.technician.task` (reuses `x_fc_is_field_staff` + `group_field_technician` — no new technician model) +- **Maintenance lifecycle** — contracts from delivered SOs, reminder crons, tokenized client self-booking +- **Billing + payment** — `repair.order → action_create_sale_order()` → invoice → Poynt terminal collection +- **Pricing automation** — service catalogue, repairs pricelist, AI matching, variance reconciliation +- **Safety + on-call** — combined weekend escalation (911 disclaimer + Monday queue + on-call manager paging) +- **Email + activities** — `fusion.email.builder.mixin` (consistent with fusion_claims styling) + +Built incrementally across 4 phases; each phase ships a usable slice. + +## Current state + +- [`fusion_repairs/`](fusion_repairs/) is an **empty folder** — no `__manifest__.py`, models, or views yet. +- No existing code in the repo extends Odoo's `repair` app. +- Closest precedents: + - [`fusion_ltc_management/models/ltc_repair.py`](fusion_ltc_management/models/ltc_repair.py) — repair workflow + SO + technician task (LTC facilities only; **keep separate**) + - [`fusion_tasks/models/technician_task.py`](fusion_tasks/models/technician_task.py) — field service scheduling with `task_type` including `repair` / `maintenance` + - [`fusion_claims/models/sale_order.py`](fusion_claims/models/sale_order.py) + [`fusion_claims/views/sale_order_views.xml`](fusion_claims/views/sale_order_views.xml) — smart buttons + automated emails via [`fusion_tasks/models/email_builder_mixin.py`](fusion_tasks/models/email_builder_mixin.py) + - [`fusion_poynt/models/sale_order.py`](fusion_poynt/models/sale_order.py) — `action_poynt_collect_payment()` after invoice + - [`fusion_schedule/`](fusion_schedule/) — public appointment booking (reuse in Phase 3 for maintenance self-scheduling) + +## Architecture decision (confirmed) + +```mermaid +flowchart TD + subgraph intake [Phase1_Intake] + Wizard["fusion.repair.intake.wizard"] + Template["fusion.repair.intake.template"] + Wizard --> Template + Wizard -->|"submit"| RO["repair.order"] + end + + subgraph dispatch [Phase1_Dispatch] + RO --> Task["fusion.technician.task"] + RO --> Activity["mail.activity reminders"] + RO --> Email["mail.mail via email_builder_mixin"] + end + + subgraph billing [Phase2_Billing] + Task -->|"visit report wizard"| RO + RO -->|"action_create_sale_order"| RepairSO["sale.order repair billing"] + RepairSO --> Invoice["account.move"] + Invoice --> Poynt["fusion_poynt wizard"] + end + + subgraph maintenance [Phase3_Maintenance] + OrigSO["sale.order original purchase"] --> Contract["fusion.repair.maintenance.contract"] + Contract --> Cron["ir.cron reminders"] + Cron --> BookingLink["tokenized email link"] + BookingLink --> Task + Task -->|"complete"| Contract + end + + OrigSO -.->|"x_fc_original_sale_order_id"| RO +``` + +**Core record:** Odoo 19 [`repair.order`](https://www.odoo.com/documentation/19.0/applications/inventory_and_mrp/repairs/repair_orders.html) (not a parallel custom repair model). + +**Why:** Odoo 19 repair orders already handle product/serial, warranty, operations/parts, and generate a linked `sale_order_id` for invoicing. Fusion adds intake, dispatch, maintenance, and pricing on top — matching your choice and Odoo's post-refactor billing model (repair → SO → invoice). + +**Separation from LTC:** `fusion_ltc_management` stays for nursing-home facilities. `fusion_repairs` targets retail/home medical equipment (beds, wheelchairs, stairlifts, porch lifts, walkers, mattresses, rollators). + +--- + +## Staff productivity feature catalogue + +Every feature below has been accepted for inclusion (full scope). Phase assignments balance staff impact against build effort. + +### CS / call intake + +| ID | Feature | Phase | Notes | +|----|---------|-------|-------| +| C1 | Duplicate-call detection | 1 | Wizard step 1: open repair from last 14 d on this phone → "add note instead?" | +| C2 | **Client history sidebar in wizard** | 1 | Last 3 repairs, maintenance status, open balance, loaner status, ADP/insurance flag — pulled lazily | +| C3 | SMS reply from wizard | 3 | Inline "Send SMS" via [`fusion_ringcentral`](fusion_ringcentral) | +| C4 | Canned scripts per issue | 4 | New model `fusion.repair.script.template` keyed off `x_fc_issue_category` | +| C5 | Outstanding-balance warning | 1 | Red banner if open invoice >$X (configurable) | +| C6 | Quote-only mode | 1 | Wizard checkbox; repair starts in `quote` state, no tech dispatch, sends quote email | + +### Dispatcher + +| ID | Feature | Phase | Notes | +|----|---------|-------|-------| +| D1 | Map view of open repairs by zone | 3 | Reuse fusion_tasks map; add repair-state filter | +| D2 | **Tech skills matrix** | 2 | `res.users.x_fc_repair_skills` Many2many → `fusion.repair.product.category`. Wizard filters candidate techs | +| D3 | Parts pre-pull checklist | 2 | Print/email next-day picklist from `catalog.default_parts_product_ids` × scheduled tasks | +| D4 | Reschedule with client SMS approval | 3 | Two-way SMS via RC; Y/N response writes to chatter | + +### Technician (mobile / in-field) + +| ID | Feature | Phase | Notes | +|----|---------|-------|-------| +| T1 | **Open in Maps button on task** | 2 | `geo:` / Apple Maps URL; one-tap | +| T2 | **AI pre-visit brief on mobile form** | 2 | Surfaces `x_fc_ai_summary` prominently; "What to bring" + safety flags | +| T3 | Labour timer via fusion_clock | 3 | Tap Start/Pause; final time pre-fills visit report | +| T4 | **Client signature on completion** | 2 | OWL signature pad on visit report wizard; attached to repair (pattern from [`fusion_authorizer_portal`](fusion_authorizer_portal)) | +| T5 | "Found another issue" button | 2 | Spawn new repair from current visit, same partner, different equipment | +| T6 | Parts replaced — serial capture | 3 | Scan/type replaced part serials; stores for OEM warranty + traceability | +| T7 | No-show photo proof | 3 | "Client not home" → camera → photo attached → repair flagged + service-call fee added | +| T8 | Tap-to-collect-payment (Poynt terminal) | 2 | Visit report "Save & Collect" → fires `action_poynt_collect_payment()` | + +### Client experience + +| ID | Feature | Phase | Notes | +|----|---------|-------|-------| +| X1 | "Tech is X min away" SMS | 3 | Auto when tech taps "On the way" on mobile task | +| X2 | **Day-before reminder SMS + email** | 1 (email) / 3 (SMS) | Cron pre-existing; SMS adds RC integration | +| X3 | Self-reschedule link in confirmation | 3 | Tokenized URL; client picks new date themselves | +| X4 | Post-visit NPS / review request | 3 | Auto-email 1 d after `done`; Google Review CTA | +| X5 | Logged-in equipment portal | 4 | `/my/equipment` — warranty, next maintenance, history, manuals | + +### Back-office & management + +| ID | Feature | Phase | Notes | +|----|---------|-------|-------| +| M1 | **Compliance inspection certificates** (stairlifts/porch lifts) | 4 | New `fusion.repair.inspection.certificate` model + PDF report + annual cron + email to client. Could become sister module `fusion_repair_compliance` | +| M2 | ADP/funder coverage for repair claims | 4 | Bridge to [`fusion_claims`](fusion_claims): if original SO is ADP-funded, create repair claim instead of charging client. Soft dep | +| M3 | Loaner auto-offer when repair >X days | 3 | Bridge to [`fusion_loaners_management`](fusion_loaners_management); offer at intake + auto-create checkout | +| M4 | Mail-in / shop repairs workflow | 4 | New state machine: pickup label → inbound shipment → shop repair → outbound. Uses [`fusion_shipping`](fusion_shipping) / [`fusion_canada_post`](fusion_canada_post) | +| M5 | Pre-paid service plans | 4 | New `fusion.repair.service.plan` product type; sold on SO; entitles N maintenance visits/year; burn-down counter. Candidate sister module `fusion_repair_plans` | +| M6 | **Repair warranty tracking (30/90 day re-do free)** | 2 | New `fusion.repair.warranty.coverage` model; if same equipment + similar issue within window → wizard banner "covered, no charge" | +| M7 | Failure rate analytics dashboard | 4 | OWL client action: failures per product, symptom clusters, first-call resolution by tech | +| M8 | OEM warranty claim filing | 4 | PDF claim doc to manufacturer; partner stores manufacturer reps | +| M9 | Margin per repair | 4 | Computed: revenue − (labour × tech cost rate + parts cost); shown on repair + dashboard | + +### Client self-service portal (public, AI-assisted, upsell-focused) + +| ID | Feature | Phase | Notes | +|----|---------|-------|-------| +| CL1 | **Public `/repair` landing page** | 1 | Anonymous; matches voicemail prompt URL | +| CL2 | **Phone-first lookup** (optional) | 1 | Client enters phone; if match found, pre-fills name/address — never reveals other client data | +| CL3 | **Equipment selection with QR pre-fill** | 1 | Category + product + optional serial; QR sticker on equipment → `/repair?sn=XYZ` pre-selects unit | +| CL4 | **Same guided question flow** | 1 | Reuses backend `fusion.repair.intake.template` via shared service | +| CL5 | **Photo / video upload from camera** | 1 | Optional but encouraged | +| CL6 | **AI self-check engine** (non-chat) | 2 | Suggests 1-3 safe self-check steps based on symptoms (e.g. "unplug for 30 sec, plug back in"). NEVER chat-style; strict structured JSON output | +| CL7 | **AI safety guardrails** | 2 | System prompt forbids medical advice / diagnostic claims; mandates escalation if uncertain; passes through `fusion.api.service` with feature='client_self_triage' | +| CL8 | **Resolved / unresolved branch** | 2 | "Did this fix it?" — if yes, optional upsell + survey; if no, escalates to repair queue | +| CL9 | **Upsell engine** | 2 | After self-check (whether resolved or not), surface 2-4 product suggestions from `fusion.repair.upsell.suggestion` rules: maintenance plan (M5), extended warranty, replacement parts, accessories. AI ranks based on intake answers | +| CL10 | **Direct-buy parts / plans** | 3 | "Add to order" buttons go to `/shop` (website_sale) or fast-checkout via Poynt portal flow | +| CL11 | **Save & resume tokenized link** | 1 | Mid-flow exit → email/SMS resume link; localStorage backup | +| CL12 | **Phone verification (SMS code) — smart** | 2 | If phone matches an existing partner → skip verification (trusted). For unknown phones → send 6-digit code via RC before submit. Anti-spam + ensures we can reach unknown callers. | +| CL13 | **reCAPTCHA / honeypot** | 1 | Anti-spam on landing + submit | +| CL14 | **Privacy notice + PHI consent** | 1 | Inline checkbox + privacy link; no PHI stored beyond explicit answers | +| CL15 | **After-hours auto-response + safety escalation** | 2 | Outside business hours: friendly banner "Tech available Mon — try self-check below". **Safety branch (combined):** if client picks "safety issue" → (1) prominent disclaimer "If anyone is hurt, call 911 immediately", (2) queue for Monday with high-priority flag + "we'll call first thing" message, (3) page next-on-call manager via SMS + email (uses `x_fc_on_call_priority` ordering on `res.users` filtered by `x_fc_on_call=True`) | +| CL16 | **Voicemail integration** | 3 | RC voicemail greeting auto-updated to mention `/repair` URL (or static recording); after-hours greeting variant | +| CL17 | **QR sticker generator** | 3 | Per-product PDF: stick on equipment → scan → `/repair?sn=` | +| CL18 | **Self-check knowledge base** | 4 | Linked manual snippets, short videos, "what does this beep mean" guides | +| CL19 | **Voice input → AI transcription** | 4 | Client speaks the problem into mic, AI transcribes + classifies | +| CL20 | **Resolution survey + Google review** | 2 | After "resolved" outcome, ask "save you time today?" + Google review CTA | + +### Sales rep portal (mirrors fusion_authorizer_portal pattern) + +| ID | Feature | Phase | Notes | +|----|---------|-------|-------| +| S1 | **Sales rep web intake form** | 1 | `/my/repair/new` — same question flow as backend wizard, mobile-friendly. Reuses `is_sales_rep_portal` flag on `res.partner` from [`fusion_authorizer_portal/security/portal_security.xml`](fusion_authorizer_portal/security/portal_security.xml) line 11 | +| S2 | Sales rep dashboard tile | 1 | Add "Service Calls" tile to `/my/sales-rep/dashboard` showing count of repairs they logged + recent 5 | +| S3 | **My Service Calls** list page | 1 | `/my/repairs` — sales rep sees their submitted repairs, status, assigned tech, scheduled date | +| S4 | View repair status from portal | 1 | `/my/repair/` — read-only timeline, chatter for non-internal messages, ability to add a comment | +| S5 | Add a note / new symptom from portal | 2 | If client calls back with more info, sales rep can append to the existing repair without backend access | +| S6 | Mobile photo upload | 1 | Direct camera capture on phone, attached to repair | +| S7 | Book maintenance on behalf of client | 2 | Sales rep can drive the booking link experience while on the call | +| S8 | View client history sidebar | 1 | Same C2 sidebar exposed to sales reps inside the portal form | +| S9 | "Quote me a repair" mode | 2 | Sales rep flags repair as `quote_only` (matches C6); price flows back to portal when ready | +| S10 | Sales rep gets follow-up activity | 2 | When their submitted repair changes state (scheduled / completed / invoiced), portal notification + email | +| S11 | Signature for warranty acknowledgement | 3 | If client is in person, sales rep captures signature on portal acknowledging "service call out of warranty" | +| S12 | Repair-aware commission view | 4 | If sales rep gets commission on repair upsells, show running total on dashboard | + +**Routing namespace:** `/my/repair/*` (intake + my list) and a `/my/sales-rep/repairs` summary route added to the existing sales rep dashboard. + +**Record rule** (mirrors [`fusion_authorizer_portal/security/portal_security.xml`](fusion_authorizer_portal/security/portal_security.xml) line 129 pattern): + +```xml + + Sales Rep Portal: Repairs they submitted + + [('x_fc_intake_user_id', '=', user.id)] + + +``` + +### Other refinements + +| ID | Feature | Phase | Notes | +|----|---------|-------|-------| +| P1 | Client preferences on `res.partner` | 1 | `x_fc_preferred_tech_id`, `x_fc_preferred_window`, `x_fc_access_notes` (dog, gate code) | +| P2 | Multi-equipment intake loop | 1 | See decision D below | +| P3 | Photo intake | 1 | See decision E below | +| P4 | Phone-first partner search | 1 | See decision K below | + +--- + +## Refined design decisions (from gap review) + +| # | Decision | Resolution | +|---|----------|------------| +| A | **RingCentral integration** (Phase 4, deferred) | Phase 1 wizard launched from menu only. Phase 4 adds "Start Service Call" button on RC inbound popup with partner pre-fill. No fusion_ringcentral hard dependency. | +| B | **Third-party equipment** | Always accept. Flag `x_fc_third_party_equipment=True`; force `under_warranty=False`; auto-add a service call-out fee line from the catalogue. | +| C | **AI assist** (Phase 2) | Use `self.env['fusion.api.service'].call_openai(consumer='fusion_repairs', feature='intake_triage', ...)` with try/fallback (no `fusion_api` dependency). Two features: (1) summarize free-text issue into technician brief, (2) suggest matching `fusion.repair.service.catalog` entry. | +| D | **Multi-equipment per call** | One `repair.order` per unit. Wizard step 2 has "+ Add Another Equipment" loop; all repair orders share the same `x_fc_intake_session_id` so they appear grouped on the partner. | +| E | **Photo / attachment intake** | `ir.attachment` Many2many `x_fc_photo_ids` on `repair.order`; wizard step 3 has drag-drop upload. Pattern reused from [`fusion_ltc_management/controllers/portal_repair.py`](fusion_ltc_management/controllers/portal_repair.py). | +| F | **Warranty determination** | `product.template.x_fc_warranty_months` (default 12). On intake, auto-tick `under_warranty` if `original_sale_order_id.delivery_date + warranty_months >= today` AND not third-party. CS can override with reason. | +| G | **Office recipients** | Reuse `res.company.x_fc_office_notification_ids` already defined in [`fusion_claims/models/res_company.py`](fusion_claims/models/res_company.py) line 56 (Many2many to `res.partner`). | +| H | **Multi-company** (Westin Healthcare + NEXA Systems) | All new models carry `company_id` default `lambda: self.env.company`; templates/pricelists/catalogs/contracts company-scoped; menus respect current company. | +| I | **Parts inventory** | Use Odoo native — `repair.order` operations of type `add` create stock moves automatically. No extra Fusion code needed. | +| J | **Mobile/field UX** | Phase 1 reuses existing `fusion.technician.task` mobile views (techs already use these daily). Visit report wizard works on mobile from the task form. | +| K | **Wizard partner search** | Step 1 = phone-first lookup using E.164 normalization (same util as `fusion_ringcentral`); show recent repairs/SOs inline; "Create New Contact" button if no match. | + +--- + +## Module dependencies + +```python +'depends': [ + 'repair', # Odoo Repairs app — repair.order + 'sale_management', + 'stock', # lot/serial tracking + 'maintenance', # optional bridge for equipment records + 'mail', + 'portal', # sales rep + maintenance booking portal + 'website', # QWeb portal templates + 'fusion_tasks', # technician tasks + fusion.email.builder.mixin + 'fusion_poynt', # payment collection + 'fusion_authorizer_portal', # sales rep portal flag + group + dashboard scaffold +] +# Phase 3 soft-add: 'appointment', 'fusion_schedule' for client self-booking +# Phase 3 soft-add: 'fusion_clock' for tech labour timer (T3) +# Phase 3 soft-add: 'fusion_loaners_management' for loaner auto-offer (M3) — already pulled transitively +# Phase 3 soft-add: 'website_sale' for direct-buy parts/plans (CL10) +# Phase 3 soft-add: 'fusion_ringcentral' for SMS verify (CL12) + voicemail greeting (CL16) + caller-ID launch (Phase 4) +# Phase 4 soft-add: 'fusion_shipping', 'fusion_canada_post' for mail-in repairs (M4) +# Soft-call (no depend) at runtime: 'fusion.api.service' via try/except per fusion-api-integration rule +# NOTE: fusion_authorizer_portal transitively pulls fusion_claims — accepted for portal reuse +``` + +Before coding any Odoo 19 view/JS, read reference files from local OrbStack Docker per project rules. + +--- + +## Data model + +### 1. Configurable intake (answers the "what questions to ask" problem) + +| Model | Purpose | +|-------|---------| +| `fusion.repair.product.category` | Medical categories: Hospital Bed, Wheelchair, Stairlift, Porch Lift, Walker, Mattress, Rollator, Other | +| `fusion.repair.intake.template` | Question set per category (+ default fallback) | +| `fusion.repair.intake.question` | Question definition: type (`char`, `text`, `selection`, `boolean`, `date`), sequence, required, help text, conditional parent answer | +| `fusion.repair.intake.answer` | Stored answers; `repair_id` → `repair.order`, `question_id`, typed value fields | + +**Seed data:** Pre-load templates with category-specific question banks, e.g.: + +- **All calls:** caller name/relationship, client name, phone, address, product category, purchase date, bought from us?, serial number, issue summary, urgency, mobility/safety concern, access instructions (stairs, parking) +- **Stairlift/Porch lift:** power status, error codes, stuck position, outdoor exposure +- **Hospital bed:** motor side, remote working, weight capacity concern, rails/mattress type +- **Wheelchair/Rollator/Walker:** brake issue, tire/tube, frame damage, battery (power chairs) + +### 2. `repair.order` extensions ([`models/repair_order.py`](fusion_repairs/models/repair_order.py)) + +Key fields on top of standard Odoo fields: + +| Field | Purpose | +|-------|---------| +| `x_fc_intake_template_id` | Template used | +| `x_fc_intake_completed` | Wizard finished | +| `x_fc_intake_user_id` | CS agent who took the call | +| `x_fc_intake_session_id` | Groups multiple repair orders from the same call | +| `x_fc_original_sale_order_id` | Original purchase SO (auto-match by partner + serial/lot) | +| `x_fc_third_party_equipment` | Boolean; True if equipment not sold by us | +| `x_fc_warranty_override_reason` | Free text when CS forces warranty status | +| `x_fc_technician_task_ids` / `x_fc_technician_task_count` | Field service link | +| `x_fc_maintenance_contract_id` | If spawned from maintenance schedule | +| `x_fc_service_catalog_id` | Matched repair type for auto-pricing | +| `x_fc_urgency` | normal / urgent / safety | +| `x_fc_issue_category` | symptom classification for automation | +| `x_fc_estimated_duration` | hours (from catalog or task type defaults) | +| `x_fc_estimated_cost` | $ from catalog at intake (pre-visit) | +| `x_fc_actual_cost` | $ from visit report (post-visit) | +| `x_fc_cost_variance_pct` | Computed: (actual − estimated)/estimated | +| `x_fc_requires_requote` | True if variance crosses threshold | +| `x_fc_intake_answer_ids` | One2many answers | +| `x_fc_photo_ids` | Many2many `ir.attachment` — call photos / videos | +| `x_fc_ai_summary` | Optional AI-generated tech brief (Phase 2) | + +**Auto-link original SO:** On wizard submit, search confirmed/done SOs for `partner_id` + `lot_id`/`product_id`; if found, set `x_fc_original_sale_order_id`. + +**Warranty determination:** Auto-tick `under_warranty` when: +- `x_fc_third_party_equipment == False` AND +- `x_fc_original_sale_order_id.commitment_date` (or first delivery) exists AND +- `today <= delivery_date + product.x_fc_warranty_months` + +CS can override and must enter `x_fc_warranty_override_reason`. + +### 3. Service catalogue & pricing ([`models/service_catalog.py`](fusion_repairs/models/service_catalog.py)) + +| Model | Purpose | +|-------|---------| +| `fusion.repair.service.catalog` | Named repair/maintenance type (e.g. "Stairlift motor replacement", "Bed motor troubleshoot") | +| Fields | `product_category_id`, `symptom_keywords`, `default_product_id` (service product), `estimated_hours`, `default_parts_product_ids`, `pricelist_id`, `auto_schedule` bool, `task_type` | + +**Pricelist:** Dedicated "Repairs & Maintenance" pricelist in [`data/pricelist_data.xml`](fusion_repairs/data/pricelist_data.xml). Service products are `type='service'`, invoiced on SO lines created from repair operations. + +**Automation flow:** Intake answers → keyword/category match → set `x_fc_service_catalog_id` → pre-fill `repair.order` operations + estimated duration → reduce CS typing. + +### 4. Maintenance scheduling ([`models/maintenance_contract.py`](fusion_repairs/models/maintenance_contract.py)) + +| Model | Purpose | +|-------|---------| +| `fusion.repair.maintenance.contract` | One per sold unit (partner + product + lot/serial) | +| Fields | `original_sale_order_id`, `product_id`, `lot_id`, `interval_months`, `next_due_date`, `last_service_date`, `state`, `booking_token` | +| Cron | Daily: find contracts due within N days → send reminder email | +| On task complete | Roll `next_due_date` forward by interval (pattern from [`fusion_plating_bridge_maintenance/models/maintenance_request.py`](fusion_plating/fusion_plating_bridge_maintenance/models/maintenance_request.py) `_maybe_schedule_from_last`) | + +**Trigger creation:** Extend `sale.order` — when a delivered SO line contains a product with `x_fc_maintenance_interval_months > 0`, auto-create contract. + +### 5. Additional models from feature catalogue + +| Model | Purpose | Feature ID | +|-------|---------|------------| +| `fusion.repair.warranty.coverage` | Our 30/90-day warranty on completed repair work; auto-detects re-do calls | M6 | +| `fusion.repair.script.template` | Canned CS scripts per issue category | C4 | +| `fusion.repair.inspection.certificate` | Annual safety inspection certs for stairlifts/porch lifts | M1 | +| `fusion.repair.service.plan` | Pre-paid service contracts (N visits/year) | M5 | +| `fusion.repair.shop.intake` | Mail-in/shop-repair workflow state machine | M4 | + +**Partner extensions ([`models/res_partner.py`](fusion_repairs/models/res_partner.py)):** + +- `x_fc_preferred_tech_id` → `res.users` +- `x_fc_preferred_window` (selection: morning / afternoon / evening) +- `x_fc_access_notes` (text — "dog in yard", "use side gate", etc.) +- `x_fc_repair_count`, `x_fc_open_balance` (computed for sidebar) + +**User extensions ([`models/res_users.py`](fusion_repairs/models/res_users.py)) — reuses existing fusion_tasks technician definition:** + +Existing in [`fusion_tasks/models/res_users.py`](fusion_tasks/models/res_users.py) (reused, do NOT recreate): +- `x_fc_is_field_staff` (Boolean) — the technician flag. fusion_repairs uses this same domain `[('x_fc_is_field_staff', '=', True)]` on every tech selector. +- `x_fc_start_address` — route-planning start location (reused for D1 map view) +- `x_fc_tech_sync_id` — cross-instance technician identifier +- Group [`fusion_tasks.group_field_technician`](fusion_tasks/security/security.xml) — reused as-is; fusion_repairs adds new ir.rule entries against this group for `repair.order` access + +New fields added by fusion_repairs: +- `x_fc_repair_skills` Many2many → `fusion.repair.product.category` (D2 — skills matrix; dispatcher filters candidate techs) +- `x_fc_tech_cost_rate` Monetary (M9 margin calc) +- `x_fc_on_call` Boolean (eligible for on-call rotation) +- `x_fc_on_call_priority` Integer (lower = paged first when combined weekend-safety path triggers) +- `x_fc_on_call_phone` Char (override — falls back to `partner_id.mobile` then `partner_id.phone`) + +**Multi-technician on a repair visit:** reuses the `technician_id` + `additional_technician_ids` Many2many pattern already on `fusion.technician.task` ([line 146](fusion_tasks/models/technician_task.py)). Two-tech installs (heavy equipment, stairlifts) just add a second tech. + +**Future expansion (Phase 4):** dedicated `fusion.repair.on.call.shift` model with date range + user for proper shift scheduling. Phase 2 ships with simple `x_fc_on_call_priority`-sorted lookup so it works immediately. + +### 6. Cross-module links + +**[`models/technician_task.py`](fusion_repairs/models/technician_task.py)** — extend `fusion.technician.task`: + +- `x_fc_repair_order_id` → `repair.order` +- On task create from repair: copy partner, address, `task_type='repair'|'maintenance'`, `duration_hours` from catalog +- On task completion: open visit report wizard (parts used, labour, upsell lines) + +**[`models/sale_order.py`](fusion_repairs/models/sale_order.py)** — extend `sale.order`: + +- `x_fc_repair_order_ids` (repair orders linked to this SO as original purchase) +- `x_fc_repair_billing_order_ids` (upsell/repair SOs spawned from repairs on this unit) +- Count fields + `action_view_*` smart buttons (pattern from [`fusion_claims/views/sale_order_views.xml`](fusion_claims/views/sale_order_views.xml)) + +**[`models/product_template.py`](fusion_repairs/models/product_template.py):** + +- `x_fc_repair_category_id`, `x_fc_maintenance_interval_months`, `x_fc_intake_template_id`, `x_fc_warranty_months` (default 12) + +--- + +## Backend intake wizard (Phase 1 — your chosen UX) + +**Model:** `fusion.repair.intake.wizard` (TransientModel) + +**Menu action:** Repairs → **New Service Call** (opens wizard, not blank repair form) + +| Step | Content | +|------|---------| +| 1 — Caller & client | **Phone-first lookup** (E.164 normalized), shows recent repairs/SOs inline. Buttons: "Use This Partner" / "Create New Contact". Service address with Google autocomplete (reuse [`fusion_ltc_management`](fusion_ltc_management) / [`fusion_schedule`](fusion_schedule) pattern). | +| 2 — Equipment | Product category, product, serial/lot lookup. Toggle "Purchased from us?" — if Yes, auto-search original SO; if No, set `x_fc_third_party_equipment=True`. **"+ Add Another Equipment"** button loops steps 2-3 for multi-unit calls. | +| 3 — Guided questions + photos | Dynamic fields rendered from `fusion.repair.intake.template`; conditional show/hide. Drag-drop photo upload area. | +| 4 — Triage | Urgency (normal / urgent / safety), preferred date/window, safety flags, internal notes. | +| 5 — Review & submit | Summary of all equipment items + answers; CS sees estimated cost from catalog match → confirms. | + +**On submit actions:** + +1. Create one `repair.order` per equipment item (sharing `x_fc_intake_session_id`) +2. Store all intake answers + attach photos +3. Match `fusion.repair.service.catalog` by category + symptom keywords → set `x_fc_service_catalog_id`, `x_fc_estimated_duration`, `x_fc_estimated_cost`, pre-fill repair operations +4. Determine warranty per logic above +5. If third-party: auto-add catalogue "Service Call-Out Fee" line +6. Create activities (see "Activities & follow-ups" section below) +7. Auto-create draft `fusion.technician.task` when `catalog.auto_schedule=True` OR `urgency='safety'` +8. **Phase 2:** call `fusion.api.service.call_openai()` to generate `x_fc_ai_summary` for technician brief (try/fallback per [fusion-api-integration rule](.cursor/rules/fusion-api-integration.mdc)) +9. Send emails: client confirmation + office CC via `_email_build()` (same styling as [`fusion_claims/data/mail_template_data.xml`](fusion_claims/data/mail_template_data.xml): 4px accent bar, 600px max-width, dark/light safe) + +--- + +## Activities & follow-ups (gap 1) + +Created automatically on intake submit: + +| Activity | Target | Deadline | Trigger | +|----------|--------|----------|---------| +| **CS callback** | Intake user | +24 h | Always — confirms call back to client if anything was missing | +| **Tech dispatch** | Office notification recipients | +4 h (safety) / +24 h (urgent) / +48 h (normal) | Until a `fusion.technician.task` is created | +| **Visit follow-up** | Assigned technician | +24 h after `scheduled_date` | Until repair moves to `done` / `cancel` | +| **Manager review** (third-party only) | Manager group | +24 h | Informational only — equipment we didn't sell | + +All activities use Fusion-standard `mail.activity.type` records; new types added in [`data/mail_activity_type_data.xml`](fusion_repairs/data/mail_activity_type_data.xml). + +--- + +## Email & reminders + +**Automated (programmatic):** Inherit `fusion.email.builder.mixin` on `repair.order` (via mixin inheritance pattern used in [`fusion_claims/models/technician_task.py`](fusion_claims/models/technician_task.py)). + +| Event | Recipients | +|-------|------------| +| Intake submitted | Client + office CC (`res.company.x_fc_office_notification_ids` pattern) | +| Technician scheduled/rescheduled | Client + technician | +| Maintenance due (30/7/1 day) | Client with booking link | +| Visit complete / invoice ready | Client | +| Payment receipt | Client (via Poynt auto-email) | + +**Manual send templates:** [`data/mail_template_data.xml`](fusion_repairs/data/mail_template_data.xml) — repair quotation, maintenance appointment confirmation, invoice (copy structure from fusion_claims ADP templates with Repairs branding colours). + +**Settings toggle:** `fusion_repairs.enable_email_notifications` in `res.config.settings` (same pattern as fusion_claims). + +### Office follow-up cron chain (gap 2) + +Four `ir.cron` jobs in [`data/ir_cron_data.xml`](fusion_repairs/data/ir_cron_data.xml), all sending to `res.company.x_fc_office_notification_ids`: + +| Cron | Frequency | Trigger | Email | +|------|-----------|---------|-------| +| **Maintenance booking unconverted** | Daily | Reminder sent ≥5 days ago + no booking made | Office digest: "X clients haven't booked maintenance" | +| **Repair without technician** | Daily | `repair.order.state='draft'` + age >7 days + no task | Office digest: "X repairs waiting for tech assignment" | +| **Overdue technician visit** | Hourly | Task `scheduled_date` >24 h ago + not completed | Office alert per overdue task | +| **Unpaid repair invoice** | Daily | Repair invoice posted >14 days + unpaid | Office digest with Poynt re-send link | + +Crons are config-toggleable via `ir.config_parameter` (`fusion_repairs.followup_*_enabled`). + +--- + +## Technician visit → billing → payment (Phase 2) + +### Visit report wizard + +**`fusion.repair.visit.report.wizard`** opened from completed technician task or repair order: + +- Labour hours (default from catalog, editable) +- Parts/consumables used (adds repair operations of type `add` — natively creates stock moves) +- Recommended upsell products (optional SO lines) +- Apply repairs pricelist (auto-selected from catalogue) +- Computes `x_fc_actual_cost` and `x_fc_cost_variance_pct` vs `x_fc_estimated_cost` + +### Pricing reconciliation (gap 4) + +Settings: +- `fusion_repairs.variance_threshold_pct` (default 20) +- `fusion_repairs.variance_threshold_amount` (default $100) + +When `actual_cost` exceeds `estimated_cost` by either threshold: + +1. Set `x_fc_requires_requote = True` +2. Block automatic invoicing +3. Create activity on manager + office notification recipients: "Re-quote needed — variance $X (Y%)" +4. Send `mail.template` requote email to client showing new estimate; CTA approve link `/repairs/requote/` → on approval, invoicing unlocks + +If variance under threshold → invoice proceeds automatically. + +### Billing chain (Odoo 19 native + Fusion) + +``` +repair.order → action_create_sale_order() → sale.order (repair billing) + → confirm → invoice → action_open_poynt_payment_wizard() +``` + +Reuse [`fusion_poynt`](fusion_poynt) without reimplementing payment logic. Add **Collect Payment** button on repair form when linked invoice is posted and unpaid. + +**Original SO traceability:** Repair billing SO gets `x_fc_source_sale_order_id` = original purchase SO (existing fusion_claims invoice-link pattern). + +### Auto-scheduling logic (gap 3) + +**Phase 1 (simple):** When `catalog.auto_schedule=True` or `urgency='safety'`: +- Create `fusion.technician.task` in `draft` state with `priority='high'` +- No date/technician assigned — dispatcher confirms in the task kanban +- Office notification email lists draft tasks needing dispatch + +**Phase 3 (smart):** Add optional `auto_assign` setting; when on: +- Find candidate technicians with relevant `task_type` skills +- Use existing `fusion_tasks` travel/buffer logic to find next available slot within urgency deadline +- Create task as `scheduled` with technician + date/time +- If no slot found in window, fall back to draft + escalate + +--- + +## Client self-service portal (Phase 1-3 — public AI-assisted intake) + +**Goal:** A client visits `/repair` (from voicemail, website, QR sticker, or email link), answers a short guided flow with AI self-check assistance, and either fixes the problem themselves (free for the company, builds goodwill, upsell opportunity) or files a properly-triaged repair request that lands in the same queue as backend/sales-rep intake. + +**Why this matters:** Weekend / after-hours coverage gap. Without this, clients call, hear voicemail, wait for Monday. With this, many weekend issues get self-resolved or properly queued by Monday morning with photos + symptoms + tried-steps already attached. + +### Architecture + +```mermaid +flowchart TD + Entry["Entry points:\n- /repair (voicemail URL)\n- QR sticker on equipment\n- SMS resume link\n- Website 'Need Repair?' button"] + Entry --> Landing["/repair landing\nLarge 'Start' button\nAfter-hours banner"] + Landing --> Phone["/repair/start\nPhone-first lookup\n(skippable)"] + Phone --> Equip["/repair/equipment\nCategory + product + serial\nQR-prefilled if applicable"] + Equip --> Symptoms["/repair/symptoms\nGuided questions from intake.template"] + Symptoms --> Photos["/repair/photos\nOptional camera capture"] + Photos --> AICheck["/repair/self-check\nAI suggests 1-3 safe steps\nPhase 2+"] + AICheck --> Worked{"Did it fix it?"} + Worked -->|"Yes"| Upsell["/repair/upsell\nMaintenance plan, warranty,\nparts \u2014 'avoid this next time'"] + Worked -->|"No"| EscalateFlow["/repair/escalate\nShow estimated cost\nfrom catalogue match"] + EscalateFlow --> Verify["/repair/verify\nSMS code\nPhase 2+"] + Verify --> Submit["POST /repair/submit\nCreates repair.order via shared service"] + Upsell --> Survey["Resolution survey\n+ Google review CTA"] + Survey --> ThanksA["/repair/thanks (resolved)"] + Submit --> ThanksB["/repair/thanks (escalated)\nETA + reference number"] +``` + +### Auth + security model + +| Concern | Approach | +|---------|----------| +| Authentication | **Public** (`auth='public'`) — no login required. Voicemail-friendly. | +| Identity verification | Optional phone lookup; **mandatory SMS code (CL12)** before final submit (Phase 2+). Phase 1 ships without SMS verify (CAPTCHA-only). | +| Spam | reCAPTCHA v3 invisible challenge + honeypot field + per-IP rate limit (`/repair/submit` capped at 10/hour) | +| CSRF | All POST routes use Odoo CSRF token (same as fusion_ltc_management public form) | +| Privacy | Inline consent checkbox; PHI not stored beyond intake answers; data retention policy in privacy notice | +| Cross-client data leak | Phone lookup returns ONLY the matched partner's name/address mask (no other PII); never returns other partners | +| AI prompt injection | Strict system prompt; user input sandboxed; output schema-validated; AI never sees other clients' data | +| AI cost runaway | `fusion.api.service` budget caps; fallback to deterministic self-check rules if AI unavailable | +| Bot abuse on AI | AI step gated behind CAPTCHA pass + valid phone format check | + +### Controllers ([`controllers/portal_client_repair.py`](fusion_repairs/controllers/portal_client_repair.py)) + +| Route | Type | Auth | Purpose | +|-------|------|------|---------| +| `/repair` | `http` | `public` | Landing page, after-hours banner, "Start" CTA | +| `/repair/start` | `http` | `public` | Phone lookup form (skippable) | +| `/repair/lookup_phone` | `jsonrpc` | `public` | E.164 normalize + safe partner match (returns only name/address) | +| `/repair/equipment` | `http` | `public` | Category/product/serial selection — accepts `?sn=` for QR pre-fill | +| `/repair/symptoms` | `http` | `public` | Guided question flow from intake template | +| `/repair/photos` | `http` POST | `public` | Multipart upload to staged attachments | +| `/repair/self_check` | `jsonrpc` | `public` | Calls `fusion.repair.ai.service.suggest_self_check(payload)` (Phase 2+) | +| `/repair/upsell` | `jsonrpc` | `public` | Returns ranked upsell suggestions | +| `/repair/buy/` | `http` | `public` | Redirect to `/shop/product/` or fast-checkout | +| `/repair/verify` | `http` POST | `public` | Send SMS code via RC; verify on submit | +| `/repair/submit` | `http` POST CSRF | `public` | Calls shared `fusion.repair.intake.service.create_repair_orders(payload, source='client_portal')` | +| `/repair/resume/` | `http` | `public` | Resume saved session token | +| `/repair/thanks` | `http` | `public` | Confirmation with reference number + tracking URL | + +Each route applies a `_check_rate_limit()` decorator + `_check_captcha()` for sensitive endpoints. + +### AI self-check (CL6/CL7) — strict architecture + +**Service:** `fusion.repair.ai.service` (AbstractModel) — single entry point so prompts and guardrails live in one place. + +**Method signature:** + +```python +def suggest_self_check(self, payload): + """ + Args payload: { + 'product_category': str, + 'product_id': int|None, + 'symptoms': list[str], + 'intake_answers': list[dict], + } + Returns: { + 'steps': [{'instruction': str, 'expected_result': str, 'safety_note': str|None}], + 'escalate_immediately': bool, # if AI thinks unsafe DIY + 'escalation_reason': str|None, + 'confidence': 'high'|'medium'|'low', + } + """ +``` + +**System prompt enforces:** + +- NEVER provide medical advice +- NEVER claim definitive diagnosis +- ONLY suggest safe, reversible self-checks (unplug, restart, check connections, replace battery, clean sensors) +- If product = stairlift / porch lift AND symptom contains motor/safety keywords → return `escalate_immediately=True` +- Maximum 3 steps; each step ≤1 sentence +- If unsure, escalate + +**Output is JSON-schema-validated** server-side — if AI returns malformed or unsafe content (contains "diagnose", "you have", "medical condition", "stop using"), drop it and fall back to deterministic rules. + +**Deterministic fallback:** [`data/self_check_data.xml`](fusion_repairs/data/self_check_data.xml) seeds `fusion.repair.self.check.rule` records keyed by `(product_category, symptom_keyword)` → static steps. Used when AI fails, exceeds budget, or returns unsafe output. + +**Per the `fusion-api-integration.mdc` rule:** uses try/fallback pattern with `consumer='fusion_repairs', feature='client_self_triage'`. No `fusion_api` hard dependency. + +### Upsell engine (CL9) + +**Model:** `fusion.repair.upsell.rule` + +| Field | Purpose | +|-------|---------| +| `name` | "Recommend extended warranty when out of warranty" | +| `condition_domain` | Domain expression evaluated against the intake context | +| `product_ids` | Many2many → `product.template` | +| `priority` | Higher = shown first | +| `pitch` | Short HTML — why this product matters | +| `active` | Toggle | + +**Phase 2 seed rules:** + +- Out of warranty + product category = stairlift → extended warranty + annual inspection (M1) +- Self-resolved → maintenance plan (M5) +- Repair scheduled → loaner equipment (M3) +- Battery-related symptom → replacement battery part +- Mattress / consumable symptom → replacement consumable + +**Ranking:** AI scores rules against intake (Phase 2 enhancement) — `fusion.repair.ai.service.rank_upsells(rules, payload)` returns top 4 with personalized pitch text. + +**Conversion tracking:** `fusion.repair.upsell.suggestion` model logs every offered/clicked/converted suggestion → feeds the M7 analytics dashboard later. + +### Weekend safety escalation (combined approach, locked in) + +When client portal `urgency='safety'` is submitted outside business hours (per `resource.calendar` of the company): + +```mermaid +flowchart TD + Submit["Client submits safety repair after hours"] + Submit --> Disclaimer["UI shows: 'If anyone is hurt, call 911 immediately'"] + Disclaimer --> Queue["repair.order created with priority=high\nactivity for office: 'call client first thing Monday'"] + Queue --> Page["Page next on-call manager"] + Page --> Lookup["res.users.search([\n ('x_fc_on_call', '=', True),\n ('x_fc_is_field_staff', '=', True) OR ('groups_id', 'in', repairs_manager_group)\n], order='x_fc_on_call_priority')"] + Lookup --> Send["Send SMS + email to first available"] + Send --> Ack["Manager taps acknowledge URL\nlogs to chatter\nfalls through to next priority if no ack in 15 min"] + Ack --> Followup["Office digest Monday 7AM: all weekend safety cases"] +``` + +**Models / helpers:** +- `res.users.x_fc_on_call`, `x_fc_on_call_priority`, `x_fc_on_call_phone` (defined above) +- `fusion.repair.on.call.service` (AbstractModel) — `find_next_on_call()` and `page_on_call(repair_id)` helpers +- Tokenized URL `/repair/on-call/ack/` for manager acknowledgement +- Cron escalates to next priority after 15 min without acknowledgement; alerts office partners on full exhaustion +- All actions logged to repair chatter for audit + +**Phase 4 enhancement:** replace the priority-int with a proper `fusion.repair.on.call.shift` model (date range + user) so rotations can be scheduled weeks ahead. Phase 2 ships with priority-int because it works immediately for small teams. + +### Voicemail integration (CL16, Phase 3) + +Two-tier: + +| Tier | Implementation | +|------|----------------| +| **Tier 1 — static recording** | Office records voicemail greeting once: "We're closed. For faster help, visit our website at fusionrepairs dot com slash repair. For emergencies, leave a message." Done outside the module. | +| **Tier 2 — dynamic RC API greeting** | Cron updates RC voicemail greeting based on business hours from `resource.calendar`. After-hours greeting includes the URL + reference. Requires fusion_ringcentral. Optional. | + +### Data model additions + +| Model | Purpose | +|-------|---------| +| `fusion.repair.self.check.rule` | Deterministic self-check steps (CL6 fallback) | +| `fusion.repair.upsell.rule` | Upsell catalogue rules (CL9) | +| `fusion.repair.upsell.suggestion` | What was offered / clicked / converted per repair (analytics) | +| `fusion.repair.intake.session` | Token-based saved session for resume + cross-route state | +| `fusion.repair.ai.service` | AbstractModel — single guardrailed AI entry point | + +**New `repair.order` fields:** +- `x_fc_self_resolved` (boolean) +- `x_fc_self_check_attempts` (text JSON — what AI suggested, what client did) +- `x_fc_upsell_suggestion_ids` (One2many) + +**New `x_fc_intake_source` values:** add `'client_portal'` to the existing selection (`backend_wizard` / `sales_rep_portal` / `client_portal`). + +### Templates ([`views/portal_client_repair_templates.xml`](fusion_repairs/views/portal_client_repair_templates.xml)) + +Single-column mobile-first layout; calls `website.layout`. Progress dots at top. Sticky bottom CTA. No sidebars. Large fonts, plain language, no jargon. Empty-state friendly (works without product selection). + +Themes adapt via project SCSS rules — no hardcoded colours per CLAUDE.md. + +### What the client CAN'T do (intentional limits per your "limit the information" requirement) + +- Browse other clients' equipment +- See diagnostic terminology or part numbers (only friendly names) +- See pricing variance / margin / cost breakdowns +- Cancel / reschedule via this form (must call or log in to portal) +- Chat with the AI freely (NOT chat-style; structured one-shot per step) +- See technician names / phone numbers +- Modify a submitted repair (read-only after submit) + +### Phase placement + +- **Phase 1:** CL1, CL2, CL3, CL4, CL5, CL11, CL13, CL14 — basic public form, no AI yet. Goes live with `/repair` URL ready for voicemail mention. +- **Phase 2:** CL6, CL7, CL8, CL9, CL12, CL15, CL20 — AI self-check, upsell engine, SMS verify, after-hours messaging, resolution survey. +- **Phase 3:** CL10, CL16, CL17 — direct-buy parts via website_sale or Poynt, voicemail RC integration, QR sticker generator. +- **Phase 4:** CL18, CL19 — knowledge base videos, voice input transcription. + +--- + +## Sales rep portal (Phase 1 — mirrors fusion_authorizer_portal) + +**Goal:** A sales rep on the road takes a client call and submits a repair request from their phone — same intake flow as backend CS wizard, no Odoo login screen. + +### Dependency strategy + +| Option | Recommendation | +|--------|----------------| +| **Hard depend on `fusion_authorizer_portal`** | RECOMMENDED — reuses the existing `is_sales_rep_portal` flag, `group_sales_rep_portal`, sales rep dashboard scaffolding. Transitively pulls fusion_claims (already core in your stack). | +| Soft depend (try/except + own fallback flag) | Possible but doubles the code: own `is_sales_rep_portal` mirror + own group. Only worth it if you ever want fusion_repairs standalone. | + +We go with hard depend. Add `fusion_authorizer_portal` to the manifest `depends` list. + +### Architecture + +```mermaid +flowchart LR + SR["Sales Rep on phone with client"] --> Portal["/my/repair/new\nMobile-friendly intake form"] + Portal --> ReuseQ["Same intake.template + questions\nas backend wizard"] + ReuseQ --> Submit["POST /my/repair/submit"] + Submit --> Repair["repair.order created\nx_fc_intake_user_id = SR\nx_fc_intake_source = 'sales_rep_portal'"] + Repair --> SameFlow["Same activities, emails,\ntech dispatch as backend intake"] + Repair --> SRView["/my/repairs list\n/my/repair/ detail"] + SRView --> Status["Read-only timeline + chatter comment"] +``` + +### Controller layout ([`controllers/portal_sales_rep_repair.py`](fusion_repairs/controllers/portal_sales_rep_repair.py)) + +Routes scoped to `is_sales_rep_portal` users (gate at controller top, pattern from [`fusion_authorizer_portal/controllers/portal_assessment.py`](fusion_authorizer_portal/controllers/portal_assessment.py) line 25): + +| Route | Type | Purpose | +|-------|------|---------| +| `/my/repair/new` | `http` `auth='user'` | Render mobile-friendly intake form | +| `/my/repair/lookup_partner` | `jsonrpc` | Phone-first partner search (reuses backend service) | +| `/my/repair/client_history` | `jsonrpc` | C2 sidebar data — last repairs, balance, warranty | +| `/my/repair/match_catalog` | `jsonrpc` | Live catalog match while sales rep types symptoms | +| `/my/repair/upload_photos` | `http` POST | Photo / video upload from phone camera | +| `/my/repair/submit` | `http` POST CSRF | Validate + create `repair.order(s)` + answers + activities | +| `/my/repairs` | `http` `auth='user'` | Sales rep's list of submitted repairs (paged) | +| `/my/repair/` | `http` `auth='user'` | Detail view with timeline + chatter comment box | +| `/my/repair//comment` | `http` POST | Add a follow-up note (S5) | + +All routes use `_check_sales_rep_access()` helper that 302-redirects non-sales-rep portal users to `/my`. + +### Service layer reuse + +The backend wizard's submit logic lives in a service method on the `fusion.repair.intake.template` model (or a dedicated `fusion.repair.intake.service` AbstractModel) — both backend wizard AND portal controller call the same `create_repair_orders(payload)` method. This guarantees identical behaviour: + +- One `repair.order` per equipment +- Same activities, emails, AI summary, warranty determination, catalogue match, third-party flag, dispatch task + +Avoids the trap of two intake flows drifting out of sync. + +### Templates ([`views/portal_sales_rep_templates.xml`](fusion_repairs/views/portal_sales_rep_templates.xml)) + +QWeb templates following [`fusion_authorizer_portal/views/portal_assessment_express.xml`](fusion_authorizer_portal/views/portal_assessment_express.xml) style: + +- `portal_repair_intake_form` — multi-step (accordion or stepper) with same 5 sections as backend wizard +- `portal_repair_list` — card list with status badge, scheduled date, tech name +- `portal_repair_detail` — timeline + chatter +- `portal_repair_intake_thanks` — confirmation page with "Submit Another" button (common on multi-call days) + +Reuses portal gradient/header style via `portal_gradient` template variable already set by [`portal_main.home()`](fusion_authorizer_portal/controllers/portal_main.py) line 85. + +### JS ([`static/src/js/portal_repair_intake.js`](fusion_repairs/static/src/js/portal_repair_intake.js)) + +Per [`environment-safety`](.cursor/rules/environment-safety.mdc) and project Odoo 19 rule #2: frontend JS uses the `Interaction` class from `@web/public/interaction`, registered via `registry.category("public.interactions")`. NOT IIFE/DOMContentLoaded. + +Interactions: +- `repair_intake_stepper` — handles step navigation + validation +- `repair_intake_partner_lookup` — debounced phone search → renders matched partners +- `repair_intake_history_sidebar` — fetches C2 data when partner picked +- `repair_intake_photo_capture` — direct camera capture + thumb preview +- `repair_intake_catalog_match` — live symptom → catalogue suggestion + +### What the sales rep CAN'T do (intentional scoping) + +- See repairs they didn't submit (record rule blocks) +- Edit / cancel a submitted repair (must call office) +- Assign a technician (dispatcher's job) +- Adjust pricing (office) +- Mark a repair `done` (technician's job) + +This keeps the portal a safe single-purpose tool — submit a request, add notes, see status. + +### Mobile UX SCSS + +[`static/src/scss/portal_repair_mobile.scss`](fusion_repairs/static/src/scss/portal_repair_mobile.scss) — tap targets ≥44px, bottom-fixed submit button, no horizontal scroll, follows `_tokens` pattern per project CLAUDE.md card styling rules. + +--- + +## Maintenance client self-booking (Phase 3) + +**Email CTA:** Signed token URL `/repairs/maintenance/book/` (controller in [`controllers/maintenance_portal.py`](fusion_repairs/controllers/maintenance_portal.py)). + +**Booking page options:** + +- **Preferred:** Integrate with [`fusion_schedule`](fusion_schedule) public booking + dedicated `appointment.type` "Equipment Maintenance" assigned to technician pool +- **Fallback:** Lightweight slot picker (date + time windows from technician availability in `fusion.technician.task`) + +**On confirm:** Create `fusion.technician.task` (`task_type='maintenance'`), link to `maintenance.contract`, send confirmation email (fusion_claims style), create calendar event if `fusion_schedule` present. + +--- + +## Smart buttons + +### On `repair.order` form + +| Button | Target | +|--------|--------| +| Technician Tasks | `fusion.technician.task` | +| Repair Sale Order | `sale_order_id` (Odoo native) | +| Original Purchase | `x_fc_original_sale_order_id` | +| Maintenance Plan | `fusion.repair.maintenance.contract` | +| Invoices | Posted invoices from repair SO | +| Intake Answers | readonly answer list | + +### On original `sale.order` form + +| Button | Icon | Target | +|--------|------|--------| +| Repairs | `fa-wrench` | `repair.order` where `x_fc_original_sale_order_id = self` | +| Maintenance | `fa-calendar-check-o` | `fusion.repair.maintenance.contract` | +| Repair Invoices | `fa-file-text-o` | Upsell/repair SOs + invoices | + +Pattern: compute count + `action_view_*` + `oe_stat_button` / `statinfo` (identical to [`fusion_claims/views/sale_order_views.xml`](fusion_claims/views/sale_order_views.xml) lines ~1176–1181). + +--- + +## UI / menus + +New app menu **Fusion Repairs**: + +- **Service Calls** → intake wizard launcher + repair order kanban/list +- **Maintenance Schedules** → contract list with upcoming due filter +- **Technician Tasks** → filtered `fusion.technician.task` (repair + maintenance) +- **Configuration** → intake templates, service catalogue, product categories, settings + +Extend repair order form view with Intake tab (answers), Maintenance tab, and stat buttons. + +--- + +## Security + +[`security/security.xml`](fusion_repairs/security/security.xml) — reuses existing fusion_tasks pattern where possible: + +**Reused (do NOT recreate):** +- [`fusion_tasks.group_field_technician`](fusion_tasks/security/security.xml) — for technician access to `repair.order` (parallel to existing tech task rules). Same domain `('technician_id', '=', user.id)` adapted as `('x_fc_technician_task_ids.technician_id', '=', user.id)` on repair orders +- [`fusion_authorizer_portal.group_sales_rep_portal`](fusion_authorizer_portal/security/portal_security.xml) — for sales rep portal access (see Sales rep portal section) + +**New groups specific to fusion_repairs:** +- `group_fusion_repairs_user` — CS intake, view repairs (implied by `base.group_user`) +- `group_fusion_repairs_dispatcher` — assign technicians, reschedule, parts pre-pull picklists +- `group_fusion_repairs_manager` — config, pricing, maintenance contracts, on-call rotation, variance overrides + +**Record rules:** +- Technicians (via `group_field_technician`): see only repairs where they are in `x_fc_technician_task_ids.all_technician_ids` for their company +- Sales reps (via `group_sales_rep_portal`): see only repairs where `x_fc_intake_user_id = user.id` +- Public client portal: `auth='public'` — no record rule (records created via sudo by controller, validated server-side) +- CS / dispatcher / manager: full access scoped by `company_id` + +--- + +## Implementation phases + +### Phase 1 — CS can take a call end-to-end + sales rep portal (MVP) + +**Goal:** A CS rep at their desk OR a sales rep on the road can submit a repair request; it lands in dispatch with reminders set. + +**Backend CS workflow:** +- Module scaffold + manifest + security + multi-company defaults +- Intake templates (seed question banks per product category) +- Shared `fusion.repair.intake.service` AbstractModel used by both wizard and portal +- 5-step backend intake wizard with: phone-first partner search (P4), client history sidebar (C2), duplicate-call detection (C1), outstanding-balance warning (C5), quote-only mode (C6), multi-equipment loop (P2), photo upload (P3), warranty auto-detect, third-party flag, AI summary (try/fallback) +- `repair.order` extensions + intake answers + `x_fc_intake_session_id` + `x_fc_intake_source` (selection: `backend_wizard` / `sales_rep_portal`) +- Auto-create draft `fusion.technician.task` when urgent/safety +- 4 activities on submit (CS callback, tech dispatch, visit follow-up, manager review) +- Automated emails (intake received, task scheduled) using `fusion.email.builder.mixin` +- Day-before reminder email (X2 email portion) +- Client preferences on `res.partner` (P1) +- Repair kanban + menu + basic security groups + +**Sales rep portal (S1-S4, S6, S8):** +- Portal controllers `/my/repair/new`, `/my/repairs`, `/my/repair/` +- Mobile-friendly QWeb templates following [`fusion_authorizer_portal/views/portal_assessment_express.xml`](fusion_authorizer_portal/views/portal_assessment_express.xml) style +- Same intake question flow as backend (via shared service layer) +- Mobile photo / camera capture +- Client history sidebar exposed in portal form +- "Service Calls" tile on existing sales rep dashboard +- Record rules scope sales rep visibility to repairs they submitted +- `Interaction`-class JS for stepper, lookups, photo capture + +**Client self-service portal (CL1-CL5, CL11, CL13, CL14):** +- Public `/repair` URL (voicemail-ready) +- Phone-first lookup (safe partner match — masks other PII) +- Equipment selection with QR pre-fill via `?sn=` +- Same guided question flow (shared service) +- Photo/camera upload +- Save & resume tokenized link + localStorage backup +- reCAPTCHA v3 + honeypot + per-IP rate limit +- Privacy notice + PHI consent checkbox +- No AI yet (Phase 2) + +### Phase 2 — Billing, mobile tech UX, repair warranty + +**Goal:** Tech can complete a visit on their phone and collect payment; the system auto-prices and tracks our own repair warranty. + +- Service catalogue + repairs pricelist + product seed data +- Catalog auto-match from intake → pre-fill operations + estimated cost +- Tech skills matrix (D2) — wizard filters candidate techs by category +- Parts pre-pull checklist (D3) — nightly cron emails picklist +- Visit report wizard with: labour hours, parts (native stock moves), upsells, **client signature pad (T4)**, "found another issue" spawn (T5), **Save & Collect Payment via Poynt terminal (T8)** +- **Maps button on mobile task form (T1)** + AI brief surfaced prominently (T2) +- Pricing reconciliation (variance → requote email + manager activity) +- `repair.order → action_create_sale_order()` → invoice → Poynt +- Smart buttons on `sale.order` and `repair.order` +- **Repair warranty tracking (M6)** — `fusion.repair.warranty.coverage`; wizard banner "covered by our 30-day warranty, no charge" when same equipment + similar issue +- AI intake summary via `fusion.api.service.call_openai()` with try/fallback (consumer='fusion_repairs') +- Sales rep portal: add a note to existing repair (S5), book maintenance on behalf of client (S7), quote-only mode in portal (S9), follow-up activities to sales rep on state change (S10) +- **Client self-service AI:** AI self-check engine with strict guardrails (CL6, CL7) + resolved/unresolved branch (CL8) + upsell engine + ranking (CL9) + **smart SMS verify (CL12 — skip if known partner)** + **combined weekend-safety escalation with on-call paging (CL15)** + resolution survey (CL20) +- `res.users` extensions: `x_fc_repair_skills`, `x_fc_tech_cost_rate`, `x_fc_on_call`, `x_fc_on_call_priority`, `x_fc_on_call_phone` +- `fusion.repair.on.call.service` AbstractModel + `/repair/on-call/ack/` acknowledgement route + escalation cron + +### Phase 3 — Maintenance lifecycle, SMS, smart scheduling + +**Goal:** Maintenance practically runs itself; client and tech communicate via SMS; rescheduling is one tap. + +- Maintenance contracts from delivered SOs + interval rolling +- Reminder crons (30/7/1 d) + tokenized client booking portal +- fusion_schedule integration for client day/time selection +- 4 office follow-up crons (maintenance unbooked, repair no-tech, overdue visit, unpaid invoice) +- Smart auto-scheduling option (D1 map view + skills-aware slot picking) +- **RC SMS integration:** + - "Tech is X min away" SMS when tech taps On-the-way (X1) + - Day-before reminder SMS (X2 SMS portion) + - Inline "Send SMS" from intake wizard (C3) + - Two-way reschedule SMS Y/N (D4) +- Self-reschedule link in confirmation email (X3) +- Post-visit NPS / Google review email (X4) +- Labour timer via fusion_clock (T3) +- Parts replaced — serial capture (T6) +- No-show photo proof (T7) +- Loaner auto-offer when repair >X days (M3) +- Sales rep portal: warranty acknowledgement signature when out of warranty (S11) +- **Client self-service Phase 3:** direct-buy parts/plans via website_sale or Poynt (CL10) + voicemail RC integration (CL16) + QR sticker generator (CL17) + +### Phase 4 — Compliance, claims, analytics, polish + +**Goal:** Long-tail features that drive revenue, reduce risk, and surface insight. + +- **Compliance inspection certificates (M1)** — `fusion.repair.inspection.certificate` + PDF cert + annual cron + client email *(candidate sister module `fusion_repair_compliance` if it grows)* +- **ADP/funder-covered repairs (M2)** — bridge to `fusion_claims` for funder-eligible repairs +- Mail-in / shop repairs workflow (M4) — return label via fusion_shipping/canada_post +- **Pre-paid service plans (M5)** — `fusion.repair.service.plan` product + burn-down counter *(candidate sister module `fusion_repair_plans`)* +- Failure rate / first-call resolution analytics dashboard (M7) +- OEM warranty claim filing (M8) +- Margin per repair (M9) +- Logged-in equipment portal `/my/equipment` (X5) +- Canned scripts per issue (C4) +- `fusion_ringcentral` "Start Service Call" button on inbound popup +- AI clarifying-question suggestions during free-text intake +- Sales rep portal: repair-aware commission view on dashboard (S12) +- **Client self-service Phase 4:** knowledge base videos / manual snippets (CL18) + voice input AI transcription (CL19) + +### Potential sister modules (if scope demands) + +These are flagged inside Phase 4 but could spin out into their own modules: + +| Module | What goes in it | When to split | +|--------|-----------------|---------------| +| `fusion_repair_compliance` | M1 inspections (annual safety certs, jurisdiction rules, regulatory reporting) | If certificate templates per jurisdiction balloon | +| `fusion_repair_plans` | M5 pre-paid plans, plan renewals, burn-down accounting | If plans + accounting integration grows | +| `fusion_repair_shop` | M4 mail-in / in-shop workflow with bench states and shop-floor UI | If in-shop technicians become a distinct user group | +| `fusion_repair_analytics` | M7, M9 dashboards + reports | If analytics need their own ETL/aggregation tables | + +Decision to split happens at the start of Phase 4, not before. + +--- + +## Key files to create + +``` +fusion_repairs/ +├── __manifest__.py +├── __init__.py +├── models/ +│ ├── repair_order.py +│ ├── intake_template.py +│ ├── intake_answer.py +│ ├── service_catalog.py +│ ├── maintenance_contract.py +│ ├── repair_warranty.py # M6 +│ ├── repair_inspection.py # M1 (Phase 4) +│ ├── repair_service_plan.py # M5 (Phase 4) +│ ├── repair_shop_intake.py # M4 (Phase 4) +│ ├── repair_script_template.py # C4 (Phase 4) +│ ├── repair_self_check_rule.py # CL6 deterministic fallback +│ ├── repair_upsell_rule.py # CL9 upsell rules + suggestions +│ ├── repair_intake_session.py # token-based resume + cross-route state +│ ├── repair_ai_service.py # AbstractModel — guardrailed AI entry point +│ ├── repair_intake_service.py # AbstractModel — shared by backend/sales-rep/client +│ ├── repair_on_call_service.py # AbstractModel — find_next_on_call + page_on_call +│ ├── sale_order.py +│ ├── technician_task.py +│ ├── product_template.py +│ ├── res_partner.py # P1 client prefs + history compute +│ ├── res_users.py # D2 skills + cost rate +│ ├── res_company.py # office notification recipients +│ └── res_config_settings.py +├── wizard/ +│ ├── repair_intake_wizard.py +│ ├── repair_intake_wizard_views.xml +│ ├── repair_visit_report_wizard.py +│ └── repair_reschedule_wizard.py # D4 +├── views/ +│ ├── repair_order_views.xml +│ ├── sale_order_views.xml +│ ├── maintenance_contract_views.xml +│ ├── intake_template_views.xml +│ ├── service_catalog_views.xml +│ ├── repair_warranty_views.xml +│ ├── repair_inspection_views.xml +│ ├── repair_service_plan_views.xml +│ ├── res_partner_views.xml +│ ├── res_users_views.xml +│ ├── technician_task_views.xml # mobile additions (Maps button, AI brief, timer) +│ ├── repair_dashboard_views.xml # M7 (Phase 4) +│ ├── portal_sales_rep_templates.xml # S1-S10 QWeb templates +│ ├── portal_client_repair_templates.xml # CL1-CL20 public client form +│ └── menus.xml +├── controllers/ +│ ├── portal_client_repair.py # CL1-CL20 public /repair self-service +│ ├── portal_sales_rep_repair.py # S1-S10 sales rep intake + list + detail +│ ├── maintenance_portal.py +│ ├── client_reschedule.py # X3 +│ ├── nps_survey.py # X4 +│ ├── equipment_portal.py # X5 (Phase 4) +│ └── requote_portal.py # pricing variance approval +├── report/ +│ ├── repair_visit_summary.xml +│ ├── repair_inspection_certificate.xml # M1 +│ └── oem_warranty_claim.xml # M8 +├── static/src/ +│ ├── js/signature_pad.js # T4 +│ ├── js/parts_picker.js # D3 +│ ├── js/portal_repair_intake.js # S1 Interaction-class stepper + lookups +│ ├── js/portal_client_repair.js # CL1-CL10 public client form Interaction class +│ ├── js/portal_client_self_check.js # CL6 AI step renderer +│ ├── js/portal_client_upsell.js # CL9 upsell card renderer +│ ├── components/history_sidebar.js # C2 (backend OWL) +│ └── scss/ +│ ├── repairs_mobile.scss # tech mobile UX +│ ├── portal_repair_mobile.scss # sales rep portal mobile +│ └── portal_client_repair.scss # CL1-CL20 client portal mobile-first +├── data/ +│ ├── intake_template_data.xml +│ ├── service_catalog_data.xml +│ ├── pricelist_data.xml +│ ├── mail_template_data.xml +│ ├── sms_template_data.xml # X1/X2/D4 (Phase 3) +│ ├── mail_activity_type_data.xml +│ ├── ir_cron_data.xml +│ ├── ir_config_parameter_data.xml +│ ├── ir_sequence_data.xml +│ ├── repair_script_data.xml # C4 +│ ├── repair_inspection_data.xml # M1 +│ ├── self_check_data.xml # CL6 deterministic self-check rules (30 seeded — Appendix D) +│ ├── upsell_rule_data.xml # CL9 seed upsell rules (15 seeded — Appendix B) +│ ├── ai_prompts_data.xml # CL7 system prompt + schema in ir.config_parameter (Appendix A) +│ └── voicemail_scripts_data.xml # CL16 voicemail script text in ir.config_parameter (Appendix C) +└── security/ + ├── security.xml + └── ir.model.access.csv +``` + +--- + +## Verification (local OrbStack) + +After implementation, test on local dev only: + +1. `docker exec odoo-dev-app odoo -d fusion-dev -i fusion_repairs --stop-after-init` +2. Install/enable Odoo **Repairs** app if not already installed +3. Run backend intake wizard for each product category — confirm repair order + answers + task +4. **Sales rep portal:** log in as a `is_sales_rep_portal` user → `/my/repair/new` → submit same scenarios → assert identical `repair.order`, activities, emails as backend wizard +5. **Sales rep isolation:** log in as second sales rep → `/my/repairs` should NOT show first rep's repairs +6. **Client public portal:** in incognito (no login) → `/repair` → submit same scenarios → assert identical `repair.order`, activities, emails as backend wizard but with `x_fc_intake_source='client_portal'` +7. **AI guardrails:** submit stairlift + motor + safety symptom → assert `escalate_immediately=True` returned and AI step skipped +8. **AI cost/fallback:** disable `fusion.api.service` → assert client form falls back to deterministic self-check rules without error +9. **Phone lookup safety:** look up an existing client's phone → assert response contains only masked name/address, never other PII +10. **Rate limit:** submit 11 client forms from same IP in an hour → assert 11th is blocked with friendly error +11. Complete technician task → visit report → SO → invoice → Poynt wizard opens +12. Deliver a product with maintenance interval → contract created → cron sends reminder → booking link works +13. Confirm smart buttons on original SO show linked repairs/maintenance/invoices +14. Verify emails render correctly in light and dark mode +15. Test all three portals on a real phone (iOS Safari + Android Chrome) — photo capture, tap targets, stepper flow +16. QR sticker scan test: print a sticker → scan on phone → confirm `/repair?sn=` pre-fills equipment +17. **Smart SMS verify:** submit client form from known partner phone → assert SMS skipped; submit from new phone → assert 6-digit SMS required and gate enforced +18. **Weekend safety escalation:** submit safety repair at 11 PM Saturday → assert (a) 911 disclaimer rendered, (b) repair created with `priority=high` + Monday-followup activity, (c) on-call manager (lowest `x_fc_on_call_priority`) received SMS + email +19. **On-call escalation:** mock no-acknowledgement → after 15 min cron → assert next-priority manager paged; full exhaustion → office partners notified +20. **Technician filter:** create a stairlift repair → assert tech dropdown shows only users with `x_fc_is_field_staff=True` AND `x_fc_repair_skills` containing the stairlift category + +--- + +## Risks & mitigations + +| Risk | Mitigation | +|------|------------| +| Odoo 19 `repair.order` API differs from docs | Read model/views from Docker before extending; verify `action_create_sale_order` in local instance | +| `fusion_schedule` dependency chain is heavy | Phase 3 only; start with token booking controller, add schedule integration when stable | +| Overlap with `fusion_ltc_management` | Clear separation: LTC = facilities; fusion_repairs = home/retail medical equipment | +| Scope creep | Strict phase gates; Phase 1 must be usable by CS before any Phase 2 work begins. Optional sister-module split at Phase 4 boundary | +| AI calls fail / exceed budget | Try/fallback per fusion-api-integration rule; `x_fc_ai_summary` is optional, never blocks intake submit | +| Variance threshold spam | Defaults conservative; per-company override; can disable per-catalogue entry (e.g. parts-heavy repairs) | +| Multi-equipment loop confusion | Wizard shows running list of added equipment with edit/remove before final submit | +| Caller mismatch (wrong partner picked) | Phone-search shows last 5 repairs/SOs + address inline before confirm | +| **Scope size from accepting all features** | Phase gating is non-negotiable; each phase must ship before next starts; Phase 4 items earmarked for sister-module split if any one balloons | +| SMS deliverability + cost (RC) | Phase 3 only; per-event opt-out on `res.partner.x_fc_sms_opt_out`; rate-limit via cron not real-time burst | +| Signature pad cross-browser issues | Use proven OWL library; fallback to typed-name acknowledgement | +| Inspection certificate jurisdictional rules | Start with one jurisdiction (Ontario); model `x_fc_jurisdiction` field; add others on demand | +| Repair warranty mis-detection (false "free re-do") | Manager approval required for warranty re-do override; clear audit trail in chatter | +| AI hallucinates an unsafe step that passes keyword filter | All AI output ends with mandatory legal disclaimer; manager-reviewable AI incident log captures rejected + accepted outputs for ongoing prompt refinement | +| Backend wizard and sales rep portal drift apart | Both call the same `fusion.repair.intake.service.create_repair_orders(payload)` AbstractModel method; no duplicate business logic | +| Sales rep accidentally sees other reps' repairs | Record rule `('x_fc_intake_user_id', '=', user.id)` scoped to `base.group_portal`; integration test asserts cross-rep isolation | +| Portal form abandoned mid-flow on call drop | Save partial state to `localStorage` keyed by partner + timestamp; "Resume" prompt on `/my/repair/new` if recent draft exists | +| fusion_authorizer_portal install becomes mandatory | Documented in module description; if a deployment doesn't want fusion_authorizer_portal, fall back to a `fusion_repairs_portal_lite` companion module that recreates only the `is_sales_rep_portal` flag | +| **Public form spam / abuse** | reCAPTCHA v3 + honeypot + per-IP rate limit + per-phone rate limit + SMS verify before submit (Phase 2). Block ASN ranges via Odoo's `ir.rule` if needed | +| **AI giving unsafe medical advice** | Strict system prompt + JSON schema validation + keyword filter (rejects "diagnose", "you have", "stop using"); falls back to deterministic rules on any malformed/unsafe output; legal disclaimer "this is not medical advice" shown on every AI step | +| **AI cost runaway from public traffic** | Hard daily/monthly budget cap via `fusion.api.service`; CAPTCHA gates AI calls; cache results for identical symptom-category pairs; deterministic fallback never costs anything | +| **PHI exposure via public form** | Inline consent before sensitive questions; data retention policy in privacy notice; PHI-flagged fields encrypted at rest (Phase 4 enhancement) | +| **Client uses public form when logged in already** | Detect logged-in client → offer "use your client portal instead" with one-click; still allow public flow if they prefer | +| **Phone lookup leaks partner data** | Lookup returns only `{matched: bool, name: , address: }`; never the full partner record. Logged for audit | +| **Voicemail URL drift** | Static recording mentions canonical `/repair`; Phase 3 dynamic update uses `ir.config_parameter` for URL so any rename is one-click | +| **Backend/sales-rep/client intake drift apart (three surfaces now)** | All three call `fusion.repair.intake.service.create_repair_orders(payload, source=...)` exclusively; CI test asserts identical outputs for identical payloads across all three sources | + +--- + +## Appendix A — AI system prompt + JSON schema (CL6 / CL7) + +### System prompt (verbatim, lives in [`data/ai_prompts_data.xml`](fusion_repairs/data/ai_prompts_data.xml) as `ir.config_parameter`) + +> You are a triage assistant for Fusion Repairs, a Canadian medical equipment service company. Your ONLY job is to suggest 1–3 safe, reversible self-check steps a client can try on their medical equipment before scheduling a technician visit. +> +> ABSOLUTE RULES: +> +> 1. NEVER provide medical advice, diagnoses, or health recommendations. +> 2. NEVER claim a definitive cause for the problem. +> 3. NEVER recommend stopping use of medical equipment. +> 4. NEVER use phrases like "you have", "I diagnose", "you should stop", "medical condition", "consult your doctor". +> 5. ONLY suggest steps that are: safe, reversible, require no tools, take under 2 minutes, and pose zero risk to the client or equipment. +> 6. If symptoms involve smoke, sparks, burning smell, motors on stairlifts/porch lifts, OR if you are uncertain → return `escalate_immediately: true`. +> 7. Maximum 3 steps. Each step ≤ 1 sentence. Grade-6 reading level. No technical jargon. +> 8. NEVER reference part numbers, prices, or other clients. +> 9. If client reports injury, equipment fire, or person trapped → `escalate_immediately: true` with `escalation_reason: "emergency"`. +> 10. You MUST output valid JSON matching the provided schema. No prose, no markdown, no commentary. + +### JSON schema (server-side validated; lives in [`models/repair_ai_service.py`](fusion_repairs/models/repair_ai_service.py)) + +```json +{ + "type": "object", + "required": ["escalate_immediately", "confidence", "steps"], + "additionalProperties": false, + "properties": { + "escalate_immediately": {"type": "boolean"}, + "escalation_reason": {"type": ["string", "null"], "maxLength": 200}, + "confidence": {"enum": ["high", "medium", "low"]}, + "steps": { + "type": "array", + "maxItems": 3, + "items": { + "type": "object", + "required": ["instruction", "expected_result"], + "additionalProperties": false, + "properties": { + "instruction": {"type": "string", "maxLength": 200}, + "expected_result": {"type": "string", "maxLength": 200}, + "safety_note": {"type": ["string", "null"], "maxLength": 200} + } + } + } + } +} +``` + +### Post-validation filters (Python, after AI response) + +1. **Schema check** — drop response if doesn't match schema +2. **Forbidden-phrase regex** — drop if `instruction|expected_result|safety_note` matches any of: + - `\b(diagnose|diagnosis|diagnosed)\b` + - `\byou have\b` + - `\bmedical condition\b` + - `\bstop using\b` + - `\bconsult (your|a) (doctor|physician)\b` + - `\b(\$|CAD|USD)\s?\d+` (price mention) +3. **Coherence check** — if `escalate_immediately=false` AND `steps` is empty → drop +4. **Cost cap** — if response tokens > 500 → drop (defends against runaway prompt injection) +5. **Cache hit check** — same `(product_category, symptom_hash)` returns cached result for 24 h +6. **On any drop** → fall back to deterministic `fusion.repair.self.check.rule` records + log incident to `fusion.repair.ai.incident` for manager review + +### Always-appended UI disclaimer + +Below every AI step rendered to the client: + +> *This is not medical advice. If you're unsure, schedule a technician visit. In an emergency, call 9-1-1.* + +--- + +## Appendix B — Seed upsell rules (CL9) + +Loaded in [`data/upsell_rule_data.xml`](fusion_repairs/data/upsell_rule_data.xml). Each row = one `fusion.repair.upsell.rule` record. + +| # | Condition | Product / suggestion | Pitch | Priority | +|---|-----------|---------------------|-------|----------| +| 1 | Self-resolved on any product | Annual Maintenance Plan (M5) | "Avoid this in the future — annual professional inspection keeps small issues from becoming repairs" | 100 | +| 2 | Product = stairlift OR porch lift AND no active warranty | Extended Warranty + Annual Safety Certificate (M1) | "Many municipalities require annual safety certification for accessibility lifts — we handle it" | 95 | +| 3 | Product = hospital bed AND `delivery_date` > 3 years ago | Replacement mattress (medical-grade) | "Medical mattresses lose support after 3–5 years — protect skin integrity with a fresh surface" | 90 | +| 4 | Symptom keyword matches `battery\|charge\|won't turn on` | Replacement battery (correct model lookup) | "Genuine replacement battery — installed during your visit, no extra trip" | 85 | +| 5 | Symptom keyword matches `remote\|controller\|button` | Spare remote control | "A spare remote on hand saves a service call when one wears out" | 80 | +| 6 | Repair scheduled AND `estimated_duration` > 4 h AND product is mobility-critical | Loaner equipment (M3) | "Don't be without equipment — borrow ours while we repair yours" | 95 | +| 7 | Product in (wheelchair, rollator, walker) AND symptom keyword `brake\|stop` | Brake service kit | "Brake pads and cables wear out — keep a spare kit ready" | 70 | +| 8 | Product = walker AND symptom keyword `wobble\|wear\|drag` | Replacement tips/glides (4-pack) | "Tips wear out — extend walker life and prevent slips" | 70 | +| 9 | Product = air mattress AND symptom keyword `leak\|deflate\|soft` | Replacement air pump | "Spare pump means no overnight surprise if yours fails" | 75 | +| 10 | Product = stairlift AND `delivery_date` > 5 years ago | Upgrade quote — newer model | "Today's stairlifts are quieter, more energy efficient, and offer better safety features" | 60 | +| 11 | Product = porch lift AND any symptom (outdoor exposure) | Annual weatherization service | "Outdoor lifts need yearly weather protection to avoid corrosion damage" | 75 | +| 12 | Product = power wheelchair AND symptom matches `charge\|battery\|won't turn on` | Replacement charger | "Faulty chargers slowly damage batteries — replacing now extends battery life" | 80 | +| 13 | Product = hospital bed AND symptom keyword `rail\|side` | Padded rail covers (consumable) | "Padded rail covers reduce skin injury risk and bruising" | 65 | +| 14 | Partner has ≥ 2 repairs in past 12 months | Service Contract / pre-paid plan (M5) | "You've had multiple visits — our service plan locks in predictable monthly cost" | 90 | +| 15 | Self-resolved AND product `delivery_date` > 5 years ago | Replacement / upgrade quote | "Equipment over 5 years often costs more to repair than to replace — let's compare" | 50 | + +**Display rule:** show top 4 by priority that pass the rule's `condition_domain`. AI ranking (Phase 2) re-orders based on intake context — e.g. if client mentioned tight budget in notes, deprioritize upgrade quote. + +--- + +## Appendix C — Voicemail scripts (CL16) + +Stored as `ir.config_parameter` keys so the office can edit without code changes. Two tiers: + +### Tier 1 — static recording (office records once) + +**Daytime / business hours greeting:** + +> Hi, you've reached Fusion Repairs. We're on the line with another client right now. Please leave your name, phone number, and a brief description of your equipment issue and we'll call you back within two hours. For faster service, visit our website at **fusionrepairs dot com slash repair** to start your repair request online — our system can also help you with simple troubleshooting. Thank you. + +**After-hours greeting (evenings, weekends, holidays):** + +> Hi, you've reached Fusion Repairs. Our office is currently closed. For non-urgent repairs, please visit our website at **fusionrepairs dot com slash repair** to submit your request — our online system can also walk you through simple troubleshooting that may solve your problem tonight. We'll respond first thing on the next business day. If anyone is hurt or in immediate danger, please hang up and dial nine-one-one. Thank you. + +**Emergency / on-call routing prompt (press 1):** + +> If this is a safety emergency where someone is stuck on a stairlift or porch lift and unable to move, please press 1 now to be routed to our on-call technician. For all other repairs, please visit fusionrepairs dot com slash repair to submit your request online and we will respond on the next business day. + +### Tier 2 — dynamic RC API update (Phase 3, optional) + +Cron `_update_rc_voicemail_greeting_cron` runs at the start/end of business hours per `resource.calendar`: + +- 9:00 AM Mon-Fri → upload daytime greeting via `fusion_ringcentral` API +- 6:00 PM Mon-Fri and at any time Sat/Sun/holidays → upload after-hours greeting +- Reads scripts from `ir.config_parameter` → renders via TTS service (RC native or 11Labs) → uploads to RC +- Office can override per `res.company` + +Falls back silently if RC API unreachable — static recording remains live. + +--- + +## Appendix D — Seed deterministic self-check rules (CL6 fallback) + +Loaded in [`data/self_check_data.xml`](fusion_repairs/data/self_check_data.xml). Each row = one `fusion.repair.self.check.rule` record keyed by `(product_category, symptom_keyword)`. + +### Hospital bed + +| Symptom keyword | Self-check instruction | Expected result | +|-----------------|------------------------|------------------| +| `won't move\|dead\|no power` | "Check the bed is plugged in and the outlet has power — try plugging a phone charger into the same outlet to confirm" | Bed responds when controls pressed | +| `slow\|sluggish` | "Unplug the bed for 30 seconds then plug it back in" | Movement returns to normal speed | +| `remote\|controller` | "Replace the remote batteries with fresh AAA batteries" | Remote lights and bed responds | +| `one section\|won't lift\|stuck` | "Check nothing is caught under the bed or jamming the mechanism (sheets, blankets, cords)" | Section moves freely | +| `beep\|alarm\|alert` | "Check both side rails are fully locked in the raised position" | Alarm stops | + +### Wheelchair — manual + +| Symptom keyword | Self-check instruction | Expected result | +|-----------------|------------------------|------------------| +| `wobble\|loose wheel` | "Try turning the axle nut gently by hand to feel if it's snug" | Wheel feels firm, no play | +| `brake\|stop` | "Push the brake lever fully to the locked position and listen for a click" | Brake holds wheel firmly | +| `footrest\|footplate` | "Slide the footrest fully into its housing until you hear a click" | Footrest feels secure | +| `hard to push\|drags` | "Check both tires for full inflation (firm to thumb pressure)" | Wheelchair rolls freely | + +### Wheelchair — power + +| Symptom keyword | Self-check instruction | Expected result | +|-----------------|------------------------|------------------| +| `won't turn on\|dead` | "Confirm the battery indicator shows charge and the key switch is in the ON position" | Display lights up | +| `error code\|flashing` | "Note the error code shown on the joystick display, then turn off and on after 30 seconds" | Error clears or specific code logged | +| `one side weaker\|pulls` | "Charge the batteries fully overnight before testing again" | Both sides equal power after charge | +| `smoke\|burning\|spark` | **ESCALATE IMMEDIATELY — no self-check** | — | + +### Stairlift + +| Symptom keyword | Self-check instruction | Expected result | +|-----------------|------------------------|------------------| +| `won't move\|stuck` | "Check the seat is fully rotated to the forward position and the seatbelt is fastened" | Stairlift responds | +| `stops midway\|halts` | "Check the track for items blocking the sensors — toys, slippers, debris" | Stairlift completes travel | +| `beep\|alarm` | "Confirm the seat swivel lock is engaged in the down position" | Beeping stops | +| `burning smell\|smoke\|person stuck` | **ESCALATE IMMEDIATELY — no self-check** | — | +| `remote\|call station` | "Replace the remote / call-station batteries with fresh batteries" | Call station responds | + +### Porch lift + +| Symptom keyword | Self-check instruction | Expected result | +|-----------------|------------------------|------------------| +| `won't move\|dead` | "Check all gate and door safety switches are fully closed" | Lift responds | +| `sticky\|stuck buttons` | "If outdoors, gently wipe controls with a dry cloth and let dry" | Controls respond | +| `smoke\|burning smell\|stuck` | **ESCALATE IMMEDIATELY — no self-check** | — | +| `won't stop\|overshoots` | "Note exactly which floor it stops at — do not attempt repeat use" | Information captured for technician | + +### Walker / rollator + +| Symptom keyword | Self-check instruction | Expected result | +|-----------------|------------------------|------------------| +| `wheel stick\|won't roll` | "Check for hair or debris wrapped around the wheel axle" | Wheel spins freely | +| `brake won't lock` | "Push the brake lever fully down until you feel a click" | Brake holds | +| `wobble\|loose` | "Check all height adjustment pins are fully engaged through both holes" | Frame feels solid | +| `seat loose` (rollator) | "Tighten the seat knobs by hand until firm" | Seat feels secure | + +### Medical mattress (air / alternating pressure) + +| Symptom keyword | Self-check instruction | Expected result | +|-----------------|------------------------|------------------| +| `deflated\|flat\|soft` | "Confirm the pump is plugged in, powered on, and the hose is firmly attached" | Mattress inflates | +| `hiss\|leak` | "Listen at the valve — push the valve cap in firmly to ensure it's sealed" | Hissing stops | +| `alarm\|beep` | "Check the pump display for the error code shown, then restart the pump by unplugging for 30 seconds" | Alarm clears | +| `cold\|won't heat` (heated mattress) | "Confirm the heat dial is set above zero and allow 15 minutes to warm" | Mattress feels warm | + +**Total:** 30 deterministic rules across 7 categories. Used when AI fails, exceeds budget, or returns unsafe content. Also used in Phase 1 before AI is enabled. + +**Each rule has** an optional `safety_note` field — when populated, displayed in red below the instruction (e.g. for any "smoke/burning" rule the note says "Do not attempt — call us or 911 immediately").