import 'dart:convert'; import 'package:drift/drift.dart' hide isNull, isNotNull; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:kooltap/database/app_database.dart'; import 'package:kooltap/models/payment_method.dart'; import 'package:kooltap/models/product.dart'; import 'package:kooltap/models/tab_event.dart'; import 'package:kooltap/services/bar_tab_service.dart'; const _cola = Product( id: 'prod-1', name: 'Cola', category: 'Drinks', stockQuantity: 100, lowStockThreshold: 10, priceInCents: 250, ); void main() { late AppDatabase database; late DriftBarTabService service; setUp(() async { database = AppDatabase(NativeDatabase.memory()); service = DriftBarTabService(database: database); await database .into(database.products) .insert( ProductsCompanion.insert( id: _cola.id, name: _cola.name, category: _cola.category, stockQuantity: _cola.stockQuantity, lowStockThreshold: _cola.lowStockThreshold, priceInCents: _cola.priceInCents, ), ); }); tearDown(() async { await database.close(); }); Future>> outbox() async { final rows = await (database.select(database.tabEvents)..orderBy([ (row) => OrderingTerm.asc(const CustomExpression('rowid')), ])) .get(); return [ for (final row in rows) jsonDecode(row.payload) as Map, ]; } Future lineIdOf(String tabId) async { return (await service.getTabById(tabId))!.items.single.id; } Future tabWithTwoColas() async { final tab = await service.createTab(customerName: 'Jan'); await service.addProductToTab(tabId: tab.id, product: _cola); await service.addProductToTab(tabId: tab.id, product: _cola); await database.delete(database.tabEvents).go(); return (database.select( database.barTabs, )..where((row) => row.id.equals(tab.id))).getSingle(); } group('line events', () { test('adding a product writes line_added with the common fields', () async { final tab = await service.createTab(customerName: 'Jan'); await service.addProductToTab(tabId: tab.id, product: _cola); final event = (await outbox()).single; expect(event['type'], 'line_added'); expect(event['tabId'], tab.id); expect(event['customerName'], 'Jan'); expect(event['productName'], 'Cola'); expect(event['unitPriceInCents'], 250); expect(event['quantity'], 1); expect(event['id'], matches(RegExp(r'^[0-9a-f-]{36}$'))); expect(event['occurredAt'], endsWith('Z')); final row = await database.select(database.tabEvents).getSingle(); expect(row.id, event['id']); }); test('adding to an existing line keeps that line\'s price', () async { final tab = await service.createTab(customerName: 'Jan'); await service.addProductToTab(tabId: tab.id, product: _cola); await service.addProductToTab( tabId: tab.id, product: const Product( id: 'prod-1', name: 'Cola', category: 'Drinks', stockQuantity: 100, lowStockThreshold: 10, priceInCents: 300, ), ); final events = await outbox(); expect(events.map((event) => event['unitPriceInCents']), [250, 250]); }); test('changing a quantity writes the difference', () async { final tab = await service.createTab(customerName: 'Jan'); await service.addProductToTab(tabId: tab.id, product: _cola); await service.addProductToTab(tabId: tab.id, product: _cola); final lineId = await lineIdOf(tab.id); await service.adjustTabItemQuantity(tabItemId: lineId, delta: 3); await service.adjustTabItemQuantity(tabItemId: lineId, delta: -4); final events = (await outbox()).skip(2).toList(); expect(events[0]['type'], 'line_added'); expect(events[0]['quantity'], 3); expect(events[1]['type'], 'line_removed'); expect(events[1]['quantity'], 4); expect(events[1]['unitPriceInCents'], 250); }); test('removing a line writes line_removed for what was left', () async { final tab = await tabWithTwoColas(); final lineId = await lineIdOf(tab.id); // More than is on the tab: only the 2 that were there are removed await service.adjustTabItemQuantity(tabItemId: lineId, delta: -5); final event = (await outbox()).single; expect(event['type'], 'line_removed'); expect(event['quantity'], 2); }); test('a change that does nothing writes no event', () async { final tab = await service.createTab(customerName: 'Jan'); await service.addProductToTab(tabId: tab.id, product: _cola); final lineId = await lineIdOf(tab.id); await service.adjustTabItemQuantity(tabItemId: lineId, delta: -1); await database.delete(database.tabEvents).go(); await service.adjustTabItemQuantity(tabItemId: lineId, delta: -1); await service.adjustTabItemQuantity(tabItemId: lineId, delta: 0); expect(await outbox(), isEmpty); }); test('deleting a tab writes line_removed for each line', () async { final tab = await tabWithTwoColas(); await service.deleteTab(tab.id); final event = (await outbox()).single; expect(event['type'], 'line_removed'); expect(event['quantity'], 2); expect(event['customerName'], 'Jan'); }); test('later events use the new name after a rename', () async { final tab = await service.createTab(customerName: 'Jan'); await service.renameTab(tabId: tab.id, customerName: 'Janneke'); expect(await outbox(), isEmpty); await service.addProductToTab(tabId: tab.id, product: _cola); expect((await outbox()).single['customerName'], 'Janneke'); }); test('a failed change writes no event', () async { await expectLater( service.addProductToTab(tabId: 'missing', product: _cola), throwsA(isA()), ); expect(await outbox(), isEmpty); }); }); group('settled events', () { test('settling writes one settled event and no removals', () async { final tab = await tabWithTwoColas(); await service.closeTab(tab.id, paymentMethod: PaymentMethod.payconiq); final event = (await outbox()).single; expect(event['type'], 'settled'); expect(event['customerName'], 'Jan'); expect(event['amountInCents'], 500); expect(event['paymentMethod'], 'payconiq'); expect(event.containsKey('commandId'), false); expect(event['items'], [ {'productName': 'Cola', 'quantity': 2, 'unitPriceInCents': 250}, ]); }); test('lines that were taken back are not settled', () async { final tab = await tabWithTwoColas(); final lineId = await lineIdOf(tab.id); await service.adjustTabItemQuantity(tabItemId: lineId, delta: -1); await service.closeTab(tab.id); final settled = (await outbox()).last; expect(settled['amountInCents'], 250); expect(settled['paymentMethod'], 'cash'); expect((settled['items'] as List).single['quantity'], 1); }); test('settling an empty tab writes nothing', () async { final tab = await service.createTab(customerName: 'Jan'); await service.closeTab(tab.id); expect(await outbox(), isEmpty); }); test('a version conflict writes nothing', () async { final tab = await tabWithTwoColas(); await expectLater( service.closeTab(tab.id, expectedVersion: tab.version - 1), throwsA(isA()), ); expect(await outbox(), isEmpty); }); test('carries the command id when given', () async { final tab = await tabWithTwoColas(); await service.closeTab( tab.id, paymentMethod: PaymentMethod.paymentPending, commandId: 'cmd-1', ); final event = (await outbox()).single; expect(event['commandId'], 'cmd-1'); expect(event['paymentMethod'], 'payment_pending'); }); }); test('occurredAt is the UTC time it happened', () { final factory = TabEventFactory( now: () => DateTime.utc(2026, 10, 24, 21, 41, 3, 120), ); final event = factory.lineAdded( tabId: 'a1b2', customerName: 'Jan', productName: 'Cola', unitPriceInCents: 250, quantity: 2, ); expect(event.payload['occurredAt'], '2026-10-24T21:41:03.120Z'); expect( () => factory.lineRemoved( tabId: 'a1b2', customerName: 'Jan', productName: 'Cola', unitPriceInCents: 250, quantity: 0, ), throwsArgumentError, ); }); }