second commit
This commit is contained in:
139
lib/domain/usecases/checkout_sale.dart
Normal file
139
lib/domain/usecases/checkout_sale.dart
Normal file
@@ -0,0 +1,139 @@
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../../core/constants/app_constants.dart';
|
||||
import '../../core/utils/formatters.dart';
|
||||
import '../entities/cart.dart';
|
||||
import '../entities/customer.dart';
|
||||
import '../entities/transaction.dart';
|
||||
import '../repositories/customer_repository.dart';
|
||||
import '../repositories/product_repository.dart';
|
||||
import '../repositories/transaction_repository.dart';
|
||||
|
||||
/// Raised when a sale cannot be completed. Carries a cashier-readable message.
|
||||
class CheckoutFailure implements Exception {
|
||||
const CheckoutFailure(this.message);
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
/// Result of a successful checkout.
|
||||
class CheckoutResult {
|
||||
const CheckoutResult({required this.transaction, this.updatedCustomer});
|
||||
|
||||
final SaleTransaction transaction;
|
||||
final Customer? updatedCustomer;
|
||||
}
|
||||
|
||||
/// Completes a sale end to end.
|
||||
///
|
||||
/// Validates tenders, persists the transaction, decrements stock and applies
|
||||
/// loyalty movement. Everything the cashier's Complete Sale button needs lives
|
||||
/// here rather than in the UI, so the flow is unit-testable in isolation.
|
||||
class CheckoutSale {
|
||||
const CheckoutSale({
|
||||
required ProductRepository productRepository,
|
||||
required CustomerRepository customerRepository,
|
||||
required TransactionRepository transactionRepository,
|
||||
}) : _products = productRepository,
|
||||
_customers = customerRepository,
|
||||
_transactions = transactionRepository;
|
||||
|
||||
final ProductRepository _products;
|
||||
final CustomerRepository _customers;
|
||||
final TransactionRepository _transactions;
|
||||
|
||||
static const _uuid = Uuid();
|
||||
|
||||
Future<CheckoutResult> call({
|
||||
required Cart cart,
|
||||
required List<PaymentSplit> payments,
|
||||
required String cashierName,
|
||||
}) async {
|
||||
_validate(cart, payments);
|
||||
|
||||
final now = DateTime.now();
|
||||
final sequence = await _transactions.nextInvoiceSequence();
|
||||
|
||||
final transaction = SaleTransaction(
|
||||
id: _uuid.v4(),
|
||||
invoiceNumber: Formatters.invoiceNumber(sequence, now),
|
||||
cart: cart,
|
||||
payments: payments,
|
||||
createdAt: now,
|
||||
cashierName: cashierName,
|
||||
);
|
||||
|
||||
await _transactions.save(transaction);
|
||||
|
||||
await _products.decrementStock({
|
||||
for (final line in cart.lines) line.product.id: line.quantity,
|
||||
});
|
||||
|
||||
Customer? updatedCustomer;
|
||||
final customer = cart.customer;
|
||||
if (customer != null) {
|
||||
updatedCustomer = await _customers.recordSale(
|
||||
customerId: customer.id,
|
||||
amount: cart.grandTotal,
|
||||
pointsEarned: cart.pointsEarned,
|
||||
pointsRedeemed: cart.pointsRedeemed,
|
||||
);
|
||||
}
|
||||
|
||||
return CheckoutResult(
|
||||
transaction: transaction,
|
||||
updatedCustomer: updatedCustomer,
|
||||
);
|
||||
}
|
||||
|
||||
void _validate(Cart cart, List<PaymentSplit> payments) {
|
||||
if (cart.isEmpty) {
|
||||
throw const CheckoutFailure('Add at least one item before charging.');
|
||||
}
|
||||
if (payments.isEmpty) {
|
||||
throw const CheckoutFailure('Select a payment method.');
|
||||
}
|
||||
|
||||
for (final line in cart.lines) {
|
||||
if (line.quantity <= 0) {
|
||||
throw CheckoutFailure('${line.product.name} has an invalid quantity.');
|
||||
}
|
||||
if (line.exceedsStock) {
|
||||
throw CheckoutFailure(
|
||||
'Only ${line.product.stock.toStringAsFixed(0)} '
|
||||
'${line.product.unit.symbol} of ${line.product.name} in stock.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (cart.pointsRedeemed > 0) {
|
||||
final available = cart.customer?.loyaltyPoints ?? 0;
|
||||
if (cart.pointsRedeemed > available) {
|
||||
throw const CheckoutFailure('Not enough loyalty points to redeem.');
|
||||
}
|
||||
}
|
||||
|
||||
final paid = payments.fold(0.0, (sum, p) => sum + p.amount);
|
||||
final shortfall = cart.grandTotal - paid;
|
||||
if (shortfall > 0.01) {
|
||||
throw CheckoutFailure(
|
||||
'${AppConstants.currencySymbol}${shortfall.toStringAsFixed(2)} '
|
||||
'still due on this bill.',
|
||||
);
|
||||
}
|
||||
|
||||
for (final p in payments) {
|
||||
if (p.amount <= 0) {
|
||||
throw CheckoutFailure('${p.method.label} amount must be positive.');
|
||||
}
|
||||
if (p.method.needsChange &&
|
||||
p.tendered != null &&
|
||||
p.tendered! < p.amount) {
|
||||
throw const CheckoutFailure('Cash tendered is less than the amount due.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user