feat: add alot
This commit is contained in:
+332
-393
@@ -1,7 +1,10 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../database/app_database.dart';
|
||||
import '../database/stock_queries.dart';
|
||||
import '../models/bar_tab.dart';
|
||||
import '../models/closed_tab.dart';
|
||||
import '../models/closed_tab_item.dart';
|
||||
@@ -9,9 +12,36 @@ import '../models/payment_method.dart';
|
||||
import '../models/product.dart';
|
||||
import '../models/tab_item.dart';
|
||||
import '../models/tab_item_purchase.dart';
|
||||
import 'product_service.dart';
|
||||
|
||||
class TabNotFoundException implements Exception {
|
||||
final String tabId;
|
||||
|
||||
const TabNotFoundException(this.tabId);
|
||||
|
||||
@override
|
||||
String toString() => 'Tab not found: $tabId';
|
||||
}
|
||||
|
||||
class TabVersionConflictException implements Exception {
|
||||
final int expectedVersion;
|
||||
final int actualVersion;
|
||||
|
||||
const TabVersionConflictException({
|
||||
required this.expectedVersion,
|
||||
required this.actualVersion,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'Tab is at version $actualVersion, expected $expectedVersion';
|
||||
}
|
||||
|
||||
abstract class BarTabService {
|
||||
/// Open tabs with their visible items, re-emitted on every change.
|
||||
Stream<List<BarTab>> watchOpenTabs();
|
||||
|
||||
/// With [includeRemoved], items whose purchases were all undone are kept
|
||||
/// so history can show them.
|
||||
Future<List<BarTab>> getOpenTabs({bool includeRemoved = false});
|
||||
|
||||
Future<BarTab?> getTabById(String id);
|
||||
@@ -21,13 +51,15 @@ abstract class BarTabService {
|
||||
Future<void> renameTab({required String tabId, required String customerName});
|
||||
|
||||
/// Deletes an open tab and returns its items to inventory.
|
||||
Future<List<TabItem>> deleteTab(String tabId);
|
||||
Future<void> deleteTab(String tabId);
|
||||
|
||||
Future<void> addProductToTab({
|
||||
required String tabId,
|
||||
required Product product,
|
||||
});
|
||||
|
||||
/// Returns the delta that was actually applied, which is smaller than
|
||||
/// [delta] when it would push the quantity below zero.
|
||||
Future<int> adjustTabItemQuantity({
|
||||
required String tabItemId,
|
||||
required int delta,
|
||||
@@ -35,9 +67,13 @@ abstract class BarTabService {
|
||||
|
||||
/// Archives the tab's current items into history and clears them.
|
||||
/// The tab itself stays open under the same customer name.
|
||||
///
|
||||
/// Throws [TabVersionConflictException] when [expectedVersion] is given and
|
||||
/// the tab changed since.
|
||||
Future<void> closeTab(
|
||||
String tabId, {
|
||||
PaymentMethod paymentMethod = PaymentMethod.cash,
|
||||
int? expectedVersion,
|
||||
});
|
||||
|
||||
Future<List<ClosedTab>> getClosedTabs();
|
||||
@@ -84,9 +120,7 @@ class DriftBarTabService implements BarTabService {
|
||||
return BarTab(
|
||||
id: row.id,
|
||||
customerName: row.customerName,
|
||||
status: row.status,
|
||||
openedAt: row.openedAt,
|
||||
closedAt: row.closedAt,
|
||||
version: row.version,
|
||||
items: items,
|
||||
);
|
||||
@@ -105,222 +139,133 @@ class DriftBarTabService implements BarTabService {
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<TabItem>> _getItemsForTab(String tabId) async {
|
||||
final query = database.select(database.tabItems)
|
||||
..where((item) => item.tabId.equals(tabId))
|
||||
..orderBy([(item) => OrderingTerm.asc(item.createdAt)]);
|
||||
/// Tabs, items and purchases in a single query, so loading N tabs doesn't
|
||||
/// cost N * items extra round trips.
|
||||
JoinedSelectStatement<HasResultSet, dynamic> _tabsQuery({String? tabId}) {
|
||||
final query = database.select(database.barTabs).join([
|
||||
leftOuterJoin(
|
||||
database.tabItems,
|
||||
database.tabItems.tabId.equalsExp(database.barTabs.id),
|
||||
),
|
||||
leftOuterJoin(
|
||||
database.tabItemPurchases,
|
||||
database.tabItemPurchases.tabItemId.equalsExp(database.tabItems.id),
|
||||
),
|
||||
]);
|
||||
|
||||
final rows = await query.get();
|
||||
if (tabId != null) query.where(database.barTabs.id.equals(tabId));
|
||||
|
||||
final items = <TabItem>[];
|
||||
query.orderBy([
|
||||
OrderingTerm.desc(database.barTabs.openedAt),
|
||||
OrderingTerm.asc(database.tabItems.createdAt),
|
||||
OrderingTerm.asc(database.tabItemPurchases.purchasedAt),
|
||||
OrderingTerm.asc(database.tabItemPurchases.id),
|
||||
]);
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
List<BarTab> _groupTabs(
|
||||
List<TypedResult> rows, {
|
||||
required bool includeRemoved,
|
||||
}) {
|
||||
final tabRows = <String, BarTabRow>{};
|
||||
final itemRowsByTab = <String, Map<String, TabItemRow>>{};
|
||||
final purchasesByItem = <String, List<TabItemPurchase>>{};
|
||||
|
||||
for (final row in rows) {
|
||||
if (row.quantity <= 0) continue;
|
||||
final tab = row.readTable(database.barTabs);
|
||||
tabRows[tab.id] = tab;
|
||||
final itemRows = itemRowsByTab.putIfAbsent(tab.id, () => {});
|
||||
|
||||
final purchases = await _getPurchaseRecords(row);
|
||||
items.add(_mapItemRow(row, purchases));
|
||||
final item = row.readTableOrNull(database.tabItems);
|
||||
if (item == null) continue;
|
||||
|
||||
itemRows[item.id] = item;
|
||||
final purchases = purchasesByItem.putIfAbsent(item.id, () => []);
|
||||
|
||||
final purchase = row.readTableOrNull(database.tabItemPurchases);
|
||||
if (purchase != null) purchases.add(_mapPurchaseRow(purchase));
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
Future<List<TabItem>> _getItemsForHistory(String tabId) async {
|
||||
final query = database.select(database.tabItems)
|
||||
..where((item) => item.tabId.equals(tabId))
|
||||
..orderBy([(item) => OrderingTerm.asc(item.createdAt)]);
|
||||
|
||||
final rows = await query.get();
|
||||
final items = <TabItem>[];
|
||||
|
||||
for (final row in rows) {
|
||||
final purchases = await _getPurchaseRecords(row);
|
||||
if (purchases.isEmpty) continue;
|
||||
|
||||
items.add(_mapItemRow(row, purchases));
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
Future<List<TabItemPurchaseRow>> _getPurchaseRows(String tabItemId) async {
|
||||
final query = database.select(database.tabItemPurchases)
|
||||
..where((purchase) => purchase.tabItemId.equals(tabItemId))
|
||||
..orderBy([
|
||||
(purchase) => OrderingTerm.asc(purchase.purchasedAt),
|
||||
(purchase) => OrderingTerm.asc(purchase.id),
|
||||
]);
|
||||
|
||||
return query.get();
|
||||
}
|
||||
|
||||
Future<List<TabItemPurchase>> _getPurchaseRecords(TabItemRow item) async {
|
||||
final rows = await _getPurchaseRows(item.id);
|
||||
final purchases = rows.map(_mapPurchaseRow).toList();
|
||||
final activeCount = purchases
|
||||
.where((purchase) => !purchase.isRemoved)
|
||||
.length;
|
||||
final missingActivePurchases = item.quantity - activeCount;
|
||||
|
||||
if (missingActivePurchases <= 0) return purchases;
|
||||
|
||||
return [
|
||||
...purchases,
|
||||
for (var index = 0; index < missingActivePurchases; index++)
|
||||
TabItemPurchase(
|
||||
id: 'legacy-${item.id}-$index',
|
||||
tabItemId: item.id,
|
||||
purchasedAt: item.createdAt,
|
||||
),
|
||||
for (final tab in tabRows.values)
|
||||
_mapTabRow(tab, [
|
||||
for (final item in itemRowsByTab[tab.id]!.values)
|
||||
if (includeRemoved
|
||||
? purchasesByItem[item.id]!.isNotEmpty
|
||||
: item.quantity > 0)
|
||||
_mapItemRow(item, purchasesByItem[item.id]!),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
Future<List<TabItemPurchaseRow>> _ensurePurchaseRows(TabItemRow item) async {
|
||||
final rows = await _getPurchaseRows(item.id);
|
||||
final activeCount = rows
|
||||
.where((purchase) => purchase.removedAt == null)
|
||||
.length;
|
||||
final missingActivePurchases = item.quantity - activeCount;
|
||||
|
||||
for (var index = 0; index < missingActivePurchases; index++) {
|
||||
await _recordPurchase(tabItemId: item.id, purchasedAt: item.createdAt);
|
||||
}
|
||||
|
||||
if (missingActivePurchases <= 0) return rows;
|
||||
|
||||
return _getPurchaseRows(item.id);
|
||||
Future<BarTabRow?> _getTabRow(String tabId) {
|
||||
return (database.select(
|
||||
database.barTabs,
|
||||
)..where((tab) => tab.id.equals(tabId))).getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<void> _recordPurchase({
|
||||
required String tabItemId,
|
||||
DateTime? purchasedAt,
|
||||
}) async {
|
||||
Future<void> _recordPurchase(String tabItemId) async {
|
||||
await database
|
||||
.into(database.tabItemPurchases)
|
||||
.insert(
|
||||
TabItemPurchasesCompanion.insert(
|
||||
id: _uuid.v4(),
|
||||
tabItemId: tabItemId,
|
||||
purchasedAt: purchasedAt ?? DateTime.now(),
|
||||
purchasedAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _decreaseProductStock(String productId, int amount) async {
|
||||
if (amount <= 0) {
|
||||
throw ArgumentError.value(amount, 'amount', 'Must be greater than zero');
|
||||
}
|
||||
/// Marks the most recent active purchases as removed, keeping them in the
|
||||
/// audit trail.
|
||||
Future<void> _removeLatestPurchases(String tabItemId, int count) async {
|
||||
final latest =
|
||||
await (database.select(database.tabItemPurchases)
|
||||
..where(
|
||||
(purchase) =>
|
||||
purchase.tabItemId.equals(tabItemId) &
|
||||
purchase.removedAt.isNull(),
|
||||
)
|
||||
..orderBy([
|
||||
(purchase) => OrderingTerm.desc(purchase.purchasedAt),
|
||||
(purchase) => OrderingTerm.desc(purchase.id),
|
||||
])
|
||||
..limit(count))
|
||||
.get();
|
||||
|
||||
final updatedRows = await database.customUpdate(
|
||||
'UPDATE products '
|
||||
'SET stock_quantity = stock_quantity - ? '
|
||||
'WHERE id = ? AND stock_quantity >= ?',
|
||||
variables: [
|
||||
Variable.withInt(amount),
|
||||
Variable.withString(productId),
|
||||
Variable.withInt(amount),
|
||||
],
|
||||
updates: {database.products},
|
||||
await (database.update(database.tabItemPurchases)
|
||||
..where((purchase) => purchase.id.isIn(latest.map((row) => row.id))))
|
||||
.write(TabItemPurchasesCompanion(removedAt: Value(DateTime.now())));
|
||||
}
|
||||
|
||||
/// Foreign keys aren't enforced, so purchases are deleted explicitly.
|
||||
Future<void> _deleteItemsOfTab(String tabId) async {
|
||||
final itemIds = database.selectOnly(database.tabItems)
|
||||
..addColumns([database.tabItems.id])
|
||||
..where(database.tabItems.tabId.equals(tabId));
|
||||
|
||||
await (database.delete(
|
||||
database.tabItemPurchases,
|
||||
)..where((purchase) => purchase.tabItemId.isInQuery(itemIds))).go();
|
||||
|
||||
await (database.delete(
|
||||
database.tabItems,
|
||||
)..where((item) => item.tabId.equals(tabId))).go();
|
||||
}
|
||||
|
||||
Future<void> _incrementTabVersion(String tabId) async {
|
||||
await (database.update(
|
||||
database.barTabs,
|
||||
)..where((tab) => tab.id.equals(tabId))).write(
|
||||
BarTabsCompanion.custom(
|
||||
version: database.barTabs.version + const Constant(1),
|
||||
),
|
||||
);
|
||||
|
||||
if (updatedRows == 1) return;
|
||||
|
||||
final product = await (database.select(
|
||||
database.products,
|
||||
)..where((row) => row.id.equals(productId))).getSingleOrNull();
|
||||
|
||||
if (product == null) {
|
||||
throw Exception('Product not found');
|
||||
}
|
||||
|
||||
throw const InsufficientStockException();
|
||||
}
|
||||
|
||||
Future<void> _increaseProductStock(String productId, int amount) async {
|
||||
if (amount <= 0) {
|
||||
throw ArgumentError.value(amount, 'amount', 'Must be greater than zero');
|
||||
}
|
||||
|
||||
final updatedRows = await database.customUpdate(
|
||||
'UPDATE products '
|
||||
'SET stock_quantity = stock_quantity + ? '
|
||||
'WHERE id = ?',
|
||||
variables: [Variable.withInt(amount), Variable.withString(productId)],
|
||||
updates: {database.products},
|
||||
);
|
||||
|
||||
if (updatedRows == 1) return;
|
||||
|
||||
throw Exception('Product not found');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<BarTab>> getOpenTabs({bool includeRemoved = false}) async {
|
||||
final query = database.select(database.barTabs)
|
||||
..where((tab) => tab.status.equals('open'))
|
||||
..orderBy([(tab) => OrderingTerm.desc(tab.openedAt)]);
|
||||
|
||||
final tabRows = await query.get();
|
||||
|
||||
final tabs = <BarTab>[];
|
||||
|
||||
for (final tabRow in tabRows) {
|
||||
final items = includeRemoved
|
||||
? await _getItemsForHistory(tabRow.id)
|
||||
: await _getItemsForTab(tabRow.id);
|
||||
|
||||
tabs.add(_mapTabRow(tabRow, items));
|
||||
}
|
||||
|
||||
return tabs;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<BarTab?> getTabById(String id) async {
|
||||
final query = database.select(database.barTabs)
|
||||
..where((tab) => tab.id.equals(id));
|
||||
|
||||
final tabRow = await query.getSingleOrNull();
|
||||
|
||||
if (tabRow == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final items = await _getItemsForTab(tabRow.id);
|
||||
|
||||
return _mapTabRow(tabRow, items);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<BarTab> createTab({required String customerName}) async {
|
||||
final id = _uuid.v4();
|
||||
final now = DateTime.now();
|
||||
|
||||
await database
|
||||
.into(database.barTabs)
|
||||
.insert(
|
||||
BarTabsCompanion.insert(
|
||||
id: id,
|
||||
customerName: customerName,
|
||||
status: const Value('open'),
|
||||
openedAt: now,
|
||||
version: const Value(1),
|
||||
),
|
||||
);
|
||||
|
||||
final tab = await getTabById(id);
|
||||
|
||||
if (tab == null) {
|
||||
throw Exception('Could not create tab.');
|
||||
}
|
||||
|
||||
return tab;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> renameTab({
|
||||
required String tabId,
|
||||
required String customerName,
|
||||
}) async {
|
||||
String _validateCustomerName(String customerName) {
|
||||
final name = customerName.trim();
|
||||
if (name.isEmpty) {
|
||||
throw ArgumentError.value(
|
||||
@@ -329,56 +274,80 @@ class DriftBarTabService implements BarTabService {
|
||||
'Must not be empty',
|
||||
);
|
||||
}
|
||||
|
||||
final updatedRows =
|
||||
await (database.update(database.barTabs)
|
||||
..where((tab) => tab.id.equals(tabId)))
|
||||
.write(BarTabsCompanion(customerName: Value(name)));
|
||||
|
||||
if (updatedRows != 1) {
|
||||
throw StateError('Tab not found.');
|
||||
}
|
||||
|
||||
await _incrementTabVersion(tabId);
|
||||
return name;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<TabItem>> deleteTab(String tabId) async {
|
||||
return database.transaction(() async {
|
||||
final tab = await (database.select(
|
||||
database.barTabs,
|
||||
)..where((row) => row.id.equals(tabId))).getSingleOrNull();
|
||||
Stream<List<BarTab>> watchOpenTabs() {
|
||||
return _tabsQuery().watch().map(
|
||||
(rows) => _groupTabs(rows, includeRemoved: false),
|
||||
);
|
||||
}
|
||||
|
||||
if (tab == null) return <TabItem>[];
|
||||
@override
|
||||
Future<List<BarTab>> getOpenTabs({bool includeRemoved = false}) async {
|
||||
final rows = await _tabsQuery().get();
|
||||
return _groupTabs(rows, includeRemoved: includeRemoved);
|
||||
}
|
||||
|
||||
final items = await _getItemsForTab(tabId);
|
||||
@override
|
||||
Future<BarTab?> getTabById(String id) async {
|
||||
final rows = await _tabsQuery(tabId: id).get();
|
||||
return _groupTabs(rows, includeRemoved: false).firstOrNull;
|
||||
}
|
||||
|
||||
for (final item in items) {
|
||||
await _increaseProductStock(item.productId, item.quantity);
|
||||
}
|
||||
@override
|
||||
Future<BarTab> createTab({required String customerName}) async {
|
||||
final id = _uuid.v4();
|
||||
|
||||
await (database.delete(
|
||||
database.tabItems,
|
||||
)..where((item) => item.tabId.equals(tabId))).go();
|
||||
await database
|
||||
.into(database.barTabs)
|
||||
.insert(
|
||||
BarTabsCompanion.insert(
|
||||
id: id,
|
||||
customerName: _validateCustomerName(customerName),
|
||||
openedAt: DateTime.now(),
|
||||
version: const Value(1),
|
||||
),
|
||||
);
|
||||
|
||||
final deletedRows = await (database.delete(
|
||||
database.barTabs,
|
||||
)..where((row) => row.id.equals(tabId))).go();
|
||||
return (await getTabById(id))!;
|
||||
}
|
||||
|
||||
if (deletedRows != 1) {
|
||||
throw StateError('Tab was changed before it could be deleted');
|
||||
}
|
||||
@override
|
||||
Future<void> renameTab({
|
||||
required String tabId,
|
||||
required String customerName,
|
||||
}) async {
|
||||
final name = _validateCustomerName(customerName);
|
||||
|
||||
return items;
|
||||
await database.transaction(() async {
|
||||
final updatedRows =
|
||||
await (database.update(database.barTabs)
|
||||
..where((tab) => tab.id.equals(tabId)))
|
||||
.write(BarTabsCompanion(customerName: Value(name)));
|
||||
|
||||
if (updatedRows != 1) throw TabNotFoundException(tabId);
|
||||
|
||||
await _incrementTabVersion(tabId);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _incrementTabVersion(String tabId) async {
|
||||
await database.customUpdate(
|
||||
'UPDATE bar_tabs SET version = version + 1 WHERE id = ?',
|
||||
variables: [Variable.withString(tabId)],
|
||||
updates: {database.barTabs},
|
||||
);
|
||||
@override
|
||||
Future<void> deleteTab(String tabId) async {
|
||||
await database.transaction(() async {
|
||||
final tab = await getTabById(tabId);
|
||||
if (tab == null) return;
|
||||
|
||||
for (final item in tab.items) {
|
||||
await database.increaseStock(item.productId, item.quantity);
|
||||
}
|
||||
|
||||
await _deleteItemsOfTab(tabId);
|
||||
await (database.delete(
|
||||
database.barTabs,
|
||||
)..where((row) => row.id.equals(tabId))).go();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -387,48 +356,44 @@ class DriftBarTabService implements BarTabService {
|
||||
required Product product,
|
||||
}) async {
|
||||
await database.transaction(() async {
|
||||
await _decreaseProductStock(product.id, 1);
|
||||
await database.decreaseStock(product.id, 1);
|
||||
|
||||
final existingItemQuery = database.select(database.tabItems)
|
||||
..where(
|
||||
(item) =>
|
||||
item.tabId.equals(tabId) & item.productId.equals(product.id),
|
||||
);
|
||||
|
||||
final existingItem = await existingItemQuery.getSingleOrNull();
|
||||
final existingItem =
|
||||
await (database.select(database.tabItems)..where(
|
||||
(item) =>
|
||||
item.tabId.equals(tabId) &
|
||||
item.productId.equals(product.id),
|
||||
))
|
||||
.getSingleOrNull();
|
||||
|
||||
if (existingItem != null) {
|
||||
final updateQuery = database.update(database.tabItems)
|
||||
..where((item) => item.id.equals(existingItem.id));
|
||||
|
||||
await updateQuery.write(
|
||||
await (database.update(
|
||||
database.tabItems,
|
||||
)..where((item) => item.id.equals(existingItem.id))).write(
|
||||
TabItemsCompanion(quantity: Value(existingItem.quantity + 1)),
|
||||
);
|
||||
await _recordPurchase(tabItemId: existingItem.id);
|
||||
await _recordPurchase(existingItem.id);
|
||||
} else {
|
||||
final tabItemId = _uuid.v4();
|
||||
|
||||
return;
|
||||
await database
|
||||
.into(database.tabItems)
|
||||
.insert(
|
||||
TabItemsCompanion.insert(
|
||||
id: tabItemId,
|
||||
tabId: tabId,
|
||||
productId: product.id,
|
||||
productName: product.name,
|
||||
quantity: 1,
|
||||
unitPriceInCents: product.priceInCents,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
await _recordPurchase(tabItemId);
|
||||
}
|
||||
|
||||
final tabItemId = _uuid.v4();
|
||||
|
||||
await database
|
||||
.into(database.tabItems)
|
||||
.insert(
|
||||
TabItemsCompanion.insert(
|
||||
id: tabItemId,
|
||||
tabId: tabId,
|
||||
productId: product.id,
|
||||
productName: product.name,
|
||||
quantity: 1,
|
||||
unitPriceInCents: product.priceInCents,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
|
||||
await _recordPurchase(tabItemId: tabItemId);
|
||||
await _incrementTabVersion(tabId);
|
||||
});
|
||||
|
||||
await _incrementTabVersion(tabId);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -436,100 +401,61 @@ class DriftBarTabService implements BarTabService {
|
||||
required String tabItemId,
|
||||
required int delta,
|
||||
}) async {
|
||||
String? tabId;
|
||||
if (delta == 0) return 0;
|
||||
|
||||
final actualDelta = await database.transaction(() async {
|
||||
return database.transaction(() async {
|
||||
final item = await (database.select(
|
||||
database.tabItems,
|
||||
)..where((row) => row.id.equals(tabItemId))).getSingleOrNull();
|
||||
|
||||
if (item == null || delta == 0 || (item.quantity == 0 && delta < 0)) {
|
||||
return 0;
|
||||
}
|
||||
if (item == null) return 0;
|
||||
|
||||
tabId = item.tabId;
|
||||
final newQuantity = max(item.quantity + delta, 0);
|
||||
final actualDelta = newQuantity - item.quantity;
|
||||
|
||||
final requestedQuantity = item.quantity + delta;
|
||||
final actualDelta = requestedQuantity <= 0 ? -item.quantity : delta;
|
||||
if (actualDelta == 0) return 0;
|
||||
|
||||
if (actualDelta > 0) {
|
||||
await _decreaseProductStock(item.productId, actualDelta);
|
||||
|
||||
await database.decreaseStock(item.productId, actualDelta);
|
||||
for (var index = 0; index < actualDelta; index++) {
|
||||
await _recordPurchase(tabItemId: item.id);
|
||||
await _recordPurchase(item.id);
|
||||
}
|
||||
} else {
|
||||
await _increaseProductStock(item.productId, -actualDelta);
|
||||
|
||||
final purchaseRows = await _ensurePurchaseRows(item);
|
||||
final activePurchaseRows =
|
||||
purchaseRows
|
||||
.where((purchase) => purchase.removedAt == null)
|
||||
.toList()
|
||||
..sort((a, b) => b.purchasedAt.compareTo(a.purchasedAt));
|
||||
final removedAt = DateTime.now();
|
||||
|
||||
for (final purchase in activePurchaseRows.take(-actualDelta)) {
|
||||
await (database.update(database.tabItemPurchases)
|
||||
..where((row) => row.id.equals(purchase.id)))
|
||||
.write(TabItemPurchasesCompanion(removedAt: Value(removedAt)));
|
||||
}
|
||||
await database.increaseStock(item.productId, -actualDelta);
|
||||
await _removeLatestPurchases(item.id, -actualDelta);
|
||||
}
|
||||
|
||||
if (requestedQuantity <= 0) {
|
||||
final updatedRows =
|
||||
await (database.update(database.tabItems)
|
||||
..where((row) => row.id.equals(tabItemId)))
|
||||
.write(const TabItemsCompanion(quantity: Value(0)));
|
||||
await (database.update(database.tabItems)
|
||||
..where((row) => row.id.equals(tabItemId)))
|
||||
.write(TabItemsCompanion(quantity: Value(newQuantity)));
|
||||
|
||||
if (updatedRows != 1) {
|
||||
throw StateError(
|
||||
'Tab item was changed before its quantity was updated',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
final updatedRows =
|
||||
await (database.update(database.tabItems)
|
||||
..where((row) => row.id.equals(tabItemId)))
|
||||
.write(TabItemsCompanion(quantity: Value(requestedQuantity)));
|
||||
|
||||
if (updatedRows != 1) {
|
||||
throw StateError(
|
||||
'Tab item was changed before its quantity was updated',
|
||||
);
|
||||
}
|
||||
}
|
||||
await _incrementTabVersion(item.tabId);
|
||||
|
||||
return actualDelta;
|
||||
});
|
||||
|
||||
if (tabId != null && actualDelta != 0) {
|
||||
await _incrementTabVersion(tabId!);
|
||||
}
|
||||
|
||||
return actualDelta;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> closeTab(
|
||||
String tabId, {
|
||||
PaymentMethod paymentMethod = PaymentMethod.cash,
|
||||
int? expectedVersion,
|
||||
}) async {
|
||||
await database.transaction(() async {
|
||||
final tabQuery = database.select(database.barTabs)
|
||||
..where((tab) => tab.id.equals(tabId));
|
||||
final tabRow = await _getTabRow(tabId);
|
||||
if (tabRow == null) throw TabNotFoundException(tabId);
|
||||
|
||||
final tabRow = await tabQuery.getSingleOrNull();
|
||||
|
||||
if (tabRow == null) {
|
||||
return;
|
||||
if (expectedVersion != null && tabRow.version != expectedVersion) {
|
||||
throw TabVersionConflictException(
|
||||
expectedVersion: expectedVersion,
|
||||
actualVersion: tabRow.version,
|
||||
);
|
||||
}
|
||||
|
||||
final items = await _getItemsForHistory(tabId);
|
||||
final rows = await _tabsQuery(tabId: tabId).get();
|
||||
final items = _groupTabs(rows, includeRemoved: true).first.items;
|
||||
|
||||
if (items.isEmpty) {
|
||||
return;
|
||||
}
|
||||
if (items.isEmpty) return;
|
||||
|
||||
final closedTabId = _uuid.v4();
|
||||
|
||||
@@ -541,44 +467,83 @@ class DriftBarTabService implements BarTabService {
|
||||
originalTabId: tabId,
|
||||
customerName: tabRow.customerName,
|
||||
closedAt: DateTime.now(),
|
||||
paymentMethod: Value(paymentMethod.value),
|
||||
paymentMethod: Value(paymentMethod),
|
||||
),
|
||||
);
|
||||
|
||||
for (final item in items) {
|
||||
for (final purchase in item.purchases) {
|
||||
await database
|
||||
.into(database.closedTabItems)
|
||||
.insert(
|
||||
ClosedTabItemsCompanion.insert(
|
||||
id: _uuid.v4(),
|
||||
closedTabId: closedTabId,
|
||||
productId: item.productId,
|
||||
productName: item.productName,
|
||||
quantity: 1,
|
||||
unitPriceInCents: item.unitPriceInCents,
|
||||
purchasedAt: Value(purchase.purchasedAt),
|
||||
removedAt: Value(purchase.removedAt),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
await database.batch((batch) {
|
||||
batch.insertAll(database.closedTabItems, [
|
||||
for (final item in items)
|
||||
for (final purchase in item.purchases)
|
||||
ClosedTabItemsCompanion.insert(
|
||||
id: _uuid.v4(),
|
||||
closedTabId: closedTabId,
|
||||
productId: item.productId,
|
||||
productName: item.productName,
|
||||
quantity: 1,
|
||||
unitPriceInCents: item.unitPriceInCents,
|
||||
purchasedAt: Value(purchase.purchasedAt),
|
||||
removedAt: Value(purchase.removedAt),
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
final deleteQuery = database.delete(database.tabItems)
|
||||
..where((item) => item.tabId.equals(tabId));
|
||||
|
||||
await deleteQuery.go();
|
||||
await _deleteItemsOfTab(tabId);
|
||||
await _incrementTabVersion(tabId);
|
||||
});
|
||||
}
|
||||
|
||||
await _incrementTabVersion(tabId);
|
||||
/// Loads the items for every row in one query instead of one per tab.
|
||||
Future<List<ClosedTab>> _withItems(List<ClosedTabsRow> tabRows) async {
|
||||
if (tabRows.isEmpty) return [];
|
||||
|
||||
final itemRows =
|
||||
await (database.select(database.closedTabItems)
|
||||
..where(
|
||||
(item) => item.closedTabId.isIn(tabRows.map((row) => row.id)),
|
||||
)
|
||||
..orderBy([
|
||||
(item) => OrderingTerm.desc(item.purchasedAt),
|
||||
(item) => OrderingTerm.desc(item.id),
|
||||
]))
|
||||
.get();
|
||||
|
||||
final itemsByTab = <String, List<ClosedTabItem>>{};
|
||||
for (final row in itemRows) {
|
||||
itemsByTab
|
||||
.putIfAbsent(row.closedTabId, () => [])
|
||||
.add(_mapClosedItemRow(row));
|
||||
}
|
||||
|
||||
return [
|
||||
for (final row in tabRows)
|
||||
ClosedTab(
|
||||
id: row.id,
|
||||
originalTabId: row.originalTabId,
|
||||
customerName: row.customerName,
|
||||
closedAt: row.closedAt,
|
||||
paymentMethod: row.paymentMethod,
|
||||
items: itemsByTab[row.id] ?? [],
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
SimpleSelectStatement<$ClosedTabsTable, ClosedTabsRow> _closedTabsQuery({
|
||||
String? customerName,
|
||||
}) {
|
||||
final query = database.select(database.closedTabs)
|
||||
..orderBy([(tab) => OrderingTerm.desc(tab.closedAt)]);
|
||||
|
||||
if (customerName != null && customerName.isNotEmpty) {
|
||||
query.where((tab) => tab.customerName.equals(customerName));
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ClosedTab>> getClosedTabs() async {
|
||||
final count = await getClosedTabCount();
|
||||
if (count == 0) return [];
|
||||
|
||||
return getClosedTabsPaginated(limit: count, offset: 0);
|
||||
return _withItems(await _closedTabsQuery().get());
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -587,53 +552,21 @@ class DriftBarTabService implements BarTabService {
|
||||
int offset = 0,
|
||||
String? customerName,
|
||||
}) async {
|
||||
var query = database.select(database.closedTabs)
|
||||
..orderBy([(tab) => OrderingTerm.desc(tab.closedAt)])
|
||||
final query = _closedTabsQuery(customerName: customerName)
|
||||
..limit(limit, offset: offset);
|
||||
|
||||
if (customerName != null && customerName.isNotEmpty) {
|
||||
query = query..where((tab) => tab.customerName.equals(customerName));
|
||||
}
|
||||
|
||||
final closedTabRows = await query.get();
|
||||
|
||||
final closedTabs = <ClosedTab>[];
|
||||
|
||||
for (final row in closedTabRows) {
|
||||
final itemsQuery = database.select(database.closedTabItems)
|
||||
..where((item) => item.closedTabId.equals(row.id))
|
||||
..orderBy([
|
||||
(item) => OrderingTerm.desc(item.purchasedAt),
|
||||
(item) => OrderingTerm.desc(item.id),
|
||||
]);
|
||||
|
||||
final itemRows = await itemsQuery.get();
|
||||
|
||||
closedTabs.add(
|
||||
ClosedTab(
|
||||
id: row.id,
|
||||
originalTabId: row.originalTabId,
|
||||
customerName: row.customerName,
|
||||
closedAt: row.closedAt,
|
||||
paymentMethod: PaymentMethod.fromValue(row.paymentMethod),
|
||||
items: itemRows.map<ClosedTabItem>(_mapClosedItemRow).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return closedTabs;
|
||||
return _withItems(await query.get());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> getClosedTabCount({String? customerName}) async {
|
||||
final countExpr = database.closedTabs.id.count();
|
||||
|
||||
var query = database.selectOnly(database.closedTabs)
|
||||
final query = database.selectOnly(database.closedTabs)
|
||||
..addColumns([countExpr]);
|
||||
|
||||
if (customerName != null && customerName.isNotEmpty) {
|
||||
query = query
|
||||
..where(database.closedTabs.customerName.equals(customerName));
|
||||
query.where(database.closedTabs.customerName.equals(customerName));
|
||||
}
|
||||
|
||||
final row = await query.getSingle();
|
||||
@@ -642,7 +575,13 @@ class DriftBarTabService implements BarTabService {
|
||||
|
||||
@override
|
||||
Future<List<String>> getDistinctCustomerNames() async {
|
||||
final rows = await database.select(database.closedTabs).get();
|
||||
return rows.map((row) => row.customerName).toSet().toList()..sort();
|
||||
final customerName = database.closedTabs.customerName;
|
||||
|
||||
final query = database.selectOnly(database.closedTabs, distinct: true)
|
||||
..addColumns([customerName])
|
||||
..orderBy([OrderingTerm.asc(customerName)]);
|
||||
|
||||
final rows = await query.get();
|
||||
return rows.map((row) => row.read(customerName)!).toList();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user