08-Project-Health / 08.03.Product-Roadmap-v2

08.03.Product Roadmap v2

08.03. Product Roadmap v2

Car Pulse Tracker

Created: 2026-02-14 | Updated: 2026-02-20 | Phase 1 WUI implemented: 2026-02-18 | Phase 2 implemented: 2026-02-20


Background

Car Pulse Tracker v1 asks users to pay before connecting their vehicle — one car at a time, with a manual model selection step that duplicates what OAuth already knows. A fleet owner with 10 Teslas repeats the entire flow 10 times.

The vehicle data industry has moved past this. Services like Tessie, Recurrent, and Teslascope all follow a connect-first pattern: the user authorizes their account, sees their vehicles, then decides what to buy. Smartcar standardized this into a multi-brand OAuth flow used by 40+ manufacturers. SaaS onboarding research (Userpilot, Baymard Institute) confirms that users who see value before paying convert at significantly higher rates.

v2 adopts connect-first for one-time report purchases: connect your car, see what you have, pick a package, pay once, get reports for all selected vehicles.


Flow

v1:  Landing → Brand/Model → Package → Payment → Connect → Report
v2:  Landing → Brand → Connect → Vehicles → Package → Payment → Reports

Key changes from v1: model selection removed (OAuth discovers vehicles), OAuth moves before payment (value-first), multi-vehicle support added.

Steps

Step 1 — Landing: Hero, supported brands, how it works, pricing overview.

Step 2 — Select Brand: User picks Tesla (or future brands). Determines which OAuth provider. Model selection is gone — OAuth reveals actual vehicles.

Step 3 — Connect (OAuth): User authorizes Car Pulse Tracker. Backend exchanges code, fetches vehicle inventory (GET /api/1/vehicles — lightweight, no car wake), creates OrderSession.

Step 4 — Select Vehicles (new): Shows all vehicles on the account. Each card shows display name, model, masked VIN, state (online/offline/asleep). User checks which ones to report on. "Select All" for fleet users. If only 1 vehicle, auto-selected.

Note: no odometer or battery shown here — that requires vehicle_data which wakes the car. Only inventory data.

Step 5 — Select Package + Quote: Three packages (Basic, Pro, Expert — tier pricing based on vehicle count, see §7 CPQ). User picks one package for all vehicles. System shows quote with tier price, optional voucher input, VAT breakdown.

Step 6 — Payment: Stripe / PayPal / Google Pay / Apple Pay. Single transaction for the total.

Step 7 — Generate Reports: Backend calls Tesla API endpoints per the selected package, for each selected vehicle. Dashboard shows progress. PDF download per vehicle.

Data Fetch Rule

API Sequence

Browser/WUI                CPT API                         Tesla Fleet API
-----------                -------                         ---------------
Click Connect        ->  POST /tesla/oauth/initiate
                     <-  {authUrl}

Redirect to Tesla    ->  Tesla authorize page
Callback to API      ->  GET /tesla/oauth/callback?code&state

                     ->  Exchange code for token
                     ->  GET /api/1/vehicles (inventory only)
                     <-  vehicles[]
                     ->  Create OrderSession(status=connected, token, vehicles)
                     <-  Redirect to WUI ?oauth=success&session_id=...

Load vehicle list    ->  GET /tesla/session/{session_id}/vehicles
                     <-  vehicles[] for user selection

Select + package     ->  POST /tesla/session/{session_id}/select
                     <-  {status: vehicles_selected}

Price quote          ->  POST /pricing/quote {session_id, voucher_code?}
                     <-  {tier_price, voucher, vat, total, currency}

Pay                  ->  POST /payment/create-intent {session_id, method}
Provider callback    ->  POST /payment/verify
                     <-  payment succeeded

Generate reports     ->  POST /reports/generate {session_id}
                     ->  For each selected vehicle (endpoints per package):
                           Basic: vehicle_data only
                           Pro:   + charging, alerts, service, warranty, release_notes, options
                           Expert: + vehicle_specs ($0.10)
                     <-  report artifacts per vehicle

Fetch reports        ->  GET /tesla/report/{session_id}
                     <-  {status: complete, reports: {vehicle_id: report}}

Solution Architecture

1. Order Session (new server-side concept)

v1 has no session. The OAuth state carries paymentId and the report is built inside the callback. v2 needs a server-side session that ties the entire flow together.

Current v1 storage (in TeslaFleetService): - _states: dict — OAuth CSRF state, keyed by random string, 15 min TTL - _reports: dict — pre-built reports, keyed by session_id, 15 min TTL, one-time retrieval

v2 replacementOrderSession:

