# Architecture Patterns - BuyerKiosk Live

This document describes the architectural patterns and conventions used throughout the BuyerKiosk Live Flutter application.

---

## Clean Architecture Implementation

The application follows Clean Architecture principles with clear separation of concerns across four layers.

### Layer Boundaries

```
lib/
+-- core/           # Shared utilities, constants, errors, theme
+-- data/           # External concerns: API, storage, model serialization
+-- domain/         # Business logic: entities, repository interfaces
+-- presentation/   # UI concerns: providers, screens, widgets
+-- router/         # Navigation configuration
```

### Dependency Flow Diagram

```
                    +-------------------+
                    |   Presentation    |
                    |  (Screens, Widgets)|
                    +--------+----------+
                             |
                             | watches/reads
                             v
                    +-------------------+
                    |     Providers     |
                    | (Riverpod State)  |
                    +--------+----------+
                             |
                             | uses
                             v
+-------------------+   +-------------------+
|      Domain       |<--|       Data        |
| (Entities, Repos) |   | (Models, Impl)    |
+-------------------+   +--------+----------+
        ^                        |
        |                        | calls
        +------------------------+
                             v
                    +-------------------+
                    |       Core        |
                    | (Network, Errors) |
                    +-------------------+
```

**Key Rules:**

1. **Domain layer has no external dependencies** - contains only pure Dart code
2. **Data layer implements domain interfaces** - repository implementations depend on domain contracts
3. **Presentation layer depends on domain entities** - never on data models directly
4. **Core is shared** - utilities and error types used across all layers

---

## Data Flow Pattern

Data flows through the application in a predictable transformation chain:

```
API (JSON) --> StoreModel (Freezed) --> Mapper --> Store (Equatable Entity) --> Provider --> UI
```

### Step-by-Step Flow

1. **API Response (JSON)**
   ```json
   {
     "typeNum": "pc00",
     "storeCity": "Anna",
     "numInQueue": 4,
     "liveFinancials": { "bGoal": "$500.00", "bCurrent": "$125.50" }
   }
   ```

2. **Data Model (Freezed)** - handles JSON serialization
   ```dart
   // lib/data/models/store_model.dart
   @freezed
   abstract class StoreModel with _$StoreModel {
     const factory StoreModel({
       required String typeNum,
       String? storeCity,
       @Default(0) int numInQueue,
       LiveFinancialsModel? liveFinancials,
     }) = _StoreModel;

     factory StoreModel.fromJson(Map<String, dynamic> json) =>
         _$StoreModelFromJson(json);
   }
   ```

3. **Mapper (Extension)** - transforms model to entity
   ```dart
   // lib/data/models/mappers/store_mapper.dart
   extension StoreModelX on StoreModel {
     Store toEntity() {
       return Store(
         typeNum: typeNum,
         storeName: displayName,
         numInQueue: queueCount,
         buyGoal: buyGoalValue,
         buyCurrent: buyCurrentValue,
         // ... computed/normalized values
       );
     }
   }
   ```

4. **Domain Entity (Equatable)** - immutable business object
   ```dart
   // lib/domain/entities/store.dart
   class Store extends Equatable {
     final String typeNum;
     final String storeName;
     final int numInQueue;
     final double buyGoal;
     final double buyCurrent;

     const Store({
       required this.typeNum,
       required this.storeName,
       required this.numInQueue,
       required this.buyGoal,
       required this.buyCurrent,
     });

     double get buyProgress => buyGoal == 0 ? 0 : (buyCurrent / buyGoal).clamp(0.0, 1.0);

     @override
     List<Object?> get props => [typeNum, storeName, numInQueue, buyGoal, buyCurrent];
   }
   ```

5. **Provider** - exposes entity to UI
   ```dart
   final dashboardProvider = AsyncNotifierProvider<DashboardNotifier, List<Store>>(
     DashboardNotifier.new,
   );
   ```

