fix: Make transactions atomic / AutoLogout

This commit is contained in:
2026-08-03 02:22:26 +02:00
parent 6060ca2713
commit 0997d110d4
14 changed files with 604 additions and 285 deletions
+130 -50
View File
@@ -4,14 +4,28 @@ 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(() {
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 {
@@ -59,14 +73,16 @@ void main() {
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(),
),
);
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();
@@ -76,22 +92,26 @@ void main() {
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,
),
);
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();
@@ -152,9 +172,38 @@ void main() {
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('updateTabItemQuantity', () {
group('adjustTabItemQuantity', () {
test('updates item quantity', () async {
final tab = await service.createTab(customerName: 'John');
final product = createTestProduct();
@@ -162,40 +211,67 @@ void main() {
final tabItemId = (await service.getTabById(tab.id))!.items.first.id;
await service.updateTabItemQuantity(
tabItemId: tabItemId,
quantity: 5,
);
await service.adjustTabItemQuantity(tabItemId: tabItemId, delta: 4);
final updatedTab = await service.getTabById(tab.id);
expect(updatedTab!.items.first.quantity, 5);
});
test('deletes item when quantity is zero', () async {
test('deletes 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.updateTabItemQuantity(tabItemId: tabItemId, quantity: 0);
await service.adjustTabItemQuantity(tabItemId: tabItemId, delta: -1);
final updatedTab = await service.getTabById(tab.id);
expect(updatedTab!.items, isEmpty);
});
test('deletes item when quantity is negative', () async {
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.updateTabItemQuantity(tabItemId: tabItemId, quantity: -1);
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(
'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', () {
@@ -257,22 +333,26 @@ void main() {
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,
),
);
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();