@dataclass
class OrderSession:
    session_id: str                  # secrets.token_urlsafe(32)
    created_at: datetime             # UTC
    expires_at: datetime             # created_at + 30 min
    status: str                      # connected | vehicles_selected | paid | generating | complete

    # OAuth (set at callback)
    access_token: str                # Tesla user token, encrypted at rest
    refresh_token: str               # for token refresh if TTL is tight
    token_expires_at: datetime

    # Vehicle inventory (set at callback, from GET /api/1/vehicles)
    vehicles: list[dict]             # [{id, vin, display_name, state, car_type}, ...]

    # User selections (set at vehicle selection)
    selected_vehicle_ids: list[int]  # vehicle IDs chosen by user
    selected_package: str            # basic | pro | expert

    # Payment (set after payment verification)
    payment_id: str
    payment_method: str
    amount_cents: int

    # Reports (set after generation)
    reports: dict[int, dict]         # {vehicle_id: VehicleReport}

Session storage: In-memory dict (same pattern as _states/_reports). Phase 2: Redis or persistent file store.

PDF + order storage: Always generate PDFs regardless of whether the client downloads them. Store in GCS bucket alongside a provider-agnostic order manifest:

gs://bnc-cpt-{env}-reports/{session_id}/
  ├── order.json              ← provider-agnostic order record
  ├── {vehicle_id_1}.pdf
  └── {vehicle_id_2}.pdf

order.json captures the order at generation time — works identically for Stripe, PayPal, MobilePay, or any future provider:

{
  "session_id": "abc123...",
  "payment_id": "cs_test123",
  "payment_method": "stripe",
  "package": "pro",
  "vehicle_count": 2,
  "vehicles": [{"id": 123, "vin": "LRW3E7EK1RC98****"}],
  "amount_cents": 2999,
  "currency": "EUR",
  "created_at": "2026-02-17T12:00:00Z"
}

Enables customer support re-sends and chargeback proof regardless of payment provider. GCS lifecycle rotation policy added later as a configurable retention parameter (GDPR).

TTL: 30 min from creation. Cleaned up by _cleanup_expired() on every API call.

Replaces: Both _states and _reports dicts. One unified session object tracks the entire order lifecycle.

2. State Machine

[start] ──OAuth callback OK──> [connected]
                                    │
                  POST /session/{id}/select
                                    │
                                    v
                            [vehicles_selected]
                                    │
                          POST /payment/verify OK
                                    │
                                    v
                                  [paid]
                                    │
                          POST /reports/generate
                                    │
                                    v
                              [generating]
                                    │
                            all reports built
                                    │
                                    v
                              [complete]

Transition guards:

From → To Guard
start → connected Valid OAuth code, token exchanged, /api/1/vehicles returned 1+ vehicles
connected/vehicles_selected → vehicles_selected selected_vehicle_ids non-empty, all IDs exist in session.vehicles, selected_package is basic, pro, or expert. Re-selection allowed (user can change vehicles/package).
vehicles_selected → paid Payment verified, amount matches tier price for selected vehicles + package (+ voucher if applied)
paid → generating Token not expired (refresh if needed), selected vehicles still in session
generating → complete All vehicle reports built (partial success allowed — mark failed vehicles)

Any request that violates the state machine returns 409 Conflict with the current status.

3. API Contracts

3.1 Modified: POST /tesla/oauth/initiate

v1: { paymentId: str, plan: str }  →  { authUrl: str, state: str }
v2: { brand: str }                 →  { authUrl: str }

paymentId and plan removed. OAuth happens before payment exists. Backend generates state internally.

3.2 Modified: GET /tesla/oauth/callback

v1: code exchange → fetch ALL vehicle data → store full report → redirect
v2: code exchange → GET /api/1/vehicles ONLY → create OrderSession → redirect

The Tesla API data-fetch calls move from callback to post-payment POST /reports/generate. Which endpoints are called depends on the selected package (see PACKAGE_MODULES in §7).

3.3 New: GET /tesla/session/{session_id}/vehicles

Guard:    session.status == connected
Response: {
  vehicles: [{
    id: 12345678901234,
    vin: "LRW3E7EK1RC98****",     // masked last 4
    display_name: "JT3",
    model: "Model 3",              // from _car_type_to_model()
    year: 2024,                    // from _vin_to_year()
    state: "online"
  }]
}

No Tesla API calls — returns data cached in session from callback.

3.4 New: POST /tesla/session/{session_id}/select

Guard:    session.status in (connected, vehicles_selected)
Body:     { vehicle_ids: [12345678901234], package: "pro" }
Response: { status: "vehicles_selected", vehicle_count: 1, package: "pro" }

Validates all vehicle_ids exist in session. Sets selected_vehicle_ids and selected_package. Accepts both connected and vehicles_selected states — this allows the user to change their vehicle selection or package without starting over.

