update cart

This commit is contained in:
Phuoc Nguyen
2025-11-14 16:19:25 +07:00
parent 4738553d2e
commit aae3c9d080
30 changed files with 5954 additions and 758 deletions

View File

@@ -0,0 +1,285 @@
/// Repository Implementation: Cart Repository
///
/// Implements the cart repository interface with:
/// - API-first strategy with local fallback
/// - Automatic sync between API and local storage
/// - Offline queue support
/// - Error handling and recovery
library;
import 'package:worker/core/errors/exceptions.dart';
import 'package:worker/features/cart/data/datasources/cart_local_datasource.dart';
import 'package:worker/features/cart/data/datasources/cart_remote_datasource.dart';
import 'package:worker/features/cart/data/models/cart_item_model.dart';
import 'package:worker/features/cart/domain/entities/cart_item.dart';
import 'package:worker/features/cart/domain/repositories/cart_repository.dart';
/// Cart Repository Implementation
///
/// Strategy: API-first with local fallback
/// 1. Try API request first
/// 2. On success, sync to local storage
/// 3. On failure, fallback to local data (for reads)
/// 4. Queue failed writes for later sync
class CartRepositoryImpl implements CartRepository {
CartRepositoryImpl({
required CartRemoteDataSource remoteDataSource,
required CartLocalDataSource localDataSource,
}) : _remoteDataSource = remoteDataSource,
_localDataSource = localDataSource;
final CartRemoteDataSource _remoteDataSource;
final CartLocalDataSource _localDataSource;
@override
Future<List<CartItem>> addToCart({
required List<String> itemIds,
required List<double> quantities,
required List<double> prices,
}) async {
try {
// Validate input
if (itemIds.length != quantities.length || itemIds.length != prices.length) {
throw const ValidationException(
'Item IDs, quantities, and prices must have the same length',
);
}
// Build API request items
final items = <Map<String, dynamic>>[];
for (int i = 0; i < itemIds.length; i++) {
items.add({
'item_id': itemIds[i],
'quantity': quantities[i],
'amount': prices[i],
});
}
// Try API first
try {
final cartItemModels = await _remoteDataSource.addToCart(items: items);
// Sync to local storage
await _localDataSource.saveCartItems(cartItemModels);
// Convert to domain entities
return cartItemModels.map(_modelToEntity).toList();
} on NetworkException catch (e) {
// If no internet, add to local cart only
if (e is NoInternetException || e is TimeoutException) {
// Add items to local cart
for (int i = 0; i < itemIds.length; i++) {
final cartItemModel = _createCartItemModel(
productId: itemIds[i],
quantity: quantities[i],
unitPrice: prices[i],
);
await _localDataSource.addCartItem(cartItemModel);
}
// TODO: Queue for sync when online
// Return local cart items
final localItems = await _localDataSource.getCartItems();
return localItems.map(_modelToEntity).toList();
}
rethrow;
}
} on StorageException {
rethrow;
} on ValidationException {
rethrow;
} on ServerException {
rethrow;
} on NetworkException {
rethrow;
} catch (e) {
throw UnknownException('Failed to add items to cart', e);
}
}
@override
Future<bool> removeFromCart({
required List<String> itemIds,
}) async {
try {
// Try API first
try {
final success = await _remoteDataSource.removeFromCart(itemIds: itemIds);
if (success) {
// Sync to local storage
await _localDataSource.removeCartItems(itemIds);
}
return success;
} on NetworkException catch (e) {
// If no internet, remove from local cart only
if (e is NoInternetException || e is TimeoutException) {
await _localDataSource.removeCartItems(itemIds);
// TODO: Queue for sync when online
return true;
}
rethrow;
}
} on StorageException {
rethrow;
} on ServerException {
rethrow;
} on NetworkException {
rethrow;
} catch (e) {
throw UnknownException('Failed to remove items from cart', e);
}
}
@override
Future<List<CartItem>> getCartItems() async {
try {
// Try API first
try {
final cartItemModels = await _remoteDataSource.getUserCart();
// Sync to local storage
await _localDataSource.saveCartItems(cartItemModels);
// Convert to domain entities
return cartItemModels.map(_modelToEntity).toList();
} on NetworkException catch (e) {
// If no internet, fallback to local storage
if (e is NoInternetException || e is TimeoutException) {
final localItems = await _localDataSource.getCartItems();
return localItems.map(_modelToEntity).toList();
}
rethrow;
}
} on StorageException {
rethrow;
} on ServerException {
rethrow;
} on NetworkException {
rethrow;
} catch (e) {
throw UnknownException('Failed to get cart items', e);
}
}
@override
Future<List<CartItem>> updateQuantity({
required String itemId,
required double quantity,
required double price,
}) async {
try {
// API doesn't have update endpoint, use add with new quantity
// This will replace the existing quantity
return await addToCart(
itemIds: [itemId],
quantities: [quantity],
prices: [price],
);
} catch (e) {
throw UnknownException('Failed to update cart item quantity', e);
}
}
@override
Future<bool> clearCart() async {
try {
// Get all cart items
final items = await getCartItems();
if (items.isEmpty) {
return true;
}
// Extract item IDs
final itemIds = items.map((item) => item.productId).toList();
// Remove all items
return await removeFromCart(itemIds: itemIds);
} catch (e) {
throw UnknownException('Failed to clear cart', e);
}
}
@override
Future<double> getCartTotal() async {
try {
// Try to calculate from API data first
try {
final items = await getCartItems();
return items.fold<double>(
0.0,
(sum, item) => sum + item.subtotal,
);
} on NetworkException catch (e) {
// If no internet, use local calculation
if (e is NoInternetException || e is TimeoutException) {
return await _localDataSource.getCartTotal();
}
rethrow;
}
} catch (e) {
throw UnknownException('Failed to get cart total', e);
}
}
@override
Future<int> getCartItemCount() async {
try {
// Try to calculate from API data first
try {
final items = await getCartItems();
final totalQuantity = items.fold<double>(
0.0,
(sum, item) => sum + item.quantity,
);
return totalQuantity.toInt();
} on NetworkException catch (e) {
// If no internet, use local calculation
if (e is NoInternetException || e is TimeoutException) {
return await _localDataSource.getCartItemCount();
}
rethrow;
}
} catch (e) {
throw UnknownException('Failed to get cart item count', e);
}
}
// ============================================================================
// Helper Methods
// ============================================================================
/// Convert CartItemModel to CartItem entity
CartItem _modelToEntity(CartItemModel model) {
return CartItem(
cartItemId: model.cartItemId,
cartId: model.cartId,
productId: model.productId,
quantity: model.quantity,
unitPrice: model.unitPrice,
subtotal: model.subtotal,
addedAt: model.addedAt,
);
}
/// Create CartItemModel from parameters
CartItemModel _createCartItemModel({
required String productId,
required double quantity,
required double unitPrice,
}) {
return CartItemModel(
cartItemId: DateTime.now().millisecondsSinceEpoch.toString(),
cartId: 'user_cart',
productId: productId,
quantity: quantity,
unitPrice: unitPrice,
subtotal: quantity * unitPrice,
addedAt: DateTime.now(),
);
}
}