/// 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 true if successful Future addToCart({ required List> items, }); /// Remove items from cart /// /// [itemIds] - List of product ERPNext item codes to remove /// Returns true if successful Future removeFromCart({ required List itemIds, }); /// Get user's cart items /// /// [limitStart] - Pagination offset (default: 0) /// [limitPageLength] - Page size (default: 0 for all) /// Returns list of cart items Future> getUserCart({ int limitStart = 0, int limitPageLength = 0, }); } /// Cart Remote Data Source Implementation class CartRemoteDataSourceImpl implements CartRemoteDataSource { CartRemoteDataSourceImpl(this._dioClient); final DioClient _dioClient; @override Future addToCart({ required List> items, }) async { try { // Build request body final requestBody = { 'items': items, }; // Make API request final response = await _dioClient.post>( '${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'); } 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 add items to cart', e); } } @override Future removeFromCart({ required List itemIds, }) async { try { // Build request body final requestBody = { 'item_ids': itemIds, }; // Make API request final response = await _dioClient.post>( '${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> 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>( '${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 = []; for (final item in message) { if (item is! Map) continue; try { // Map API response to CartItemModel // API fields: name, item, quantity, amount, item_code, item_name, image, conversion_of_sm final quantity = (item['quantity'] as num?)?.toDouble() ?? 0.0; final unitPrice = (item['amount'] as num?)?.toDouble() ?? 0.0; 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: quantity, unitPrice: unitPrice, subtotal: quantity * unitPrice, addedAt: DateTime.now(), // API doesn't provide timestamp // Product details from cart API - no need to fetch separately itemName: item['item_name'] as String?, image: item['image'] as String?, conversionOfSm: (item['conversion_of_sm'] as num?)?.toDouble(), ); 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(); } } }