3.5 New: POST /pricing/quote

Body:     { session_id: str, voucher_code?: str }
Response: {
  package: "pro",
  vehicle_count: 2,
  tier_label: "2 vehicles",
  tier_price_cents: 2999,
  voucher: null,
  total_cents: 2999,
  vat_cents: 609,
  currency: "EUR"
}

Pricing engine looks up tier table (package + vehicle count), applies voucher if provided. Server-side pricing — client cannot set amount. Recalculated on each call.

3.6 Modified: POST /payment/create-intent

v1: { plan: str, amount: int, method: str }
v2: { session_id: str, method: str }

Server reads package and vehicle count from session. Computes amount internally. Client provides only session and payment method.

3.7 New: POST /reports/generate

Guard:    session.status == paid
Body:     { session_id: str }
Response: { status: "generating", vehicle_count: 2 }

Uses stored access_token to call _build_vehicle_report() for each selected vehicle (same logic as current v1, just moved here from callback). Stores results in session.reports.

Idempotent: if session is already complete, returns existing reports.

3.8 Modified: GET /tesla/report/{session_id}

v1: single report, one-time retrieval, deleted after return
v2: all reports for session, reusable (not deleted)

Guard:    session.status == complete
Response: {
  status: "complete",
  reports: {
    "12345678901234": { vehicle: {...}, vehicleData: {...}, chargingHistory: [...], ... },
    "98765432109876": { vehicle: {...}, vehicleData: {...}, chargingHistory: [...], ... }
  }
}

4. WUI Architecture Changes

AppStep Type

// v1
type AppStep = 'landing' | 'payment' | 'payment-gateway' | 'payment-success'
             | 'oauth' | 'report' | 'verify' | 'support'

// v2 (implemented)
type AppStep = 'landing' | 'oauth' | 'package' | 'payment-gateway'
             | 'generating' | 'report' | 'support'

Pinia Store (implemented)

// Removed
selectedModelId          // model selection gone

// Added (persisted to localStorage)
sessionId                // OrderSession ID, set after OAuth callback
sessionVehicles          // SessionVehicle[], persisted to survive page refresh
vehicleReports           // Record<string, VehicleReport>, persisted for refresh survival
activeReportVin          // string | null, which tab is active in ReportDashboard

// Kept (persisted)
selectedBrandId          // drives OAuth provider selection
selectedPlan             // basic | pro | expert
paymentId                // set after payment
paymentMethod            // stripe | paypal | googlepay | applepay

StepIndicator (implemented)

v1 (5 visual steps): Vehicle → Package → Payment → Connect → Report
v2 (5 visual steps): Brand → Connect → Package → Payment → Report

Step 1 (Brand/landing) is clickable for back-navigation. Steps 2-5 are not navigable backwards.

Component Changes (implemented)

Component Status
BrandSelector.vue Kept. Drives OAuth provider.
ModelSelector.vue Dead code (not imported). Delete pending.
OAuthStep.vue Modified. Calls initiateTeslaOAuth(brand) — no paymentId/plan.
PaymentStep.vue Modified. Now serves as Package step (step 3). Shows vehicle selection checkboxes + package selection + payment method + terms. Vehicle selection: 1 car = auto-selected, 2+ = user must choose.
PaymentGateway.vue Modified. Auto-advances to 'generating' step after 2s.
GeneratingStep.vue New (replaces VerificationStep.vue). Calls POST /reports/generate.
VerificationStep.vue Deleted.
ReportDashboard.vue Unchanged (Phase 1: single report view).
VehicleSelector.vue Not implemented as separate component — vehicle selection integrated into PaymentStep.vue.
PackageQuote.vue Not implemented — Phase 2 (CPQ/pricing engine needed first).

5. Edge Cases

Scenario Behavior
OAuth returns 0 vehicles Error: "No vehicles found on your Tesla account." Return to brand selection. No OrderSession created.
OAuth returns 1 vehicle Auto-select it. Show vehicle card (read-only, no checkbox). Skip directly to package selection.
OAuth returns 2+ vehicles Show VehicleSelector with checkboxes + "Select All".
User connects but closes browser Session expires server-side after 30 min. sessionId is persisted in localStorage — if user returns within TTL, they resume at vehicle selection. If expired, 410 Gone → reset to landing.
Token expires before report generation Attempt refresh using refresh_token. If refresh fails: "Your Tesla connection expired. Please reconnect." → return to OAuth step. Payment is still valid — user doesn't re-pay.
Car is asleep at report generation Tesla Fleet API handles wake internally for vehicle_data. May add latency (up to 30s). If timeout after 60s, mark vehicle report as partial with available data.
Payment succeeds but report generation fails Reports are paid for. Allow retry via POST /reports/generate (idempotent). Show partial results. Contact support link for unresolved failures.
User goes back from vehicle selection Clear selectedVehicleIds. Session and token stay valid. If they return to brand selection and pick a different brand, current session is abandoned.
Session TTL expires mid-flow Any API call returns 410 Gone. WUI catches this, shows "Session expired. Please start again.", resets to landing.
Concurrent browser tabs session_id is per-OAuth-flow. Each tab that completes OAuth gets its own session. No conflict.

