update cart
This commit is contained in:
179
lib/features/cart/data/datasources/cart_local_datasource.dart
Normal file
179
lib/features/cart/data/datasources/cart_local_datasource.dart
Normal file
@@ -0,0 +1,179 @@
|
||||
/// Local Data Source: Cart Storage
|
||||
///
|
||||
/// Handles local storage of cart items using Hive for offline access.
|
||||
/// Supports offline-first functionality and cart persistence.
|
||||
library;
|
||||
|
||||
import 'package:hive_ce/hive.dart';
|
||||
|
||||
import 'package:worker/core/constants/storage_constants.dart';
|
||||
import 'package:worker/core/database/hive_service.dart';
|
||||
import 'package:worker/core/errors/exceptions.dart';
|
||||
import 'package:worker/features/cart/data/models/cart_item_model.dart';
|
||||
|
||||
/// Cart Local Data Source Interface
|
||||
abstract class CartLocalDataSource {
|
||||
/// Save cart items to local storage
|
||||
///
|
||||
/// [items] - List of cart items to save
|
||||
Future<void> saveCartItems(List<CartItemModel> items);
|
||||
|
||||
/// Get cart items from local storage
|
||||
///
|
||||
/// Returns list of cart items
|
||||
Future<List<CartItemModel>> getCartItems();
|
||||
|
||||
/// Add item to local cart
|
||||
///
|
||||
/// [item] - Cart item to add
|
||||
Future<void> addCartItem(CartItemModel item);
|
||||
|
||||
/// Update item in local cart
|
||||
///
|
||||
/// [item] - Cart item to update
|
||||
Future<void> updateCartItem(CartItemModel item);
|
||||
|
||||
/// Remove items from local cart
|
||||
///
|
||||
/// [itemIds] - Product IDs to remove
|
||||
Future<void> removeCartItems(List<String> itemIds);
|
||||
|
||||
/// Clear all items from local cart
|
||||
Future<void> clearCart();
|
||||
|
||||
/// Get cart item count
|
||||
///
|
||||
/// Returns total number of items
|
||||
Future<int> getCartItemCount();
|
||||
|
||||
/// Get cart total
|
||||
///
|
||||
/// Returns total amount
|
||||
Future<double> getCartTotal();
|
||||
}
|
||||
|
||||
/// Cart Local Data Source Implementation
|
||||
class CartLocalDataSourceImpl implements CartLocalDataSource {
|
||||
CartLocalDataSourceImpl(this._hiveService);
|
||||
|
||||
final HiveService _hiveService;
|
||||
|
||||
/// Get cart box as Box<dynamic> (following best practices)
|
||||
Box<dynamic> get _cartBox => _hiveService.getBox<dynamic>(HiveBoxNames.cartBox);
|
||||
|
||||
@override
|
||||
Future<void> saveCartItems(List<CartItemModel> items) async {
|
||||
try {
|
||||
// Clear existing items
|
||||
await _cartBox.clear();
|
||||
|
||||
// Save new items with productId as key
|
||||
for (final item in items) {
|
||||
await _cartBox.put(item.productId, item);
|
||||
}
|
||||
} catch (e) {
|
||||
throw StorageException('Failed to save cart items: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<CartItemModel>> getCartItems() async {
|
||||
try {
|
||||
// Get all cart items from box using .whereType() for type safety
|
||||
final items = _cartBox.values
|
||||
.whereType<CartItemModel>()
|
||||
.toList();
|
||||
|
||||
return items;
|
||||
} catch (e) {
|
||||
throw StorageException('Failed to get cart items: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> addCartItem(CartItemModel item) async {
|
||||
try {
|
||||
// Check if item already exists
|
||||
final existingItem = _cartBox.get(item.productId);
|
||||
|
||||
if (existingItem != null && existingItem is CartItemModel) {
|
||||
// Update quantity if item exists
|
||||
final updatedItem = existingItem.copyWith(
|
||||
quantity: existingItem.quantity + item.quantity,
|
||||
subtotal: (existingItem.quantity + item.quantity) * existingItem.unitPrice,
|
||||
);
|
||||
await _cartBox.put(item.productId, updatedItem);
|
||||
} else {
|
||||
// Add new item
|
||||
await _cartBox.put(item.productId, item);
|
||||
}
|
||||
} catch (e) {
|
||||
throw StorageException('Failed to add cart item: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateCartItem(CartItemModel item) async {
|
||||
try {
|
||||
// Update or add item
|
||||
await _cartBox.put(item.productId, item);
|
||||
} catch (e) {
|
||||
throw StorageException('Failed to update cart item: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> removeCartItems(List<String> itemIds) async {
|
||||
try {
|
||||
// Remove items by productId keys
|
||||
for (final itemId in itemIds) {
|
||||
await _cartBox.delete(itemId);
|
||||
}
|
||||
} catch (e) {
|
||||
throw StorageException('Failed to remove cart items: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clearCart() async {
|
||||
try {
|
||||
await _cartBox.clear();
|
||||
} catch (e) {
|
||||
throw StorageException('Failed to clear cart: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> getCartItemCount() async {
|
||||
try {
|
||||
final items = await getCartItems();
|
||||
|
||||
// Sum up all quantities
|
||||
final totalQuantity = items.fold<double>(
|
||||
0.0,
|
||||
(sum, item) => sum + item.quantity,
|
||||
);
|
||||
|
||||
return totalQuantity.toInt();
|
||||
} catch (e) {
|
||||
throw StorageException('Failed to get cart item count: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<double> getCartTotal() async {
|
||||
try {
|
||||
final items = await getCartItems();
|
||||
|
||||
// Sum up all subtotals
|
||||
final total = items.fold<double>(
|
||||
0.0,
|
||||
(sum, item) => sum + item.subtotal,
|
||||
);
|
||||
|
||||
return total;
|
||||
} catch (e) {
|
||||
throw StorageException('Failed to get cart total: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
269
lib/features/cart/data/datasources/cart_remote_datasource.dart
Normal file
269
lib/features/cart/data/datasources/cart_remote_datasource.dart
Normal file
@@ -0,0 +1,269 @@
|
||||
/// Remote Data Source: Cart API
|
||||
///
|
||||
/// Handles all cart-related API requests to the backend.
|
||||
/// Uses Frappe/ERPNext API endpoints for cart operations.
|
||||
library;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import 'package:worker/core/constants/api_constants.dart';
|
||||
import 'package:worker/core/errors/exceptions.dart';
|
||||
import 'package:worker/core/network/dio_client.dart';
|
||||
import 'package:worker/features/cart/data/models/cart_item_model.dart';
|
||||
|
||||
/// Cart Remote Data Source Interface
|
||||
abstract class CartRemoteDataSource {
|
||||
/// Add items to cart
|
||||
///
|
||||
/// [items] - List of items with item_id, quantity, and amount
|
||||
/// Returns list of cart items from API
|
||||
Future<List<CartItemModel>> addToCart({
|
||||
required List<Map<String, dynamic>> items,
|
||||
});
|
||||
|
||||
/// Remove items from cart
|
||||
///
|
||||
/// [itemIds] - List of product ERPNext item codes to remove
|
||||
/// Returns true if successful
|
||||
Future<bool> removeFromCart({
|
||||
required List<String> itemIds,
|
||||
});
|
||||
|
||||
/// Get user's cart items
|
||||
///
|
||||
/// [limitStart] - Pagination offset (default: 0)
|
||||
/// [limitPageLength] - Page size (default: 0 for all)
|
||||
/// Returns list of cart items
|
||||
Future<List<CartItemModel>> getUserCart({
|
||||
int limitStart = 0,
|
||||
int limitPageLength = 0,
|
||||
});
|
||||
}
|
||||
|
||||
/// Cart Remote Data Source Implementation
|
||||
class CartRemoteDataSourceImpl implements CartRemoteDataSource {
|
||||
CartRemoteDataSourceImpl(this._dioClient);
|
||||
|
||||
final DioClient _dioClient;
|
||||
|
||||
@override
|
||||
Future<List<CartItemModel>> addToCart({
|
||||
required List<Map<String, dynamic>> items,
|
||||
}) async {
|
||||
try {
|
||||
// Build request body
|
||||
final requestBody = {
|
||||
'items': items,
|
||||
};
|
||||
|
||||
// Make API request
|
||||
final response = await _dioClient.post<Map<String, dynamic>>(
|
||||
'${ApiConstants.frappeApiMethod}${ApiConstants.addToCart}',
|
||||
data: requestBody,
|
||||
);
|
||||
|
||||
// Check response status
|
||||
if (response.statusCode != 200 && response.statusCode != 201) {
|
||||
throw ServerException(
|
||||
'Failed to add items to cart',
|
||||
response.statusCode,
|
||||
);
|
||||
}
|
||||
|
||||
// Parse response
|
||||
// Expected format: { "message": [{ "item_id": "...", "success": true, "message": "..." }] }
|
||||
final responseData = response.data;
|
||||
|
||||
if (responseData == null) {
|
||||
throw const ParseException('Invalid response format from add to cart API');
|
||||
}
|
||||
|
||||
// After adding, fetch updated cart
|
||||
return await getUserCart();
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioException(e);
|
||||
} catch (e) {
|
||||
if (e is NetworkException ||
|
||||
e is ServerException ||
|
||||
e is ParseException) {
|
||||
rethrow;
|
||||
}
|
||||
throw UnknownException('Failed to add items to cart', e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> removeFromCart({
|
||||
required List<String> itemIds,
|
||||
}) async {
|
||||
try {
|
||||
// Build request body
|
||||
final requestBody = {
|
||||
'item_ids': itemIds,
|
||||
};
|
||||
|
||||
// Make API request
|
||||
final response = await _dioClient.post<Map<String, dynamic>>(
|
||||
'${ApiConstants.frappeApiMethod}${ApiConstants.removeFromCart}',
|
||||
data: requestBody,
|
||||
);
|
||||
|
||||
// Check response status
|
||||
if (response.statusCode != 200) {
|
||||
throw ServerException(
|
||||
'Failed to remove items from cart',
|
||||
response.statusCode,
|
||||
);
|
||||
}
|
||||
|
||||
// Parse response
|
||||
// Expected format: { "message": [{ "item_id": "...", "success": true, "message": "..." }] }
|
||||
final responseData = response.data;
|
||||
|
||||
if (responseData == null) {
|
||||
throw const ParseException('Invalid response format from remove from cart API');
|
||||
}
|
||||
|
||||
final message = responseData['message'];
|
||||
if (message is List && message.isNotEmpty) {
|
||||
// Check if all items were removed successfully
|
||||
final allSuccess = message.every((item) => item['success'] == true);
|
||||
return allSuccess;
|
||||
}
|
||||
|
||||
return true;
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioException(e);
|
||||
} catch (e) {
|
||||
if (e is NetworkException ||
|
||||
e is ServerException ||
|
||||
e is ParseException) {
|
||||
rethrow;
|
||||
}
|
||||
throw UnknownException('Failed to remove items from cart', e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<CartItemModel>> getUserCart({
|
||||
int limitStart = 0,
|
||||
int limitPageLength = 0,
|
||||
}) async {
|
||||
try {
|
||||
// Build request body
|
||||
final requestBody = {
|
||||
'limit_start': limitStart,
|
||||
'limit_page_length': limitPageLength,
|
||||
};
|
||||
|
||||
// Make API request
|
||||
final response = await _dioClient.post<Map<String, dynamic>>(
|
||||
'${ApiConstants.frappeApiMethod}${ApiConstants.getUserCart}',
|
||||
data: requestBody,
|
||||
);
|
||||
|
||||
// Check response status
|
||||
if (response.statusCode != 200) {
|
||||
throw ServerException(
|
||||
'Failed to get cart items',
|
||||
response.statusCode,
|
||||
);
|
||||
}
|
||||
|
||||
// Parse response
|
||||
// Expected format: { "message": [{ "name": "...", "item": "...", "quantity": 0, "amount": 0, ... }] }
|
||||
final responseData = response.data;
|
||||
|
||||
if (responseData == null) {
|
||||
throw const ParseException('Invalid response format from get user cart API');
|
||||
}
|
||||
|
||||
final message = responseData['message'];
|
||||
if (message == null || message is! List) {
|
||||
throw const ParseException('Invalid message format in get user cart response');
|
||||
}
|
||||
|
||||
// Convert to CartItemModel list
|
||||
final cartItems = <CartItemModel>[];
|
||||
for (final item in message) {
|
||||
if (item is! Map<String, dynamic>) continue;
|
||||
|
||||
try {
|
||||
// Map API response to CartItemModel
|
||||
// API fields: name, item, quantity, amount, item_code, item_name, image, conversion_of_sm
|
||||
final cartItem = CartItemModel(
|
||||
cartItemId: item['name'] as String? ?? '',
|
||||
cartId: 'user_cart', // Fixed cart ID for user's cart
|
||||
productId: item['item_code'] as String? ?? item['item'] as String? ?? '',
|
||||
quantity: (item['quantity'] as num?)?.toDouble() ?? 0.0,
|
||||
unitPrice: (item['amount'] as num?)?.toDouble() ?? 0.0,
|
||||
subtotal: ((item['quantity'] as num?)?.toDouble() ?? 0.0) *
|
||||
((item['amount'] as num?)?.toDouble() ?? 0.0),
|
||||
addedAt: DateTime.now(), // API doesn't provide timestamp
|
||||
);
|
||||
|
||||
cartItems.add(cartItem);
|
||||
} catch (e) {
|
||||
// Skip invalid items but don't fail the whole request
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return cartItems;
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioException(e);
|
||||
} catch (e) {
|
||||
if (e is NetworkException ||
|
||||
e is ServerException ||
|
||||
e is ParseException) {
|
||||
rethrow;
|
||||
}
|
||||
throw UnknownException('Failed to get cart items', e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle Dio exceptions and convert to custom exceptions
|
||||
Exception _handleDioException(DioException e) {
|
||||
switch (e.type) {
|
||||
case DioExceptionType.connectionTimeout:
|
||||
case DioExceptionType.sendTimeout:
|
||||
case DioExceptionType.receiveTimeout:
|
||||
return const TimeoutException();
|
||||
|
||||
case DioExceptionType.connectionError:
|
||||
return const NoInternetException();
|
||||
|
||||
case DioExceptionType.badResponse:
|
||||
final statusCode = e.response?.statusCode;
|
||||
if (statusCode != null) {
|
||||
if (statusCode == 401) {
|
||||
return const UnauthorizedException();
|
||||
} else if (statusCode == 403) {
|
||||
return const ForbiddenException();
|
||||
} else if (statusCode == 404) {
|
||||
return const NotFoundException('Cart not found');
|
||||
} else if (statusCode == 429) {
|
||||
return const RateLimitException();
|
||||
} else if (statusCode >= 500) {
|
||||
return ServerException(
|
||||
'Server error: ${e.response?.statusMessage ?? "Unknown error"}',
|
||||
statusCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
return NetworkException(
|
||||
e.response?.statusMessage ?? 'Network error',
|
||||
statusCode: statusCode,
|
||||
);
|
||||
|
||||
case DioExceptionType.cancel:
|
||||
return const NetworkException('Request cancelled');
|
||||
|
||||
case DioExceptionType.badCertificate:
|
||||
return const NetworkException('Invalid SSL certificate');
|
||||
|
||||
case DioExceptionType.unknown:
|
||||
return const NoInternetException();
|
||||
}
|
||||
}
|
||||
}
|
||||
50
lib/features/cart/data/providers/cart_data_providers.dart
Normal file
50
lib/features/cart/data/providers/cart_data_providers.dart
Normal file
@@ -0,0 +1,50 @@
|
||||
/// Cart Data Providers
|
||||
///
|
||||
/// State management for cart data layer using Riverpod.
|
||||
/// Provides access to datasources and repositories.
|
||||
library;
|
||||
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:worker/core/database/hive_service.dart';
|
||||
import 'package:worker/core/network/dio_client.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/repositories/cart_repository_impl.dart';
|
||||
import 'package:worker/features/cart/domain/repositories/cart_repository.dart';
|
||||
|
||||
part 'cart_data_providers.g.dart';
|
||||
|
||||
/// Cart Local DataSource Provider
|
||||
///
|
||||
/// Provides instance of CartLocalDataSource.
|
||||
/// keepAlive: true to persist with cart provider.
|
||||
@Riverpod(keepAlive: true)
|
||||
CartLocalDataSource cartLocalDataSource(Ref ref) {
|
||||
final hiveService = HiveService();
|
||||
return CartLocalDataSourceImpl(hiveService);
|
||||
}
|
||||
|
||||
/// Cart Remote DataSource Provider
|
||||
///
|
||||
/// Provides instance of CartRemoteDataSource with DioClient.
|
||||
/// keepAlive: true to persist with cart provider.
|
||||
@Riverpod(keepAlive: true)
|
||||
Future<CartRemoteDataSource> cartRemoteDataSource(Ref ref) async {
|
||||
final dioClient = await ref.watch(dioClientProvider.future);
|
||||
return CartRemoteDataSourceImpl(dioClient);
|
||||
}
|
||||
|
||||
/// Cart Repository Provider
|
||||
///
|
||||
/// Provides instance of CartRepository implementation.
|
||||
/// keepAlive: true to persist with cart provider.
|
||||
@Riverpod(keepAlive: true)
|
||||
Future<CartRepository> cartRepository(Ref ref) async {
|
||||
final remoteDataSource = await ref.watch(cartRemoteDataSourceProvider.future);
|
||||
final localDataSource = ref.watch(cartLocalDataSourceProvider);
|
||||
|
||||
return CartRepositoryImpl(
|
||||
remoteDataSource: remoteDataSource,
|
||||
localDataSource: localDataSource,
|
||||
);
|
||||
}
|
||||
180
lib/features/cart/data/providers/cart_data_providers.g.dart
Normal file
180
lib/features/cart/data/providers/cart_data_providers.g.dart
Normal file
@@ -0,0 +1,180 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'cart_data_providers.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Cart Local DataSource Provider
|
||||
///
|
||||
/// Provides instance of CartLocalDataSource.
|
||||
/// keepAlive: true to persist with cart provider.
|
||||
|
||||
@ProviderFor(cartLocalDataSource)
|
||||
const cartLocalDataSourceProvider = CartLocalDataSourceProvider._();
|
||||
|
||||
/// Cart Local DataSource Provider
|
||||
///
|
||||
/// Provides instance of CartLocalDataSource.
|
||||
/// keepAlive: true to persist with cart provider.
|
||||
|
||||
final class CartLocalDataSourceProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
CartLocalDataSource,
|
||||
CartLocalDataSource,
|
||||
CartLocalDataSource
|
||||
>
|
||||
with $Provider<CartLocalDataSource> {
|
||||
/// Cart Local DataSource Provider
|
||||
///
|
||||
/// Provides instance of CartLocalDataSource.
|
||||
/// keepAlive: true to persist with cart provider.
|
||||
const CartLocalDataSourceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'cartLocalDataSourceProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$cartLocalDataSourceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<CartLocalDataSource> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
CartLocalDataSource create(Ref ref) {
|
||||
return cartLocalDataSource(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(CartLocalDataSource value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<CartLocalDataSource>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$cartLocalDataSourceHash() =>
|
||||
r'81a8c1688dff786d4ecebbd8239ae1c8174008c0';
|
||||
|
||||
/// Cart Remote DataSource Provider
|
||||
///
|
||||
/// Provides instance of CartRemoteDataSource with DioClient.
|
||||
/// keepAlive: true to persist with cart provider.
|
||||
|
||||
@ProviderFor(cartRemoteDataSource)
|
||||
const cartRemoteDataSourceProvider = CartRemoteDataSourceProvider._();
|
||||
|
||||
/// Cart Remote DataSource Provider
|
||||
///
|
||||
/// Provides instance of CartRemoteDataSource with DioClient.
|
||||
/// keepAlive: true to persist with cart provider.
|
||||
|
||||
final class CartRemoteDataSourceProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<CartRemoteDataSource>,
|
||||
CartRemoteDataSource,
|
||||
FutureOr<CartRemoteDataSource>
|
||||
>
|
||||
with
|
||||
$FutureModifier<CartRemoteDataSource>,
|
||||
$FutureProvider<CartRemoteDataSource> {
|
||||
/// Cart Remote DataSource Provider
|
||||
///
|
||||
/// Provides instance of CartRemoteDataSource with DioClient.
|
||||
/// keepAlive: true to persist with cart provider.
|
||||
const CartRemoteDataSourceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'cartRemoteDataSourceProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$cartRemoteDataSourceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<CartRemoteDataSource> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<CartRemoteDataSource> create(Ref ref) {
|
||||
return cartRemoteDataSource(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$cartRemoteDataSourceHash() =>
|
||||
r'758905224c472f1e088c3be7c7451c2321959bd8';
|
||||
|
||||
/// Cart Repository Provider
|
||||
///
|
||||
/// Provides instance of CartRepository implementation.
|
||||
/// keepAlive: true to persist with cart provider.
|
||||
|
||||
@ProviderFor(cartRepository)
|
||||
const cartRepositoryProvider = CartRepositoryProvider._();
|
||||
|
||||
/// Cart Repository Provider
|
||||
///
|
||||
/// Provides instance of CartRepository implementation.
|
||||
/// keepAlive: true to persist with cart provider.
|
||||
|
||||
final class CartRepositoryProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<CartRepository>,
|
||||
CartRepository,
|
||||
FutureOr<CartRepository>
|
||||
>
|
||||
with $FutureModifier<CartRepository>, $FutureProvider<CartRepository> {
|
||||
/// Cart Repository Provider
|
||||
///
|
||||
/// Provides instance of CartRepository implementation.
|
||||
/// keepAlive: true to persist with cart provider.
|
||||
const CartRepositoryProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'cartRepositoryProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$cartRepositoryHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<CartRepository> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<CartRepository> create(Ref ref) {
|
||||
return cartRepository(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$cartRepositoryHash() => r'f6bbe5ab247737887e6b51f7ca8050bb6898ac2a';
|
||||
285
lib/features/cart/data/repositories/cart_repository_impl.dart
Normal file
285
lib/features/cart/data/repositories/cart_repository_impl.dart
Normal 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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user