feat: add historical data
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
@@ -10,6 +11,7 @@ import '../models/closed_tab.dart';
|
||||
import '../models/closed_tab_item.dart';
|
||||
import '../models/payment_method.dart';
|
||||
import '../models/product.dart';
|
||||
import '../models/tab_event.dart';
|
||||
import '../models/tab_item.dart';
|
||||
import '../models/tab_item_purchase.dart';
|
||||
|
||||
@@ -69,11 +71,13 @@ abstract class BarTabService {
|
||||
/// The tab itself stays open under the same customer name.
|
||||
///
|
||||
/// Throws [TabVersionConflictException] when [expectedVersion] is given and
|
||||
/// the tab changed since.
|
||||
/// the tab changed since. [commandId] is the remote command that requested
|
||||
/// the settlement, if any.
|
||||
Future<void> closeTab(
|
||||
String tabId, {
|
||||
PaymentMethod paymentMethod = PaymentMethod.cash,
|
||||
int? expectedVersion,
|
||||
String? commandId,
|
||||
});
|
||||
|
||||
Future<List<ClosedTab>> getClosedTabs();
|
||||
@@ -91,9 +95,11 @@ abstract class BarTabService {
|
||||
|
||||
class DriftBarTabService implements BarTabService {
|
||||
final AppDatabase database;
|
||||
final TabEventFactory eventFactory;
|
||||
final _uuid = const Uuid();
|
||||
|
||||
DriftBarTabService({required this.database});
|
||||
DriftBarTabService({required this.database, TabEventFactory? eventFactory})
|
||||
: eventFactory = eventFactory ?? TabEventFactory();
|
||||
|
||||
TabItemPurchase _mapPurchaseRow(TabItemPurchaseRow row) {
|
||||
return TabItemPurchase(
|
||||
@@ -255,6 +261,21 @@ class DriftBarTabService implements BarTabService {
|
||||
)..where((item) => item.tabId.equals(tabId))).go();
|
||||
}
|
||||
|
||||
/// Queues [event] in the outbox. Call it inside the transaction that makes
|
||||
/// the change, so the change and its event are stored together or not at
|
||||
/// all.
|
||||
Future<void> _recordEvent(TabEvent event) async {
|
||||
await database
|
||||
.into(database.tabEvents)
|
||||
.insert(
|
||||
TabEventsCompanion.insert(
|
||||
id: event.id,
|
||||
payload: jsonEncode(event.payload),
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _incrementTabVersion(String tabId) async {
|
||||
await (database.update(
|
||||
database.barTabs,
|
||||
@@ -341,6 +362,15 @@ class DriftBarTabService implements BarTabService {
|
||||
|
||||
for (final item in tab.items) {
|
||||
await database.increaseStock(item.productId, item.quantity);
|
||||
await _recordEvent(
|
||||
eventFactory.lineRemoved(
|
||||
tabId: tab.id,
|
||||
customerName: tab.customerName,
|
||||
productName: item.productName,
|
||||
unitPriceInCents: item.unitPriceInCents,
|
||||
quantity: item.quantity,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await _deleteItemsOfTab(tabId);
|
||||
@@ -356,6 +386,9 @@ class DriftBarTabService implements BarTabService {
|
||||
required Product product,
|
||||
}) async {
|
||||
await database.transaction(() async {
|
||||
final tabRow = await _getTabRow(tabId);
|
||||
if (tabRow == null) throw TabNotFoundException(tabId);
|
||||
|
||||
await database.decreaseStock(product.id, 1);
|
||||
|
||||
final existingItem =
|
||||
@@ -373,6 +406,16 @@ class DriftBarTabService implements BarTabService {
|
||||
TabItemsCompanion(quantity: Value(existingItem.quantity + 1)),
|
||||
);
|
||||
await _recordPurchase(existingItem.id);
|
||||
// The line keeps the price it was first added at
|
||||
await _recordEvent(
|
||||
eventFactory.lineAdded(
|
||||
tabId: tabId,
|
||||
customerName: tabRow.customerName,
|
||||
productName: existingItem.productName,
|
||||
unitPriceInCents: existingItem.unitPriceInCents,
|
||||
quantity: 1,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final tabItemId = _uuid.v4();
|
||||
|
||||
@@ -390,6 +433,15 @@ class DriftBarTabService implements BarTabService {
|
||||
),
|
||||
);
|
||||
await _recordPurchase(tabItemId);
|
||||
await _recordEvent(
|
||||
eventFactory.lineAdded(
|
||||
tabId: tabId,
|
||||
customerName: tabRow.customerName,
|
||||
productName: product.name,
|
||||
unitPriceInCents: product.priceInCents,
|
||||
quantity: 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await _incrementTabVersion(tabId);
|
||||
@@ -429,6 +481,20 @@ class DriftBarTabService implements BarTabService {
|
||||
..where((row) => row.id.equals(tabItemId)))
|
||||
.write(TabItemsCompanion(quantity: Value(newQuantity)));
|
||||
|
||||
final tabRow = await _getTabRow(item.tabId);
|
||||
final lineEvent = actualDelta > 0
|
||||
? eventFactory.lineAdded
|
||||
: eventFactory.lineRemoved;
|
||||
await _recordEvent(
|
||||
lineEvent(
|
||||
tabId: item.tabId,
|
||||
customerName: tabRow?.customerName ?? '',
|
||||
productName: item.productName,
|
||||
unitPriceInCents: item.unitPriceInCents,
|
||||
quantity: actualDelta.abs(),
|
||||
),
|
||||
);
|
||||
|
||||
await _incrementTabVersion(item.tabId);
|
||||
|
||||
return actualDelta;
|
||||
@@ -440,6 +506,7 @@ class DriftBarTabService implements BarTabService {
|
||||
String tabId, {
|
||||
PaymentMethod paymentMethod = PaymentMethod.cash,
|
||||
int? expectedVersion,
|
||||
String? commandId,
|
||||
}) async {
|
||||
await database.transaction(() async {
|
||||
final tabRow = await _getTabRow(tabId);
|
||||
@@ -488,6 +555,34 @@ class DriftBarTabService implements BarTabService {
|
||||
]);
|
||||
});
|
||||
|
||||
// One settlement, not a removal per line: these drinks were sold
|
||||
final settledItems = [
|
||||
for (final item in items)
|
||||
if (item.quantity > 0) item,
|
||||
];
|
||||
if (settledItems.isNotEmpty) {
|
||||
await _recordEvent(
|
||||
eventFactory.settled(
|
||||
tabId: tabId,
|
||||
customerName: tabRow.customerName,
|
||||
amountInCents: settledItems.fold(
|
||||
0,
|
||||
(total, item) => total + item.lineTotalInCents,
|
||||
),
|
||||
paymentMethod: paymentMethod,
|
||||
commandId: commandId,
|
||||
items: [
|
||||
for (final item in settledItems)
|
||||
(
|
||||
productName: item.productName,
|
||||
quantity: item.quantity,
|
||||
unitPriceInCents: item.unitPriceInCents,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await _deleteItemsOfTab(tabId);
|
||||
await _incrementTabVersion(tabId);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
@@ -45,6 +46,9 @@ class RemoteSyncService {
|
||||
static const _venueIdKey = 'remote_venue_id';
|
||||
static const _syncStateId = 'default';
|
||||
|
||||
/// Most outbox events sent per sync; a larger backlog drains over syncs.
|
||||
static const maxEventsPerSync = 500;
|
||||
|
||||
static const _pollingInterval = Duration(
|
||||
seconds: kSyncPollingIntervalSeconds,
|
||||
);
|
||||
@@ -227,6 +231,8 @@ class RemoteSyncService {
|
||||
|
||||
Future<void> _doSync(_Credentials credentials) async {
|
||||
final state = await _getSyncState();
|
||||
// Read before the snapshot, so the snapshot is never older than the events
|
||||
final outbox = await _pendingEvents();
|
||||
final openTabs = await barTabService.getOpenTabs();
|
||||
|
||||
final response = await apiClient.sync(
|
||||
@@ -235,9 +241,15 @@ class RemoteSyncService {
|
||||
deviceId: credentials.deviceId,
|
||||
cursor: state?.serverCursor,
|
||||
openTabs: openTabs.map(_toSnapshot).toList(),
|
||||
events: [
|
||||
for (final row in outbox)
|
||||
jsonDecode(row.payload) as Map<String, dynamic>,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
await _clearSentEvents(response);
|
||||
|
||||
for (final command in response.commands) {
|
||||
await _processCommand(command, credentials);
|
||||
}
|
||||
@@ -251,6 +263,38 @@ class RemoteSyncService {
|
||||
await _updateSyncState(success: true, cursor: response.cursor);
|
||||
}
|
||||
|
||||
Future<List<TabEventRow>> _pendingEvents() {
|
||||
return (database.select(database.tabEvents)
|
||||
..orderBy([
|
||||
(row) => OrderingTerm.asc(row.createdAt),
|
||||
// Same-second events keep their insertion order
|
||||
(row) => OrderingTerm.asc(const CustomExpression<int>('rowid')),
|
||||
])
|
||||
..limit(maxEventsPerSync))
|
||||
.get();
|
||||
}
|
||||
|
||||
/// Events the server stored or refused leave the outbox. Events created
|
||||
/// while the sync was in flight are in neither list and go next time.
|
||||
Future<void> _clearSentEvents(SyncResponse response) async {
|
||||
for (final rejected in response.rejectedEvents) {
|
||||
debugPrint(
|
||||
'RemoteSyncService: event ${rejected.id} rejected: ${rejected.reason}',
|
||||
);
|
||||
}
|
||||
|
||||
final ids = {
|
||||
...response.acknowledgedEventIds,
|
||||
for (final rejected in response.rejectedEvents)
|
||||
if (rejected.id != null) rejected.id!,
|
||||
};
|
||||
if (ids.isEmpty) return;
|
||||
|
||||
await (database.delete(
|
||||
database.tabEvents,
|
||||
)..where((row) => row.id.isIn(ids))).go();
|
||||
}
|
||||
|
||||
SnapshotTab _toSnapshot(BarTab tab) {
|
||||
return SnapshotTab(
|
||||
id: tab.id,
|
||||
@@ -309,6 +353,7 @@ class RemoteSyncService {
|
||||
command.tabId,
|
||||
paymentMethod: PaymentMethod.fromValue(command.paymentMethod),
|
||||
expectedVersion: command.expectedVersion,
|
||||
commandId: command.id,
|
||||
);
|
||||
|
||||
_eventController.add(
|
||||
|
||||
Reference in New Issue
Block a user