6. Security

Concern Mitigation
Token stored server-side longer than v1 Encrypted at rest. 30 min TTL (vs 15 min in v1). Deleted on session expiry or completion. Never sent to WUI. Never logged.
Session ID as bearer token 32 bytes via secrets.token_urlsafe(32). Same as current v1. Rate-limited endpoints.
Price tampering Pricing engine does tier lookup server-side from session.selected_vehicle_ids + session.selected_package. Client sends only session_id + payment method.
Report re-generation abuse State machine: POST /reports/generate only works when status == paid. Idempotent — re-calling returns cached reports.
Abandoned sessions with tokens _cleanup_expired() runs on every API call. Sessions older than 30 min are deleted including tokens.

7. CPQ — Configure, Price, Quote

Entities

CPQ separates three concerns. Each is a distinct entity:

Entity What it is CPT example
Product What is being sold. Has a name and feature set. Basic Report, Pro Report, Expert Report
Price Tier table: package + vehicle count band → fixed total price. Easy to read, easy to change. Pro, 2 vehicles → €29.99
Quote Captures product + tier price before any voucher/campaign adjustments. Becomes the order record once payment is confirmed. "Pro (2 vehicles) = €29.99, voucher −100% → €0.00"

Product and Price are separate entities — price is never a column on Product. This is the universal pattern across Salesforce CPQ, Oscar, Saleor, Medusa, Shopify, and every major commerce platform.

Pricing Engine

The pricing engine does a tier table lookup: given a package and vehicle count, return the fixed total price for that band. No per-vehicle multiplication, no percentage discounts — just a table.

Tier table (example prices — easy to change in one place):

Vehicles Basic Pro Expert
1 €9.99 €19.99 €29.99
2 €14.99 €29.99 €44.99
3–5 €29.99 €49.99 €79.99
6–10 €49.99 €79.99 €129.99
11+ Contact us Contact us Contact us

The more vehicles, the better the per-vehicle value — built into the tier price, not calculated.

Vouchers / campaigns are applied after the tier lookup:

Type Example Effect
Test pilot PILOT2026 100% off (free report for beta testers)
Percentage LAUNCH10 −10% off tier price
Percentage FLEET20 −20% off tier price

The engine is simple:

input:   package + vehicle_count + optional voucher_code
step 1:  look up tier table → tier_price
step 2:  if voucher_code → apply voucher adjustment
step 3:  compute VAT on final price
output:  quote

Why a tier table? It's the pricing data — readable, changeable. Today it's a dict in cpq.py. When an admin panel exists, it reads from a config file or API. Changing a price = changing one number in the table, not touching any logic.

Price & Purchase Flow — Step by Step

STEP  USER                          API / SYSTEM                        STATE AFTER
────  ────────────────────────────  ──────────────────────────────────  ──────────────────

 1    Clicks "Connect Tesla"        OAuth redirect to Tesla             —
                                    Tesla callback with code
                                    → Exchange code for token
                                    → GET /api/1/vehicles               connected
                                    → Create OrderSession               (3 vehicles found)

 2    Selects vehicles:             POST /session/{id}/select           vehicles_selected
      ☑ Model 3 "JT3"              → Validate IDs in session
      ☑ Model Y "Family"           → Set package = pro
      Picks: Pro

 3    Sees quote                    POST /pricing/quote                 —
                                    → Tier lookup: pro + 2 = €29.99
                                    → VAT 25.5% = €6.09
                                    ← Quote: €29.99 incl. VAT

 4a   Enters voucher "PILOT2026"   POST /pricing/validate-voucher      —
                                    ← valid, 100% off, "Test pilot"

 4b   Clicks [Apply]               POST /pricing/quote                 —
                                    → €29.99 − 100% = €0.00
                                    ← Updated quote: €0.00

      (or: no voucher)             ← Quote stays: €29.99               —

 5    Clicks [Pay €29.99]          POST /payment/create-intent         —
                                    → Stripe PaymentIntent: €29.99
                                    ← client_secret for Stripe.js

 6    Completes Stripe payment     POST /payment/verify (webhook)      paid
                                    → Verify amount = quote total
                                    → Quote → ORDER RECORD

 7    Waits (progress shown)       POST /reports/generate              generating → complete
                                    → Per vehicle, per package:
                                      Pro = vehicle_data + charging
                                            + alerts + service + warranty
                                            + release_notes + options
                                    → Store reports in session

 8    Views report / downloads     GET /tesla/report/{session_id}      —
                                    ← { vehicle_1: report, vehicle_2: report }
                                    → PDF download per vehicle

