fix(fusion_repairs): persona-driven workflow audit - 6 real bugs

Full end-to-end walk acting as customer, CS rep, dispatcher, technician,
and manager surfaced 6 real bugs (1 critical state-machine, 4 missing UX
wires, 1 docstring). Server endpoints existed for everything but several
were not wired into the templates.

B1 (HIGH) - Visit-report wizard never closed the repair
  Tech submitted visit -> state stayed 'draft' -> x_fc_done_at never
  stamped -> NPS cron never fired -> the whole post-visit flow died
  silently. Customers never got their NPS email.

  Fix: action_confirm() now drives the Odoo native state machine
  draft -> action_validate (with _action_repair_confirm fallback) ->
  action_repair_start -> action_repair_end. Each step guarded by the
  current state and exception-logged. Leaves the repair open if:
    - requires_requote=True (variance flag - office must re-quote)
    - no_show=True (office reschedules)
    - x_fc_is_quote_only (still a quote)
    - found_another_issue spawned a stub
  Posts a clear chatter line on success or failure.
  Verified: e2e walk now shows state=done + x_fc_done_at stamped +
  NPS cron fires + flags x_fc_nps_email_sent=True.

B2 (HIGH) - /repair/new form never called /repair/self_check
  The AI self-check engine was the headline weekend feature but it was
  invisible to the client. The endpoint worked server-side, just had
  no frontend.

  Fix: new portal_client_repair.js (Interaction class, registered on
  registry.category('public.interactions')). 'Try 1-3 safe self-check
  steps first' button POSTs to /repair/self_check, renders steps via
  createElement + textContent (no innerHTML - all server output is
  treated as untrusted text). Shows the AI's safety disclaimer on
  every result. On escalate_immediately, shows a clear 'submit the
  form, we'll come to you' message instead of the steps.
  Verified: HTTP POST returns full JSON with instruction +
  expected_result + disclaimer; new button + result panel appear in
  rendered HTML.

B3 (HIGH) - No phone-lookup UI for returning clients
  Same problem - endpoint existed but no UI. Returning clients had to
  retype everything from scratch.

  Fix:
  - lookup_phone now returns a 'partners' array (id, name, email,
    street, city) - cap of 3 results, rate-limited, every match logged
    at INFO level for audit. Privacy compromise: a phone holder
    deserves to see their own pre-fill; rate limit caps harvesting.
  - JS lookup widget at the top of the form posts to /repair/lookup_phone
    and pre-fills the 5 contact fields + writes the partner_id to a
    hidden #fr_known_partner_id input.
  - controller /repair/submit now trusts known_partner_id if present
    (skips the phone re-match) so we don't create duplicate partners
    when the lookup widget already identified the right one.
  Verified: HTTP POST returns the 2 partner records we have for
  +19055551234 with full id/name/email/street/city.

B4 (MEDIUM) - /repair?sn=<serial> from QR sticker did nothing
  Spec: 'Client scans QR sticker - portal pre-fills the unit info.'
  Reality: the form had no serial field; ?sn= was ignored.

  Fix: new _resolve_serial_info(serial) on the controller resolves
  the lot via stock.lot.search([('name','=',sn)]) and returns
  {serial, lot_id, product_id, product_name, category_id}. Both
  /repair (landing) and /repair/new pass it as serial_info template
  context. Templates show 'Recognized X (Serial: Y)' + auto-select
  the matching category in the dropdown. Hidden #fr_serial_number
  carries it through to /repair/submit, which attaches the lot_id +
  uses the QR category as fallback if user didn't pick one.
  Verified: ?sn=stella23-20040164 produces 'Pre-filled from QR scan:'
  banner + hidden input populated.

B5 (MEDIUM) - No upsell after submit
  Spec required an upsell - 'reduce future calls'. Page was a bare
  'Got it'.

  Fix: /repair/thanks now shows a 2-card layout:
    - 'Want to avoid this next time?' with 4 bullets (priority booking,
      free inspection cert, discounted parts, annual reminder) +
      'See our maintenance plans' CTA to /shop?category=maintenance
    - 'What happens next' 4-step bulleted explanation
  Verified: both cards render.

