Files
Odoo-Modules/docs/superpowers/specs/2026-05-20-fusion-repairs-design.md
gsinghpal 79fbfec61f 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 <cursoragent@cursor.com>
2026-05-20 21:22:01 -04:00

88 KiB
Raw Blame History

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 + paymentrepair.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 + activitiesfusion.email.builder.mixin (consistent with fusion_claims styling)

Built incrementally across 4 phases; each phase ships a usable slice.

Current state

Architecture decision (confirmed)

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 (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
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)
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: 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; 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_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=<serial>
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 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/<id> — 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 line 129 pattern):

<record id="rule_repair_order_sales_rep_portal" model="ir.rule">
    <field name="name">Sales Rep Portal: Repairs they submitted</field>
    <field name="model_id" ref="repair.model_repair_order"/>
    <field name="domain_force">[('x_fc_intake_user_id', '=', user.id)]</field>
    <field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>

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.
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 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

'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_idrepair.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)

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)

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. 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)

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 _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):

  • x_fc_preferred_tech_idres.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) — reuses existing fusion_tasks technician definition:

Existing in 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 — 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). 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.

models/technician_task.py — extend fusion.technician.task:

  • x_fc_repair_order_idrepair.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 — 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)

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_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)
  9. Send emails: client confirmation + office CC via _email_build() (same styling as 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.


Email & reminders

Automated (programmatic): Inherit fusion.email.builder.mixin on repair.order (via mixin inheritance pattern used in 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 — 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, 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/<token> → 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 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

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)

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=<serial> 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/<int:product_id> http public Redirect to /shop/product/<slug> 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/<token> 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:

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 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):

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/<token> 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)

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

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/<id> detail"]
    SRView --> Status["Read-only timeline + chatter comment"]

Controller layout (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 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/<int:repair_id> http auth='user' Detail view with timeline + chatter comment box
/my/repair/<int:repair_id>/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)

QWeb templates following 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() line 85.

JS (static/src/js/portal_repair_intake.js)

Per environment-safety 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 — 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/<token> (controller in controllers/maintenance_portal.py).

Booking page options:

  • Preferred: Integrate with 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 lines ~11761181).


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 — reuses existing fusion_tasks pattern where possible:

Reused (do NOT recreate):

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/<id>
  • Mobile-friendly QWeb templates following 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=<serial>
  • 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/<token> 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=<serial> 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: <first name + last initial>, address: <masked>}; 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 as ir.config_parameter)

You are a triage assistant for Fusion Repairs, a Canadian medical equipment service company. Your ONLY job is to suggest 13 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)

{
  "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. 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 35 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. 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").