Quote Screen — What the User Sees

  ┌────────────────────────────────────────────────┐
  │ Your Quote                                     │
  │                                                │
  │ Pro Report — 2 vehicles              €29.99    │
  │   Tesla Model 3 — "JT3"                       │
  │   Tesla Model Y — "Family"                     │
  │                                                │
  │   Voucher: [PILOT2026___]  [ Apply ]           │
  │   Test pilot discount (100%)        − €29.99   │
  │                                     ────────   │
  │   Total (VAT incl.)                   €0.00    │
  │                                                │
  │            [ Get Free Report ]                 │
  └────────────────────────────────────────────────┘

The quote recalculates whenever the user changes vehicle selection, package, or voucher. It becomes the order record once payment is confirmed.

What Each Package Includes

The product (package) determines which Tesla API endpoints are called per vehicle. Verified against real Tesla Fleet API responses (2024 Model 3, VIN LRW3E7EK1RC988948, 2026-02-10).

Data point Tesla API source Basic Pro Expert
Vehicle identity (make, model, year, VIN) vehicle_data → root + vehicle_config.car_type x x x
Battery & charging state (SOC %, range, limit, charging status) vehicle_datacharge_state x x x
Odometer vehicle_datavehicle_state.odometer x x x
Software version & update status vehicle_datavehicle_state.car_version, software_update x x x
Climate & temps (inside/outside, COP, preconditioning) vehicle_dataclimate_state x x x
Tire pressure (TPMS, 4 corners + warnings) vehicle_datavehicle_state.tpms_pressure_*, tpms_soft_warning_* x x x
Vehicle config (drivetrain, wheels, color, charge port) vehicle_datavehicle_config x x x
Security (sentry mode, locked, valet mode) vehicle_datavehicle_state.sentry_mode, locked, valet_mode x x x
Dashcam status vehicle_datavehicle_state.dashcam_state x x x
GUI settings (units: km, C, bar, kW) vehicle_datagui_settings x x x
Charging history (sessions, locations, costs) charging/historydata[] x x
Recent alerts recent_alertsresponse.recent_alerts[] x x
Service status service_dataresponse (empty if not in service) x x
Warranty details warranty/detailsresponse (requires VIN) x x
Firmware release notes release_notesresponse.release_notes[] x x
Factory options (trim, wheels, color, AP) optionscodes[] (option code + displayName) x x
Full vehicle specs /vehicle_specs (requires vehicle_specs scope) x
Tesla API calls per vehicle 1 7 8
Tesla API cost per vehicle $0.002 $0.014 ~$0.11

Notes on real API responses (tested 2026-02-10): - charging/history returns "data" array (not "response"), with totalResults: 107 - options returns 6 option codes: Autopilot, interior color, exterior color, wheels, trim+drivetrain, supercharger access - service_data returns empty {} when vehicle is not in service — this is expected - "Subscription eligibility" and "Upgrade eligibility" were removed — no such Tesla API endpoints exist

Must fix before Phase 1 launch (unverified endpoints): - warranty/details — returned 500 due to empty VIN bug. Fix: pass VIN explicitly in _build_vehicle_report(). Response structure unknown until re-tested. - vehicle_specs — returned 403 Unauthorized missing scopes. Fix: vehicle_specs scope added to config.py. Response structure unknown until re-tested. - drive_statenot returned in vehicle_data response. TypeScript types define it (latitude, longitude, heading, power) but real API did not include it. Investigate: scope issue, vehicle state (parked/asleep), or needs explicit endpoints= parameter. PDF template currently renders latitude/longitude — will show empty.

Action: re-run OAuth flow after deploying VIN fix + new scopes → capture warranty, specs, and drive_state responses → update types + package table.

The vehicle_specs endpoint ($0.10/call) is 50x more expensive than the others — the natural package boundary for Expert.

At these API costs, margins are 95%+ at any package level.

Architecture — Pricing Engine Service

The pricing engine lives in app/services/cpq.py. Two parts:

1. Tier table — the pricing data (one dict, easy to change):

