922 lines
31 KiB
Dart
922 lines
31 KiB
Dart
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/product.dart';
|
||
import 'package:kooltap/services/bar_tab_service.dart';
|
||
import 'package:kooltap/services/product_service.dart';
|
||
|
||
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: 'prod-1',
|
||
name: 'Test Beer',
|
||
category: 'Drinks',
|
||
stockQuantity: 100,
|
||
lowStockThreshold: 10,
|
||
priceInCents: 400,
|
||
),
|
||
);
|
||
});
|
||
|
||
tearDown(() async {
|
||
await database.close();
|
||
});
|
||
|
||
Product createTestProduct({
|
||
String id = 'prod-1',
|
||
String name = 'Test Beer',
|
||
int priceInCents = 400,
|
||
}) {
|
||
return Product(
|
||
id: id,
|
||
name: name,
|
||
category: 'Drinks',
|
||
stockQuantity: 100,
|
||
lowStockThreshold: 10,
|
||
priceInCents: priceInCents,
|
||
);
|
||
}
|
||
|
||
group('DriftBarTabService', () {
|
||
group('createTab', () {
|
||
test('creates an empty tab at version 1', () async {
|
||
final tab = await service.createTab(customerName: 'John');
|
||
|
||
expect(tab.customerName, 'John');
|
||
expect(tab.version, 1);
|
||
expect(tab.items, isEmpty);
|
||
});
|
||
|
||
test('rejects a blank customer name', () async {
|
||
await expectLater(
|
||
service.createTab(customerName: ' '),
|
||
throwsA(isA<ArgumentError>()),
|
||
);
|
||
});
|
||
|
||
test('creates tab with unique id', () async {
|
||
final tab1 = await service.createTab(customerName: 'John');
|
||
final tab2 = await service.createTab(customerName: 'Jane');
|
||
|
||
expect(tab1.id, isNot(equals(tab2.id)));
|
||
});
|
||
});
|
||
|
||
group('getOpenTabs', () {
|
||
test('returns empty list when no tabs', () async {
|
||
final tabs = await service.getOpenTabs();
|
||
expect(tabs, isEmpty);
|
||
});
|
||
|
||
test('tabs sorted by openedAt descending', () async {
|
||
final now = DateTime.now();
|
||
await database
|
||
.into(database.barTabs)
|
||
.insert(
|
||
BarTabsCompanion.insert(
|
||
id: 'tab-first',
|
||
customerName: 'First',
|
||
openedAt: now.subtract(const Duration(hours: 1)),
|
||
),
|
||
);
|
||
await database
|
||
.into(database.barTabs)
|
||
.insert(
|
||
BarTabsCompanion.insert(
|
||
id: 'tab-second',
|
||
customerName: 'Second',
|
||
openedAt: now,
|
||
),
|
||
);
|
||
|
||
final tabs = await service.getOpenTabs();
|
||
|
||
expect(tabs.first.customerName, 'Second');
|
||
expect(tabs.last.customerName, 'First');
|
||
});
|
||
});
|
||
|
||
group('getTabById', () {
|
||
test('returns tab when exists', () async {
|
||
final created = await service.createTab(customerName: 'Test');
|
||
|
||
final tab = await service.getTabById(created.id);
|
||
|
||
expect(tab, isNotNull);
|
||
expect(tab!.customerName, 'Test');
|
||
});
|
||
|
||
test('returns null when not exists', () async {
|
||
final tab = await service.getTabById('nonexistent');
|
||
expect(tab, isNull);
|
||
});
|
||
});
|
||
|
||
group('renameTab', () {
|
||
test('updates the tab customer name', () async {
|
||
final tab = await service.createTab(customerName: 'John');
|
||
|
||
await service.renameTab(tabId: tab.id, customerName: ' Jane ');
|
||
|
||
final updatedTab = await service.getTabById(tab.id);
|
||
expect(updatedTab!.customerName, 'Jane');
|
||
});
|
||
});
|
||
|
||
group('deleteTab', () {
|
||
test('deletes the tab and restores item stock', () async {
|
||
final tab = await service.createTab(customerName: 'John');
|
||
final product = createTestProduct();
|
||
await service.addProductToTab(tabId: tab.id, product: product);
|
||
await service.addProductToTab(tabId: tab.id, product: product);
|
||
|
||
await service.deleteTab(tab.id);
|
||
|
||
expect(await service.getTabById(tab.id), isNull);
|
||
|
||
final updatedProduct = await (database.select(
|
||
database.products,
|
||
)..where((row) => row.id.equals(product.id))).getSingle();
|
||
expect(updatedProduct.stockQuantity, 100);
|
||
|
||
expect(await database.select(database.tabItems).get(), isEmpty);
|
||
expect(await database.select(database.tabItemPurchases).get(), isEmpty);
|
||
});
|
||
});
|
||
|
||
group('addProductToTab', () {
|
||
test('adds new product to tab', () async {
|
||
final tab = await service.createTab(customerName: 'John');
|
||
final product = createTestProduct();
|
||
|
||
await service.addProductToTab(tabId: tab.id, product: product);
|
||
|
||
final updatedTab = await service.getTabById(tab.id);
|
||
expect(updatedTab!.items.length, 1);
|
||
expect(updatedTab.items.first.productName, 'Test Beer');
|
||
expect(updatedTab.items.first.quantity, 1);
|
||
});
|
||
|
||
test('increments quantity when product already in tab', () async {
|
||
final tab = await service.createTab(customerName: 'John');
|
||
final product = createTestProduct();
|
||
|
||
await service.addProductToTab(tabId: tab.id, product: product);
|
||
await service.addProductToTab(tabId: tab.id, product: product);
|
||
await service.addProductToTab(tabId: tab.id, product: product);
|
||
|
||
final updatedTab = await service.getTabById(tab.id);
|
||
expect(updatedTab!.items.length, 1);
|
||
expect(updatedTab.items.first.quantity, 3);
|
||
});
|
||
|
||
test('records each addition as a separate purchase event', () async {
|
||
final tab = await service.createTab(customerName: 'John');
|
||
final product = createTestProduct();
|
||
|
||
await service.addProductToTab(tabId: tab.id, product: product);
|
||
await service.addProductToTab(tabId: tab.id, product: product);
|
||
|
||
final barTab = (await service.getOpenTabs()).first;
|
||
final historyTab = (await service.getOpenTabs(
|
||
includeRemoved: true,
|
||
)).first;
|
||
|
||
expect(barTab.items.first.quantity, 2);
|
||
expect(barTab.items.first.purchases, hasLength(2));
|
||
expect(historyTab.items.first.purchases, hasLength(2));
|
||
expect(
|
||
historyTab.items.first.purchases.every(
|
||
(purchase) => !purchase.isRemoved,
|
||
),
|
||
isTrue,
|
||
);
|
||
});
|
||
|
||
test('adding product calculates correct line total', () async {
|
||
final tab = await service.createTab(customerName: 'John');
|
||
final product = createTestProduct(priceInCents: 450);
|
||
|
||
await service.addProductToTab(tabId: tab.id, product: product);
|
||
await service.addProductToTab(tabId: tab.id, product: product);
|
||
|
||
final updatedTab = await service.getTabById(tab.id);
|
||
expect(updatedTab!.items.first.lineTotalInCents, 900);
|
||
});
|
||
|
||
test('decreases stock as part of adding the item', () async {
|
||
final tab = await service.createTab(customerName: 'John');
|
||
final product = createTestProduct();
|
||
|
||
await service.addProductToTab(tabId: tab.id, product: product);
|
||
|
||
final storedProduct = await (database.select(
|
||
database.products,
|
||
)..where((row) => row.id.equals(product.id))).getSingle();
|
||
expect(storedProduct.stockQuantity, 99);
|
||
});
|
||
|
||
test('still sells when the stock is at zero', () async {
|
||
final tab = await service.createTab(customerName: 'John');
|
||
final product = createTestProduct();
|
||
|
||
await (database.update(database.products)
|
||
..where((row) => row.id.equals(product.id)))
|
||
.write(const ProductsCompanion(stockQuantity: Value(0)));
|
||
|
||
await service.addProductToTab(tabId: tab.id, product: product);
|
||
|
||
final updatedTab = await service.getTabById(tab.id);
|
||
expect(updatedTab!.items.single.quantity, 1);
|
||
final storedProduct = await (database.select(
|
||
database.products,
|
||
)..where((row) => row.id.equals(product.id))).getSingle();
|
||
expect(storedProduct.stockQuantity, -1);
|
||
});
|
||
});
|
||
|
||
// The website changes prices: drinks already on a tab keep theirs, the
|
||
// next one is charged the new price
|
||
group('addProductToTab after a price or name change', () {
|
||
test(
|
||
'a new price starts a new line; earlier drinks keep theirs',
|
||
() async {
|
||
final tab = await service.createTab(customerName: 'John');
|
||
await service.addProductToTab(
|
||
tabId: tab.id,
|
||
product: createTestProduct(),
|
||
);
|
||
await service.addProductToTab(
|
||
tabId: tab.id,
|
||
product: createTestProduct(),
|
||
);
|
||
|
||
await service.addProductToTab(
|
||
tabId: tab.id,
|
||
product: createTestProduct(priceInCents: 450),
|
||
);
|
||
|
||
final items = (await service.getTabById(tab.id))!.items;
|
||
expect([
|
||
for (final item in items) (item.unitPriceInCents, item.quantity),
|
||
], unorderedEquals([(400, 2), (450, 1)]));
|
||
expect((await service.getTabById(tab.id))!.totalInCents, 1250);
|
||
},
|
||
);
|
||
|
||
test('more drinks at the new price go on the new line', () async {
|
||
final tab = await service.createTab(customerName: 'John');
|
||
await service.addProductToTab(
|
||
tabId: tab.id,
|
||
product: createTestProduct(),
|
||
);
|
||
|
||
for (var i = 0; i < 3; i++) {
|
||
await service.addProductToTab(
|
||
tabId: tab.id,
|
||
product: createTestProduct(priceInCents: 450),
|
||
);
|
||
}
|
||
|
||
final items = (await service.getTabById(tab.id))!.items;
|
||
expect(items, hasLength(2));
|
||
expect(
|
||
items.firstWhere((item) => item.unitPriceInCents == 450).quantity,
|
||
3,
|
||
);
|
||
});
|
||
|
||
test('a price going back joins the line at that price again', () async {
|
||
final tab = await service.createTab(customerName: 'John');
|
||
await service.addProductToTab(
|
||
tabId: tab.id,
|
||
product: createTestProduct(),
|
||
);
|
||
await service.addProductToTab(
|
||
tabId: tab.id,
|
||
product: createTestProduct(priceInCents: 300),
|
||
);
|
||
|
||
await service.addProductToTab(
|
||
tabId: tab.id,
|
||
product: createTestProduct(),
|
||
);
|
||
|
||
final items = (await service.getTabById(tab.id))!.items;
|
||
expect([
|
||
for (final item in items) (item.unitPriceInCents, item.quantity),
|
||
], unorderedEquals([(400, 2), (300, 1)]));
|
||
});
|
||
|
||
test('a renamed product starts a new line under its new name', () async {
|
||
final tab = await service.createTab(customerName: 'John');
|
||
await service.addProductToTab(
|
||
tabId: tab.id,
|
||
product: createTestProduct(),
|
||
);
|
||
|
||
await service.addProductToTab(
|
||
tabId: tab.id,
|
||
product: createTestProduct(name: 'Pils'),
|
||
);
|
||
|
||
final items = (await service.getTabById(tab.id))!.items;
|
||
expect([
|
||
for (final item in items) item.productName,
|
||
], unorderedEquals(['Test Beer', 'Pils']));
|
||
});
|
||
|
||
test('both lines count down the stock of the one product', () async {
|
||
final tab = await service.createTab(customerName: 'John');
|
||
await service.addProductToTab(
|
||
tabId: tab.id,
|
||
product: createTestProduct(),
|
||
);
|
||
await service.addProductToTab(
|
||
tabId: tab.id,
|
||
product: createTestProduct(priceInCents: 450),
|
||
);
|
||
|
||
final stored = await (database.select(
|
||
database.products,
|
||
)..where((row) => row.id.equals('prod-1'))).getSingle();
|
||
expect(stored.stockQuantity, 98);
|
||
});
|
||
|
||
test('settling charges each line at its own price', () async {
|
||
final tab = await service.createTab(customerName: 'John');
|
||
await service.addProductToTab(
|
||
tabId: tab.id,
|
||
product: createTestProduct(),
|
||
);
|
||
await service.addProductToTab(
|
||
tabId: tab.id,
|
||
product: createTestProduct(priceInCents: 450),
|
||
);
|
||
|
||
await service.closeTab(tab.id);
|
||
|
||
final closed = (await service.getClosedTabs()).single;
|
||
expect(closed.totalInCents, 850);
|
||
expect([
|
||
for (final item in closed.items) item.unitPriceInCents,
|
||
], unorderedEquals([400, 450]));
|
||
});
|
||
});
|
||
|
||
// Foo has 2 at the old price; the website changes the price; staff tap +
|
||
// on Foo's line. The 2 stay at the old price, every new one is charged
|
||
// the new price.
|
||
group('+ on a line after a price change', () {
|
||
Future<void> setPrice(int priceInCents, {String? name}) async {
|
||
await (database.update(
|
||
database.products,
|
||
)..where((row) => row.id.equals('prod-1'))).write(
|
||
ProductsCompanion(
|
||
priceInCents: Value(priceInCents),
|
||
name: name == null ? const Value.absent() : Value(name),
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<String> fooWithTwoAtOldPrice() async {
|
||
final tab = await service.createTab(customerName: 'Foo');
|
||
await service.addProductToTab(
|
||
tabId: tab.id,
|
||
product: createTestProduct(),
|
||
);
|
||
await service.addProductToTab(
|
||
tabId: tab.id,
|
||
product: createTestProduct(),
|
||
);
|
||
await setPrice(450);
|
||
return tab.id;
|
||
}
|
||
|
||
Future<List<(int, int)>> lines(String tabId) async {
|
||
return [
|
||
for (final item in (await service.getTabById(tabId))!.items)
|
||
(item.unitPriceInCents, item.quantity),
|
||
];
|
||
}
|
||
|
||
Future<String> lineAt(String tabId, int priceInCents) async {
|
||
return (await service.getTabById(tabId))!.items
|
||
.firstWhere((item) => item.unitPriceInCents == priceInCents)
|
||
.id;
|
||
}
|
||
|
||
test(
|
||
'the old drinks keep their price, the new one gets the new price',
|
||
() async {
|
||
final tabId = await fooWithTwoAtOldPrice();
|
||
|
||
final added = await service.adjustTabItemQuantity(
|
||
tabItemId: await lineAt(tabId, 400),
|
||
delta: 1,
|
||
);
|
||
|
||
expect(added, 1);
|
||
expect(await lines(tabId), unorderedEquals([(400, 2), (450, 1)]));
|
||
expect((await service.getTabById(tabId))!.totalInCents, 1250);
|
||
},
|
||
);
|
||
|
||
test(
|
||
'tapping + on the old line again keeps adding at the new price',
|
||
() async {
|
||
final tabId = await fooWithTwoAtOldPrice();
|
||
final oldLine = await lineAt(tabId, 400);
|
||
|
||
await service.adjustTabItemQuantity(tabItemId: oldLine, delta: 1);
|
||
await service.adjustTabItemQuantity(tabItemId: oldLine, delta: 1);
|
||
|
||
expect(await lines(tabId), unorderedEquals([(400, 2), (450, 2)]));
|
||
},
|
||
);
|
||
|
||
test('+ on the line at the new price adds there', () async {
|
||
final tabId = await fooWithTwoAtOldPrice();
|
||
await service.adjustTabItemQuantity(
|
||
tabItemId: await lineAt(tabId, 400),
|
||
delta: 1,
|
||
);
|
||
|
||
await service.adjustTabItemQuantity(
|
||
tabItemId: await lineAt(tabId, 450),
|
||
delta: 1,
|
||
);
|
||
|
||
expect(await lines(tabId), unorderedEquals([(400, 2), (450, 2)]));
|
||
});
|
||
|
||
test('the tile and + end up on the same new line', () async {
|
||
final tabId = await fooWithTwoAtOldPrice();
|
||
|
||
await service.addProductToTab(
|
||
tabId: tabId,
|
||
product: createTestProduct(priceInCents: 450),
|
||
);
|
||
await service.adjustTabItemQuantity(
|
||
tabItemId: await lineAt(tabId, 400),
|
||
delta: 1,
|
||
);
|
||
|
||
expect(await lines(tabId), unorderedEquals([(400, 2), (450, 2)]));
|
||
});
|
||
|
||
test('+ several at once adds them all at the new price', () async {
|
||
final tabId = await fooWithTwoAtOldPrice();
|
||
|
||
final added = await service.adjustTabItemQuantity(
|
||
tabItemId: await lineAt(tabId, 400),
|
||
delta: 3,
|
||
);
|
||
|
||
expect(added, 3);
|
||
expect(await lines(tabId), unorderedEquals([(400, 2), (450, 3)]));
|
||
});
|
||
|
||
test('− on the old line takes off a drink at the old price', () async {
|
||
final tabId = await fooWithTwoAtOldPrice();
|
||
await service.adjustTabItemQuantity(
|
||
tabItemId: await lineAt(tabId, 400),
|
||
delta: 1,
|
||
);
|
||
|
||
await service.adjustTabItemQuantity(
|
||
tabItemId: await lineAt(tabId, 400),
|
||
delta: -1,
|
||
);
|
||
|
||
expect(await lines(tabId), unorderedEquals([(400, 1), (450, 1)]));
|
||
});
|
||
|
||
test('a price going back makes + add to the old line again', () async {
|
||
final tabId = await fooWithTwoAtOldPrice();
|
||
await setPrice(400);
|
||
|
||
await service.adjustTabItemQuantity(
|
||
tabItemId: await lineAt(tabId, 400),
|
||
delta: 1,
|
||
);
|
||
|
||
expect(await lines(tabId), [(400, 3)]);
|
||
});
|
||
|
||
test('a renamed product gets a new line under its new name', () async {
|
||
final tab = await service.createTab(customerName: 'Foo');
|
||
await service.addProductToTab(
|
||
tabId: tab.id,
|
||
product: createTestProduct(),
|
||
);
|
||
await setPrice(400, name: 'Pils');
|
||
|
||
await service.adjustTabItemQuantity(
|
||
tabItemId: (await service.getTabById(tab.id))!.items.single.id,
|
||
delta: 1,
|
||
);
|
||
|
||
final names = [
|
||
for (final item in (await service.getTabById(tab.id))!.items)
|
||
(item.productName, item.quantity),
|
||
];
|
||
expect(names, unorderedEquals([('Test Beer', 1), ('Pils', 1)]));
|
||
});
|
||
|
||
test(
|
||
'a hidden (archived) product is still charged its latest price',
|
||
() async {
|
||
final tabId = await fooWithTwoAtOldPrice();
|
||
await (database.update(database.products)
|
||
..where((row) => row.id.equals('prod-1')))
|
||
.write(const ProductsCompanion(active: Value(false)));
|
||
|
||
await service.adjustTabItemQuantity(
|
||
tabItemId: await lineAt(tabId, 400),
|
||
delta: 1,
|
||
);
|
||
|
||
expect(await lines(tabId), unorderedEquals([(400, 2), (450, 1)]));
|
||
},
|
||
);
|
||
|
||
test('a line whose product is gone keeps its own price', () async {
|
||
final tab = await service.createTab(customerName: 'Foo');
|
||
await service.addProductToTab(
|
||
tabId: tab.id,
|
||
product: createTestProduct(),
|
||
);
|
||
await database.delete(database.products).go();
|
||
|
||
await service.adjustTabItemQuantity(
|
||
tabItemId: (await service.getTabById(tab.id))!.items.single.id,
|
||
delta: 1,
|
||
);
|
||
|
||
expect(await lines(tab.id), [(400, 2)]);
|
||
});
|
||
|
||
test('another tab without the product gets the new price', () async {
|
||
await fooWithTwoAtOldPrice();
|
||
final bar = await service.createTab(customerName: 'Bar');
|
||
|
||
await service.addProductToTab(
|
||
tabId: bar.id,
|
||
product: createTestProduct(priceInCents: 450),
|
||
);
|
||
|
||
expect(await lines(bar.id), [(450, 1)]);
|
||
});
|
||
|
||
test('every drink counts down the stock of the one product', () async {
|
||
final tabId = await fooWithTwoAtOldPrice();
|
||
|
||
await service.adjustTabItemQuantity(
|
||
tabItemId: await lineAt(tabId, 400),
|
||
delta: 2,
|
||
);
|
||
|
||
final stored = await (database.select(
|
||
database.products,
|
||
)..where((row) => row.id.equals('prod-1'))).getSingle();
|
||
expect(stored.stockQuantity, 96);
|
||
});
|
||
});
|
||
|
||
group('adjustTabItemQuantity', () {
|
||
test('updates item quantity', () async {
|
||
final tab = await service.createTab(customerName: 'John');
|
||
final product = createTestProduct();
|
||
await service.addProductToTab(tabId: tab.id, product: product);
|
||
|
||
final tabItemId = (await service.getTabById(tab.id))!.items.first.id;
|
||
|
||
await service.adjustTabItemQuantity(tabItemId: tabItemId, delta: 4);
|
||
|
||
final updatedTab = await service.getTabById(tab.id);
|
||
expect(updatedTab!.items.first.quantity, 5);
|
||
});
|
||
|
||
test('hides item when the delta removes all quantity', () async {
|
||
final tab = await service.createTab(customerName: 'John');
|
||
final product = createTestProduct();
|
||
await service.addProductToTab(tabId: tab.id, product: product);
|
||
|
||
final tabItemId = (await service.getTabById(tab.id))!.items.first.id;
|
||
|
||
await service.adjustTabItemQuantity(tabItemId: tabItemId, delta: -1);
|
||
|
||
final updatedTab = await service.getTabById(tab.id);
|
||
expect(updatedTab!.items, isEmpty);
|
||
});
|
||
|
||
test('restores stock when quantity decreases', () async {
|
||
final tab = await service.createTab(customerName: 'John');
|
||
final product = createTestProduct();
|
||
await service.addProductToTab(tabId: tab.id, product: product);
|
||
|
||
final tabItemId = (await service.getTabById(tab.id))!.items.first.id;
|
||
|
||
await service.adjustTabItemQuantity(tabItemId: tabItemId, delta: -1);
|
||
|
||
final updatedTab = await service.getTabById(tab.id);
|
||
expect(updatedTab!.items, isEmpty);
|
||
|
||
final updatedProduct = await (database.select(
|
||
database.products,
|
||
)..where((row) => row.id.equals(product.id))).getSingle();
|
||
expect(updatedProduct.stockQuantity, 100);
|
||
});
|
||
|
||
test('keeps a removed purchase in the history audit trail', () async {
|
||
final tab = await service.createTab(customerName: 'John');
|
||
final product = createTestProduct();
|
||
await service.addProductToTab(tabId: tab.id, product: product);
|
||
await service.addProductToTab(tabId: tab.id, product: product);
|
||
|
||
final tabItemId = (await service.getTabById(tab.id))!.items.first.id;
|
||
|
||
await service.adjustTabItemQuantity(tabItemId: tabItemId, delta: -1);
|
||
|
||
final historyTab = (await service.getOpenTabs(
|
||
includeRemoved: true,
|
||
)).first;
|
||
final item = historyTab.items.first;
|
||
|
||
expect(item.quantity, 1);
|
||
expect(
|
||
item.purchases.where((purchase) => purchase.isRemoved),
|
||
hasLength(1),
|
||
);
|
||
expect(
|
||
item.purchases.where((purchase) => !purchase.isRemoved),
|
||
hasLength(1),
|
||
);
|
||
});
|
||
|
||
test(
|
||
'applies concurrent deltas without losing stock consistency',
|
||
() async {
|
||
final tab = await service.createTab(customerName: 'John');
|
||
final product = createTestProduct();
|
||
await service.addProductToTab(tabId: tab.id, product: product);
|
||
|
||
final tabItemId = (await service.getTabById(tab.id))!.items.first.id;
|
||
|
||
final deltas = await Future.wait([
|
||
service.adjustTabItemQuantity(tabItemId: tabItemId, delta: 1),
|
||
service.adjustTabItemQuantity(tabItemId: tabItemId, delta: 1),
|
||
]);
|
||
|
||
expect(deltas, [1, 1]);
|
||
final updatedTab = await service.getTabById(tab.id);
|
||
expect(updatedTab!.items.first.quantity, 3);
|
||
|
||
final updatedProduct = await (database.select(
|
||
database.products,
|
||
)..where((row) => row.id.equals(product.id))).getSingle();
|
||
expect(updatedProduct.stockQuantity, 97);
|
||
},
|
||
);
|
||
});
|
||
|
||
group('closeTab', () {
|
||
test('archives tab items to closed tabs', () async {
|
||
final tab = await service.createTab(customerName: 'John');
|
||
final product = createTestProduct();
|
||
await service.addProductToTab(tabId: tab.id, product: product);
|
||
await service.addProductToTab(tabId: tab.id, product: product);
|
||
|
||
await service.closeTab(tab.id);
|
||
|
||
final closedTabs = await service.getClosedTabs();
|
||
expect(closedTabs.length, 1);
|
||
expect(closedTabs.first.customerName, 'John');
|
||
expect(closedTabs.first.items.length, 2);
|
||
expect(
|
||
closedTabs.first.items.every((item) => item.quantity == 1),
|
||
true,
|
||
);
|
||
expect(
|
||
closedTabs.first.items.every((item) => item.purchasedAt != null),
|
||
true,
|
||
);
|
||
});
|
||
|
||
test(
|
||
'keeps removed purchases visible but excludes them from totals',
|
||
() async {
|
||
final tab = await service.createTab(customerName: 'John');
|
||
final product = createTestProduct();
|
||
await service.addProductToTab(tabId: tab.id, product: product);
|
||
await service.addProductToTab(tabId: tab.id, product: product);
|
||
|
||
final tabItemId = (await service.getTabById(tab.id))!.items.first.id;
|
||
await service.adjustTabItemQuantity(tabItemId: tabItemId, delta: -1);
|
||
await service.closeTab(tab.id);
|
||
|
||
final closedTab = (await service.getClosedTabs()).first;
|
||
|
||
expect(closedTab.items, hasLength(2));
|
||
expect(closedTab.items.where((item) => item.isRemoved), hasLength(1));
|
||
expect(closedTab.itemCount, 1);
|
||
expect(closedTab.totalInCents, 400);
|
||
},
|
||
);
|
||
|
||
test('clears tab items after closing', () async {
|
||
final tab = await service.createTab(customerName: 'John');
|
||
final product = createTestProduct();
|
||
await service.addProductToTab(tabId: tab.id, product: product);
|
||
|
||
await service.closeTab(tab.id);
|
||
|
||
final updatedTab = await service.getTabById(tab.id);
|
||
expect(updatedTab!.items, isEmpty);
|
||
});
|
||
|
||
test(
|
||
'keeps the tab under the same name and drops its purchases',
|
||
() async {
|
||
final tab = await service.createTab(customerName: 'John');
|
||
final product = createTestProduct();
|
||
await service.addProductToTab(tabId: tab.id, product: product);
|
||
|
||
await service.closeTab(tab.id);
|
||
|
||
final updatedTab = await service.getTabById(tab.id);
|
||
expect(updatedTab!.customerName, 'John');
|
||
expect(updatedTab.items, isEmpty);
|
||
expect(
|
||
await database.select(database.tabItemPurchases).get(),
|
||
isEmpty,
|
||
);
|
||
},
|
||
);
|
||
|
||
test('throws when the tab changed since the expected version', () async {
|
||
final tab = await service.createTab(customerName: 'John');
|
||
final product = createTestProduct();
|
||
await service.addProductToTab(tabId: tab.id, product: product);
|
||
|
||
await expectLater(
|
||
service.closeTab(tab.id, expectedVersion: tab.version),
|
||
throwsA(isA<TabVersionConflictException>()),
|
||
);
|
||
|
||
expect((await service.getTabById(tab.id))!.items, hasLength(1));
|
||
expect(await service.getClosedTabs(), isEmpty);
|
||
});
|
||
|
||
test('closes when the expected version matches', () async {
|
||
final tab = await service.createTab(customerName: 'John');
|
||
final product = createTestProduct();
|
||
await service.addProductToTab(tabId: tab.id, product: product);
|
||
final current = await service.getTabById(tab.id);
|
||
|
||
await service.closeTab(tab.id, expectedVersion: current!.version);
|
||
|
||
expect(await service.getClosedTabs(), hasLength(1));
|
||
});
|
||
|
||
test('throws for an unknown tab', () async {
|
||
await expectLater(
|
||
service.closeTab('missing'),
|
||
throwsA(isA<TabNotFoundException>()),
|
||
);
|
||
});
|
||
|
||
test('closed tab preserves item prices', () async {
|
||
final tab = await service.createTab(customerName: 'John');
|
||
final product = createTestProduct(priceInCents: 550);
|
||
await service.addProductToTab(tabId: tab.id, product: product);
|
||
|
||
await service.closeTab(tab.id);
|
||
|
||
final closedTabs = await service.getClosedTabs();
|
||
expect(closedTabs.first.items.first.unitPriceInCents, 550);
|
||
});
|
||
});
|
||
|
||
group('versioning', () {
|
||
test('every change bumps the tab version', () async {
|
||
final tab = await service.createTab(customerName: 'John');
|
||
final product = createTestProduct();
|
||
|
||
await service.addProductToTab(tabId: tab.id, product: product);
|
||
final afterAdd = (await service.getTabById(tab.id))!;
|
||
|
||
await service.adjustTabItemQuantity(
|
||
tabItemId: afterAdd.items.first.id,
|
||
delta: 1,
|
||
);
|
||
final afterAdjust = (await service.getTabById(tab.id))!;
|
||
|
||
await service.renameTab(tabId: tab.id, customerName: 'Jane');
|
||
final afterRename = (await service.getTabById(tab.id))!;
|
||
|
||
expect(afterAdd.version, tab.version + 1);
|
||
expect(afterAdjust.version, afterAdd.version + 1);
|
||
expect(afterRename.version, afterAdjust.version + 1);
|
||
});
|
||
});
|
||
|
||
group('watchOpenTabs', () {
|
||
test('emits again when an item is added', () async {
|
||
final tab = await service.createTab(customerName: 'John');
|
||
final emissions = service.watchOpenTabs();
|
||
|
||
final expectation = expectLater(
|
||
emissions.map((tabs) => tabs.single.itemCount),
|
||
emitsThrough(1),
|
||
);
|
||
await service.addProductToTab(
|
||
tabId: tab.id,
|
||
product: createTestProduct(),
|
||
);
|
||
|
||
await expectation;
|
||
});
|
||
});
|
||
|
||
group('getDistinctCustomerNames', () {
|
||
test('returns each closed customer once, sorted', () async {
|
||
for (final name in ['Zoe', 'Adam', 'Zoe']) {
|
||
final tab = await service.createTab(customerName: name);
|
||
await service.addProductToTab(
|
||
tabId: tab.id,
|
||
product: createTestProduct(),
|
||
);
|
||
await service.closeTab(tab.id);
|
||
}
|
||
|
||
expect(await service.getDistinctCustomerNames(), ['Adam', 'Zoe']);
|
||
});
|
||
});
|
||
|
||
group('getClosedTabs', () {
|
||
test('returns empty when no closed tabs', () async {
|
||
final tabs = await service.getClosedTabs();
|
||
expect(tabs, isEmpty);
|
||
});
|
||
|
||
test('returns closed tabs sorted by closedAt descending', () async {
|
||
final now = DateTime.now();
|
||
await database
|
||
.into(database.closedTabs)
|
||
.insert(
|
||
ClosedTabsCompanion.insert(
|
||
id: 'closed-first',
|
||
originalTabId: 'original-1',
|
||
customerName: 'First',
|
||
closedAt: now.subtract(const Duration(hours: 1)),
|
||
),
|
||
);
|
||
await database
|
||
.into(database.closedTabs)
|
||
.insert(
|
||
ClosedTabsCompanion.insert(
|
||
id: 'closed-second',
|
||
originalTabId: 'original-2',
|
||
customerName: 'Second',
|
||
closedAt: now,
|
||
),
|
||
);
|
||
|
||
final closedTabs = await service.getClosedTabs();
|
||
|
||
expect(closedTabs.first.customerName, 'Second');
|
||
expect(closedTabs.last.customerName, 'First');
|
||
});
|
||
});
|
||
|
||
group('hidden products', () {
|
||
test('a tab holding a deleted product can still be changed', () async {
|
||
final tab = await service.createTab(customerName: 'John');
|
||
await service.addProductToTab(
|
||
tabId: tab.id,
|
||
product: createTestProduct(),
|
||
);
|
||
await DriftProductService(database: database).deleteProduct('prod-1');
|
||
|
||
final item = (await service.getTabById(tab.id))!.items.single;
|
||
await service.adjustTabItemQuantity(tabItemId: item.id, delta: -1);
|
||
await service.deleteTab(tab.id);
|
||
|
||
expect(await service.getTabById(tab.id), isNull);
|
||
});
|
||
});
|
||
});
|
||
}
|