B6 (LOW) - SyntaxWarning '\-->' in repair_service_plan.py
  Made the module docstring a raw string (r''') so the ASCII flowchart
  arrows don't trigger Python's invalid-escape-sequence warning.

Bumped to 19.0.1.8.0.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
gsinghpal
2026-05-21 01:06:12 -04:00
parent b4b59cc3c9
commit 4f1b7c2df6
6 changed files with 418 additions and 29 deletions

View File

@@ -13,9 +13,18 @@
<h1 class="display-5 fw-bold">Need a repair?</h1>
<p class="lead text-muted mb-4">
Tell us about your equipment and what's going wrong.
We'll respond on the next business day - or sooner if it's urgent.
We'll get to it on the next business day - or sooner if urgent.
</p>
<a href="/repair/new" class="btn btn-primary btn-lg px-5 py-3">
<!-- B4: surface the QR ?sn= context so the user knows we recognized their device -->
<t t-if="serial_info">
<div class="alert alert-success text-start mb-4" role="alert">
<i class="fa fa-qrcode me-1"/>
Recognized <strong t-out="serial_info.get('product_name')"/>
(Serial: <code t-out="serial_info.get('serial')"/>).
We'll pre-fill your service request.
</div>
</t>
<a t-att-href="form_url" class="btn btn-primary btn-lg px-5 py-3">
Start a Service Request
</a>
<div class="text-muted mt-4 small">
@@ -26,6 +35,9 @@
<strong>Is anyone hurt right now?</strong>
If you have a medical emergency, please hang up and dial <strong>9-1-1</strong>.
</div>
<div class="text-muted mt-4 small">
Already a customer? Have your phone number handy - we'll recognize your account.
</div>
</div>
</div>
</section>
@@ -61,9 +73,16 @@
</t>
<form action="/repair/submit" method="POST"
enctype="multipart/form-data" class="card shadow-sm">
enctype="multipart/form-data"
class="card shadow-sm"
id="fr_repair_form"
data-fr-client-form="1">
<input type="hidden" name="csrf_token"
t-att-value="request.csrf_token()"/>
<!-- B3: phone lookup pre-fills + ?sn= QR pre-fills -->
<input type="hidden" name="known_partner_id" id="fr_known_partner_id" value=""/>
<input type="hidden" name="serial_number" id="fr_serial_number"
t-att-value="serial_info and serial_info.get('serial') or ''"/>
<!-- Honeypot. Real users never see this. -->
<div style="position:absolute;left:-9999px;top:-9999px;" aria-hidden="true">
<label>Company name</label>
@@ -72,37 +91,62 @@
<div class="card-body p-4">
<!-- B3: phone-first lookup for returning clients -->
<div class="alert alert-info mb-3" id="fr_lookup_panel">
<h6 class="mb-2"><i class="fa fa-search me-1"/>Already a client?</h6>
<div class="input-group">
<input type="tel" id="fr_lookup_phone"
class="form-control"
placeholder="Enter phone (e.g. 905-555-1234)"/>
<button class="btn btn-outline-primary" type="button"
id="fr_lookup_btn">Look me up</button>
</div>
<small class="text-muted">
We'll pre-fill your contact info so you don't have to retype it.
</small>
<div id="fr_lookup_result" class="mt-2"></div>
</div>
<h5>1. Your contact details</h5>
<div class="mb-3">
<label class="form-label">Your name <span class="text-danger">*</span></label>
<input type="text" name="client_name" class="form-control form-control-lg" required="required"/>
<input type="text" name="client_name" id="fr_client_name" class="form-control form-control-lg" required="required"/>
</div>
<div class="mb-3">
<label class="form-label">Phone number <span class="text-danger">*</span></label>
<input type="tel" name="client_phone" class="form-control form-control-lg" required="required" placeholder="(519) 555-1234"/>
<input type="tel" name="client_phone" id="fr_client_phone" class="form-control form-control-lg" required="required" placeholder="(519) 555-1234"/>
</div>
<div class="mb-3">
<label class="form-label">Email (so we can send a confirmation)</label>
<input type="email" name="client_email" class="form-control"/>
<input type="email" name="client_email" id="fr_client_email" class="form-control"/>
</div>
<div class="mb-3">
<label class="form-label">Street address</label>
<input type="text" name="client_street" class="form-control"/>
<input type="text" name="client_street" id="fr_client_street" class="form-control"/>
</div>
<div class="mb-3">
<label class="form-label">City</label>
<input type="text" name="client_city" class="form-control"/>
<input type="text" name="client_city" id="fr_client_city" class="form-control"/>
</div>
<hr/>
<h5>2. What equipment needs service?</h5>
<!-- B4: surface ?sn= context so user knows we identified their device -->
<t t-if="serial_info">
<div class="alert alert-success mb-3">
<i class="fa fa-qrcode me-1"/>
Pre-filled from QR scan: <strong t-out="serial_info.get('product_name')"/>
(Serial <code t-out="serial_info.get('serial')"/>)
</div>
</t>
<div class="mb-3">
<label class="form-label">Equipment category <span class="text-danger">*</span></label>
<select name="category_id" class="form-select form-select-lg" required="required">
<select name="category_id" id="fr_category_id" class="form-select form-select-lg" required="required">
<option value="">Choose one...</option>
<t t-foreach="categories" t-as="cat">
<option t-att-value="cat.id">
<option t-att-value="cat.id"
t-att-selected="serial_info and serial_info.get('category_id') == cat.id">
<t t-out="cat.name"/>
</option>
</t>
@@ -120,7 +164,7 @@
<h5>3. What's wrong?</h5>
<div class="mb-3">
<label class="form-label">Short description <span class="text-danger">*</span></label>
<input type="text" name="issue_summary" class="form-control form-control-lg" required="required" placeholder="e.g. 'stairlift beeps and won't move'"/>
<input type="text" name="issue_summary" id="fr_issue_summary" class="form-control form-control-lg" required="required" placeholder="e.g. 'stairlift beeps and won't move'"/>
</div>
<div class="mb-3">
<label class="form-label">Anything else we should know?</label>
@@ -131,6 +175,21 @@
<input type="file" name="photos" class="form-control" accept="image/*,video/*" multiple="multiple" capture="environment"/>
</div>
<!-- B2: AI self-check button. Triggered manually so we don't
spam the AI / fallback on every keystroke. -->
<div class="mb-3">
<button type="button" class="btn btn-outline-info"
id="fr_selfcheck_btn">
<i class="fa fa-magic me-1"/>
Try 1-3 safe self-check steps first (optional)
</button>
<small class="text-muted d-block mt-1">
We'll suggest a couple of things you can safely try in under 2 minutes.
If they don't help, just submit and we'll come to you.
</small>
<div id="fr_selfcheck_result" class="mt-3"></div>
</div>
<hr/>
<h5>4. How urgent is it?</h5>
@@ -166,15 +225,58 @@
<t t-call="website.layout">
<div id="wrap" class="o_fusion_repairs_client">
<section class="container py-5">
<div class="row justify-content-center text-center">
<div class="col-12 col-lg-7">
<div class="row justify-content-center">
<div class="col-12 col-lg-7 text-center">
<i class="fa fa-check-circle fa-4x text-success mb-3"/>
<h1 class="mb-3">Got it!</h1>
<p class="lead text-muted">
Your service request <strong t-if="ref"><t t-out="ref"/></strong> was received.
We'll get back to you on the next business day or sooner if you marked it urgent.
</p>
<a href="/repair" class="btn btn-outline-secondary mt-3">Back to home</a>
</div>
<!-- B5: post-submit upsell - maintenance plan + next steps -->
<div class="col-12 col-lg-7 mt-4">
<div class="card border-info">
<div class="card-body">
<h5 class="card-title">
<i class="fa fa-lightbulb-o me-1 text-warning"/>
Want to avoid this next time?
</h5>
<p class="card-text">
Most of our regular clients enrol in an <strong>annual
maintenance plan</strong> - we visit twice a year, catch wear
before it becomes a breakdown, and you pay a lot less for
peace of mind than for an emergency call-out.
</p>
<ul class="small text-muted">
<li>Priority booking - your calls jump the queue</li>
<li>Free safety inspection certificate (stairlifts, porch lifts)</li>
<li>Discounted parts</li>
<li>Annual reminder so you never forget</li>
</ul>
<a href="/shop?category=maintenance" class="btn btn-info">
See our maintenance plans
</a>
</div>
</div>
<div class="card mt-3">
<div class="card-body">
<h6 class="card-title mb-2">
<i class="fa fa-info-circle me-1"/>
What happens next
</h6>
<ol class="small mb-0">
<li>You will get a confirmation email within a few minutes.</li>
<li>Our office reviews your request the next business day.</li>
<li>We call you to confirm a technician visit time.</li>
<li>You will get a reminder the day before the visit.</li>
</ol>
</div>
</div>
<div class="text-center mt-4">
<a href="/repair" class="btn btn-outline-secondary">Back to home</a>
</div>
</div>
</div>
</section>