TIER_TABLE = {
    "basic":  {1: 999, 2: 1499, 5: 2999, 10: 4999},
    "pro":    {1: 1999, 2: 2999, 5: 4999, 10: 7999},
    "expert": {1: 2999, 2: 4499, 5: 7999, 10: 12999},
}
# Keys = max vehicles in band (1, 2, 5, 10). Values = total price in cents.
# Lookup: find the smallest key >= vehicle_count.

2. Quote builder — tier lookup + optional voucher:

quote = create_quote(session, voucher_code=None)
  tier_price = lookup(package, vehicle_count)
  if voucher_code: apply voucher (fixed, percentage, or free)
  compute VAT on final price
  return Quote (package, vehicles, tier_price, voucher_adjustment, vat, total)

Recalculated on each call. After payment, the quote becomes the order record.

Product modules — which Tesla API endpoints each package calls (separate from pricing):

PACKAGE_MODULES = {
    "basic":  ["vehicle_data"],
    "pro":    ["vehicle_data", "charging_history", "recent_alerts", "service_data", "warranty", "release_notes", "options"],
    "expert": ["vehicle_data", "charging_history", "recent_alerts", "service_data", "warranty", "release_notes", "options", "vehicle_specs"],
}

API Endpoints — Complete v2 Surface

Tesla / Session
  POST /tesla/oauth/initiate              →  Build Tesla OAuth URL, return authUrl + state
  GET  /tesla/oauth/callback              →  Tesla redirects here with code+state → exchange token, fetch vehicles, create OrderSession → redirect WUI
  GET  /tesla/session/{id}/vehicles       →  Return vehicles[] from session (for vehicle selector)
  POST /tesla/session/{id}/select         →  Set selected vehicles + package → state: vehicles_selected
  GET  /tesla/report/{id}                 →  Return generated reports (per vehicle)

Pricing / CPQ
  GET  /pricing/catalog                   →  Tier table: packages x vehicle bands with prices (for landing page)
  POST /pricing/quote                     →  Tier lookup + optional voucher → quote {tier_price, voucher, vat, total}
  POST /pricing/validate-voucher          →  Check voucher code validity and effect (for UI preview)

Payment
  POST /payment/create-intent             →  Reads quote total from session, creates Stripe/PayPal intent
  POST /payment/verify                    →  Webhook: verify payment, quote → order record

Reports
  POST /reports/generate                  →  Per selected vehicle: call Tesla API endpoints per package → store reports
  POST /tesla/vehicle-report/pdf          →  Generate PDF from report data (existing)

WUI Impact


Migration Path

Phase 1: Connect-First (minimal) — IMPLEMENTED 2026-02-18

Landing → Brand → Connect → Package (with vehicle selection) → Payment → Report

Done (API — commit fd35279): - [x] OrderSession dataclass with state machine (connected → vehicles_selected → paid → generating → complete) - [x] POST /tesla/oauth/initiate — takes { brand } only - [x] OAuth callback creates OrderSession, fetches vehicle inventory - [x] GET /tesla/session/{id}/vehicles — returns cached vehicles - [x] POST /tesla/session/{id}/select — sets vehicles + package - [x] POST /reports/generate — builds reports post-payment - [x] GET /tesla/report/{id} — returns all reports - [x] Payment bridge: create-intent reads price from session - [x] valid_packages fix: "expert" → "bulk" (Phase 2 reverses to "expert") - [x] 107 unit tests passing

Done (WUI — commit d8c1a5f): - [x] AppStep type: 'landing' | 'oauth' | 'package' | 'payment-gateway' | 'generating' | 'report' | 'support' - [x] api.ts rewritten for v2 endpoints - [x] stores/app.ts: sessionId + sessionVehicles persisted in localStorage - [x] App.vue: connect-first flow, OAuth/Stripe/PayPal callback handling - [x] OAuthStep.vue: calls initiateTeslaOAuth(brand) — no paymentId/plan - [x] PaymentStep.vue: vehicle selection checkboxes + package + payment method + terms - [x] PaymentGateway.vue: auto-advances to report generation - [x] GeneratingStep.vue: new, replaces VerificationStep - [x] StepIndicator.vue: 5-step v2 flow with 'package' AppStep - [x] i18n (en/fi/sv) updated - [x] Build passes (vue-tsc + vite)

Not done (Phase 1 remaining): - [x] E2E test on dev.carpulsetracker.com — DONE 2026-02-20: OAuth → vehicle selection → package → payment → report generation tested with 2 real Teslas - [x] GCP secret bnc-cpt-dev-proxy-shared-secretRESOLVED 2026-02-20 - [ ] PDF + order storage in GCS (moved to Phase 3) - [x] Fix warranty/details VIN bug — DONE: VIN is passed correctly in generate_reports()_build_vehicle_report(access_token, vehicle_tag, vin) → warranty/details endpoint - [ ] Investigate missing drive_state (moved to Phase 3)

