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 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>
Stripe requires payment methods to be attached to a customer before they
can be set as default_payment_method on a subscription. The
createSubscription() method was skipping this step, causing 500 errors
on checkout with: "The customer does not have a payment method with the
ID pm_xxx".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Vehicles service and subscriptions code still queried user_profiles by
auth0_sub after the UUID migration, causing 500 errors on GET /api/vehicles.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
16 controllers still used request.user.sub (Auth0 ID) instead of
request.userContext.userId (UUID) after the user_id column migration,
causing 500 errors on all authenticated endpoints including dashboard.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Backend test fixtures:
- Replace auth0|xxx format with UUID in all test userId values
- Update admin tests for new id/userProfileId schema
- Add missing deletionRequestedAt/deletionScheduledFor to auth test mocks
- Fix admin integration test supertest usage (app.server)
Frontend:
- AdminUser type: auth0Sub -> id + userProfileId
- admin.api.ts: all user management methods use userId (UUID) params
- useUsers/useAdmins hooks: auth0Sub -> userId/id in mutations
- AdminUsersPage + AdminUsersMobileScreen: user.auth0Sub -> user.id
- Remove encodeURIComponent (UUIDs don't need encoding)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- audit-log: JOIN on user_profiles.id instead of auth0_sub
- backup: use userContext.userId instead of auth0Sub
- ocr: use request.userContext.userId instead of request.user.sub
- user-profile controller: use getById() with UUID instead of getOrCreateProfile()
- user-profile service: accept UUID userId for all admin-focused methods
- user-profile repository: fix admin JOIN aliases from auth0_sub to id
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Migrate admin controller, routes, validation, and users controller
from auth0Sub identifiers to UUID. Admin CRUD now uses admin UUID id,
user management routes use user_profiles UUID. Clean up debug logging.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Updated user-profile.repository.ts to use UUID instead of auth0_sub:
- Added getById(id) method for UUID-based lookups
- Changed all methods (except getByAuth0Sub, getOrCreate) to accept userId (UUID) instead of auth0Sub
- Updated SQL WHERE clauses from auth0_sub to id for UUID-based queries
- Fixed cross-table joins in listAllUsers and getUserWithAdminStatus to use user_profile_id
- Updated hardDeleteUser to use UUID for all DELETE statements
- Updated auth.plugin.ts to call updateEmail and updateEmailVerified with userId (UUID)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Delete resolveStripeCustomerId() and replace with ensureStripeCustomer()
that includes orphaned Stripe customer cleanup on DB failure. Make
syncTierToUserProfile() blocking (errors propagate). Add null guards to
cancel/reactivate for admin-set subscriptions. Fix getInvoices() null
check. Clean controller comment. Add deleteCustomer() to StripeClient.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove admin_override_ placeholder from createForAdminOverride(), use NULL.
Update mapSubscriptionRow() with ?? null. Make stripeCustomerId optional
in create() method.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Make stripe_customer_id NULLABLE via migration, clean up admin_override_*
values to NULL, and update Subscription/SubscriptionResponse/UpdateSubscriptionData
types in both backend and frontend.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The backend SUPPORTED_IMAGE_TYPES set excluded application/pdf, returning
415 before the request ever reached the OCR microservice. Added PDF to
the allowed types in both controller and service validation layers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Backend: Add authenticated endpoints for pending association CRUD
(GET/POST/DELETE /api/email-ingestion/pending). Service methods for
resolving (creates fuel/maintenance record) and dismissing associations.
Frontend: New email-ingestion feature with types, API client, hooks,
PendingAssociationBanner (dashboard), PendingAssociationList, and
ResolveAssociationDialog. Mobile-first responsive with 44px touch
targets and full-screen dialogs on small screens.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Extract all notification logic from EmailIngestionService into
dedicated EmailIngestionNotificationHandler class
- Add notification_logs entries for every email sent (success/failure)
- Add in-app user_notifications for all error scenarios (no vehicles,
no attachments, OCR failure, processing failure)
- Update email templates with enhanced variables: merchantName,
totalAmount, date, guidance
- Update pending vehicle notification title to 'Vehicle Selection Required'
- Add sample variables for receipt templates in test email flow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New ReceiptClassifier module with keyword-based classification for
fuel vs maintenance receipts from email text and OCR raw text
- Classifier-first pipeline: classify from email subject/body keywords
before falling back to OCR-based classification
- Fuel keywords: gas, fuel, gallons, octane, pump, diesel, unleaded,
shell, chevron, exxon, bp
- Maintenance keywords: oil change, brake, alignment, tire, rotation,
inspection, labor, parts, service, repair, transmission, coolant
- Confident classification (>= 2 keyword matches) routes to specific
OCR endpoint; unclassified falls back to both endpoints + rawText
classification + field-count heuristic
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add receipt_document_id FK on maintenance_records, update types/repo/service
to support receipt linking on create and return document metadata on GET.
Add OCR proxy endpoint POST /api/ocr/extract/maintenance-receipt with
tier gating (maintenance.receiptScan) through full chain: routes -> controller
-> service -> client.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add filename .pdf extension fallback and %PDF magic bytes validation to
extractManual controller. Update getJobStatus to return 410 Gone for
expired jobs. Add 16 unit tests covering all acceptance criteria.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add 5000ms timeout to Places Text Search API call in searchStationByName.
Timeout errors log a warning instead of error and return null gracefully.
Add timeout test case to station-matching unit tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add/update documentation across backend, Python OCR service, and frontend
for receipt scanning, manual extraction, and Gemini integration. Create
new CLAUDE.md files for engines/, fuel-logs/, documents/, and maintenance/
features.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add POST /api/ocr/extract/manual endpoint that proxies to the Python
OCR service's manual extraction pipeline. Includes Pro tier gating via
document.scanMaintenanceSchedule, PDF-only validation, 200MB file size
limit, and async 202 job response for polling via existing job status
endpoint.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add Google Places Text Search to match receipt merchant names (e.g.
"Shell", "COSTCO #123") to real gas stations. Backend exposes
POST /api/stations/match endpoint. Frontend calls it after OCR
extraction and pre-fills locationData with matched station's placeId,
name, and address. Users can clear the match in the review modal.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add POST /api/ocr/extract/receipt endpoint that proxies to the Python
OCR service's /extract/receipt for receipt-specific field extraction.
- ReceiptExtractionResponse type with receiptType, extractedFields, rawText
- OcrClient.extractReceipt() with optional receipt_type form field
- OcrService.extractReceipt() with 10MB max, image-only validation
- OcrController.extractReceipt() with file upload and error mapping
- Route with auth middleware
- 9 unit tests covering normal, edge, and error scenarios
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
OCR Service (Python/FastAPI):
- POST /extract for synchronous OCR extraction
- POST /jobs and GET /jobs/{job_id} for async processing
- Image preprocessing (deskew, denoise) for accuracy
- HEIC conversion via pillow-heif
- Redis job queue for async processing
Backend (Fastify):
- POST /api/ocr/extract - authenticated proxy to OCR
- POST /api/ocr/jobs - async job submission
- GET /api/ocr/jobs/:jobId - job polling
- Multipart file upload handling
- JWT authentication required
File size limits: 10MB sync, 200MB async
Processing time target: <3 seconds for typical photos
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Changed INSERT to INSERT...ON CONFLICT DO UPDATE so the migration works for:
- Fresh deployments (inserts new template)
- Existing databases (updates template to fix variable substitution)
Removed unnecessary migration 008.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The original migration already inserted the template with Handlebars conditionals.
This migration updates the existing record to use simple variable substitution.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The TemplateService only supports {{variable}} substitution, not Handlebars-style
conditionals. Changed to use a single {{additionalInfo}} variable that is built
in the service code based on upgrade/downgrade status.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds email and in-app notifications when user subscription tier changes:
- Extended TemplateKey type with 'subscription_tier_change'
- Added migration for tier change email template with HTML
- Added sendTierChangeNotification() to NotificationsService
- Integrated notifications into upgradeSubscription, downgradeSubscription, adminOverrideTier
- Integrated notifications into grace-period.job.ts for auto-downgrades
Notifications include previous tier, new tier, and reason for change.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- GET /api/vehicles now uses getUserVehiclesWithTierStatus() and filters
out vehicles with tierStatus='locked' so only selected vehicles appear
in the vehicle list
- GET /api/vehicles/:id now checks tier status and returns 403 TIER_REQUIRED
if user tries to access a locked vehicle directly
This ensures that after a user selects 2 vehicles during downgrade to
free tier, only those 2 vehicles appear in the summary and details screens.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The grace-period job was using 'user_id' to query user_profiles table,
but the correct column name is 'auth0_sub'. This would cause the tier
sync to fail during grace period auto-downgrade.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add adminOverrideTier() method to SubscriptionsService that atomically
updates both subscriptions.tier and user_profiles.subscription_tier
using database transactions.
Changes:
- SubscriptionsRepository: Add updateTierByUserId() and
createForAdminOverride() methods with transaction support
- SubscriptionsService: Add adminOverrideTier() method with transaction
wrapping for atomic dual-table updates
- UsersController: Replace userProfileService.updateSubscriptionTier()
with subscriptionsService.adminOverrideTier()
This ensures admin tier changes properly sync to both database tables,
fixing the Settings page "Current Plan" display mismatch.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>