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>
88 KiB
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):- Backend wizard for CS on phone calls
- Sales rep mobile portal (
/my/repair/new) for reps on the road - Public AI-assisted client portal (
/repair) with self-check + upsell engine — voicemail-ready
- Field service dispatch via existing
fusion.technician.task(reusesx_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/is an empty folder — no__manifest__.py, models, or views yet.- No existing code in the repo extends Odoo's
repairapp. - Closest precedents:
fusion_ltc_management/models/ltc_repair.py— repair workflow + SO + technician task (LTC facilities only; keep separate)fusion_tasks/models/technician_task.py— field service scheduling withtask_typeincludingrepair/maintenancefusion_claims/models/sale_order.py+fusion_claims/views/sale_order_views.xml— smart buttons + automated emails viafusion_tasks/models/email_builder_mixin.pyfusion_poynt/models/sale_order.py—action_poynt_collect_payment()after invoicefusion_schedule/— public appointment booking (reuse in Phase 3 for maintenance self-scheduling)
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_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)
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 == FalseANDx_fc_original_sale_order_id.commitment_date(or first delivery) exists ANDtoday <= 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_id→res.usersx_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 forrepair.orderaccess
New fields added by fusion_repairs:
x_fc_repair_skillsMany2many →fusion.repair.product.category(D2 — skills matrix; dispatcher filters candidate techs)x_fc_tech_cost_rateMonetary (M9 margin calc)x_fc_on_callBoolean (eligible for on-call rotation)x_fc_on_call_priorityInteger (lower = paged first when combined weekend-safety path triggers)x_fc_on_call_phoneChar (override — falls back topartner_id.mobilethenpartner_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.
6. Cross-module links
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_hoursfrom 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 fromfusion_claims/views/sale_order_views.xml)
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:
- Create one
repair.orderper equipment item (sharingx_fc_intake_session_id) - Store all intake answers + attach photos
- Match
fusion.repair.service.catalogby category + symptom keywords → setx_fc_service_catalog_id,x_fc_estimated_duration,x_fc_estimated_cost, pre-fill repair operations - Determine warranty per logic above
- If third-party: auto-add catalogue "Service Call-Out Fee" line
- Create activities (see "Activities & follow-ups" section below)
- Auto-create draft
fusion.technician.taskwhencatalog.auto_schedule=TrueORurgency='safety' - Phase 2: call
fusion.api.service.call_openai()to generatex_fc_ai_summaryfor technician brief (try/fallback per fusion-api-integration rule) - Send emails: client confirmation + office CC via
_email_build()(same styling asfusion_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 | |
|---|---|---|---|
| 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_costandx_fc_cost_variance_pctvsx_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:
- Set
x_fc_requires_requote = True - Block automatic invoicing
- Create activity on manager + office notification recipients: "Re-quote needed — variance $X (Y%)"
- Send
mail.templaterequote 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.taskindraftstate withpriority='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_typeskills - Use existing
fusion_taskstravel/buffer logic to find next available slot within urgency deadline - Create task as
scheduledwith 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()andpage_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
/repairURL 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.orderper 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 wizardportal_repair_list— card list with status badge, scheduled date, tech nameportal_repair_detail— timeline + chatterportal_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 + validationrepair_intake_partner_lookup— debounced phone search → renders matched partnersrepair_intake_history_sidebar— fetches C2 data when partner pickedrepair_intake_photo_capture— direct camera capture + thumb previewrepair_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_schedulepublic booking + dedicatedappointment.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 ~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 — reuses existing fusion_tasks pattern where possible:
Reused (do NOT recreate):
fusion_tasks.group_field_technician— for technician access torepair.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 ordersfusion_authorizer_portal.group_sales_rep_portal— 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 bybase.group_user)group_fusion_repairs_dispatcher— assign technicians, reschedule, parts pre-pull picklistsgroup_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 inx_fc_technician_task_ids.all_technician_idsfor their company - Sales reps (via
group_sales_rep_portal): see only repairs wherex_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.serviceAbstractModel 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.orderextensions + intake answers +x_fc_intake_session_id+x_fc_intake_source(selection:backend_wizard/sales_rep_portal)- Auto-create draft
fusion.technician.taskwhen 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.xmlstyle - 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
/repairURL (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.orderandrepair.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.usersextensions:x_fc_repair_skills,x_fc_tech_cost_rate,x_fc_on_call,x_fc_on_call_priority,x_fc_on_call_phonefusion.repair.on.call.serviceAbstractModel +/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 modulefusion_repair_complianceif it grows) - ADP/funder-covered repairs (M2) — bridge to
fusion_claimsfor funder-eligible repairs - Mail-in / shop repairs workflow (M4) — return label via fusion_shipping/canada_post
- Pre-paid service plans (M5) —
fusion.repair.service.planproduct + burn-down counter (candidate sister modulefusion_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:
docker exec odoo-dev-app odoo -d fusion-dev -i fusion_repairs --stop-after-init- Install/enable Odoo Repairs app if not already installed
- Run backend intake wizard for each product category — confirm repair order + answers + task
- Sales rep portal: log in as a
is_sales_rep_portaluser →/my/repair/new→ submit same scenarios → assert identicalrepair.order, activities, emails as backend wizard - Sales rep isolation: log in as second sales rep →
/my/repairsshould NOT show first rep's repairs - Client public portal: in incognito (no login) →
/repair→ submit same scenarios → assert identicalrepair.order, activities, emails as backend wizard but withx_fc_intake_source='client_portal' - AI guardrails: submit stairlift + motor + safety symptom → assert
escalate_immediately=Truereturned and AI step skipped - AI cost/fallback: disable
fusion.api.service→ assert client form falls back to deterministic self-check rules without error - Phone lookup safety: look up an existing client's phone → assert response contains only masked name/address, never other PII
- Rate limit: submit 11 client forms from same IP in an hour → assert 11th is blocked with friendly error
- Complete technician task → visit report → SO → invoice → Poynt wizard opens
- Deliver a product with maintenance interval → contract created → cron sends reminder → booking link works
- Confirm smart buttons on original SO show linked repairs/maintenance/invoices
- Verify emails render correctly in light and dark mode
- Test all three portals on a real phone (iOS Safari + Android Chrome) — photo capture, tap targets, stepper flow
- QR sticker scan test: print a sticker → scan on phone → confirm
/repair?sn=<serial>pre-fills equipment - 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
- 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 (lowestx_fc_on_call_priority) received SMS + email - On-call escalation: mock no-acknowledgement → after 15 min cron → assert next-priority manager paged; full exhaustion → office partners notified
- Technician filter: create a stairlift repair → assert tech dropdown shows only users with
x_fc_is_field_staff=TrueANDx_fc_repair_skillscontaining 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 1–3 safe, reversible self-check steps a client can try on their medical equipment before scheduling a technician visit.
ABSOLUTE RULES:
- NEVER provide medical advice, diagnoses, or health recommendations.
- NEVER claim a definitive cause for the problem.
- NEVER recommend stopping use of medical equipment.
- NEVER use phrases like "you have", "I diagnose", "you should stop", "medical condition", "consult your doctor".
- ONLY suggest steps that are: safe, reversible, require no tools, take under 2 minutes, and pose zero risk to the client or equipment.
- If symptoms involve smoke, sparks, burning smell, motors on stairlifts/porch lifts, OR if you are uncertain → return
escalate_immediately: true.- Maximum 3 steps. Each step ≤ 1 sentence. Grade-6 reading level. No technical jargon.
- NEVER reference part numbers, prices, or other clients.
- If client reports injury, equipment fire, or person trapped →
escalate_immediately: truewithescalation_reason: "emergency".- 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)
- Schema check — drop response if doesn't match schema
- Forbidden-phrase regex — drop if
instruction|expected_result|safety_notematches 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)
- Coherence check — if
escalate_immediately=falseANDstepsis empty → drop - Cost cap — if response tokens > 500 → drop (defends against runaway prompt injection)
- Cache hit check — same
(product_category, symptom_hash)returns cached result for 24 h - On any drop → fall back to deterministic
fusion.repair.self.check.rulerecords + log incident tofusion.repair.ai.incidentfor 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 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_ringcentralAPI - 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").