275 lines
8.3 KiB
Dart
275 lines
8.3 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 true if successful
|
|
Future<bool> 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<bool> 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');
|
|
}
|
|
|
|
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<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 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();
|
|
}
|
|
}
|
|
}
|