Files
kooltab/test/bar_tab_service_test.dart

476 lines
16 KiB
Dart

import 'package:drift/drift.dart' hide isNull, isNotNull;
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:kooltab2/database/app_database.dart';
import 'package:kooltab2/models/product.dart';
import 'package:kooltab2/services/bar_tab_service.dart';
import 'package:kooltab2/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 tab with open status', () async {
final tab = await service.createTab(customerName: 'John');
expect(tab.customerName, 'John');
expect(tab.status, 'open');
expect(tab.items, isEmpty);
});
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('returns only open tabs', () async {
await service.createTab(customerName: 'Open Tab');
await database
.into(database.barTabs)
.insert(
BarTabsCompanion.insert(
id: 'closed-tab',
customerName: 'Closed Tab',
status: const Value('closed'),
openedAt: DateTime.now(),
),
);
final tabs = await service.getOpenTabs();
expect(tabs.length, 1);
expect(tabs.first.customerName, 'Open Tab');
});
test('tabs sorted by openedAt descending', () async {
final now = DateTime.now();
await database
.into(database.barTabs)
.insert(
BarTabsCompanion.insert(
id: 'tab-first',
customerName: 'First',
status: const Value('open'),
openedAt: now.subtract(const Duration(hours: 1)),
),
);
await database
.into(database.barTabs)
.insert(
BarTabsCompanion.insert(
id: 'tab-second',
customerName: 'Second',
status: const Value('open'),
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);
final deletedItems = await service.deleteTab(tab.id);
expect(deletedItems.length, 1);
expect(deletedItems.first.quantity, 2);
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);
final remainingItems = await database.select(database.tabItems).get();
expect(remainingItems, 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('rolls back the tab insert when stock is unavailable', () 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 expectLater(
service.addProductToTab(tabId: tab.id, product: product),
throwsA(isA<InsufficientStockException>()),
);
final updatedTab = await service.getTabById(tab.id);
expect(updatedTab!.items, isEmpty);
});
});
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('tab remains open but items cleared', () 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!.isOpen, true);
expect(updatedTab.items, isEmpty);
});
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('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');
});
});
});
}