12 KiB
12 KiB
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_logstable - Migration system functional
Database Schema Changes
New Fields to Add
-- 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
-- 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
gallonsandprice_per_gallonfields during transition - Update application logic to use new fields preferentially
- Plan deprecation of old fields in future migration
Data Validation Rules
Core Business Rules
- Distance Requirement: Either
trip_distanceORodometer_readingmust be provided - Fuel Type Validation: Must be one of: 'gasoline', 'diesel', 'electric'
- Fuel Grade Validation: Must match fuel type options
- Positive Values: All numeric fields must be > 0
- DateTime: Cannot be in the future
Fuel Grade Validation Logic
-- 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
// 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
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
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
- ✅ Create migration file
002_enhance_fuel_logs_schema.sql - ✅ Add new columns with appropriate types
- ✅ Migrate existing data to new schema
- ✅ Add database constraints and triggers
- ✅ Create performance indexes
Type System Tasks
- ✅ Define fuel system enums
- ✅ Create unit system types
- ✅ Update core FuelLog interface
- ✅ Update request/response interfaces
- ✅ Add location data structure
Validation Tasks
- ✅ Create validation utility class
- ✅ Implement distance requirement validation
- ✅ Implement fuel grade validation
- ✅ Add positive value checks
- ✅ Add datetime validation
Testing Requirements
Database Testing
-- 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