# Maintenance Feature Implementation Plan ## Project Context MotoVaultPro is a hybrid platform using: - **Architecture**: Modular monolith with feature capsules - **Backend**: Fastify + PostgreSQL + Redis - **Frontend**: React + TypeScript + Material-UI - **Development**: Docker-first, production-only testing - **Requirements**: Mobile + Desktop support for ALL features ## Feature Overview Implement comprehensive maintenance tracking with three main categories: 1. **Routine Maintenance** - Regular service items (27 subtypes) 2. **Repair** - Fix/repair work (5 subtypes) 3. **Performance Upgrade** - Enhancements (5 subtypes) ### Key Capabilities - Track completed maintenance (records) - Schedule recurring maintenance (schedules) - Calculate next due dates (date-based and/or mileage-based) - View upcoming/overdue maintenance - Support multiple subtypes per record (checkboxes, not single select) ### Display Format - **List View**: "Category (count)" e.g., "Routine Maintenance (3)" - **Detail View**: Show all selected subtypes with full details ## Database Schema ### Tables to Create Drop existing maintenance tables (001_create_maintenance_tables.sql) and create new schema. **Migration: `backend/src/features/maintenance/migrations/002_recreate_maintenance_tables.sql`** ```sql -- Drop existing tables (clean slate) DROP TABLE IF EXISTS maintenance_schedules CASCADE; DROP TABLE IF EXISTS maintenance_logs CASCADE; -- Create maintenance_records table CREATE TABLE maintenance_records ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id VARCHAR(255) NOT NULL, vehicle_id UUID NOT NULL, category VARCHAR(50) NOT NULL, -- 'routine_maintenance', 'repair', 'performance_upgrade' subtypes TEXT[] NOT NULL, -- PostgreSQL array of selected subtypes date DATE NOT NULL, odometer_reading INTEGER, cost DECIMAL(10, 2), shop_name VARCHAR(200), notes TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_maintenance_vehicle FOREIGN KEY (vehicle_id) REFERENCES vehicles(id) ON DELETE CASCADE, CONSTRAINT check_category CHECK (category IN ('routine_maintenance', 'repair', 'performance_upgrade')), CONSTRAINT check_subtypes_not_empty CHECK (array_length(subtypes, 1) > 0) ); -- Create maintenance_schedules table CREATE TABLE maintenance_schedules ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id VARCHAR(255) NOT NULL, vehicle_id UUID NOT NULL, category VARCHAR(50) NOT NULL, subtypes TEXT[] NOT NULL, interval_months INTEGER, interval_miles INTEGER, last_service_date DATE, last_service_mileage INTEGER, next_due_date DATE, next_due_mileage INTEGER, is_active BOOLEAN DEFAULT true, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_schedule_vehicle FOREIGN KEY (vehicle_id) REFERENCES vehicles(id) ON DELETE CASCADE, CONSTRAINT check_schedule_category CHECK (category IN ('routine_maintenance', 'repair', 'performance_upgrade')) ); -- Indexes for performance CREATE INDEX idx_maintenance_records_user_id ON maintenance_records(user_id); CREATE INDEX idx_maintenance_records_vehicle_id ON maintenance_records(vehicle_id); CREATE INDEX idx_maintenance_records_date ON maintenance_records(date DESC); CREATE INDEX idx_maintenance_records_category ON maintenance_records(category); CREATE INDEX idx_maintenance_schedules_user_id ON maintenance_schedules(user_id); CREATE INDEX idx_maintenance_schedules_vehicle_id ON maintenance_schedules(vehicle_id); CREATE INDEX idx_maintenance_schedules_next_due_date ON maintenance_schedules(next_due_date); CREATE INDEX idx_maintenance_schedules_active ON maintenance_schedules(is_active) WHERE is_active = true; -- Triggers for updated_at DROP TRIGGER IF EXISTS update_maintenance_records_updated_at ON maintenance_records; CREATE TRIGGER update_maintenance_records_updated_at BEFORE UPDATE ON maintenance_records FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); DROP TRIGGER IF EXISTS update_maintenance_schedules_updated_at ON maintenance_schedules; CREATE TRIGGER update_maintenance_schedules_updated_at BEFORE UPDATE ON maintenance_schedules FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); ``` ## Category and Subtype Definitions ### Routine Maintenance (27 subtypes) ``` Accelerator Pedal Air Filter Element Brakes and Traction Control Cabin Air Filter / Purifier Coolant Doors Drive Belt Engine Oil Evaporative Emissions System Exhaust System Fluid - A/T Fluid - Differential Fluid - M/T Fluid Filter - A/T Fluids Fuel Delivery and Air Induction Hood Shock / Support Neutral Safety Switch Parking Brake System Restraints and Safety Systems Shift Interlock, A/T Spark Plug Steering and Suspension Tires Trunk / Liftgate Shock / Support Washer Fluid Wiper Blade ``` ### Repair (5 subtypes) ``` Engine Transmission Drivetrain Exterior Interior ``` ### Performance Upgrade (5 subtypes) ``` Engine Drivetrain Suspension Wheels/Tires Exterior ``` ## Backend Implementation ### File Structure ``` backend/src/features/maintenance/ ├── README.md # Feature documentation ├── index.ts # Public API exports ├── api/ │ ├── maintenance.routes.ts # Fastify routes │ ├── maintenance.controller.ts # HTTP handlers │ └── maintenance.validation.ts # Request validation ├── domain/ │ ├── maintenance.types.ts # TypeScript types + constants │ └── maintenance.service.ts # Business logic ├── data/ │ └── maintenance.repository.ts # Database queries ├── migrations/ │ └── 002_recreate_maintenance_tables.sql └── tests/ ├── unit/ │ └── maintenance.service.test.ts └── integration/ └── maintenance.integration.test.ts ``` ### Phase 1: Domain Layer **File: `backend/src/features/maintenance/domain/maintenance.types.ts`** ```typescript /** * @ai-summary Type definitions for maintenance feature * @ai-context Supports three categories with specific subtypes, multiple selections allowed */ // Category types export type MaintenanceCategory = 'routine_maintenance' | 'repair' | 'performance_upgrade'; // Subtype definitions (constants for validation) export const ROUTINE_MAINTENANCE_SUBTYPES = [ 'Accelerator Pedal', 'Air Filter Element', 'Brakes and Traction Control', 'Cabin Air Filter / Purifier', 'Coolant', 'Doors', 'Drive Belt', 'Engine Oil', 'Evaporative Emissions System', 'Exhaust System', 'Fluid - A/T', 'Fluid - Differential', 'Fluid - M/T', 'Fluid Filter - A/T', 'Fluids', 'Fuel Delivery and Air Induction', 'Hood Shock / Support', 'Neutral Safety Switch', 'Parking Brake System', 'Restraints and Safety Systems', 'Shift Interlock, A/T', 'Spark Plug', 'Steering and Suspension', 'Tires', 'Trunk / Liftgate Shock / Support', 'Washer Fluid', 'Wiper Blade' ] as const; export const REPAIR_SUBTYPES = [ 'Engine', 'Transmission', 'Drivetrain', 'Exterior', 'Interior' ] as const; export const PERFORMANCE_UPGRADE_SUBTYPES = [ 'Engine', 'Drivetrain', 'Suspension', 'Wheels/Tires', 'Exterior' ] as const; // Database record types export interface MaintenanceRecord { id: string; user_id: string; vehicle_id: string; category: MaintenanceCategory; subtypes: string[]; date: string; odometer_reading?: number; cost?: number; shop_name?: string; notes?: string; created_at: string; updated_at: string; } export interface MaintenanceSchedule { id: string; user_id: string; vehicle_id: string; category: MaintenanceCategory; subtypes: string[]; interval_months?: number; interval_miles?: number; last_service_date?: string; last_service_mileage?: number; next_due_date?: string; next_due_mileage?: number; is_active: boolean; created_at: string; updated_at: string; } // Request types export interface CreateMaintenanceRecordRequest { vehicle_id: string; category: MaintenanceCategory; subtypes: string[]; // Must have at least one date: string; odometer_reading?: number; cost?: number; shop_name?: string; notes?: string; } export interface UpdateMaintenanceRecordRequest { category?: MaintenanceCategory; subtypes?: string[]; date?: string; odometer_reading?: number; cost?: number; shop_name?: string; notes?: string; } export interface CreateScheduleRequest { vehicle_id: string; category: MaintenanceCategory; subtypes: string[]; interval_months?: number; interval_miles?: number; } export interface UpdateScheduleRequest { category?: MaintenanceCategory; subtypes?: string[]; interval_months?: number; interval_miles?: number; is_active?: boolean; } // Response types export interface MaintenanceRecordResponse extends MaintenanceRecord { subtype_count: number; // For list display: "Routine Maintenance (3)" } export interface MaintenanceScheduleResponse extends MaintenanceSchedule { subtype_count: number; is_due_soon?: boolean; // Within 30 days or 500 miles is_overdue?: boolean; } // Validation helpers export function getSubtypesForCategory(category: MaintenanceCategory): readonly string[] { switch (category) { case 'routine_maintenance': return ROUTINE_MAINTENANCE_SUBTYPES; case 'repair': return REPAIR_SUBTYPES; case 'performance_upgrade': return PERFORMANCE_UPGRADE_SUBTYPES; } } export function validateSubtypes(category: MaintenanceCategory, subtypes: string[]): boolean { if (!subtypes || subtypes.length === 0) return false; const validSubtypes = getSubtypesForCategory(category); return subtypes.every(st => validSubtypes.includes(st as any)); } export function getCategoryDisplayName(category: MaintenanceCategory): string { switch (category) { case 'routine_maintenance': return 'Routine Maintenance'; case 'repair': return 'Repair'; case 'performance_upgrade': return 'Performance Upgrade'; } } ``` **File: `backend/src/features/maintenance/domain/maintenance.service.ts`** Key methods to implement: - `createRecord(data, userId)` - Validate vehicle ownership, validate subtypes match category, create record - `getRecords(userId, filters?)` - Get user's records, apply filters (vehicleId, category, dateRange) - `getRecord(id, userId)` - Get single record with ownership check - `updateRecord(id, data, userId)` - Update with validation - `deleteRecord(id, userId)` - Soft delete or hard delete - `getRecordsByVehicle(vehicleId, userId)` - Vehicle-specific records - `createSchedule(data, userId)` - Create recurring schedule, calculate initial next_due - `getSchedules(userId, filters?)` - Get schedules with filters - `updateSchedule(id, data, userId)` - Update schedule, recalculate next_due if intervals change - `deleteSchedule(id, userId)` - Remove schedule - `getUpcomingMaintenance(vehicleId, userId)` - Get schedules that are due soon or overdue - `calculateNextDue(schedule, currentDate, currentMileage)` - Calculate next due date/mileage based on intervals **Cache Strategy:** - Records: `maintenance:records:user:{userId}` - 5 min TTL - Vehicle records: `maintenance:records:vehicle:{vehicleId}` - 5 min TTL - Schedules: `maintenance:schedules:vehicle:{vehicleId}` - 5 min TTL - Upcoming: `maintenance:upcoming:{vehicleId}` - 1 hour TTL ### Phase 2: Data Layer **File: `backend/src/features/maintenance/data/maintenance.repository.ts`** Key methods (all use prepared statements, all filter by user_id): - `insert(record)` - INSERT with PostgreSQL array for subtypes - `findById(id, userId)` - SELECT with user_id check - `findByUserId(userId)` - SELECT user's records - `findByVehicleId(vehicleId, userId)` - SELECT vehicle records with ownership check - `update(id, userId, data)` - UPDATE with user_id check - `delete(id, userId)` - DELETE with user_id check - `insertSchedule(schedule)` - INSERT schedule - `findSchedulesByVehicle(vehicleId, userId)` - SELECT vehicle schedules - `updateSchedule(id, userId, data)` - UPDATE schedule - `deleteSchedule(id, userId)` - DELETE schedule - `findDueSchedules(vehicleId, userId, currentDate, currentMileage)` - Complex query for due/overdue **PostgreSQL Array Handling:** ```typescript // Insert with array await pool.query( 'INSERT INTO maintenance_records (subtypes, ...) VALUES ($1, ...)', [[subtype1, subtype2, subtype3], ...] ); // Query with array contains await pool.query( 'SELECT * FROM maintenance_records WHERE $1 = ANY(subtypes)', [searchSubtype] ); ``` ### Phase 3: API Layer **File: `backend/src/features/maintenance/api/maintenance.routes.ts`** ```typescript import { FastifyInstance } from 'fastify'; import { MaintenanceController } from './maintenance.controller'; export async function maintenanceRoutes(app: FastifyInstance) { const controller = new MaintenanceController(); // All routes require authentication app.addHook('preHandler', app.authenticate); // Maintenance Records app.post('/maintenance/records', controller.createRecord.bind(controller)); app.get('/maintenance/records', controller.listRecords.bind(controller)); app.get('/maintenance/records/:id', controller.getRecord.bind(controller)); app.put('/maintenance/records/:id', controller.updateRecord.bind(controller)); app.delete('/maintenance/records/:id', controller.deleteRecord.bind(controller)); app.get('/maintenance/records/vehicle/:vehicleId', controller.getRecordsByVehicle.bind(controller)); // Maintenance Schedules app.post('/maintenance/schedules', controller.createSchedule.bind(controller)); app.get('/maintenance/schedules', controller.listSchedules.bind(controller)); app.get('/maintenance/schedules/vehicle/:vehicleId', controller.getSchedulesByVehicle.bind(controller)); app.put('/maintenance/schedules/:id', controller.updateSchedule.bind(controller)); app.delete('/maintenance/schedules/:id', controller.deleteSchedule.bind(controller)); // Utility endpoints app.get('/maintenance/upcoming/:vehicleId', controller.getUpcomingMaintenance.bind(controller)); app.get('/maintenance/subtypes/:category', controller.getSubtypes.bind(controller)); } ``` **File: `backend/src/features/maintenance/api/maintenance.controller.ts`** Follow pattern from `backend/src/features/documents/api/documents.controller.ts`: - Extract userId from `request.user.sub` - Use structured logging with logger - Return proper HTTP status codes (201 for create, 200 for success, 404 for not found, etc.) - Handle errors gracefully **File: `backend/src/features/maintenance/api/maintenance.validation.ts`** Use validation schemas (Fastify schema or Zod): - Validate category is valid enum - Validate subtypes is non-empty array - Validate subtypes match category (server-side validation) - Validate dates, numeric values - Validate UUIDs **File: `backend/src/features/maintenance/index.ts`** ```typescript export { maintenanceRoutes } from './api/maintenance.routes'; export * from './domain/maintenance.types'; ``` **File: `backend/src/app.ts`** Update to register routes (remove lines 118-134 placeholder): ```typescript import { maintenanceRoutes } from './features/maintenance'; // ... in buildApp() await app.register(maintenanceRoutes, { prefix: '/api' }); ``` ### Phase 4: Testing **File: `backend/src/features/maintenance/tests/unit/maintenance.service.test.ts`** Test cases: - Create record with valid data - Reject invalid category - Reject invalid subtypes for category - Reject empty subtypes array - Calculate next due date correctly - Identify due soon vs overdue - Handle edge cases (no previous service, etc.) **File: `backend/src/features/maintenance/tests/integration/maintenance.integration.test.ts`** Test full API workflow: - Create, read, update, delete records - Create and manage schedules - Get upcoming maintenance - Test authentication (reject without token) - Test authorization (reject access to other user's data) - Test PostgreSQL array operations ## Frontend Implementation ### File Structure ``` frontend/src/features/maintenance/ ├── types/ │ └── maintenance.types.ts # Mirror backend types ├── api/ │ └── maintenance.api.ts # API client ├── hooks/ │ ├── useMaintenanceRecords.ts # Records query/mutation │ ├── useMaintenanceSchedules.ts # Schedules query/mutation │ └── useUpcomingMaintenance.ts # Upcoming items ├── components/ # Desktop components │ ├── MaintenanceRecordForm.tsx │ ├── MaintenanceRecordsList.tsx │ ├── MaintenanceRecordDetail.tsx │ ├── MaintenanceScheduleForm.tsx │ ├── MaintenanceSchedulesList.tsx │ ├── UpcomingMaintenanceCard.tsx │ └── SubtypeCheckboxGroup.tsx # Reusable checkbox component ├── mobile/ # Mobile components │ └── MaintenanceMobileScreen.tsx └── pages/ └── MaintenancePage.tsx # Desktop page ``` ### Phase 5: Frontend Types and API **File: `frontend/src/features/maintenance/types/maintenance.types.ts`** Copy types from backend, export constants for dropdowns. **File: `frontend/src/features/maintenance/api/maintenance.api.ts`** ```typescript import { api } from '../../../core/api/client'; export const maintenanceApi = { // Records createRecord: (data: CreateMaintenanceRecordRequest) => api.post('/api/maintenance/records', data), getRecords: () => api.get('/api/maintenance/records'), getRecord: (id: string) => api.get(`/api/maintenance/records/${id}`), updateRecord: (id: string, data: UpdateMaintenanceRecordRequest) => api.put(`/api/maintenance/records/${id}`, data), deleteRecord: (id: string) => api.delete(`/api/maintenance/records/${id}`), getRecordsByVehicle: (vehicleId: string) => api.get(`/api/maintenance/records/vehicle/${vehicleId}`), // Schedules createSchedule: (data: CreateScheduleRequest) => api.post('/api/maintenance/schedules', data), getSchedules: () => api.get('/api/maintenance/schedules'), getSchedulesByVehicle: (vehicleId: string) => api.get(`/api/maintenance/schedules/vehicle/${vehicleId}`), updateSchedule: (id: string, data: UpdateScheduleRequest) => api.put(`/api/maintenance/schedules/${id}`, data), deleteSchedule: (id: string) => api.delete(`/api/maintenance/schedules/${id}`), // Utility getUpcoming: (vehicleId: string) => api.get(`/api/maintenance/upcoming/${vehicleId}`), getSubtypes: (category: MaintenanceCategory) => api.get(`/api/maintenance/subtypes/${category}`) }; ``` ### Phase 6: React Hooks **File: `frontend/src/features/maintenance/hooks/useMaintenanceRecords.ts`** Use React Query pattern from fuel-logs: ```typescript import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { maintenanceApi } from '../api/maintenance.api'; export function useMaintenanceRecords(vehicleId?: string) { const queryClient = useQueryClient(); const { data: records, isLoading, error } = useQuery({ queryKey: vehicleId ? ['maintenance-records', vehicleId] : ['maintenance-records'], queryFn: () => vehicleId ? maintenanceApi.getRecordsByVehicle(vehicleId) : maintenanceApi.getRecords() }); const createMutation = useMutation({ mutationFn: maintenanceApi.createRecord, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['maintenance-records'] }); } }); const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: string; data: UpdateMaintenanceRecordRequest }) => maintenanceApi.updateRecord(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['maintenance-records'] }); } }); const deleteMutation = useMutation({ mutationFn: maintenanceApi.deleteRecord, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['maintenance-records'] }); } }); return { records, isLoading, error, createRecord: createMutation.mutate, updateRecord: updateMutation.mutate, deleteRecord: deleteMutation.mutate }; } ``` Similar hooks for schedules and upcoming maintenance. ### Phase 7: Desktop Components **File: `frontend/src/features/maintenance/components/SubtypeCheckboxGroup.tsx`** Reusable component for subtype selection: ```typescript interface SubtypeCheckboxGroupProps { category: MaintenanceCategory; selectedSubtypes: string[]; onChange: (subtypes: string[]) => void; } export function SubtypeCheckboxGroup({ category, selectedSubtypes, onChange }: SubtypeCheckboxGroupProps) { const subtypes = getSubtypesForCategory(category); const handleToggle = (subtype: string) => { if (selectedSubtypes.includes(subtype)) { onChange(selectedSubtypes.filter(s => s !== subtype)); } else { onChange([...selectedSubtypes, subtype]); } }; return (
{subtypes.map(subtype => ( handleToggle(subtype)} /> } label={subtype} /> ))}
); } ``` **File: `frontend/src/features/maintenance/components/MaintenanceRecordForm.tsx`** Form structure: 1. Category dropdown (Routine Maintenance, Repair, Performance Upgrade) 2. SubtypeCheckboxGroup (dynamically shows based on category) 3. Date picker 4. Odometer input (optional) 5. Cost input (optional) 6. Shop name input (optional) 7. Notes textarea (optional) 8. Submit and Cancel buttons Validation: - Category required - At least one subtype required - Selected subtypes must match category - Date required - Show error messages inline **File: `frontend/src/features/maintenance/components/MaintenanceRecordsList.tsx`** Table or card list showing: - Date (sortable, default newest first) - Category with count: "Routine Maintenance (3)" - Odometer reading - Cost - Shop name - Actions (view details, edit, delete) Click row to navigate to detail view. **File: `frontend/src/features/maintenance/components/MaintenanceRecordDetail.tsx`** Full detail view: - All fields displayed - Subtypes shown as chips/badges - Edit and Delete buttons - Back button **File: `frontend/src/features/maintenance/pages/MaintenancePage.tsx`** Tabbed interface: - **Records Tab**: List of completed maintenance - **Schedules Tab**: Recurring maintenance schedules - **Upcoming Tab**: Due soon and overdue items Include: - Vehicle selector (dropdown) - Add new record/schedule buttons - Filters (category, date range) ### Phase 8: Mobile Components **File: `frontend/src/features/maintenance/mobile/MaintenanceMobileScreen.tsx`** Mobile-optimized design: - GlassCard components (match existing pattern from documents/fuel-logs) - Touch-friendly form inputs - Large checkboxes (min 44x44px touch target) - Single column layout - Bottom sheet or full-screen modal for add/edit forms - Swipe actions for delete - Pull to refresh Collapsible sections: - Tap category to expand/collapse subtype checkboxes - Accordion style to save vertical space **Mobile-specific considerations:** - Virtual scrolling for long lists - Optimistic updates for instant feedback - Loading skeletons - Error boundaries ### Phase 9: Route Integration **File: `frontend/src/App.tsx`** Desktop routes (update line 554): ```typescript import { lazy } from 'react'; const MaintenancePage = lazy(() => import('./features/maintenance/pages/MaintenancePage').then(m => ({ default: m.MaintenancePage }))); // In Routes: } /> ``` Mobile navigation: - Maintenance already in Layout.tsx navigation (line 42) - Consider if it should be in bottom nav or remain in hamburger menu - If adding to bottom nav, update mobile nav items in App.tsx ## Display Format Guidelines ### List View (Records) ``` ┌─────────────────────────────────────────┐ │ Jan 15, 2024 │ │ Routine Maintenance (3) │ │ 45,230 miles | $127.50 | Joe's Auto │ └─────────────────────────────────────────┘ ``` ### Detail View (Full Record) ``` Date: January 15, 2024 Category: Routine Maintenance Subtypes: • Engine Oil • Air Filter Element • Cabin Air Filter / Purifier Odometer: 45,230 miles Cost: $127.50 Shop: Joe's Auto Service Notes: Used synthetic 5W-30 oil. Recommended tire rotation at next visit. ``` ### Upcoming Maintenance (Color-Coded) ``` 🟢 Good - Not due yet 🟡 Due Soon - Within 30 days or 500 miles 🔴 Overdue - Past due date or mileage ``` ## Business Rules ### Validation Rules 1. Category must be one of: routine_maintenance, repair, performance_upgrade 2. Subtypes must be non-empty array 3. All subtypes must be valid for the selected category 4. Date required for records 5. Vehicle must belong to user (ownership check) 6. Interval (months OR miles OR both) required for schedules ### Next Due Calculation (Schedules) ``` If interval_months AND interval_miles both set: next_due_date = last_service_date + interval_months next_due_mileage = last_service_mileage + interval_miles Due when EITHER condition is met (whichever comes first) If only interval_months: next_due_date = last_service_date + interval_months next_due_mileage = null If only interval_miles: next_due_date = null next_due_mileage = last_service_mileage + interval_miles ``` ### Due Soon Logic ``` Due Soon (Yellow): - next_due_date within 30 days of today - OR next_due_mileage within 500 miles of current odometer Overdue (Red): - next_due_date in the past - OR next_due_mileage < current odometer ``` ## Security Requirements 1. **All queries user-scoped**: Every database query MUST filter by user_id 2. **Vehicle ownership**: Validate user owns vehicle before any operation 3. **Prepared statements**: NEVER concatenate SQL strings 4. **Authentication**: All routes require valid JWT token 5. **Authorization**: Users can only access their own data Example repository pattern: ```typescript async findByVehicleId(vehicleId: string, userId: string): Promise { // CORRECT - filters by both vehicle_id AND user_id const result = await pool.query( 'SELECT * FROM maintenance_records WHERE vehicle_id = $1 AND user_id = $2', [vehicleId, userId] ); return result.rows; } ``` ## Caching Strategy Use Redis for caching: ```typescript // Records cache - 5 minutes const cacheKey = `maintenance:records:user:${userId}`; const ttl = 300; // 5 minutes // Vehicle-specific cache - 5 minutes const vehicleCacheKey = `maintenance:records:vehicle:${vehicleId}`; // Upcoming maintenance - 1 hour (less frequently changing) const upcomingCacheKey = `maintenance:upcoming:${vehicleId}`; const upcomingTTL = 3600; // 1 hour // Invalidate on create/update/delete await cacheService.del(cacheKey); await cacheService.del(vehicleCacheKey); await cacheService.del(upcomingCacheKey); ``` ## Testing Strategy ### Backend Tests **Unit Tests** (`backend/src/features/maintenance/tests/unit/`) - Test service methods with mocked repository - Test validation logic (category, subtypes) - Test next due calculation - Test due soon/overdue logic - Test edge cases (no previous service, missing data) **Integration Tests** (`backend/src/features/maintenance/tests/integration/`) - Test full API endpoints with test database - Test authentication (401 without token) - Test authorization (403 for other user's data) - Test PostgreSQL array operations - Test cascade deletes (vehicle deletion) Run tests: ```bash make shell-backend npm test -- features/maintenance ``` ### Frontend Tests Test components: - Form validation (category, subtypes) - Checkbox selection/deselection - Mobile touch interactions - Responsive layout ### Manual Testing (Docker-Only) 1. After each change: `make rebuild` 2. Test mobile viewport: 375px width 3. Test desktop viewport: 1920px width 4. Test touch interactions on mobile 5. Verify all linting hooks pass (zero tolerance) ## Documentation Requirements **File: `backend/src/features/maintenance/README.md`** Follow pattern from fuel-logs README: ```markdown # Maintenance Feature Capsule ## Quick Summary (50 tokens) Tracks vehicle maintenance including routine service, repairs, and performance upgrades. Supports multiple subtypes per record, recurring schedules, and upcoming/overdue calculations. User-scoped data with vehicle ownership enforcement. ## API Endpoints [List all endpoints with descriptions] ## Structure - **api/** - HTTP endpoints, routes, validators - **domain/** - Business logic, types, rules - **data/** - Repository, database queries - **migrations/** - Feature-specific schema - **tests/** - All feature tests ## Categories and Subtypes [List all three categories and their subtypes] ## Dependencies - Internal: core/auth, core/cache, core/config - Database: maintenance_records, maintenance_schedules tables - Feature: vehicles (vehicle_id FK) ## Business Rules [Document validation, calculation logic, etc.] ## Testing [Document test commands and examples] ``` ## Success Criteria Checklist - [ ] Database migrations run cleanly (`make migrate`) - [ ] Backend compiles without errors - [ ] All backend unit tests pass - [ ] All backend integration tests pass - [ ] All TypeScript types are correct (no `any` types) - [ ] All linting rules pass (ESLint, Prettier) - [ ] Category dropdown works correctly - [ ] Subtype checkboxes populate based on selected category - [ ] Multiple subtypes can be selected - [ ] Subtype validation prevents invalid selections - [ ] Records display as "Category (count)" in list view - [ ] Detail view shows all selected subtypes - [ ] Schedule creation works - [ ] Next due date calculation is correct - [ ] Upcoming maintenance shows correct items - [ ] Works on mobile (375px viewport) - [ ] Touch targets are 44x44px minimum on mobile - [ ] Works on desktop (1920px viewport) - [ ] Responsive between mobile and desktop breakpoints - [ ] No console errors - [ ] No TODOs remaining in code - [ ] README.md is complete and accurate - [ ] Feature is registered in backend app.ts - [ ] Feature is registered in frontend App.tsx routes ## Common Pitfalls to Avoid 1. **PostgreSQL Arrays**: Use proper array syntax `TEXT[]` and array operations `= ANY(subtypes)` 2. **User Scoping**: NEVER forget `AND user_id = $X` in queries 3. **Category Validation**: Server-side validation is required (don't trust client) 4. **Empty Subtypes**: Validate array is non-empty before saving 5. **Mobile Touch Targets**: Checkboxes must be 44x44px minimum 6. **Cache Invalidation**: Invalidate ALL relevant cache keys on update 7. **String Concatenation**: NEVER concatenate SQL strings - use prepared statements 8. **Type Safety**: Don't use `any` types - define proper interfaces ## Reference Files For implementation patterns, refer to these existing features: - **Documents Feature**: `backend/src/features/documents/` (most recent, best example) - **Fuel Logs Feature**: `backend/src/features/fuel-logs/` (similar complexity) - **Documents Frontend**: `frontend/src/features/documents/` (mobile + desktop patterns) ## Implementation Order 1. Backend database migration 2. Backend domain types 3. Backend repository 4. Backend service 5. Backend API (routes, controller, validation) 6. Backend tests 7. Register routes in app.ts 8. Frontend types 9. Frontend API client 10. Frontend hooks 11. Desktop components (form, list, detail) 12. Desktop page with tabs 13. Mobile components 14. Update routes in App.tsx 15. Manual testing (docker rebuild) 16. Documentation (README.md) 17. Final validation (all criteria met)