6. **UI** - consumes entity via provider
   ```dart
   Widget build(BuildContext context, WidgetRef ref) {
     final storesAsync = ref.watch(dashboardProvider);
     return storesAsync.when(
       data: (stores) => ListView.builder(...),
       loading: () => LoadingIndicator(),
       error: (e, _) => ErrorDisplay.fromError(error: e, onRetry: ...),
     );
   }
   ```

---

## State Management (Riverpod 3.x)

### AsyncNotifier Pattern

The standard pattern for async data providers:

```dart
// lib/presentation/providers/auth_provider.dart
class AuthNotifier extends AsyncNotifier<bool> {
  @override
  Future<bool> build() async {
    // Initial data fetch - called automatically
    final authRepository = ref.read(authRepositoryProvider);
    return await authRepository.checkInstallation();
  }

  // Manual refresh method
  Future<void> checkInstallation() async {
    state = const AsyncValue.loading();
    state = await AsyncValue.guard(() async {
      final authRepository = ref.read(authRepositoryProvider);
      return await authRepository.checkInstallation();
    });
  }

  // Action method with error handling
  Future<bool> validateApiKey(String apiKey) async {
    state = const AsyncValue.loading();
    try {
      final authRepository = ref.read(authRepositoryProvider);
      final isValid = await authRepository.validateApiKey(apiKey);
      if (isValid) {
        await authRepository.saveApiKey(apiKey);
        state = const AsyncValue.data(true);
        return true;
      } else {
        state = const AsyncValue.data(false);
        return false;
      }
    } catch (e, stackTrace) {
      state = AsyncValue.error(e, stackTrace);
      return false;
    }
  }
}

// Provider declaration
final authProvider = AsyncNotifierProvider<AuthNotifier, bool>(
  AuthNotifier.new,
);
```

### Error Caching Pattern (Dashboard Provider)

Prevents repeated failed API calls after auth errors:

```dart
// lib/presentation/providers/dashboard_provider.dart
class DashboardNotifier extends AsyncNotifier<List<Store>> {
  // Track auth errors to prevent API spam
  bool _hasAuthError = false;
  DateTime? _lastAuthErrorTime;

  @override
  Future<List<Store>> build() async {
    // Check for cached auth error (30 second cooldown)
    if (_hasAuthError && _lastAuthErrorTime != null) {
      final elapsed = DateTime.now().difference(_lastAuthErrorTime!);
      if (elapsed.inSeconds < 30) {
        throw AuthException('Invalid API key. Please check your API key in Settings.');
      }
    }

    try {
      final dashboardRepository = ref.read(dashboardRepositoryProvider);
      final result = await dashboardRepository.getDashboard();
      // Clear error flag on success
      _hasAuthError = false;
      _lastAuthErrorTime = null;
      return result;
    } catch (e) {
      // Cache auth errors
      if (_isAuthError(e)) {
        _hasAuthError = true;
        _lastAuthErrorTime = DateTime.now();
      }
      rethrow;
    }
  }

  // Manual fetch clears the error cache
  Future<void> fetchDashboard() async {
    _hasAuthError = false;
    _lastAuthErrorTime = null;
    state = const AsyncValue.loading();
    state = await AsyncValue.guard(() async { /* ... */ });
  }

  bool _isAuthError(Object e) {
    final errorStr = e.toString().toLowerCase();
    return e is AuthException ||
        errorStr.contains('403') ||
        errorStr.contains('401') ||
        errorStr.contains('invalid api key');
  }
}
```

### Family Provider Pattern

For providers that require parameters (e.g., store-specific data):

