391 lines
12 KiB
Markdown
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) |