feat: make server owner of tabs
This commit is contained in:
+116
-34
@@ -1,14 +1,11 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
import '../models/remote_command.dart';
|
||||
import '../models/tab_event.dart';
|
||||
import 'app_database.dart';
|
||||
|
||||
class InsufficientStockException implements Exception {
|
||||
const InsufficientStockException();
|
||||
|
||||
@override
|
||||
String toString() => 'Not enough stock';
|
||||
}
|
||||
|
||||
class ProductNotFoundException implements Exception {
|
||||
final String productId;
|
||||
|
||||
@@ -20,44 +17,129 @@ class ProductNotFoundException implements Exception {
|
||||
|
||||
/// Atomic stock updates shared by the product and tab services, so they can
|
||||
/// run inside either one's transaction.
|
||||
///
|
||||
/// Stock may go below zero: the bar keeps serving when the count was off.
|
||||
extension StockQueries on AppDatabase {
|
||||
/// Products with infinite stock are only checked for existence.
|
||||
Future<void> decreaseStock(String productId, int amount) async {
|
||||
_validateStockAmount(amount);
|
||||
|
||||
final updatedRows =
|
||||
await (update(products)..where(
|
||||
(row) =>
|
||||
row.id.equals(productId) &
|
||||
row.infiniteStock.not() &
|
||||
row.stockQuantity.isBiggerOrEqualValue(amount),
|
||||
))
|
||||
.write(
|
||||
ProductsCompanion.custom(
|
||||
stockQuantity: products.stockQuantity - Variable(amount),
|
||||
),
|
||||
);
|
||||
|
||||
if (updatedRows == 1) return;
|
||||
|
||||
final product = await _findProduct(productId);
|
||||
if (!product.infiniteStock) throw const InsufficientStockException();
|
||||
if (await changeTrackedStock(productId, -amount) == 0) {
|
||||
await _findProduct(productId);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> increaseStock(String productId, int amount) async {
|
||||
_validateStockAmount(amount);
|
||||
|
||||
final updatedRows =
|
||||
await (update(products)..where(
|
||||
(row) => row.id.equals(productId) & row.infiniteStock.not(),
|
||||
))
|
||||
.write(
|
||||
ProductsCompanion.custom(
|
||||
stockQuantity: products.stockQuantity + Variable(amount),
|
||||
),
|
||||
);
|
||||
if (await changeTrackedStock(productId, amount) == 0) {
|
||||
await _findProduct(productId);
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedRows != 1) await _findProduct(productId);
|
||||
/// Adds [delta] to the stock of [productId] when its stock is tracked.
|
||||
/// Returns the number of products changed: 0 when the product has infinite
|
||||
/// stock or no longer exists, which is not an error.
|
||||
Future<int> changeTrackedStock(String productId, int delta) {
|
||||
return (update(products)
|
||||
..where((row) => row.id.equals(productId) & row.infiniteStock.not()))
|
||||
.write(
|
||||
ProductsCompanion.custom(
|
||||
stockQuantity: products.stockQuantity + Variable(delta),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Queues a `stock_set` event, so a stock count or tracking change made on
|
||||
/// this tablet reaches the server. Call it inside the transaction that
|
||||
/// writes the stock.
|
||||
Future<void> queueStockSet(TabEvent event) async {
|
||||
await into(tabEvents).insert(
|
||||
TabEventsCompanion.insert(
|
||||
id: event.id,
|
||||
payload: jsonEncode(event.payload),
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Replaces the local stock of the products in [serverStock] with the
|
||||
/// server's, then replays the outbox events the server hasn't applied yet
|
||||
/// on top, the way the server will. The events the server acknowledged
|
||||
/// must have left the outbox first: they are in [serverStock] already.
|
||||
///
|
||||
/// Products the server doesn't know keep their local stock.
|
||||
Future<void> rebaseStock(List<ServerStock> serverStock) {
|
||||
return transaction(() async {
|
||||
final stock = {
|
||||
for (final entry in serverStock)
|
||||
entry.productId: (stock: entry.stock, countedAt: entry.countedAt),
|
||||
};
|
||||
|
||||
// Read here, so changes made while the sync was in flight are included
|
||||
final outbox =
|
||||
await (select(tabEvents)..orderBy([
|
||||
(row) => OrderingTerm.asc(row.createdAt),
|
||||
(row) => OrderingTerm.asc(const CustomExpression<int>('rowid')),
|
||||
]))
|
||||
.get();
|
||||
for (final row in outbox) {
|
||||
final event = jsonDecode(row.payload) as Map<String, dynamic>;
|
||||
final productId = event['productId'] as String?;
|
||||
final current = stock[productId];
|
||||
if (productId == null || current == null) continue;
|
||||
|
||||
final occurredAt = DateTime.tryParse(
|
||||
event['occurredAt'] as String? ?? '',
|
||||
);
|
||||
if (occurredAt == null) continue;
|
||||
final countedAt = current.countedAt;
|
||||
|
||||
switch (TabEventType.fromValue(event['type'] as String? ?? '')) {
|
||||
// An older count than the server's is outdated
|
||||
case TabEventType.stockSet:
|
||||
if (countedAt != null && occurredAt.isBefore(countedAt)) continue;
|
||||
stock[productId] = (
|
||||
stock: event['quantity'] as int?,
|
||||
countedAt: occurredAt,
|
||||
);
|
||||
// A sale before the count is already in the count
|
||||
case TabEventType.lineAdded || TabEventType.lineRemoved
|
||||
when current.stock != null &&
|
||||
(countedAt == null || occurredAt.isAfter(countedAt)):
|
||||
final quantity = event['quantity'] as int? ?? 0;
|
||||
final sign = event['type'] == TabEventType.lineAdded.value ? -1 : 1;
|
||||
stock[productId] = (
|
||||
stock: current.stock! + sign * quantity,
|
||||
countedAt: countedAt,
|
||||
);
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for (final product in await select(products).get()) {
|
||||
final rebased = stock[product.id];
|
||||
if (rebased == null) continue;
|
||||
|
||||
final infinite = rebased.stock == null;
|
||||
// An untracked product keeps its last count for when tracking resumes
|
||||
final quantity = rebased.stock ?? product.stockQuantity;
|
||||
if (product.infiniteStock == infinite &&
|
||||
product.stockQuantity == quantity) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await (update(
|
||||
products,
|
||||
)..where((row) => row.id.equals(product.id))).write(
|
||||
ProductsCompanion(
|
||||
stockQuantity: Value(quantity),
|
||||
infiniteStock: Value(infinite),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<ProductRow> _findProduct(String productId) async {
|
||||
|
||||
Reference in New Issue
Block a user