Files
Odoo-Modules/docs/plans/fusion_maintenance_brainstorm.md
gsinghpal 139e917e09 docs(fusion_maintenance): record Step 0 live-grounding results from Westin prod
Ran Step 0 against Westin prod (westin-v19 on odoo-westin). Resolved the APP/DB placeholders (DO boxes dead; migrated on-prem to odoo-dev-app), added a dated STEP 0 RESULTS section, and corrected the open questions the live inspection disproved: no stair/porch lifts in Westin ADP data; Enterprise appointment already ships native token booking; fusion_repairs contract engine not deployed; device_type is the ADP billing-code catalog taxonomy, not the install base.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 00:22:51 -04:00

12 KiB

fusion_maintenance — Brainstorm & Handoff Brief

Status: research/brainstorm only — no code, no final decisions. Written from a Claude Code web session that could not reach the private network (no Tailscale, no docker daemon, Supabase KB unreachable). Resume from a Tailscale-connected env (dev box or a host that can reach Westin production) and do the live inspection in Step 0 before committing to the design.

Goal (user's words, paraphrased)

Automated maintenance follow-ups for mobility/accessibility equipment we've sold, to turn service into recurring revenue. Reminder emails → client books maintenance → booking happens in real time and lands in our calendar. Leverage Odoo Enterprise's appointment system. Decide whether this lives in fusion_repairs or a new module — the result must be seamless and production-ready.

Decisions locked with the user (this session)

  • Same DB: fusion_claims + fusion_repairs run on one database → new module may depend on both.
  • Enterprise appointment is available → build real-time booking ON it (appointment.type / appointment.slot / calendar.event), do not hand-roll a calendar.
  • Public self-serve booking → reminder email carries a token link to a no-login slot picker (extend the existing /repairs/maintenance/book/<token> pattern). Elderly clients shouldn't log in.
  • Target box for grounding = Westin production (where fusion_claims runs day-to-day).

Key findings from repo exploration

fusion_repairs (v19.0.2.2.6) ALREADY has a maintenance engine — reuse it, don't fork

  • fusion.repair.maintenance.contract: interval, due/last-service dates, state machine. Auto-spawned on SO confirm when product.template.x_fc_maintenance_interval_months > 0.
  • Daily reminder cron cron_maintenance_due_reminders → 30/7/1-day bands → branded email email_template_maintenance_due_reminder with tokenized link /repairs/maintenance/book/<token>.
  • Booking controller: controllers/portal_maintenance_booking.pysingle date-confirm form, NO slot availability, NO conflict check, NO calendar event. ← this is the real gap.
  • Contract roll-forward on technician-task completion (next_due_date += interval).
  • fusion.repair.service.plan.subscription: pre-paid visit plans (recurring-revenue primitive).
  • Deps: repair, maintenance, sale_management, stock, purchase, website, portal, fusion_tasks, fusion_poynt, fusion_authorizer_portal. ~8.3k LOC, 25+ models.

fusion_claims (v19.0.9.2.0) is the ideal trigger source

  • Claim container = sale.order (x_fc_sale_type: adp, odsp, wsib, insurance, march_of_dimes, …).
  • Equipment unit = sale.order.line.x_fc_serial_number + product_id.
  • Equipment category = fusion.adp.device.code.device_type (wheelchair, walker, hospital bed, stair lift, porch lift, custom ramp, …) — matches the user's "sale groups".
  • Schedule anchors: x_fc_adp_delivery_date, x_fc_service_start_date; gate on x_fc_adp_approved.
  • Customer = sale.order.partner_id; prescriber = x_fc_authorizer_id.
  • Already depends on calendar, fusion_tasks, ai, fusion_ringcentral.

Proposed architecture (PENDING live verification)

New module fusion_maintenance depending on fusion_repairs, fusion_claims, appointment. Reuses the existing contract/reminder/roll-forward engine; adds the 3 genuinely-missing pieces:

  1. fusion.maintenance.policy (ops-configurable, no code per category): device_typeinterval_months, reminder bands, service_product_id (priced visit), appointment_type_id, required technician skill. Turns "stair lift = 6 mo, $X" into data.
  2. Claims bridge (daily cron): scan fusion_claims sale.order.line for delivered+approved devices whose device_type matches an active policy → ensure a maintenance contract exists, anchored at delivery_date + interval. Idempotent (key on serial / sale-line). Extend the reused contract with x_fc_source_claim_line_id, x_fc_device_type, x_fc_policy_id so the repairs path and claims path both feed one contract model.
  3. Real-time booking on appointment: token link → slot picker backed by appointment.type (partner pre-resolved from token, no login). Slot pick → real calendar.event → hook spawns repair.order + technician task, assigns by skill/zone, advances reminder band, rolls contract forward.

Recurring revenue: each policy carries service_product_id → booked visit drafts a priced SO/invoice; optional pre-paid annual plan via existing service.plan.subscription; optional door payment via existing fusion_poynt.

STEP 0 — run on Westin production FIRST (grounding before any decision)

Replace APP/DB with the real Westin container + database. CLAUDE.md rule #1: never code from memory — read the real Enterprise appointment source before building the booking layer.

# RESOLVED 2026-06-02 — Westin Odoo prod migrated OFF Digital Ocean onto the on-prem Proxmox
# cluster. Old DO IPs (152.42.146.204 / 178.128.229.92) are DEAD (:22 timeout). Live box:
# host `odoo-westin` = 192.168.1.40 via the `supabase-prod` Tailscale jump (Windows OpenSSH
# ProxyCommand → run `ssh odoo-westin ...` from PowerShell). App container `odoo-dev-app`
# (odoo:19, Enterprise); DB container `odoo-dev-db`; DB `westin-v19`; user `odoo` (local-socket
# trust inside odoo-dev-db). Enterprise addons → /mnt/enterprise-addons, custom → /mnt/extra-addons.
#   SQL:      ssh odoo-westin 'docker exec odoo-dev-db  psql -U odoo -d westin-v19 -c "..."'
#   FS read:  ssh odoo-westin 'docker exec odoo-dev-app sed -n 1,160p /mnt/enterprise-addons/...'
APP=odoo-dev-app ; DB=westin-v19 ; DBC=odoo-dev-db

# 1) Install matrix — confirm same-DB + Enterprise appointment present + versions
docker exec "$APP" psql -U odoo -d "$DB" -c \
"SELECT name,state,latest_version FROM ir_module_module \
 WHERE name IN ('fusion_claims','fusion_repairs','fusion_maintenance','calendar','maintenance','repair') \
    OR name LIKE 'appointment%' ORDER BY name;"

# 2) Real device_type distribution (drives per-category policies)
docker exec "$APP" psql -U odoo -d "$DB" -c \
"SELECT device_type, count(*) FROM fusion_adp_device_code GROUP BY device_type ORDER BY 2 DESC;"

# 3) Locate the Enterprise appointment source (read, don't guess the API)
docker exec "$APP" bash -lc 'ls -d /mnt/enterprise-addons/appointment 2>/dev/null || \
  find / -maxdepth 6 -type d -name appointment 2>/dev/null | grep -i addons | head'

# 4) Appointment model surface to build booking on (adjust path from #3)
docker exec "$APP" cat <appointment_path>/models/appointment_type.py | head -160
docker exec "$APP" ls <appointment_path>/controllers/    # find the public booking controller

# 5) How fusion_repairs maintenance contracts already look in live data
docker exec "$APP" psql -U odoo -d "$DB" -c \
"SELECT state, count(*) FROM fusion_repair_maintenance_contract GROUP BY state;"

STEP 0 — RESULTS (ran 2026-06-02 against Westin prod westin-v19)

Grounding facts only — no design decisions made. These correct several assumptions above.

Connection (resolved): host odoo-westin (192.168.1.40) via the supabase-prod Tailscale jump. App container odoo-dev-app (odoo:19, Enterprise), DB container odoo-dev-db, DB westin-v19, user odoo. Old Digital Ocean boxes are DEAD — Westin migrated on-prem.

1) Install matrixappointment 19.0.1.3 installed (+ appointment_account_payment, _crm, _hr, _microsoft_calendar, _sms). All deps present: calendar, maintenance, repair, sale_management, portal, website, resource, phone_validation, web_gantt. fusion_claims 19.0.9.2.0 installed. fusion_repairs and fusion_maintenance are absent entirely (no records). → a module depending on appointment installs cleanly; "reuse the fusion_repairs engine" means deploy fusion_repairs to Westin first (heavy) or own a lean contract model here. Note Odoo's native maintenance (CMMS) is installed — an under-considered third reuse option.

