- 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>
API Endpoints (all authenticated):
- GET /api/subscriptions - current subscription status
- POST /api/subscriptions/checkout - create Stripe subscription
- POST /api/subscriptions/cancel - schedule cancellation at period end
- POST /api/subscriptions/reactivate - cancel pending cancellation
- PUT /api/subscriptions/payment-method - update payment method
- GET /api/subscriptions/invoices - billing history
Grace Period Job:
- Daily cron at 2:30 AM to check expired grace periods
- Downgrades to free tier when 30-day grace period expires
- Syncs tier to user_profiles.subscription_tier
Email Templates:
- payment_failed_immediate (first failure)
- payment_failed_7day (7 days before grace ends)
- payment_failed_1day (1 day before grace ends)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Create 4 new tables: subscriptions, subscription_events, donations, tier_vehicle_selections
- Add StripeClient wrapper with createCustomer, createSubscription, cancelSubscription,
updatePaymentMethod, createPaymentIntent, constructWebhookEvent methods
- Implement SubscriptionsRepository with full CRUD and mapRow case conversion
- Add domain types for all subscription entities
- Install stripe npm package v20.2.0
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
PostgreSQL DECIMAL columns return as strings from pg driver.
- Add Number() conversion for fuelUnits and costPerUnit in toEnhancedResponse()
- Add query invalidation for 'all' key to fix dynamic updates
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Enhanced repository methods were incorrectly calling mapRow() which
converts snake_case to camelCase, but the service's toEnhancedResponse()
expects raw database rows with snake_case properties. This caused
"Invalid time value" errors when calling new Date(row.created_at).
Fixed methods:
- createEnhanced
- findByVehicleIdEnhanced
- findByUserIdEnhanced
- findByIdEnhanced
- getPreviousLogByOdometer
- getLatestLogForVehicle
- updateEnhanced
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The upload was hanging silently because breaking early from a
`for await` loop on a Node.js stream corrupts the stream's internal
state. The remaining stream could not be used afterward.
Changes:
- Collect ALL chunks from the file stream before processing
- Use subarray() for file type detection header (first 4100 bytes)
- Create single readable stream from complete buffer for storage
- Remove broken headerStream + remainingStream piping logic
This fixes the root cause where uploads would hang after logging
"Document upload requested" without ever completing or erroring.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Updates documents backend service and API to support multi-vehicle insurance documents:
- Service: createDocument/updateDocument validate and handle sharedVehicleIds for insurance docs
- Service: addVehicleToDocument validates ownership and adds vehicles to shared array
- Service: removeVehicleFromDocument with context-aware delete logic:
- Shared vehicle only: remove from array
- Primary with no shared: soft delete document
- Primary with shared: promote first shared to primary
- Service: getDocumentsByVehicle returns all docs for a vehicle (primary or shared)
- Controller: Added handlers for listByVehicle, addVehicle, removeVehicle with proper error handling
- Routes: Added POST/DELETE /documents/:id/vehicles/:vehicleId and GET /documents/by-vehicle/:vehicleId
- Validation: Added DocumentVehicleParamsSchema for vehicle management routes
Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add migration 004_add_shared_vehicle_ids.sql with UUID array column and GIN index
- Update DocumentRecord interface to include sharedVehicleIds field
- Add sharedVehicleIds to CreateDocumentBody and UpdateDocumentBody schemas
- Update repository mapDocumentRecord() to map shared_vehicle_ids from database
- Update insert() and batchInsert() to handle sharedVehicleIds
- Update updateMetadata() to support sharedVehicleIds updates
- Add addSharedVehicle() method using atomic array_append()
- Add removeSharedVehicle() method using atomic array_remove()
- Add listByVehicle() method to query by primary or shared vehicle
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The ownership_costs table was created with an outdated schema that had
different column names (start_date/end_date vs period_start/period_end)
and was missing the notes column. This migration aligns the database
schema with the current code expectations.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Milestone 6: Change empty interface to type alias to fix
@typescript-eslint/no-empty-object-type error
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Milestone 2: Auto-create ownership_cost when insurance/registration
document is created with cost data (premium or cost field).
- Add OwnershipCostsService integration
- Auto-create cost on document create when amount > 0
- Sync cost changes on document update
- mapDocumentTypeToCostType() validation
- extractCostAmount() for premium/cost field extraction
- CASCADE delete handled by FK constraint
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Milestone 1: Complete backend feature with:
- Migration with CHECK (amount > 0) constraint
- Repository with mapRow() for snake_case -> camelCase
- Service with CRUD and vehicle authorization
- Controller with HTTP handlers
- Routes registered at /api/ownership-costs
- Validation with Zod schemas
- README with endpoint documentation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Create ownership_costs table for recurring vehicle costs
- Add backend feature capsule with types, repository, service, routes
- Update TCO calculation to use ownership_costs (with fallback to legacy vehicle fields)
- Add taxCosts and otherCosts to TCO response
- Create frontend ownership-costs feature with form, list, API, hooks
- Update TCODisplay to show all cost types
This implements a more flexible approach to tracking recurring ownership costs
(insurance, registration, tax, other) with explicit date ranges and optional
document association.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Quality Review Fixes:
- Add comprehensive unit tests for getTCO() method (12 test cases)
- Add tests for normalizeRecurringCost() via getTCO integration
- Add future date validation guard in calculateMonthsOwned()
- Fix pre-existing unused React import in VehicleLimitDialog.test.tsx
- Fix pre-existing test parameter types in vehicles.service.test.ts
Test Coverage:
- Vehicle not found / unauthorized access
- Missing optional TCO fields handling
- Zero odometer (costPerDistance = 0)
- Monthly/semi-annual/annual cost normalization
- Division by zero guard (new purchase)
- Future purchase date handling
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add GET /api/vehicles/:id/tco route
- Add getTCO controller method with error handling
- Returns 200 with TCO data, 404 for not found, 403 for unauthorized
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add TCOResponse interface
- Add getTCO() method aggregating all cost sources
- Add normalizeRecurringCost() with division-by-zero guard
- Integrate FuelLogsService and MaintenanceService for cost data
- Respect user preferences for distance unit and currency
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add MaintenanceCostStats interface
- Add getVehicleMaintenanceCosts() method to maintenance service
- Validates numeric cost values and throws on invalid data
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add CostInterval type and PAYMENTS_PER_YEAR constant
- Add 7 TCO fields to Vehicle, CreateVehicleRequest, UpdateVehicleRequest
- Update VehicleResponse and Body types
- Update mapRow() with snake_case to camelCase mapping
- Update create(), update(), batchInsert() for new fields
- Add Zod validation for TCO fields with interval enum
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>