# Application class diagram Fuel Tax Tracker is a Flutter app. Screens read and mutate through `AppState` (a `ChangeNotifier` provided at the root). Persistence is `DatabaseService` (SQLite); cloud I/O goes through `CloudStorageProvider` so sync never depends on a specific backend. Private Flutter `State` / painter classes are omitted. Functions that are not classes (`buildFuelReport`, `estimatedFuelRefund`, `acquireLock`) are noted where they sit in the design. ## Domain and application facade ```mermaid classDiagram class Vehicle { +String id +String vin +String? nickname +DateTime updatedAt +DateTime? deletedAt +String displayLabel +copyWith() Vehicle +toMap() Map +fromMap(map) Vehicle } class FuelEntry { +String id +String vehicleId +DateTime date +double gallons +double pricePerGallon +double totalCost +String? receiptImagePath +String? receiptDriveFileId +DateTime updatedAt +DateTime? deletedAt +bool isReceiptUploadedToDrive +bool needsReceiptUpload +copyWith() FuelEntry +toMap() Map +fromMap(map) FuelEntry } class DuplicateVinException { +String vin } class AppState { +DatabaseService database +AdService adService +PurchaseService purchaseService +List~CloudStorageProvider~ availableProviders +CloudStorageProvider? activeProvider +CloudSyncService? cloudSync +List~Vehicle~ vehicles +List~FuelEntry~ fuelEntries +DateTime? adFreeUntil +bool adsCurrentlyDisabled +bool isCloudConnected +bool hasCloudBackupConfigured +init() +addVehicle() +updateVehicle() +deleteVehicle() +addFuelEntry() FuelEntry +updateFuelEntry() +deleteFuelEntry() +syncNow() +connectProvider() +disconnectCloud() +purgeAllData() } class DatabaseService { +Directory rootDirectory +Database rawDb +init() +getVehicles() List~Vehicle~ +getFuelEntries() List~FuelEntry~ +getAdFreeUntil() DateTime? +setAdFreeUntil() +vinExists() bool +saveVehicle() +saveFuelEntry() +softDeleteVehicle() +softDeleteFuelEntry() +storeReceiptImage() String +purgeAllData() } Vehicle "1" <-- "0..*" FuelEntry : vehicleId AppState "1" *-- "1" DatabaseService AppState "1" --> "*" Vehicle : caches AppState "1" --> "*" FuelEntry : caches AppState ..> DuplicateVinException : throws DatabaseService --> Vehicle DatabaseService --> FuelEntry AppState --|> ChangeNotifier ``` `ad_free_entitlement` has **no** Dart model. `DatabaseService.getAdFreeUntil` / `setAdFreeUntil` read and write that singleton row; `AppState.adFreeUntil` is the in-memory cache. ## Cloud storage and sync ```mermaid classDiagram class CloudProviderId { <> googleDrive dropbox oneDrive webdav } class CloudStorageProvider { <> +CloudProviderId id +String displayName +bool isSignedIn +String? accountLabel +attemptSilentSignIn() bool +signIn() String +signOut() +beginSession() CloudStorageSession } class CloudStorageSession { <> +bool supportsSharedWithMe +listFolders() List~CloudFolder~ +findOrCreateFolder() String +moveFolder() String +findFile() CloudFileInfo? +downloadFileBytes() List~int~ +uploadFile() CloudFileInfo +deleteFile() +createLockFile() String +listLockFiles() List~CloudLockFile~ +close() } class ManualCredentialCloudStorageProvider { <> +signInWithCredentials() String } class GoogleDriveProvider class DropboxProvider class OneDriveProvider class WebDavProvider class GoogleDriveSession class DropboxSession class OneDriveSession class WebDavSession class CloudFolder { +String id +String name } class CloudFileInfo { +String id +String? versionTag } class CloudLockFile { +String id +String username +DateTime createdAtUtc } class CloudNotAuthorizedException { +String providerName } class CloudSyncService { +CloudStorageProvider provider +DatabaseService databaseService +bool isConfigured +configure(appFolderId) +clearConfiguration() +selectAppFolder() String +syncNow() SyncResult } class SyncResult { +bool ranSync +Object? error +bool adGateBlocked +skipped() SyncResult +success() SyncResult +failure(error) SyncResult +adGateBlocked() SyncResult } CloudStorageProvider <|.. GoogleDriveProvider CloudStorageProvider <|.. DropboxProvider CloudStorageProvider <|.. OneDriveProvider CloudStorageProvider <|.. WebDavProvider ManualCredentialCloudStorageProvider <|.. WebDavProvider CloudStorageSession <|.. GoogleDriveSession CloudStorageSession <|.. DropboxSession CloudStorageSession <|.. OneDriveSession CloudStorageSession <|.. WebDavSession CloudStorageProvider --> CloudStorageSession : beginSession() CloudStorageProvider --> CloudProviderId CloudStorageSession --> CloudFolder CloudStorageSession --> CloudFileInfo CloudStorageSession --> CloudLockFile CloudSyncService --> CloudStorageProvider CloudSyncService --> DatabaseService CloudSyncService --> SyncResult AppState o-- CloudStorageProvider : activeProvider AppState o-- CloudSyncService : cloudSync AppState "1" *-- "*" CloudStorageProvider : availableProviders ``` Lock acquisition is a free function, `acquireLock` in `lock_coordinator.dart`, injected with create/list/delete callbacks so it can be unit-tested without a network. `CloudSyncService` is the only production caller. OAuth helpers (`PkcePair`, `CloudOAuthConfig`) support Dropbox and OneDrive sign-in. Google uses its own SDK; WebDAV uses HTTP Basic Auth. ## Reports, OCR, ads, purchases ```mermaid classDiagram class FuelReport { +DateTime startDate +DateTime endDate +List~VehicleReportRow~ rows +double totalGallons +double totalCost +int fillCount +List~ReportEntry~ allEntries +List~MonthlyTotal~ monthlyTotals } class VehicleReportRow { +Vehicle vehicle +List~FuelEntry~ entries +double totalGallons +double totalCost +int entryCount } class ReportEntry { +FuelEntry entry +Vehicle vehicle } class MonthlyTotal { +DateTime month +int fillCount +double totalGallons +double totalCost +double avgPricePerGallon } class ParsedReceipt { +double? gallons +double? pricePerGallon +double? totalCost +DateTime? date +String? state +String rawText } class ReceiptParser { +parse(text) ParsedReceipt$ } class OcrService { +recognizeText(imageFile) String +dispose() } class VinParser { +parse(text) String?$ } class AdService { +initialize() +showGateAd() AdGateResult } class AdGateResult { <> alreadyOpen justShown blocked } class PurchaseService { +String adFreeYearProductId$ +String? priceLabel +initialize() +buyAdFreeYear() +dispose() } class ReceiptImageLoadResult { +Map bytesByEntryId +List~FuelEntry~ failedEntries } FuelReport "1" *-- "*" VehicleReportRow VehicleReportRow --> Vehicle VehicleReportRow --> FuelEntry FuelReport --> ReportEntry ReportEntry --> FuelEntry ReportEntry --> Vehicle FuelReport --> MonthlyTotal ReceiptParser --> ParsedReceipt OcrService ..> ReceiptParser : raw text in AdService --> AdGateResult AppState *-- AdService AppState *-- PurchaseService ``` `buildFuelReport(...)` (in `fuel_report.dart`) constructs a `FuelReport` from the in-memory vehicle/entry lists. `buildFuelReportPdf(...)` renders it; `loadReceiptImageBytes(...)` resolves local or cloud receipt photos into a `ReceiptImageLoadResult`. `estimatedFuelRefund(gallons)` is a pure function (Missouri highway rate, $0.125/gal). ## UI structure Widgets consume `AppState` via `Provider` / `context.watch`. They do not talk to SQLite or cloud APIs directly. ```mermaid classDiagram class FuelTaxTrackerApp { <> } class AppRoot { <> } class UserAgreementScreen { <> } class MainShell { <> } class ReceiptsScreen { <> } class HomeScreen { <> } class ReportScreen { <> } class SettingsScreen { <> } class DataSettingsScreen { <> } class UiSettingsScreen { <> } class FaqScreen { <> } class AddEditVehicleScreen { <> } class VehicleDetailScreen { <> } class ConfirmFuelEntryScreen { <> } class EditFuelEntryScreen { <> } class ReceiptDetailScreen { <> } class ReceiptImageScreen { <> } class CloudFolderBrowserScreen { <> } FuelTaxTrackerApp --> AppRoot FuelTaxTrackerApp --> AppState : ChangeNotifierProvider AppRoot --> UserAgreementScreen : if not accepted AppRoot --> MainShell : after agreement MainShell --> ReceiptsScreen MainShell --> HomeScreen MainShell --> ReportScreen MainShell --> SettingsScreen SettingsScreen --> DataSettingsScreen SettingsScreen --> UiSettingsScreen SettingsScreen --> FaqScreen DataSettingsScreen --> CloudFolderBrowserScreen HomeScreen --> AddEditVehicleScreen HomeScreen --> VehicleDetailScreen HomeScreen --> FaqScreen VehicleDetailScreen --> ConfirmFuelEntryScreen VehicleDetailScreen --> EditFuelEntryScreen VehicleDetailScreen --> ReceiptImageScreen ReceiptsScreen --> ConfirmFuelEntryScreen ReceiptsScreen --> ReceiptDetailScreen ReceiptsScreen --> EditFuelEntryScreen ReceiptDetailScreen --> ReceiptImageScreen ReportScreen --> FuelReport : buildFuelReport() ``` Shared widgets (not expanded above): `HeroBanner`, `ReceiptThumbnail`, `ReceiptCapture`, `OnboardingTourOverlay`, `BackupReminder`, `AdFreeUpsellDialog`, `ImageSourceSheet`. Theme tokens live on `AppTheme`. ## How the layers connect ``` UI screens/widgets │ Provider ▼ AppState ──────────► PurchaseService │ AdService ├── DatabaseService ── SQLite (vehicles, fuel_entries, ad_free_entitlement) │ └── local receipts/ folder └── CloudSyncService ── CloudStorageProvider ├── GoogleDriveProvider ├── DropboxProvider ├── OneDriveProvider └── WebDavProvider ```