Files
worker/lib/features/cart/data/datasources/cart_remote_datasource.dart
Phuoc Nguyen aae3c9d080 update cart
2025-11-14 16:19:25 +07:00

270 lines
8.1 KiB
Dart

/// 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();
}
}
}