feat: add more detailed history and group per day
This commit is contained in:
@@ -196,6 +196,29 @@ void main() {
|
||||
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);
|
||||
@@ -251,7 +274,7 @@ void main() {
|
||||
expect(updatedTab!.items.first.quantity, 5);
|
||||
});
|
||||
|
||||
test('deletes item when the delta removes all quantity', () async {
|
||||
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);
|
||||
@@ -282,6 +305,32 @@ void main() {
|
||||
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 {
|
||||
@@ -320,10 +369,38 @@ void main() {
|
||||
final closedTabs = await service.getClosedTabs();
|
||||
expect(closedTabs.length, 1);
|
||||
expect(closedTabs.first.customerName, 'John');
|
||||
expect(closedTabs.first.items.length, 1);
|
||||
expect(closedTabs.first.items.first.quantity, 2);
|
||||
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();
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
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/pin_lock_service.dart';
|
||||
import 'package:kooltab2/services/product_service.dart';
|
||||
import 'package:kooltab2/services/settings_service.dart';
|
||||
import 'package:kooltab2/viewmodels/dev_menu_view_model.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MockBarTabService extends Mock implements BarTabService {}
|
||||
|
||||
class MockPinLockService extends Mock implements PinLockService {}
|
||||
|
||||
class MockProductService extends Mock implements ProductService {}
|
||||
|
||||
class MockSettingsService extends Mock implements SettingsService {}
|
||||
|
||||
void main() {
|
||||
late AppDatabase database;
|
||||
late MockProductService productService;
|
||||
late DevMenuViewModel viewModel;
|
||||
|
||||
setUp(() {
|
||||
database = AppDatabase(NativeDatabase.memory());
|
||||
productService = MockProductService();
|
||||
|
||||
when(() => productService.getProducts()).thenAnswer(
|
||||
(_) async => const [
|
||||
Product(
|
||||
id: 'product-1',
|
||||
name: 'Cola',
|
||||
category: 'Drinks',
|
||||
stockQuantity: 100,
|
||||
lowStockThreshold: 10,
|
||||
priceInCents: 250,
|
||||
),
|
||||
Product(
|
||||
id: 'product-2',
|
||||
name: 'Chips',
|
||||
category: 'Snacks',
|
||||
stockQuantity: 100,
|
||||
lowStockThreshold: 10,
|
||||
priceInCents: 150,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
viewModel = DevMenuViewModel(
|
||||
database: database,
|
||||
productService: productService,
|
||||
barTabService: MockBarTabService(),
|
||||
settingsService: MockSettingsService(),
|
||||
pinLockService: MockPinLockService(),
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await database.close();
|
||||
});
|
||||
|
||||
test('generates individual timestamped mock history items', () async {
|
||||
await viewModel.generateMockOrders(count: 12);
|
||||
|
||||
final tabs = await database.select(database.closedTabs).get();
|
||||
final items = await database.select(database.closedTabItems).get();
|
||||
|
||||
expect(tabs, hasLength(12));
|
||||
expect(items, isNotEmpty);
|
||||
expect(items.every((item) => item.quantity == 1), isTrue);
|
||||
expect(items.every((item) => item.purchasedAt != null), isTrue);
|
||||
expect(
|
||||
viewModel.lastAction,
|
||||
startsWith('Generated 12 mock orders across '),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:kooltab2/models/closed_tab_item.dart';
|
||||
import 'package:kooltab2/utils/history_grouping.dart';
|
||||
|
||||
void main() {
|
||||
test('groups items by purchase day with newest day first', () {
|
||||
final items = [
|
||||
_closedItem('older', DateTime(2026, 8, 1, 22)),
|
||||
_closedItem('newer-b', DateTime(2026, 8, 3, 18)),
|
||||
_closedItem('newer-a', DateTime(2026, 8, 3, 10)),
|
||||
];
|
||||
|
||||
final groups = groupClosedTabItemsByDay(
|
||||
items,
|
||||
fallbackDate: DateTime(2026, 8, 4),
|
||||
);
|
||||
|
||||
expect(groups, hasLength(2));
|
||||
expect(groups.first.day, DateTime(2026, 8, 3));
|
||||
expect(groups.first.items.map((item) => item.id), ['newer-b', 'newer-a']);
|
||||
expect(groups.last.day, DateTime(2026, 8, 1));
|
||||
});
|
||||
}
|
||||
|
||||
ClosedTabItem _closedItem(String id, DateTime purchasedAt) {
|
||||
return ClosedTabItem(
|
||||
id: id,
|
||||
closedTabId: 'closed-tab',
|
||||
productId: 'product-$id',
|
||||
productName: id,
|
||||
quantity: 1,
|
||||
unitPriceInCents: 100,
|
||||
purchasedAt: purchasedAt,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:kooltab2/models/bar_tab.dart';
|
||||
import 'package:kooltab2/models/closed_tab.dart';
|
||||
import 'package:kooltab2/models/payment_method.dart';
|
||||
import 'package:kooltab2/services/bar_tab_service.dart';
|
||||
import 'package:kooltab2/viewmodels/history_view_model.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MockBarTabService extends Mock implements BarTabService {}
|
||||
|
||||
void main() {
|
||||
late MockBarTabService service;
|
||||
late HistoryViewModel viewModel;
|
||||
|
||||
setUp(() {
|
||||
service = MockBarTabService();
|
||||
viewModel = HistoryViewModel(barTabService: service);
|
||||
|
||||
when(
|
||||
() => service.getClosedTabsPaginated(
|
||||
limit: 20,
|
||||
offset: 0,
|
||||
customerName: null,
|
||||
),
|
||||
).thenAnswer((_) async => []);
|
||||
when(
|
||||
() => service.getClosedTabCount(customerName: null),
|
||||
).thenAnswer((_) async => 0);
|
||||
when(() => service.getDistinctCustomerNames()).thenAnswer((_) async => []);
|
||||
when(
|
||||
() => service.getOpenTabs(includeRemoved: true),
|
||||
).thenAnswer((_) async => [_openTab]);
|
||||
});
|
||||
|
||||
test('loads open tabs for the top of history', () async {
|
||||
await viewModel.load();
|
||||
|
||||
expect(viewModel.openTabs, hasLength(1));
|
||||
expect(viewModel.openTabs.first.customerName, 'Alice');
|
||||
expect(viewModel.customerNames, ['Alice']);
|
||||
});
|
||||
|
||||
test('search filters open tabs as well as closed tabs', () async {
|
||||
await viewModel.load();
|
||||
|
||||
viewModel.search('bob');
|
||||
|
||||
expect(viewModel.openTabs, isEmpty);
|
||||
});
|
||||
|
||||
test('sorts closed and open tabs newest first', () async {
|
||||
final olderClosedTab = _closedTab('closed-old', DateTime(2026, 8, 1));
|
||||
final newerClosedTab = _closedTab('closed-new', DateTime(2026, 8, 3));
|
||||
final olderOpenTab = _barTab('open-old', DateTime(2026, 8, 1));
|
||||
final newerOpenTab = _barTab('open-new', DateTime(2026, 8, 3));
|
||||
|
||||
when(
|
||||
() => service.getClosedTabsPaginated(
|
||||
limit: 20,
|
||||
offset: 0,
|
||||
customerName: null,
|
||||
),
|
||||
).thenAnswer((_) async => [olderClosedTab, newerClosedTab]);
|
||||
when(
|
||||
() => service.getClosedTabCount(customerName: null),
|
||||
).thenAnswer((_) async => 2);
|
||||
when(
|
||||
() => service.getOpenTabs(includeRemoved: true),
|
||||
).thenAnswer((_) async => [olderOpenTab, newerOpenTab]);
|
||||
|
||||
await viewModel.load();
|
||||
|
||||
expect(viewModel.closedTabs.map((tab) => tab.id), [
|
||||
'closed-new',
|
||||
'closed-old',
|
||||
]);
|
||||
expect(viewModel.openTabs.map((tab) => tab.id), ['open-new', 'open-old']);
|
||||
});
|
||||
}
|
||||
|
||||
ClosedTab _closedTab(String id, DateTime closedAt) {
|
||||
return ClosedTab(
|
||||
id: id,
|
||||
originalTabId: id,
|
||||
customerName: id,
|
||||
closedAt: closedAt,
|
||||
paymentMethod: PaymentMethod.cash,
|
||||
items: const [],
|
||||
);
|
||||
}
|
||||
|
||||
BarTab _barTab(String id, DateTime openedAt) {
|
||||
return BarTab(
|
||||
id: id,
|
||||
customerName: id,
|
||||
status: 'open',
|
||||
openedAt: openedAt,
|
||||
items: const [],
|
||||
);
|
||||
}
|
||||
|
||||
final _openTab = BarTab(
|
||||
id: 'tab-1',
|
||||
customerName: 'Alice',
|
||||
status: 'open',
|
||||
openedAt: DateTime(2026, 8, 2, 12),
|
||||
items: const [],
|
||||
);
|
||||
Reference in New Issue
Block a user