import '../entities/shift_report.dart'; import '../entities/sync_event.dart'; import '../entities/transaction.dart'; /// Result of one upload pass. class SyncOutcome { const SyncOutcome({ required this.attempted, required this.uploaded, this.rejected = 0, this.error, this.isRetryable = true, }); final int attempted; final int uploaded; /// Bills the back office looked at and refused. These stay on the terminal /// but sending them again unchanged will fail again, so they need a person. final int rejected; final String? error; /// Whether trying again could plausibly work. False for a bad credential or /// an unconfigured endpoint — the drain engine halts rather than retrying /// something that cannot succeed. final bool isRetryable; bool get isSuccess => error == null; bool get hadNothingToDo => attempted == 0; int get remaining => attempted - uploaded; } /// One row of the order sync log. class OrderSyncRow { const OrderSyncRow({ required this.orderId, required this.invoiceNumber, required this.total, required this.createdAt, required this.isSynced, this.syncedAt, this.attempts = 0, this.error, }); final String orderId; final String invoiceNumber; final double total; final DateTime createdAt; final bool isSynced; final DateTime? syncedAt; final int attempts; final String? error; } /// The terminal's network touchpoints. /// /// Pull the catalogue; upload every order still at `sync_status = 0`. Nothing /// else leaves the device. *When* the upload runs is not decided here — see /// `SyncEngine`, which owns triggers and retry policy. abstract class SyncRepository { bool get hasCatalogue; DateTime? get lastImportAt; String? get catalogueRevision; /// Morning step — downloads products and writes them to SQLite. Future importCatalogue({ void Function(double progress, String stage)? onProgress, }); /// How many bills are still held locally. Future unsyncedCount(); Future> unsyncedOrders(); /// Today's trading totals, read back from SQLite. /// /// Terminal-wide by default. Set [scopeToCashier] to cover only the bills /// [cashierName] rang — what a till is actually settled against. Future todayReport({ required String terminalId, required String cashierName, bool scopeToCashier = false, }); /// One upload pass — sends pending orders and flips the ones the back office /// confirmed to `sync_status = 1`. Failures leave every row untouched at 0. Future syncOrders({ void Function(double progress, String stage)? onProgress, }); /// How many shoppers registered at this till are still waiting to go up. Future unsyncedCustomerCount(); /// Uploads shoppers registered at this till. /// /// Deliberately separate from [syncOrders]. A registration is not a financial /// record: it can be replayed safely, and it must not be stuck behind a bill /// the back office has refused. Run it first so a bill referring to a new /// shopper arrives after the shopper does. Future syncCustomers(); /// Retires confirmed bills past their retention window. Archived totals are /// untouched. Future purgeExpired(); Future> orderSyncRows({int limit = 200}); List get events; }