```dart
// lib/presentation/providers/store_detail_provider.dart

// 1. Define parameter class with equality
class StoreDetailParams {
  final String typeNum;
  final String? storeName;

  const StoreDetailParams({required this.typeNum, this.storeName});

  // REQUIRED: Override equality for proper caching
  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is StoreDetailParams &&
          runtimeType == other.runtimeType &&
          typeNum == other.typeNum;

  @override
  int get hashCode => typeNum.hashCode;
}

// 2. Notifier accepts params in constructor
class StoreDetailNotifier extends AsyncNotifier<StoreDetail> {
  StoreDetailNotifier(this._params);
  final StoreDetailParams _params;

  @override
  Future<StoreDetail> build() async {
    final storeRepository = ref.read(storeRepositoryProvider);
    return await storeRepository.getStoreDetail(_params.typeNum);
  }

  Future<void> refresh() async {
    state = await AsyncValue.guard(() async {
      final storeRepository = ref.read(storeRepositoryProvider);
      return await storeRepository.getStoreDetail(_params.typeNum);
    });
  }
}

// 3. Family provider declaration
final storeDetailProvider = AsyncNotifierProvider.family<
    StoreDetailNotifier,
    StoreDetail,
    StoreDetailParams
>(
  StoreDetailNotifier.new,
);

// 4. Usage in widgets
final params = StoreDetailParams(typeNum: 'pc00');
final detailAsync = ref.watch(storeDetailProvider(params));
```

**Key Requirements for Family Providers:**

- Parameter class MUST override `==` and `hashCode`
- Only include fields that affect data identity in equality
- Use `const` constructors for better performance

---

## Repository Pattern

### Interface in Domain Layer

```dart
// lib/domain/repositories/dashboard_repository.dart
abstract class DashboardRepository {
  Future<List<Store>> getDashboard();
}
```

### Implementation in Data Layer

```dart
// lib/data/repositories/dashboard_repository_impl.dart
class DashboardRepositoryImpl implements DashboardRepository {
  final Dio _dio;

  DashboardRepositoryImpl({required Dio dio}) : _dio = dio;

  @override
  Future<List<Store>> getDashboard() async {
    try {
      final response = await _dio.post(
        ApiConstants.dashboardEndpoint,
        options: Options(
          contentType: ApiConstants.contentTypeFormEncoded,
        ),
      );

      if (response.statusCode == 200 && response.data != null) {
        final data = response.data;
        if (data is List) {
          return data.map((json) => _storeFromJson(json)).toList();
        }
      }
      return [];
    } on DioException catch (e) {
      // Convert to domain exceptions
      if (e.response?.statusCode == 401 || e.response?.statusCode == 403) {
        throw AuthException('Invalid API key');
      }
      if (e.type == DioExceptionType.connectionTimeout) {
        throw NetworkException('Unable to connect to server');
      }
      throw ServerException('Server error: ${e.message}');
    }
  }

  Store _storeFromJson(Map<String, dynamic> json) {
    final model = StoreModel.fromJson(json);
    return model.toEntity();
  }
}
```

### Dio Injection via Provider

```dart
// lib/presentation/providers/providers.dart

// Dio provider with interceptors
final dioProvider = Provider<Dio>((ref) {
  final dio = Dio(
    BaseOptions(
      baseUrl: ApiConstants.baseUrl,
      connectTimeout: ApiConstants.timeout,
      receiveTimeout: ApiConstants.timeout,
    ),
  );

  // API key interceptor
  dio.interceptors.add(
    InterceptorsWrapper(
      onRequest: (options, handler) async {
        final secureStorage = ref.read(secureStorageProvider);
        final apiKey = await secureStorage.read(key: SecureStorageKeys.apiKey);
        if (apiKey != null && apiKey.isNotEmpty) {
          if (options.method == 'POST') {
            final bodyData = Map<String, dynamic>.from(options.data ?? {});
            bodyData[ApiConstants.paramApiKey] = apiKey;
            options.data = bodyData;
          }
        }
        return handler.next(options);
      },
    ),
  );

  return dio;
});

// Repository provider
final dashboardRepositoryProvider = Provider<DashboardRepository>((ref) {
  return DashboardRepositoryImpl(
    dio: ref.watch(dioProvider),
  );
});
```

---

## Error Handling

### Exception Hierarchy

