feat: make server own products

This commit is contained in:
2026-10-21 12:10:42 +02:00
parent 93acfe15a9
commit b15d94a805
21 changed files with 1717 additions and 106 deletions
+73 -7
View File
@@ -9,6 +9,7 @@ import 'package:http/http.dart' as http;
import 'package:sentry_flutter/sentry_flutter.dart';
import '../database/app_database.dart';
import '../database/catalog_queries.dart';
import '../database/stock_queries.dart';
import '../models/bar_tab.dart';
import '../models/payment_method.dart';
@@ -19,6 +20,16 @@ import 'kooltap_api_client.dart';
typedef _Credentials = ({String deviceId, String token});
/// Parts of a sync response the tablet could not apply; the rest was applied.
class SyncApplyException implements Exception {
final List<String> failures;
const SyncApplyException(this.failures);
@override
String toString() => failures.join('\n');
}
typedef _CommandOutcome = ({CommandResult result, String? message});
class RemoteSyncService {
@@ -225,7 +236,8 @@ class RemoteSyncService {
} on RateLimitedException catch (e) {
nextDelay = await _recordFailure(e, retryAfter: e.retryAfter);
} catch (e, stack) {
if (!_isConnectivityError(e)) {
// A failed part was reported when it failed
if (!_isConnectivityError(e) && e is! SyncApplyException) {
unawaited(Sentry.captureException(e, stackTrace: stack));
}
nextDelay = await _recordFailure(e);
@@ -252,7 +264,13 @@ class RemoteSyncService {
debugPrint('RemoteSyncService: sync failed: $error');
_consecutiveFailures++;
await _updateSyncState(success: false);
_emit(RemoteSyncEventType.syncFailed, message: error.toString());
_eventController.add(
RemoteSyncEvent(
type: RemoteSyncEventType.syncFailed,
message: error.toString(),
offline: _isConnectivityError(error),
),
);
return retryAfter ?? _backoff();
}
@@ -293,18 +311,46 @@ class RemoteSyncService {
await _clearSentEvents(response);
await _markStockResultsReceived(response);
await barTabService.recordSettlements(response.settlements);
// Each part is applied on its own: one that fails (a tab or settlement
// the tablet can't store) must not keep prices and stock from arriving
final failures = <String>[];
final settlementsApplied = await _applyPart(
'settlements',
failures,
() => barTabService.recordSettlements(response.settlements),
);
final serverTabs = response.tabs;
if (serverTabs != null) {
// After the acked events left the outbox: what is left still has to
// reach the server and is replayed on top
await barTabService.applyServerState(serverTabs);
await _applyPart(
'tabs',
failures,
() => barTabService.applyServerState(serverTabs),
);
}
// The website manages the products; first, so new ones get their stock
final catalog = response.catalog;
if (catalog != null) {
await _applyPart(
'products',
failures,
() => database.applyServerCatalog(catalog),
);
}
// Same rebase for the stock: the server counts every tablet's sales
final serverStock = response.stock;
if (serverStock != null) await database.rebaseStock(serverStock);
if (serverStock != null) {
await _applyPart(
'stock',
failures,
() => database.rebaseStock(serverStock),
);
}
for (final command in response.commands) {
await _processCommand(command, credentials);
@@ -329,15 +375,35 @@ class RemoteSyncService {
}
}
// Settlements that weren't stored are asked for again next time
await _updateSyncState(
success: true,
success: failures.isEmpty,
cursor: response.cursor,
settlementsCursor: response.settlementsCursor,
settlementsCursor: settlementsApplied ? response.settlementsCursor : null,
);
if (failures.isNotEmpty) throw SyncApplyException(failures);
return appliedStock;
}
/// Runs one part of applying a sync response. Returns whether it worked; a
/// failure is reported and added to [failures] instead of thrown.
Future<bool> _applyPart(
String part,
List<String> failures,
Future<void> Function() apply,
) async {
try {
await apply();
return true;
} catch (e, stack) {
debugPrint('RemoteSyncService: applying $part failed: $e');
unawaited(Sentry.captureException(e, stackTrace: stack));
failures.add('$part: $e');
return false;
}
}
Future<List<TabEventRow>> _pendingEvents() {
return (database.select(database.tabEvents)
..orderBy([