Moved to Phase 2 (done): - [x] Deploy with vehicle_specs scope — part of Expert package PACKAGE_MODULES - [x] Delete dead code: ModelSelector.vue, PaymentSuccessStep.vue, VehicleForm.vue

Phase 2: CPQ Pricing Engine + Multi-Report — IMPLEMENTED 2026-02-20

Vehicle selection and multi-vehicle support already in Phase 1. Phase 2 adds server-side pricing, vouchers, package-aware report generation, and multi-vehicle report dashboard.

Status: All 17 implementation steps complete. Deployed and tested on dev.carpulsetracker.com with 2 real Teslas (Model 3 + Model Y). Reports generated successfully with live Tesla data.

Confirmed Design Decisions

Decision Resolution
Package naming basic / pro / expert — rename "bulk" → "expert" everywhere
Tier prices Use roadmap prices exactly (§7 tier table)
11+ vehicles Use the 6–10 band price (no "contact us" cap)
UX flow Single 'package' step with 4 sections: vehicles → packages → quote → payment method
Landing page pricing No — prices visible only after OAuth
Voucher storage JSON config file, configurable per environment (config/vouchers.json)
PACKAGE_MODULES Yes — Basic=1 API call, Pro=7, Expert=8 (see §7 table)
Multi-report Tabbed view per vehicle in ReportDashboard
Dead code Delete ModelSelector.vue, PaymentSuccessStep.vue, VehicleForm.vue

Implementation (API — bnc-cpt-api)

  1. Rename "bulk" → "expert":
  2. app/models/payment.pyPaymentPlan.BULKPaymentPlan.EXPERT
  3. app/config.pyPAYMENT_PLANS["bulk"]PAYMENT_PLANS["expert"]
  4. app/services/tesla_fleet.pyvalid_packages set
  5. app/services/pdf.py — display text
  6. All tests using "bulk"

  7. New app/models/pricing.py:

  8. QuoteRequest(session_id, voucher_code?)
  9. QuoteResponse(package, vehicle_count, tier_label, tier_price_cents, voucher, vat_cents, total_cents, currency)
  10. VoucherValidateRequest(code), VoucherValidateResponse(valid, code, type, label, discount_description)
  11. CatalogResponse(packages: [{name, features, tiers: [{max_vehicles, price_cents}]}])

  12. New app/services/cpq.py:

  13. TIER_TABLE — package → {max_vehicles: price_cents}
  14. PACKAGE_MODULES — package → list of Tesla API module names
  15. PACKAGE_FEATURES — package → feature descriptions for catalog
  16. load_vouchers() — reads config/vouchers.json
  17. lookup_tier_price(package, vehicle_count) — find smallest band key ≥ count
  18. validate_voucher(code) → VoucherInfo or None
  19. create_quote(session, voucher_code?) → QuoteResponse (tier + voucher + VAT)
  20. get_catalog() → CatalogResponse
  21. get_package_modules(package) → list[str]

  22. New app/api/v1/routers/pricing.py:

  23. GET /pricing/catalog — tier table with packages/bands/features
  24. POST /pricing/quote — session_id + voucher_code? → computed quote (also stores in session)
  25. POST /pricing/validate-voucher — code → validity + effect

  26. OrderSession + payment flow:

  27. Add to OrderSession: quote_price_cents, quote_voucher_code, quote_vat_cents
  28. create_payment_intent() reads session.quote_price_cents instead of PAYMENT_PLANS
  29. If quote_price_cents = 0 (100% voucher): skip payment, mark session paid directly

  30. PACKAGE_MODULES in report generation:

  31. generate_reports() gets modules from get_package_modules(session.selected_package)
  32. _build_vehicle_report(access_token, vehicle_tag, vin, modules) conditionally calls endpoints
  33. New endpoints: release_notes, options (Pro+Expert), vehicle_specs (Expert only, $0.10)

  34. New config/vouchers.json: json { "PILOT2026": {"type": "percentage", "value": 100, "label": "Test pilot (100% off)", "active": true}, "LAUNCH10": {"type": "percentage", "value": 10, "label": "Launch discount (10% off)", "active": true}, "FLEET20": {"type": "percentage", "value": 20, "label": "Fleet discount (20% off)", "active": true} } Config: VOUCHER_CONFIG_PATH = "config/vouchers.json"

  35. Tests: test_cpq.py (tier lookup, vouchers, quote math, PACKAGE_MODULES), test_pricing.py (router endpoints), update test_payment.py

