feat: make server own products
This commit is contained in:
@@ -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([
|
||||
|
||||
Reference in New Issue
Block a user