diff --git a/docs/plans/fusion_maintenance_brainstorm.md b/docs/plans/fusion_maintenance_brainstorm.md new file mode 100644 index 00000000..f3ec900d --- /dev/null +++ b/docs/plans/fusion_maintenance_brainstorm.md @@ -0,0 +1,116 @@ +# 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/` 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/`. +- Booking controller: `controllers/portal_maintenance_booking.py` — **single 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_type` → `interval_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. + +```bash +APP= ; 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 /models/appointment_type.py | head -160 +docker exec "$APP" ls /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;" +``` + +## Open questions to resolve with the user (in the connected session) +- **MVP cut**: reminders+booking for which device categories first (e.g. stair/porch lifts — the + compliance-heavy, highest recurring-revenue ones)? +- **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**: enhance the existing `/repairs/maintenance/book/` in place + (fusion_repairs isn't production-deployed yet) vs. add a new `/maintenance/book/` route. + +## 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`).