Files
nearle_pos/lib/domain/repositories/sync_repository.dart
2026-08-06 19:29:23 +05:30

116 lines
3.5 KiB
Dart

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.itemCount = 0,
this.syncedAt,
this.attempts = 0,
this.error,
});
final String orderId;
final String invoiceNumber;
final double total;
final DateTime createdAt;
final bool isSynced;
/// Units on the bill. Fractional because loose goods are sold by weight.
final double itemCount;
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<SyncEvent> importCatalogue({
void Function(double progress, String stage)? onProgress,
});
/// How many bills are still held locally.
Future<int> unsyncedCount();
Future<List<SaleTransaction>> 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<ShiftReport> 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<SyncOutcome> syncOrders({
void Function(double progress, String stage)? onProgress,
});
/// How many shoppers registered at this till are still waiting to go up.
Future<int> 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<SyncOutcome> syncCustomers();
/// Retires confirmed bills past their retention window. Archived totals are
/// untouched.
Future<int> purgeExpired();
Future<List<OrderSyncRow>> orderSyncRows({int limit = 200});
List<SyncEvent> get events;
}