Implementation (WUI — bnc-cpt-wui)

  1. Rename "bulk" → "expert" in store types, i18n keys, component references
  2. New types in types/index.ts: TierBand, PackageCatalog, CatalogResponse, Quote, VoucherValidation
  3. New API functions in api.ts: getPricingCatalog(), getQuote(), validateVoucher()
  4. Store: add currentQuote, catalog refs (not persisted — fetched from API)
  5. PaymentStep.vue restructure — 4 sections:
    • Section 1: Vehicle checkboxes (existing)
    • Section 2: Package cards with dynamic prices from catalog (computed for vehicle count)
    • Section 3: Quote summary (package + vehicles + tier price + voucher input + VAT + total)
    • Section 4: Payment method + terms + [Pay €XX.XX] button
    • On vehicle/package change: call POST /pricing/quote → live update
  6. PaymentGateway.vue — show amount from quote
  7. ReportDashboard.vue — tabbed view per vehicle (1 vehicle = no tabs, 2+ = tabs with display_name)
  8. i18n: remove hardcoded prices (€19/€29/€149), rename bulk→expert keys, add quote/voucher keys
  9. Delete dead code: ModelSelector.vue, PaymentSuccessStep.vue, VehicleForm.vue

Post-Deploy Fixes (2026-02-20)

Three issues found during live testing on dev.carpulsetracker.com with 2 real Teslas:

Fix Repo Commit Details
State machine re-selection API dc9aa7b select_vehicles_and_package guard only accepted connected state. After first selection, session was vehicles_selected, so changing package/vehicles returned 409. Fixed: accept both connected and vehicles_selected.
Charging history VIN filter API ed0b8e8 Tesla's GET /api/1/dx/charging/history returns account-level data (all vehicles). Both cars showed identical charging sessions. Fixed: filter chargingHistory by vin field in _build_vehicle_report().
vehicleReports localStorage WUI 25efb37 vehicleReports and activeReportVin were not persisted to localStorage. On page refresh, reports disappeared even though session was still alive. Fixed: added STORAGE_KEYS + watch-based persistence (same pattern as other store fields).

Note on charging/history: Tesla's GET /api/1/dx/charging/history is account-level, not per-vehicle. Each charging session has a vin field. The API filters by VIN server-side before returning data to the client.

Phase 3: Fleet + Admin

Brand Refresh Follow-Up

Current gap:

Needed implementation:

  1. Export the approved CPT logo from the concept into reusable SVG asset(s).
  2. Add the product branding asset(s) under src/vue/app/src/assets/images/branding/.
  3. Replace the inline header logo in src/vue/app/src/components/Header.vue.
  4. Update related styling in src/vue/app/src/style.css.
  5. Keep OEM manufacturer logos separate from CPT product branding.
  6. Decide whether the visible text brand remains Car Pulse Tracker or shifts to a stronger CPT lockup.
  7. Re-run WUI build/tests after the swap.

Open Questions


Research Sources

Source What URL
Smartcar Connect Multi-brand vehicle OAuth standard (40+ brands) https://smartcar.com/product/connect
Recurrent Auto EV battery health reports, uses Smartcar, connect-first flow https://www.recurrentauto.com/for-owners
Tessie Tesla companion app, connect-first + free trial, 500k+ users https://www.tessie.com
Teslascope Tesla data service, freemium, per-vehicle pricing https://teslascope.com/about
Carfax VIN-based vehicle history reports, bundle pricing https://www.carfax.com/vehicle-history-reports/
Userpilot SaaS signup flow UX research https://userpilot.com/blog/saas-signup-flow/
Baymard Institute Checkout usability research (55% abandon on surprise costs) https://baymard.com/research/checkout-usability
Smartcar Blog Vehicle onboarding UX patterns https://smartcar.com/blog/smartcar-connect-vehicle-onboarding
Salesforce CPQ Industry-standard CPQ: separate Product, Price, Quote entities https://www.salesforce.com/products/cpq/
Medusa Commerce Open-source commerce engine — Product/Price separation, tiered pricing https://medusajs.com
Saleor Commerce Open-source GraphQL commerce — Product ≠ Price architecture https://saleor.io
Oscar Commerce Django e-commerce — Product/StockRecord separation (same pattern) https://django-oscar.readthedocs.io
Stripe Pricing Table Embedded pricing table with tiered plans, Checkout integration https://stripe.com/docs/payments/checkout/pricing-table
Stripe Coupons Coupon/promotion code system — percentage, fixed, free trial https://stripe.com/docs/billing/subscriptions/coupons
Tesla Fleet API Official Tesla Fleet API documentation (OAuth, vehicle data, commands) https://developer.tesla.com/docs/fleet-api
Tesla Fleet API Pricing Tesla API endpoint pricing ($0.001–$0.10 per call by category) https://developer.tesla.com/docs/fleet-api/getting-started/subscription-plans