2) device_type — 119 distinct values, but fusion.adp.device.code is the ADP billing-code CATALOG (_order='device_type, device_code'), so counts are catalog codes per type, NOT units installed. Top entries are seating COMPONENTS (Seat Cushion 564, Back Support 375, Headrest 193). The maintainable equipment classes ≈ wheelchairs (manual + power tilt), power bases, power scooters, wheeled walkers / walking frames, paediatric standing frames, specialty strollers (~6-8 clean categories). → device_type can't be a 1:1 policy key (119 values, mostly parts); needs a grouping/whitelist. Real install base must be sized on sale.order.line (x_fc_adp_device_type [computed from product's x_fc_adp_device_code_id.device_type], x_fc_serial_number, x_fc_adp_approved; delivery dates x_fc_adp_delivery_date / x_fc_service_start_date) — NOT yet run; this is the next grounding step.

3) + 4) Enterprise appointment source/mnt/enterprise-addons/appointment. The no-login token slot-picker is mostly NATIVE — don't hand-roll it: public booking (auth="public"), invite tokens (appointment.invite, /appointment/<id>?…invite_token), live availability (/appointment/<id>/update_available_slots, jsonrpc/public), slot submit → real calendar.event (/appointment/<id>/submit), auto/manual staff+resource assignment, capacity, booked/cancelled mail templates. Model appointment.type; controller controllers/appointment.py. → the module mainly needs to: seed an appointment.type per category, drop a partner-bound invite link into the reminder email, and hook calendar.event create → spawn the service task + advance the contract. appointment_account_payment is installed → native pay-to-book is on the table for the revenue mechanic.