All custom exceptions extend the base `AppException`:

```dart
// lib/core/errors/exceptions.dart

// Base exception
abstract class AppException implements Exception {
  final String message;
  final dynamic cause;
  const AppException(this.message, [this.cause]);
}

// Server/API errors
class ServerException extends AppException {
  final int? statusCode;
  final Map<String, dynamic>? errorData;
  const ServerException(super.message, [super.cause, this.statusCode, this.errorData]);
}

// Authentication errors (401, 403, invalid API key)
class AuthException extends AppException {
  final String? errorCode;
  const AuthException(super.message, [super.cause, this.errorCode]);
}

// Network connectivity errors
class NetworkException extends AppException {
  const NetworkException(super.message, [super.cause]);
}

// Additional exception types:
// - CacheException: Storage read/write errors
// - ValidationException: Input validation failures
// - ParsingException: JSON parsing errors
// - NotFoundException: 404 errors
// - TimeoutException: Request timeouts
// - UnauthorizedException: Permission denied
// - StorageException: Secure storage errors
// - ConfigurationException: Missing/invalid config
```

### Error Display Pattern

Use `ErrorDisplay.fromError()` factory for consistent error UI:

```dart
// lib/presentation/widgets/common/error_widget.dart

// Get user-friendly message from any error
String getErrorMessage(Object error) {
  final errorString = error.toString().toLowerCase();

  if (errorString.contains('invalid api key') ||
      errorString.contains('authexception')) {
    return 'Invalid API key. Please check your API key in Settings.';
  }

  if (errorString.contains('network') ||
      errorString.contains('socket') ||
      errorString.contains('connection')) {
    return 'Unable to connect to the server. Please check your internet connection.';
  }

  if (errorString.contains('timeout')) {
    return 'The request took too long. Please try again.';
  }

  // ... additional error mappings

  return 'An unexpected error occurred. Please try again.';
}

class ErrorDisplay extends StatelessWidget {
  final String message;
  final VoidCallback? onRetry;

  const ErrorDisplay({required this.message, this.onRetry});

  // Factory for automatic error message conversion
  factory ErrorDisplay.fromError({
    required Object error,
    VoidCallback? onRetry,
  }) {
    return ErrorDisplay(
      message: getErrorMessage(error),
      onRetry: onRetry,
    );
  }

  // Widget implementation...
}

// Usage in screens
storesAsync.when(
  data: (stores) => StoreList(stores: stores),
  loading: () => const LoadingIndicator(),
  error: (error, _) => ErrorDisplay.fromError(
    error: error,
    onRetry: () => ref.read(dashboardProvider.notifier).fetchDashboard(),
  ),
);
```

---

## Navigation (GoRouter)

### Route Structure

```dart
// lib/router/app_router.dart

/install                          --> InstallationScreen
/                                 --> DashboardScreen
/settings                         --> SettingsScreen
/settings/permissions             --> PermissionSettingsScreen
/store/:typeNum                   --> StoreDetailScreen
/store/:typeNum/queue             --> BuyQueueScreen
/store/:typeNum/completed         --> CompletedBuysScreen
/store/:typeNum/stats             --> BuyerStatsScreen
/store/:typeNum/metrics           --> StoreMetricsScreen
/store/:typeNum/notes             --> WorkbookNotesScreen
/store/:typeNum/notes/create      --> WorkbookNoteCreateScreen
/store/:typeNum/tasks/today       --> TodayTasksScreen
/store/:typeNum/tasks/edit        --> TasksScreen
/store/:typeNum/tasks/edit/create --> TaskCreateScreen
/store/:typeNum/tasks/edit/:taskId --> TaskEditScreen
```

### Auth Redirect Pattern

