import 'package:drift/drift.dart'; import 'package:uuid/uuid.dart'; import '../database/app_database.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> watchProducts(); Future> getProducts(); Future getProductById(String id); Future createProduct({ required String name, required String category, required int stockQuantity, required int lowStockThreshold, bool infiniteStock = false, required int priceInCents, required String? imagePath, }); Future updateProduct(Product product); Future deleteProduct(String id); Future decreaseStock(String productId, int amount); Future 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> watchProducts() { return _activeProductsQuery().map(_mapRowToProduct).watch(); } @override Future> getProducts() { return _activeProductsQuery().map(_mapRowToProduct).get(); } @override Future 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); } @override Future 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), ), ); // The server starts a product it doesn't know as untracked if (!infiniteStock) { await database.queueStockSet( eventFactory.stockSet(productId: id, quantity: stockQuantity), ); } }); } /// A changed stock or tracking is queued for the server as a count. @override Future 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 stockChanged = product.infiniteStock ? !current.infiniteStock : current.infiniteStock || current.stockQuantity != product.stockQuantity; if (stockChanged) { await database.queueStockSet( eventFactory.stockSet( productId: product.id, quantity: product.infiniteStock ? null : product.stockQuantity, ), ); } }); } /// Only hidden, since open tabs and history still refer to it. @override Future deleteProduct(String id) async { final query = database.update(database.products) ..where((product) => product.id.equals(id)); await query.write(const ProductsCompanion(active: Value(false))); } @override Future decreaseStock(String productId, int amount) { return database.decreaseStock(productId, amount); } @override Future increaseStock(String productId, int amount) { return database.increaseStock(productId, amount); } }