5) Maintenance-contract staterelation "fusion_repair_maintenance_contract" does not exist → confirms the fusion_repairs maintenance engine is not on Westin.

Headline correction: Westin's ADP data has zero stair lifts / porch lifts / ramps / hospital beds — those belong to the fusion_repairs / EN-Tech (mobility) domain. Westin's recurring-revenue play is wheelchairs / power bases / scooters / walkers / seating. Open questions updated below.

Open questions to resolve with the user (in the connected session)

  • MVP cut: reminders+booking for which device categories first? (Step 0 update: stair/porch lifts do NOT exist in Westin's ADP data. Candidates are the powered units most likely to need recurring service — power wheelchairs, power bases, power scooters — then manual wheelchairs / walkers.)
  • Revenue mechanic: auto-draft a priced SO/invoice per booking, vs. pre-paid annual plan, vs. pay-at-door via Poynt — which is the default?
  • Technician assignment: auto-assign by skill+zone at booking time, or leave dispatch manual (fusion_tasks) and only reserve the calendar slot?
  • Booking-portal strategy: Step 0 shows Enterprise appointment already ships public, token-based real-time booking (appointment.invite + /appointment/<id>/..., auth="public"). Ride on that (generate an invite per reminder, partner pre-bound, no login) vs. a custom /maintenance/book/<token> route? (The /repairs/... route is moot — fusion_repairs isn't on Westin.)

Applicable CLAUDE.md rules (don't relearn the hard way)

  • Rule #1: read reference files from the running instance before coding (esp. the appointment source).
  • Odoo 19: res.users.group_ids (not groups_id); ir.cron has no numbercall; declarative models.Constraint/models.Index; HTTP routes type="jsonrpc"; OWL uses standalone rpc().
  • No sale.subscription model exists — a subscription is a sale.order with is_subscription=True.
  • New fields use x_fc_ prefix; Canadian English; $ Monetary + currency_id.
  • Route attachment opens through fusion_pdf_preview (att.action_fusion_preview(...)).
  • Tests need --http-port=0 --gevent-port=0. Westin prod is Enterprise; local dev is Community (so the appointment-dependent module can't be installed/tested on odoo-modsdev-app).