Files
motovaultpro/docs/changes/fuel-logs-v1/FUEL-LOGS-PHASE-1.md
Eric Gullickson a052040e3a Initial Commit
2025-09-17 16:09:15 -05:00

391 lines
12 KiB
Markdown

# Phase 1: Database Schema & Core Logic
## Overview
Establish the database foundation for enhanced fuel logs with new fields, validation rules, and core type system updates.
## Prerequisites
- Existing fuel logs feature (basic implementation)
- PostgreSQL database with current `fuel_logs` table
- Migration system functional
## Database Schema Changes
### New Fields to Add
```sql
-- Add these columns to fuel_logs table
ALTER TABLE fuel_logs ADD COLUMN trip_distance INTEGER; -- Alternative to odometer reading
ALTER TABLE fuel_logs ADD COLUMN fuel_type VARCHAR(20) NOT NULL DEFAULT 'gasoline';
ALTER TABLE fuel_logs ADD COLUMN fuel_grade VARCHAR(10);
ALTER TABLE fuel_logs ADD COLUMN fuel_units DECIMAL(8,3); -- Replaces gallons for metric support
ALTER TABLE fuel_logs ADD COLUMN cost_per_unit DECIMAL(6,3); -- Replaces price_per_gallon
ALTER TABLE fuel_logs ADD COLUMN location_data JSONB; -- Future Google Maps integration
ALTER TABLE fuel_logs ADD COLUMN date_time TIMESTAMP WITH TIME ZONE; -- Enhanced date/time
-- Add constraints
ALTER TABLE fuel_logs ADD CONSTRAINT fuel_type_check
CHECK (fuel_type IN ('gasoline', 'diesel', 'electric'));
-- Add conditional constraint: either trip_distance OR odometer_reading required
ALTER TABLE fuel_logs ADD CONSTRAINT distance_required_check
CHECK ((trip_distance IS NOT NULL AND trip_distance > 0) OR (odometer_reading IS NOT NULL AND odometer_reading > 0));
-- Add indexes for performance
CREATE INDEX idx_fuel_logs_fuel_type ON fuel_logs(fuel_type);
CREATE INDEX idx_fuel_logs_date_time ON fuel_logs(date_time);
```
### Migration Strategy
#### Step 1: Additive Migration
**File**: `backend/src/features/fuel-logs/migrations/002_enhance_fuel_logs_schema.sql`
```sql
-- Migration: 002_enhance_fuel_logs_schema.sql
BEGIN;
-- Add new columns (nullable initially for data migration)
ALTER TABLE fuel_logs ADD COLUMN IF NOT EXISTS trip_distance INTEGER;
ALTER TABLE fuel_logs ADD COLUMN IF NOT EXISTS fuel_type VARCHAR(20);
ALTER TABLE fuel_logs ADD COLUMN IF NOT EXISTS fuel_grade VARCHAR(10);
ALTER TABLE fuel_logs ADD COLUMN IF NOT EXISTS fuel_units DECIMAL(8,3);
ALTER TABLE fuel_logs ADD COLUMN IF NOT EXISTS cost_per_unit DECIMAL(6,3);
ALTER TABLE fuel_logs ADD COLUMN IF NOT EXISTS location_data JSONB;
ALTER TABLE fuel_logs ADD COLUMN IF NOT EXISTS date_time TIMESTAMP WITH TIME ZONE;
-- Migrate existing data
UPDATE fuel_logs SET
fuel_type = 'gasoline',
fuel_units = gallons,
cost_per_unit = price_per_gallon,
date_time = date::timestamp + interval '12 hours' -- Default to noon
WHERE fuel_type IS NULL;
-- Add constraints after data migration
ALTER TABLE fuel_logs ALTER COLUMN fuel_type SET NOT NULL;
ALTER TABLE fuel_logs ALTER COLUMN fuel_type SET DEFAULT 'gasoline';
-- Add check constraints
ALTER TABLE fuel_logs ADD CONSTRAINT fuel_type_check
CHECK (fuel_type IN ('gasoline', 'diesel', 'electric'));
-- Distance requirement constraint (either trip_distance OR odometer_reading)
ALTER TABLE fuel_logs ADD CONSTRAINT distance_required_check
CHECK ((trip_distance IS NOT NULL AND trip_distance > 0) OR
(odometer_reading IS NOT NULL AND odometer_reading > 0));
-- Add performance indexes
CREATE INDEX IF NOT EXISTS idx_fuel_logs_fuel_type ON fuel_logs(fuel_type);
CREATE INDEX IF NOT EXISTS idx_fuel_logs_date_time ON fuel_logs(date_time);
COMMIT;
```
#### Step 2: Backward Compatibility Plan
- Keep existing `gallons` and `price_per_gallon` fields during transition
- Update application logic to use new fields preferentially
- Plan deprecation of old fields in future migration
### Data Validation Rules
#### Core Business Rules
1. **Distance Requirement**: Either `trip_distance` OR `odometer_reading` must be provided
2. **Fuel Type Validation**: Must be one of: 'gasoline', 'diesel', 'electric'
3. **Fuel Grade Validation**: Must match fuel type options
4. **Positive Values**: All numeric fields must be > 0
5. **DateTime**: Cannot be in the future
#### Fuel Grade Validation Logic
```sql
-- Fuel grade validation by type
CREATE OR REPLACE FUNCTION validate_fuel_grade()
RETURNS TRIGGER AS $$
BEGIN
-- Gasoline grades
IF NEW.fuel_type = 'gasoline' AND
NEW.fuel_grade NOT IN ('87', '88', '89', '91', '93') THEN
RAISE EXCEPTION 'Invalid fuel grade % for gasoline', NEW.fuel_grade;
END IF;
-- Diesel grades
IF NEW.fuel_type = 'diesel' AND
NEW.fuel_grade NOT IN ('#1', '#2') THEN
RAISE EXCEPTION 'Invalid fuel grade % for diesel', NEW.fuel_grade;
END IF;
-- Electric (no grades)
IF NEW.fuel_type = 'electric' AND
NEW.fuel_grade IS NOT NULL THEN
RAISE EXCEPTION 'Electric fuel type cannot have a grade';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Create trigger
CREATE TRIGGER fuel_grade_validation_trigger
BEFORE INSERT OR UPDATE ON fuel_logs
FOR EACH ROW EXECUTE FUNCTION validate_fuel_grade();
```
## TypeScript Type System Updates
### New Core Types
**File**: `backend/src/features/fuel-logs/domain/fuel-logs.types.ts`
```typescript
// Fuel system enums
export enum FuelType {
GASOLINE = 'gasoline',
DIESEL = 'diesel',
ELECTRIC = 'electric'
}
export enum GasolineFuelGrade {
REGULAR_87 = '87',
MIDGRADE_88 = '88',
MIDGRADE_89 = '89',
PREMIUM_91 = '91',
PREMIUM_93 = '93'
}
export enum DieselFuelGrade {
DIESEL_1 = '#1',
DIESEL_2 = '#2'
}
export type FuelGrade = GasolineFuelGrade | DieselFuelGrade | null;
// Unit system types
export enum UnitSystem {
IMPERIAL = 'imperial',
METRIC = 'metric'
}
export interface UnitConversion {
fuelUnits: string; // 'gallons' | 'liters'
distanceUnits: string; // 'miles' | 'kilometers'
efficiencyUnits: string; // 'mpg' | 'l/100km'
}
// Enhanced location data structure
export interface LocationData {
address?: string;
coordinates?: {
latitude: number;
longitude: number;
};
googlePlaceId?: string;
stationName?: string;
// Future: station prices, fuel availability
}
// Updated core FuelLog interface
export interface FuelLog {
id: string;
userId: string;
vehicleId: string;
dateTime: Date; // Enhanced from simple date
// Distance tracking (either/or required)
odometerReading?: number;
tripDistance?: number;
// Fuel system
fuelType: FuelType;
fuelGrade?: FuelGrade;
fuelUnits: number; // Replaces gallons
costPerUnit: number; // Replaces pricePerGallon
totalCost: number; // Auto-calculated
// Location (future Google Maps integration)
locationData?: LocationData;
// Legacy fields (maintain during transition)
gallons?: number; // Deprecated
pricePerGallon?: number; // Deprecated
// Metadata
notes?: string;
mpg?: number; // Calculated efficiency
createdAt: Date;
updatedAt: Date;
}
```
### Request/Response Type Updates
```typescript
export interface CreateFuelLogRequest {
vehicleId: string;
dateTime: string; // ISO datetime string
// Distance (either required)
odometerReading?: number;
tripDistance?: number;
// Fuel system
fuelType: FuelType;
fuelGrade?: FuelGrade;
fuelUnits: number;
costPerUnit: number;
// totalCost calculated automatically
// Location
locationData?: LocationData;
notes?: string;
}
export interface UpdateFuelLogRequest {
dateTime?: string;
odometerReading?: number;
tripDistance?: number;
fuelType?: FuelType;
fuelGrade?: FuelGrade;
fuelUnits?: number;
costPerUnit?: number;
locationData?: LocationData;
notes?: string;
}
```
## Core Validation Logic
### Business Rule Validation
**File**: `backend/src/features/fuel-logs/domain/fuel-logs.validation.ts`
```typescript
export class FuelLogValidation {
static validateDistanceRequirement(data: CreateFuelLogRequest | UpdateFuelLogRequest): void {
const hasOdometer = data.odometerReading && data.odometerReading > 0;
const hasTripDistance = data.tripDistance && data.tripDistance > 0;
if (!hasOdometer && !hasTripDistance) {
throw new ValidationError('Either odometer reading or trip distance is required');
}
if (hasOdometer && hasTripDistance) {
throw new ValidationError('Cannot specify both odometer reading and trip distance');
}
}
static validateFuelGrade(fuelType: FuelType, fuelGrade?: FuelGrade): void {
switch (fuelType) {
case FuelType.GASOLINE:
if (fuelGrade && !Object.values(GasolineFuelGrade).includes(fuelGrade as GasolineFuelGrade)) {
throw new ValidationError(`Invalid gasoline grade: ${fuelGrade}`);
}
break;
case FuelType.DIESEL:
if (fuelGrade && !Object.values(DieselFuelGrade).includes(fuelGrade as DieselFuelGrade)) {
throw new ValidationError(`Invalid diesel grade: ${fuelGrade}`);
}
break;
case FuelType.ELECTRIC:
if (fuelGrade) {
throw new ValidationError('Electric vehicles cannot have fuel grades');
}
break;
}
}
static validatePositiveValues(data: CreateFuelLogRequest | UpdateFuelLogRequest): void {
if (data.fuelUnits && data.fuelUnits <= 0) {
throw new ValidationError('Fuel units must be positive');
}
if (data.costPerUnit && data.costPerUnit <= 0) {
throw new ValidationError('Cost per unit must be positive');
}
if (data.odometerReading && data.odometerReading <= 0) {
throw new ValidationError('Odometer reading must be positive');
}
if (data.tripDistance && data.tripDistance <= 0) {
throw new ValidationError('Trip distance must be positive');
}
}
static validateDateTime(dateTime: string): void {
const date = new Date(dateTime);
const now = new Date();
if (date > now) {
throw new ValidationError('Cannot create fuel logs in the future');
}
}
}
```
## Implementation Tasks
### Database Tasks
1. ✅ Create migration file `002_enhance_fuel_logs_schema.sql`
2. ✅ Add new columns with appropriate types
3. ✅ Migrate existing data to new schema
4. ✅ Add database constraints and triggers
5. ✅ Create performance indexes
### Type System Tasks
1. ✅ Define fuel system enums
2. ✅ Create unit system types
3. ✅ Update core FuelLog interface
4. ✅ Update request/response interfaces
5. ✅ Add location data structure
### Validation Tasks
1. ✅ Create validation utility class
2. ✅ Implement distance requirement validation
3. ✅ Implement fuel grade validation
4. ✅ Add positive value checks
5. ✅ Add datetime validation
## Testing Requirements
### Database Testing
```sql
-- Test distance requirement constraint
INSERT INTO fuel_logs (...) -- Should fail without distance
INSERT INTO fuel_logs (trip_distance = 150, ...) -- Should succeed
INSERT INTO fuel_logs (odometer_reading = 50000, ...) -- Should succeed
INSERT INTO fuel_logs (trip_distance = 150, odometer_reading = 50000, ...) -- Should fail
-- Test fuel type/grade validation
INSERT INTO fuel_logs (fuel_type = 'gasoline', fuel_grade = '87', ...) -- Should succeed
INSERT INTO fuel_logs (fuel_type = 'gasoline', fuel_grade = '#1', ...) -- Should fail
INSERT INTO fuel_logs (fuel_type = 'electric', fuel_grade = '87', ...) -- Should fail
```
### Unit Tests Required
- Validation logic for all business rules
- Type conversion utilities
- Migration data integrity
- Constraint enforcement
## Success Criteria
### Phase 1 Complete When:
- ✅ Database migration runs successfully
- ✅ All new fields available with proper types
- ✅ Existing data migrated and preserved
- ✅ Database constraints enforce business rules
- ✅ TypeScript interfaces updated and compiling
- ✅ Core validation logic implemented and tested
- ✅ No breaking changes to existing functionality
### Ready for Phase 2 When:
- All database changes deployed and tested
- Type system fully updated
- Core validation passes all tests
- Existing fuel logs feature still functional
---
**Next Phase**: [Phase 2 - Enhanced Business Logic](FUEL-LOGS-PHASE-2.md)