Initial Commit
This commit is contained in:
391
docs/changes/fuel-logs-v1/FUEL-LOGS-PHASE-1.md
Normal file
391
docs/changes/fuel-logs-v1/FUEL-LOGS-PHASE-1.md
Normal file
@@ -0,0 +1,391 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user