Commit Graph

147 Commits

Author SHA1 Message Date
Eric Gullickson
2a34f8225e feat: migrate backend logging from Winston to Pino with correlation IDs (refs #82)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 4m3s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 32s
Deploy to Staging / Verify Staging (pull_request) Successful in 2m29s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
- Replace Winston with Pino using API-compatible wrapper
- Add LOG_LEVEL env var support with validation and fallback
- Add correlation ID middleware (X-Request-Id from Traefik or UUID)
- Configure PostgreSQL logging env vars (POSTGRES_LOG_STATEMENT, POSTGRES_LOG_MIN_DURATION)
- Configure Redis loglevel via command args

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 20:04:30 -06:00
Eric Gullickson
852c9013b5 feat: add core OCR API integration (refs #65)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 5m59s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 31s
Deploy to Staging / Verify Staging (pull_request) Successful in 2m19s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
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>
2026-02-01 16:02:11 -06:00
Eric Gullickson
3781b05d72 fix: move user-profile before documents in migration order (refs #64)
Some checks failed
Deploy to Staging / Build Images (pull_request) Successful in 31s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 31s
Deploy to Staging / Verify Staging (pull_request) Failing after 53s
Deploy to Staging / Notify Staging Ready (pull_request) Has been skipped
Deploy to Staging / Notify Staging Failure (pull_request) Successful in 8s
The documents migration 003_reset_scan_for_maintenance_free_users.sql
depends on user_profiles table which is created by user-profile feature.
Move user-profile earlier in MIGRATION_ORDER to fix staging deployment.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 13:28:07 -06:00
Eric Gullickson
1614ef697b fix: use upsert for tier change template migration (refs #59)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 3m5s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 30s
Deploy to Staging / Verify Staging (pull_request) Successful in 8s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
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>
2026-01-31 20:22:53 -06:00
Eric Gullickson
706851f396 fix: add migration to update existing tier change template (refs #59)
Some checks failed
Deploy to Staging / Build Images (pull_request) Successful in 3m4s
Deploy to Staging / Deploy to Staging (pull_request) Has been cancelled
Deploy to Staging / Verify Staging (pull_request) Has been cancelled
Deploy to Staging / Notify Staging Ready (pull_request) Has been cancelled
Deploy to Staging / Notify Staging Failure (pull_request) Has been cancelled
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>
2026-01-31 20:21:17 -06:00
Eric Gullickson
86b2e46798 fix: replace template conditionals with simple variable substitution (refs #59)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 3m33s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 39s
Deploy to Staging / Verify Staging (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
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>
2026-01-31 20:02:51 -06:00
Eric Gullickson
cc2898f6ff feat: send notifications when subscription tier changes (refs #59)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 7m15s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 30s
Deploy to Staging / Verify Staging (pull_request) Successful in 8s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
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>
2026-01-31 19:50:34 -06:00
Eric Gullickson
68948484a4 fix: filter locked vehicles after tier downgrade selection (refs #60)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 2m38s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 29s
Deploy to Staging / Verify Staging (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
- 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>
2026-01-24 11:51:36 -06:00
Eric Gullickson
684615a8a2 feat: add needs-vehicle-selection endpoint (refs #60)
- Add GET /api/subscriptions/needs-vehicle-selection endpoint
- Returns { needsSelection, vehicleCount, maxAllowed }
- Checks: free tier, >2 vehicles, no existing selections

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 11:25:57 -06:00
Eric Gullickson
8c86d8d492 fix: correct user_profiles column name in grace-period job (refs #58)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 3m9s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 29s
Deploy to Staging / Verify Staging (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
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>
2026-01-19 09:53:45 -06:00
Eric Gullickson
2c0cbd5bf7 fix: sync subscription tier on admin override (refs #58)
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>
2026-01-19 09:03:50 -06:00
Eric Gullickson
0674056e7e fix: add subscriptions to migration order (refs #55)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 2m41s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 38s
Deploy to Staging / Verify Staging (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
The subscriptions feature migration was not being run because it was
missing from the MIGRATION_ORDER array. Added it after ownership-costs
since it depends on user-profile (for subscription_tier enum) and
vehicles (for FK relationships).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 20:22:42 -06:00
Eric Gullickson
1718e8d41b fix: use file-based secrets for Stripe API keys (refs #55) 2026-01-18 18:02:10 -06:00
Eric Gullickson
1cf4b78075 docs: update subscription feature documentation - M7 (refs #55)
Some checks failed
Deploy to Staging / Build Images (pull_request) Successful in 6m58s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 28s
Deploy to Staging / Verify Staging (pull_request) Failing after 17s
Deploy to Staging / Notify Staging Ready (pull_request) Has been skipped
Deploy to Staging / Notify Staging Failure (pull_request) Successful in 6s
2026-01-18 16:52:50 -06:00
Eric Gullickson
56da99de36 feat: add donations feature with one-time payments - M6 (refs #55) 2026-01-18 16:51:20 -06:00
Eric Gullickson
6c1a100eb9 feat: add vehicle selection and downgrade flow - M5 (refs #55) 2026-01-18 16:44:45 -06:00
Eric Gullickson
e7461a4836 feat: add subscription API endpoints and grace period job - M3 (refs #55)
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>
2026-01-18 16:16:58 -06:00
Eric Gullickson
7a0c09b83f feat: add subscriptions service layer and webhook endpoint - M2 (refs #55)
- Implement SubscriptionsService with getSubscription, createSubscription,
  upgradeSubscription, cancelSubscription, reactivateSubscription
- Add handleWebhookEvent for Stripe webhook processing with idempotency
- Handle 5 webhook events: subscription.created/updated/deleted, invoice.payment_succeeded/failed
- Auto-sync tier changes to user_profiles.subscription_tier
- Add public webhook endpoint POST /api/webhooks/stripe (signature verified)
- Implement 30-day grace period on payment failure

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 16:10:20 -06:00
Eric Gullickson
88b820b1c3 feat: add subscriptions feature capsule - M1 database schema and Stripe client (refs #55)
- 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>
2026-01-18 16:04:11 -06:00
Eric Gullickson
5c62b6ac96 fix: convert DECIMAL columns to numbers in fuel logs API (refs #49)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 4m50s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 38s
Deploy to Staging / Verify Staging (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
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>
2026-01-17 22:37:59 -06:00
Eric Gullickson
574acf3e87 fix: return raw rows from enhanced repository methods (refs #47)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 2m28s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 29s
Deploy to Staging / Verify Staging (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
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>
2026-01-17 22:08:23 -06:00
Eric Gullickson
60aa0acbe0 chore: remove file
All checks were successful
Deploy to Staging / Build Images (push) Successful in 2m23s
Deploy to Staging / Deploy to Staging (push) Successful in 38s
Deploy to Staging / Verify Staging (push) Successful in 7s
Deploy to Staging / Notify Staging Ready (push) Successful in 7s
Deploy to Staging / Notify Staging Failure (push) Has been skipped
2026-01-14 21:28:57 -06:00
ec8e6ee5d2 Merge pull request 'feat: Document feature enhancements (#31)' (#32) from issue-31-document-enhancements into main
All checks were successful
Deploy to Staging / Build Images (push) Successful in 4m43s
Deploy to Staging / Deploy to Staging (push) Successful in 38s
Deploy to Staging / Verify Staging (push) Successful in 7s
Deploy to Staging / Notify Staging Ready (push) Successful in 6s
Deploy to Staging / Notify Staging Failure (push) Has been skipped
Reviewed-on: #32
2026-01-15 02:35:55 +00:00
Eric Gullickson
a3b119a953 fix: resolve document upload hang by fixing stream pipeline (refs #33)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 2m22s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 29s
Deploy to Staging / Verify Staging (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
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>
2026-01-14 20:28:19 -06:00
Eric Gullickson
5dbc17e28d feat: add document-vehicle API endpoints and context-aware delete (refs #31)
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>
2026-01-14 19:28:00 -06:00
Eric Gullickson
57debe4252 feat: add shared_vehicle_ids schema and repository methods (refs #31)
- 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>
2026-01-14 19:24:34 -06:00
Eric Gullickson
025ab30726 fix: add schema migration for ownership_costs table (refs #29)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 2m21s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 28s
Deploy to Staging / Verify Staging (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
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>
2026-01-13 21:51:44 -06:00
Eric Gullickson
1d95eba395 fix: resolve lint error in ownership-costs types (refs #29)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 4m37s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 37s
Deploy to Staging / Verify Staging (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
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>
2026-01-13 21:37:30 -06:00
Eric Gullickson
7928b87ef5 feat: integrate DocumentsService with ownership_costs (refs #29)
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>
2026-01-13 21:30:02 -06:00
Eric Gullickson
81b1c3dd70 feat: create ownership_costs backend feature capsule (refs #29)
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>
2026-01-13 21:28:43 -06:00
Eric Gullickson
395670c3bd fix: add ownership-costs to migration order and improve error handling (refs #15)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 4m41s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 38s
Deploy to Staging / Verify Staging (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
- Add 'features/ownership-costs' to MIGRATION_ORDER in run-all.ts
- Improve OwnershipCostsList error display to not block the page
- Show friendly message when feature needs migration
2026-01-13 08:15:53 -06:00
Eric Gullickson
a8c4eba8d1 feat: add ownership-costs feature capsule (refs #15)
- 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>
2026-01-12 21:28:25 -06:00
Eric Gullickson
5c93150a58 fix: add TCO unit tests and fix blocking issues (refs #15)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 4m34s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 28s
Deploy to Staging / Verify Staging (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
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>
2026-01-12 20:32:15 -06:00
Eric Gullickson
47de6898cd feat: add TCO API endpoint (refs #15)
- 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>
2026-01-12 20:02:15 -06:00
Eric Gullickson
381f602e9f feat: add TCO calculation service (refs #15)
- 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>
2026-01-12 20:01:24 -06:00
Eric Gullickson
35fd1782b4 feat: add maintenance cost aggregation for TCO (refs #15)
- 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>
2026-01-12 19:59:41 -06:00
Eric Gullickson
8517b1ded2 feat: add TCO types and repository updates (refs #15)
- 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>
2026-01-12 19:58:59 -06:00
Eric Gullickson
b0d79a26ae feat: add TCO fields migration (refs #15)
Add database columns for Total Cost of Ownership:
- purchase_price, purchase_date
- insurance_cost, insurance_interval
- registration_cost, registration_interval
- tco_enabled toggle

Includes CHECK constraints for interval values and non-negative costs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 19:56:30 -06:00
Eric Gullickson
28574b0eb4 fix: preserve vehicle identity by checking ID first in merge mode (refs #26)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 2m21s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 28s
Deploy to Staging / Verify Staging (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
Critical fix for merge mode vehicle matching logic.

Problem:
- Vehicles with same license plate but no VIN were matched to the same existing vehicle
- Example: 2 vehicles with license plate "TEST-123" both updated the same vehicle
- Result: "Updated: 2" but only 1 vehicle in database, second vehicle overwrites first

Root Cause:
- Matching order was: VIN → license plate
- Both vehicles had no VIN and same license plate
- Both matched the same existing vehicle by license plate

Solution:
- New matching order: ID → VIN → license plate
- Preserves vehicle identity across export/import cycles
- Vehicles exported with IDs will update the same vehicle on re-import
- New vehicles (no matching ID) will be created as new records
- Security check: Verify ID belongs to same user before matching

Benefits:
- Export-modify-import workflow now works correctly
- Vehicles maintain identity across imports
- Users can safely import data with duplicate license plates
- Prevents unintended overwrites

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-11 21:15:30 -06:00
Eric Gullickson
62b4dc31ab debug: add comprehensive logging to vehicle import merge (refs #26)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 2m52s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 28s
Deploy to Staging / Verify Staging (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
Added detailed logging to diagnose import merge issues:

- Log vehicle count at merge start
- Log each vehicle being processed with VIN/make/model/year
- Log when existing vehicles are found (by VIN or license plate)
- Log successful vehicle creation with new vehicle ID
- Log errors with full context (userId, VIN, make, model, error message)
- Log merge completion with summary statistics

This will help diagnose why vehicles show as "successfully imported" but don't appear in the UI.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-11 21:03:20 -06:00
Eric Gullickson
f48a18287b fix: prevent vehicle duplication and enforce tier limits in merge mode (refs #26)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 2m20s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 28s
Deploy to Staging / Verify Staging (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
Critical bug fixes for import merge mode:

1. Vehicle duplication bug (RULE 0 - CRITICAL):
   - Previous: Vehicles without VINs always inserted as new, creating duplicates
   - Fixed: Check by VIN first, then fallback to license plate matching
   - Impact: Prevents duplicate vehicles on repeated imports

2. Vehicle limit bypass (RULE 0 - CRITICAL):
   - Previous: Direct repo.create() bypassed tier-based vehicle limits
   - Fixed: Use VehiclesService.createVehicle() which enforces FOR UPDATE locking and tier checks
   - Impact: Free users properly limited to 1 vehicle, prevents limit violations

Changes:
- Added VehiclesService to import service constructor
- Updated mergeVehicles() to check VIN then license plate for matches
- Replace repo.create() with service.createVehicle() for limit enforcement
- Added VehicleLimitExceededError handling with clear error messages

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-11 20:54:38 -06:00
Eric Gullickson
197927ef31 test: add integration tests and documentation (refs #26)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 4m37s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 29s
Deploy to Staging / Verify Staging (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-11 20:05:06 -06:00
Eric Gullickson
068db991a4 chore: Update footer 2026-01-11 19:51:34 -06:00
Eric Gullickson
a35d05f08a feat: add import service and API layer (refs #26)
Implements Milestone 3: Backend import service and API with:

Service Layer (user-import.service.ts):
- generatePreview(): extract archive, validate, detect VIN conflicts
- executeMerge(): chunk-based import (100 records/batch), UPDATE existing by VIN, INSERT new via batchInsert
- executeReplace(): transactional DELETE all user data, batchInsert all records
- Conflict detection: VIN duplicates in vehicles
- Error handling: collect errors per record, continue, report in summary
- File handling: copy vehicle images and documents from archive to storage
- Cleanup: delete temp directory in finally block

API Layer:
- POST /api/user/import: multipart upload, mode selection (merge/replace)
- POST /api/user/import/preview: preview without executing import
- Authentication: fastify.authenticate preHandler
- Content-Type validation: application/gzip or application/x-gzip
- Magic byte validation: FileType.fromBuffer verifies tar.gz
- Request validation: Zod schema for mode selection
- Response: ImportResult with success, mode, summary, warnings

Files Created:
- backend/src/features/user-import/domain/user-import.service.ts
- backend/src/features/user-import/api/user-import.controller.ts
- backend/src/features/user-import/api/user-import.routes.ts
- backend/src/features/user-import/api/user-import.validation.ts

Files Updated:
- backend/src/app.ts: register userImportRoutes with /api prefix

Quality:
- Type-check: PASS (0 errors)
- Linting: PASS (0 errors, 470 warnings - all pre-existing)
- Repository pattern: snake_case→camelCase conversion
- User-scoped: all queries filter by user_id
- Transaction boundaries: Replace mode atomic, Merge mode per-batch

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-11 19:50:59 -06:00
Eric Gullickson
ffadc48b4f feat: add archive extraction and validation service (refs #26)
Implement archive service to extract and validate user data import archives. Validates manifest structure, data files, and ensures archive format compatibility with export feature.

- user-import.types.ts: Type definitions for import feature
- user-import-archive.service.ts: Archive extraction and validation
- Validates manifest version (1.0.0) and required fields
- Validates all data files exist and contain valid JSON
- Temp directory pattern mirrors export (/tmp/user-import-work)
- Cleanup method for archive directories

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-11 19:30:43 -06:00
Eric Gullickson
e6af7ed5d5 feat: add batch insert operations to repositories (refs #26)
Add batchInsert methods to vehicles, fuel-logs, maintenance, and documents repositories. Multi-value INSERT syntax provides 10-100x performance improvement over individual operations for bulk data import.

- vehicles.repository: batchInsert for vehicles
- fuel-logs.repository: batchInsert for fuel logs
- maintenance.repository: batchInsertRecords and batchInsertSchedules
- documents.repository: batchInsert for documents
- All methods support empty array (immediate return) and optional transaction client
- Fix lint error: replace require() with ES6 import in test mock

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-11 19:28:11 -06:00
Eric Gullickson
8703e7758a fix: Replace COUNT(*) with SELECT id in FOR UPDATE query (refs #23)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 2m18s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 29s
Deploy to Staging / Verify Staging (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
PostgreSQL error 0A000 (feature_not_supported) occurs when using
FOR UPDATE with aggregate functions like COUNT(*). Row-level locking
requires actual rows to lock.

Changes:
- Select id column instead of COUNT(*) aggregate
- Count rows in application using .length
- Maintains transaction isolation and race condition prevention

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-11 18:08:49 -06:00
Eric Gullickson
20189a1d37 feat: Add tier-based vehicle limit enforcement (refs #23)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 4m37s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 38s
Deploy to Staging / Verify Staging (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
Backend:
- Add VEHICLE_LIMITS configuration to feature-tiers.ts
- Add getVehicleLimit, canAddVehicle helper functions
- Implement transaction-based limit check with FOR UPDATE locking
- Add VehicleLimitExceededError and 403 TIER_REQUIRED response
- Add countByUserId to VehiclesRepository
- Add comprehensive tests for all limit logic

Frontend:
- Add getResourceLimit, isAtResourceLimit to useTierAccess hook
- Create VehicleLimitDialog component with mobile/desktop modes
- Add useVehicleLimitCheck shared hook for limit state
- Update VehiclesPage with limit checks and lock icon
- Update VehiclesMobileScreen with limit checks
- Add tests for VehicleLimitDialog

Implements vehicle limits per tier (Free: 2, Pro: 5, Enterprise: unlimited)
with race condition prevention and consistent UX across mobile/desktop.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-11 16:36:53 -06:00
Eric Gullickson
a6607d5882 feat: Add fuzzy matching to VIN decode for partial model/trim names (refs #9)
Some checks failed
Deploy to Staging / Build Images (pull_request) Failing after 3m1s
Deploy to Staging / Deploy to Staging (pull_request) Has been skipped
Deploy to Staging / Verify Staging (pull_request) Has been skipped
Deploy to Staging / Notify Staging Ready (pull_request) Has been skipped
Deploy to Staging / Notify Staging Failure (pull_request) Successful in 6s
Backend: Enhanced matchField function with prefix and contains matching
so NHTSA values like "Sierra" match dropdown options like "Sierra 1500".

Matching hierarchy:
1. Exact match (case-insensitive) -> high confidence
2. Normalized match (remove special chars) -> medium confidence
3. Prefix match (option starts with value) -> medium confidence (NEW)
4. Contains match (option contains value) -> medium confidence (NEW)

Frontend: Fixed VIN decode form population by loading dropdown options
before setting form values, preventing cascade useEffects from clearing
decoded values.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 16:12:09 -06:00
Eric Gullickson
9b4f94e1ee docs: Update vehicles README with VIN decode endpoint (refs #9)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 4m36s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 28s
Deploy to Staging / Verify Staging (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
- Add VIN decode endpoint to API section
- Document request/response format with confidence levels
- Add error response examples (400, 403, 502)
- Update architecture diagram with external/ directory

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 13:56:32 -06:00