node-postgres returns numeric/decimal columns as JavaScript strings,
but the TypeScript interfaces for MaintenanceRecord and OwnershipCost
declare numeric fields as number. The mappers were passing values
through raw, breaking type-safe arithmetic and display (e.g. the
amount column on the vehicle summary screen was empty until the
recent frontend workaround in PR #240, and OwnershipCostsList silently
no-ops toLocaleString on the string).
Backend
- mapMaintenanceRecord: coerce cost via Number() when non-null.
- ownership-costs mapRow: coerce amount via Number().
Frontend (remove now-redundant workarounds)
- MaintenanceRecordsList: drop Number() coercion on cost and
odometerReading; use the number values directly.
- VehicleDetailPage / VehicleDetailMobile: revert the PR #240 cost
coercion to the simple typeof number guard now that the backend
honors the type.
Scope notes
- Other repositories with the same pattern (stations, community-stations,
fuel-logs enhanced methods) are tracked separately because they have
unclear downstream consumers and warrant their own investigation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Postgres numeric columns come back as strings via node-postgres, so
typeof rec.cost === 'number' was false and the amount column rendered
as '—'. Coerce with Number() (matching the pattern in
MaintenanceRecordsList) so the cost displays as a dollar amount.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Vehicle Records section on /garage/vehicles/:id never called
useMaintenanceRecords, so maintenance rows always rendered empty even
when records existed for the vehicle. Wire the existing hook into both
the desktop VehicleDetailPage and mobile VehicleDetailMobile, merge
records into the unified list with category + subtypes + shop name,
and include the maintenance loading state in the section's loading
and empty-state guards.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
new Date("YYYY-MM-DD") parses as UTC midnight per ES2015. toLocaleDateString()
then displays in local time, shifting the date back one day for users west of
UTC. This caused the list view and edit dialog to show different dates.
Fixed in: MaintenanceRecordsList (display + sort + delete confirm),
VehicleDetailPage (display + sort), VehicleDetailMobile (display + sort),
MaintenanceRecordForm (receipt title), OwnershipCostsList (formatDate).
Sorting now uses string comparison (YYYY-MM-DD is lexicographically sortable).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The parseServiceDate function used toISOString().split('T')[0] which converts
to UTC, shifting dates by one day depending on timezone. Standard parsing now
uses getFullYear/getMonth/getDate (local time). MM/DD/YYYY parsing now formats
directly from regex groups without round-tripping through a Date object.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove AutomaticFunctionCallingConfig(max_remote_calls=3) which caused
pydantic validation error on the installed google-genai version
- Log full Gemini raw JSON response in OCR engine for debugging
- Add engine/transmission to backend raw values log
- Add hasTrim/hasEngine/hasTransmission to decode success log
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The installed google-genai version does not support max_remote_calls on
AutomaticFunctionCallingConfig, causing a pydantic validation error that
broke VIN decode on staging.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace engine._model/engine._generation_config mocks with
engine._client/engine._model_name. Update sys.modules patches
from vertexai to google.genai. Remove dead if-False branch.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace vertexai.generative_models with google.genai client pattern.
Add Google Search grounding tool to VIN decode for improved accuracy.
Convert response schema types to uppercase per Vertex AI Schema spec.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
gemini-3-flash-preview was hallucinating year (e.g., returning 1993
instead of 2023 for position-10 code P). Prompt now includes the full
1980-2039 year code table and position-7 disambiguation rule.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Default 10s API client timeout caused frontend "Failed to decode" errors
when Gemini engine cold-starts (34s+ on first call).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prevent lower-confidence Gemini results from overwriting higher-confidence
cache entries, add reverse-contains matching so values like "X5 xDrive35i"
match DB option "X5", and show amber hint when dropdown matching fails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rename nhtsaValue to sourceValue in frontend MatchedField type and
VinOcrReviewModal component. Update NHTSA references to vehicle
database across guide pages, hooks, and API documentation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Delete vehicles/external/nhtsa/ directory (3 files), remove VPICVariable
and VPICResponse from platform models. Update all documentation to
reflect Gemini VIN decode via OCR service architecture.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace NHTSAClient with OcrClient in vehicles controller. Move cache
logic into VehiclesService with format-aware reads (Gemini vs legacy
NHTSA entries). Rename nhtsaValue to sourceValue in MatchedField.
Remove vpic config from Zod schema and YAML config files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add VinDecodeResponse type and OcrClient.decodeVin() method that sends
JSON POST to the new /decode/vin OCR endpoint. Unlike other OCR methods,
this uses JSON body instead of multipart since there is no file upload.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add POST /decode/vin endpoint using Gemini 2.5 Flash for VIN string
decoding. Returns structured vehicle data (year, make, model, trim,
body/drive/fuel type, engine, transmission) with confidence score.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add missing $ prefix to template literal expression so the year
renders as "2026" instead of literal "{new Date().getFullYear()}".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Change default FROM to hello@notify.motovaultpro.com across app and CI
senders. Replace broken {{unsubscribeUrl}} placeholder with real Settings
page URL. Add RFC 8058 List-Unsubscribe headers for email client support.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three fixes to the Stripe subscription flow:
1. Change payment_behavior from 'default_incomplete' to
'error_if_incomplete' so Stripe charges the card immediately instead
of leaving the subscription in incomplete status waiting for frontend
payment confirmation that never happens.
2. Read currentPeriodStart/End from subscription items instead of the
top-level subscription object. Stripe moved these fields to
items.data[0] in API version 2025-03-31.basil, causing epoch-zero
dates (Dec 31, 1969).
3. Map Stripe 'incomplete' status to 'active' in mapStripeStatus() so
it doesn't fall through to the default 'canceled' mapping.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>