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>
This commit is contained in:
Eric Gullickson
2026-01-12 21:28:25 -06:00
parent 5c93150a58
commit a8c4eba8d1
19 changed files with 1644 additions and 15 deletions

View File

@@ -28,6 +28,7 @@ import { NHTSAClient, NHTSADecodeResponse, DecodedVehicleData, MatchedField } fr
import { canAddVehicle, getVehicleLimitConfig } from '../../../core/config/feature-tiers';
import { UserProfileRepository } from '../../user-profile/data/user-profile.repository';
import { SubscriptionTier } from '../../user-profile/domain/user-profile.types';
import { OwnershipCostsService } from '../../ownership-costs';
import { FuelLogsService } from '../../fuel-logs/domain/fuel-logs.service';
import { FuelLogsRepository } from '../../fuel-logs/data/fuel-logs.repository';
import { MaintenanceService } from '../../maintenance/domain/maintenance.service';
@@ -430,20 +431,35 @@ export class VehiclesService {
// Get fixed costs from vehicle record
const purchasePrice = vehicle.purchasePrice || 0;
// Normalize recurring costs based on purchase date
const insuranceCosts = this.normalizeRecurringCost(
vehicle.insuranceCost,
vehicle.insuranceInterval,
vehicle.purchaseDate
);
const registrationCosts = this.normalizeRecurringCost(
vehicle.registrationCost,
vehicle.registrationInterval,
vehicle.purchaseDate
);
// Get recurring ownership costs from ownership-costs service
const ownershipCostsService = new OwnershipCostsService(this.pool);
let insuranceCosts = 0;
let registrationCosts = 0;
let taxCosts = 0;
let otherCosts = 0;
try {
const ownershipStats = await ownershipCostsService.getVehicleCostStats(id, userId);
insuranceCosts = ownershipStats.insuranceCosts || 0;
registrationCosts = ownershipStats.registrationCosts || 0;
taxCosts = ownershipStats.taxCosts || 0;
otherCosts = ownershipStats.otherCosts || 0;
} catch {
// Vehicle may have no ownership cost records
// Fall back to legacy vehicle fields if they exist
insuranceCosts = this.normalizeRecurringCost(
vehicle.insuranceCost,
vehicle.insuranceInterval,
vehicle.purchaseDate
);
registrationCosts = this.normalizeRecurringCost(
vehicle.registrationCost,
vehicle.registrationInterval,
vehicle.purchaseDate
);
}
// Calculate lifetime total
const lifetimeTotal = purchasePrice + insuranceCosts + registrationCosts + fuelCosts + maintenanceCosts;
// Calculate lifetime total (includes all ownership costs: insurance, registration, tax, other)
const lifetimeTotal = purchasePrice + insuranceCosts + registrationCosts + taxCosts + otherCosts + fuelCosts + maintenanceCosts;
// Calculate cost per distance
const odometerReading = vehicle.odometerReading || 0;
@@ -454,6 +470,8 @@ export class VehiclesService {
purchasePrice,
insuranceCosts,
registrationCosts,
taxCosts,
otherCosts,
fuelCosts,
maintenanceCosts,
lifetimeTotal,

View File

@@ -201,6 +201,8 @@ export interface TCOResponse {
purchasePrice: number;
insuranceCosts: number;
registrationCosts: number;
taxCosts: number;
otherCosts: number;
fuelCosts: number;
maintenanceCosts: number;
lifetimeTotal: number;