Files
kooltab/lib/services/product_service.dart
T

229 lines
7.0 KiB
Dart

import 'package:drift/drift.dart';
import 'package:uuid/uuid.dart';
import '../database/app_database.dart';
import '../database/catalog_queries.dart';
import '../database/stock_queries.dart';
import '../models/product.dart';
import '../models/tab_event.dart';
export '../database/stock_queries.dart' show ProductNotFoundException;
abstract class ProductService {
/// Active products, re-emitted whenever the products table changes.
Stream<List<Product>> watchProducts();
Future<List<Product>> getProducts();
Future<Product?> getProductById(String id);
Future<void> createProduct({
required String name,
required String category,
required int stockQuantity,
required int lowStockThreshold,
bool infiniteStock = false,
required int priceInCents,
required String? imagePath,
});
Future<void> updateProduct(Product product);
Future<void> deleteProduct(String id);
Future<void> decreaseStock(String productId, int amount);
Future<void> increaseStock(String productId, int amount);
}
class DriftProductService implements ProductService {
final AppDatabase database;
final TabEventFactory eventFactory;
final _uuid = const Uuid();
DriftProductService({required this.database, TabEventFactory? eventFactory})
: eventFactory = eventFactory ?? TabEventFactory();
Product _mapRowToProduct(ProductRow row) {
return Product(
id: row.id,
name: row.name,
category: row.category,
stockQuantity: row.stockQuantity,
lowStockThreshold: row.lowStockThreshold,
infiniteStock: row.infiniteStock,
priceInCents: row.priceInCents,
imagePath: row.imagePath,
active: row.active,
);
}
SimpleSelectStatement<$ProductsTable, ProductRow> _activeProductsQuery() {
return database.select(database.products)
..where((product) => product.active.equals(true))
..orderBy([(product) => OrderingTerm.asc(product.name)]);
}
@override
Stream<List<Product>> watchProducts() {
return _activeProductsQuery().map(_mapRowToProduct).watch();
}
@override
Future<List<Product>> getProducts() {
return _activeProductsQuery().map(_mapRowToProduct).get();
}
@override
Future<Product?> getProductById(String id) async {
final query = database.select(database.products)
..where((product) => product.id.equals(id));
final row = await query.getSingleOrNull();
if (row == null) {
return null;
}
return _mapRowToProduct(row);
}
/// Queues the new product for the server (which takes this tablet's id), its
/// starting stock as a count, and its picture for upload.
@override
Future<void> createProduct({
required String name,
required String category,
required int stockQuantity,
required int lowStockThreshold,
bool infiniteStock = false,
required int priceInCents,
required String? imagePath,
}) async {
final id = _uuid.v4();
await database.transaction(() async {
await database
.into(database.products)
.insert(
ProductsCompanion.insert(
id: id,
name: name,
category: category,
stockQuantity: stockQuantity,
lowStockThreshold: lowStockThreshold,
infiniteStock: Value(infiniteStock),
priceInCents: priceInCents,
imagePath: Value(imagePath),
),
);
await database.queueEvent(
eventFactory.productCreated(
productId: id,
fields: {
'name': name,
'category': _serverCategory(category),
'priceInCents': priceInCents,
'lowStockThreshold': lowStockThreshold,
},
),
);
// The server starts a product it doesn't know as untracked
if (!infiniteStock) {
await database.queueEvent(
eventFactory.stockSet(productId: id, quantity: stockQuantity),
);
}
if (imagePath != null) await database.queueImageUpload(id, imagePath);
});
}
/// Queues what changed for the server: the changed fields, a changed stock
/// or tracking as a count, and a new picture for upload.
@override
Future<void> updateProduct(Product product) async {
await database.transaction(() async {
final current = await (database.select(
database.products,
)..where((row) => row.id.equals(product.id))).getSingleOrNull();
await (database.update(
database.products,
)..where((row) => row.id.equals(product.id))).write(
ProductsCompanion(
name: Value(product.name),
category: Value(product.category),
stockQuantity: Value(product.stockQuantity),
lowStockThreshold: Value(product.lowStockThreshold),
infiniteStock: Value(product.infiniteStock),
priceInCents: Value(product.priceInCents),
imagePath: Value(product.imagePath),
active: Value(product.active),
),
);
if (current == null) return;
final fields = <String, Object?>{
if (current.name != product.name) 'name': product.name,
if (current.category != product.category)
'category': _serverCategory(product.category),
if (current.priceInCents != product.priceInCents)
'priceInCents': product.priceInCents,
if (current.lowStockThreshold != product.lowStockThreshold)
'lowStockThreshold': product.lowStockThreshold,
};
if (fields.isNotEmpty) {
await database.queueEvent(
eventFactory.productUpdated(productId: product.id, fields: fields),
);
}
final stockChanged = product.infiniteStock
? !current.infiniteStock
: current.infiniteStock ||
current.stockQuantity != product.stockQuantity;
if (stockChanged) {
await database.queueEvent(
eventFactory.stockSet(
productId: product.id,
quantity: product.infiniteStock ? null : product.stockQuantity,
),
);
}
final picture = product.imagePath;
if (picture != null && picture != current.imagePath) {
await database.queueImageUpload(product.id, picture);
}
});
}
/// Only hidden, since open tabs and history still refer to it; the server
/// archives it for every tablet.
@override
Future<void> deleteProduct(String id) async {
await database.transaction(() async {
await (database.update(database.products)
..where((product) => product.id.equals(id)))
.write(const ProductsCompanion(active: Value(false)));
await database.queueEvent(eventFactory.productArchived(productId: id));
});
}
/// The server has no category for an empty one.
static String? _serverCategory(String category) {
final trimmed = category.trim();
return trimmed.isEmpty ? null : trimmed;
}
@override
Future<void> decreaseStock(String productId, int amount) {
return database.decreaseStock(productId, amount);
}
@override
Future<void> increaseStock(String productId, int amount) {
return database.increaseStock(productId, amount);
}
}