```dart
// lib/router/app_router.dart

// Listenable to refresh router on auth changes
class AuthNotifierListenable extends ChangeNotifier {
  AuthNotifierListenable(this._ref) {
    _ref.listen(authProvider, (previous, next) {
      notifyListeners();
    });
  }
  final Ref _ref;
}

final routerProvider = Provider<GoRouter>((ref) {
  final authState = ref.watch(authProvider);
  final permissionState = ref.watch(permissionProvider);

  return GoRouter(
    initialLocation: '/',
    refreshListenable: AuthNotifierListenable(ref),
    redirect: (BuildContext context, GoRouterState state) {
      final isLoading = authState.isLoading;
      final isInstalled = authState.when(
        data: (value) => value,
        loading: () => false,
        error: (_, __) => false,
      );
      final isGoingToInstall = state.matchedLocation == '/install';

      // Don't redirect while loading
      if (isLoading) return null;

      // Not installed -> redirect to install
      if (!isInstalled) {
        return isGoingToInstall ? null : '/install';
      }

      // Already installed -> redirect away from install
      if (isGoingToInstall) return '/';

      // Check permissions for protected routes
      if (!permissionState.isLoading) {
        final appPage = _getAppPageFromLocation(state.matchedLocation);
        if (appPage != null && !permissionState.canAccess(appPage)) {
          return '/';
        }
      }

      return null;
    },
    routes: [ /* route definitions */ ],
  );
});
```

### Permission Guards

```dart
// Helper to map route to AppPage enum
AppPage? _getAppPageFromLocation(String location) {
  final path = location.split('?').first;

  if (path == '/') return AppPage.dashboard;
  if (path == '/settings') return AppPage.settings;
  if (path == '/settings/permissions') return AppPage.permissionSettings;

  // Store-specific routes
  final storeRoutePattern = RegExp(r'^/store/[^/]+(/.*)?$');
  if (storeRoutePattern.hasMatch(path)) {
    if (path.endsWith('/queue')) return AppPage.buyQueue;
    if (path.endsWith('/completed')) return AppPage.completedBuys;
    if (path.endsWith('/stats')) return AppPage.buyerStats;
    if (path.endsWith('/metrics')) return AppPage.storeMetrics;
    if (path.contains('/notes')) return AppPage.shiftNotes;
    if (path.endsWith('/tasks/today')) return AppPage.todayTasks;
    if (path.contains('/tasks/edit')) return AppPage.editTaskList;
    if (RegExp(r'^/store/[^/]+$').hasMatch(path)) return AppPage.storeDetail;
  }

  return null;
}

// Permission check in redirect
if (appPage != null && !permissionState.canAccess(appPage)) {
  return '/';  // Redirect to dashboard if no permission
}
```

### Navigation Helper Extensions

```dart
// Convenience methods for type-safe navigation
extension GoRouterExtension on BuildContext {
  void goToDashboard() => go('/');
  void goToSettings() => go('/settings');

  void goToStoreDetail(String typeNum, {String? storeName}) {
    final uri = Uri(
      path: '/store/$typeNum',
      queryParameters: storeName != null ? {'storeName': storeName} : null,
    );
    go(uri.toString());
  }

  void goToQueue(String typeNum, {String? storeName}) {
    final uri = Uri(
      path: '/store/$typeNum/queue',
      queryParameters: storeName != null ? {'storeName': storeName} : null,
    );
    go(uri.toString());
  }

  // ... additional navigation helpers
}
```

---

## Summary

| Pattern | Location | Purpose |
|---------|----------|---------|
| Clean Architecture | `lib/` structure | Layer separation and dependency management |
| Data Flow | Model -> Mapper -> Entity | Type-safe data transformation |
| AsyncNotifier | `presentation/providers/` | Async state management |
| Error Caching | Dashboard provider | Prevent API spam on auth errors |
| Family Provider | Store-specific providers | Parameterized state management |
| Repository | `domain/` + `data/repositories/` | Data access abstraction |
| Exception Hierarchy | `core/errors/` | Typed error handling |
| ErrorDisplay | `presentation/widgets/common/` | Consistent error UI |
| GoRouter | `router/app_router.dart` | Declarative navigation with guards |
