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,415 @@
# Cart API Integration - Complete Implementation
## Overview
This document describes the complete cart API integration following clean architecture principles for the Worker Flutter app.
## Architecture
```
Presentation Layer (UI)
Domain Layer (Business Logic)
Data Layer (API + Local Storage)
```
## Files Created
### 1. API Constants
**File**: `/Users/ssg/project/worker/lib/core/constants/api_constants.dart`
**Added Endpoints**:
- `addToCart` - POST /api/method/building_material.building_material.api.user_cart.add_to_cart
- `removeFromCart` - POST /api/method/building_material.building_material.api.user_cart.remove_from_cart
- `getUserCart` - POST /api/method/building_material.building_material.api.user_cart.get_user_cart
### 2. Domain Repository Interface
**File**: `/Users/ssg/project/worker/lib/features/cart/domain/repositories/cart_repository.dart`
**Methods**:
- `addToCart()` - Add items to cart
- `removeFromCart()` - Remove items from cart
- `getCartItems()` - Get all cart items
- `updateQuantity()` - Update item quantity
- `clearCart()` - Clear all cart items
- `getCartTotal()` - Get total cart value
- `getCartItemCount()` - Get total item count
**Returns**: Domain entities (`CartItem`), not models.
### 3. Remote Data Source
**File**: `/Users/ssg/project/worker/lib/features/cart/data/datasources/cart_remote_datasource.dart`
**Class**: `CartRemoteDataSourceImpl`
**Features**:
- Uses `DioClient` for HTTP requests
- Proper error handling with custom exceptions
- Converts Dio exceptions to app-specific exceptions
- Maps API response to `CartItemModel`
**API Request/Response Mapping**:
#### Add to Cart
```dart
// Request
{
"items": [
{
"item_id": "Gạch ốp Signature SIG.P-8806",
"amount": 4000000,
"quantity": 33
}
]
}
// Response
{
"message": [
{
"item_id": "Gạch ốp Signature SIG.P-8806",
"success": true,
"message": "Updated quantity in cart"
}
]
}
```
#### Get Cart Items
```dart
// Request
{
"limit_start": 0,
"limit_page_length": 0
}
// Response
{
"message": [
{
"name": "rfsbgqusrj",
"item": "Gạch ốp Signature SIG.P-8806",
"quantity": 33.0,
"amount": 4000000.0,
"item_code": "Gạch ốp Signature SIG.P-8806",
"item_name": "Gạch ốp Signature SIG.P-8806",
"image": null,
"conversion_of_sm": 0.0
}
]
}
```
**Error Handling**:
- `TimeoutException` - Connection/send/receive timeout
- `NoInternetException` - No network connection
- `UnauthorizedException` - 401 errors
- `ForbiddenException` - 403 errors
- `NotFoundException` - 404 errors
- `RateLimitException` - 429 errors
- `ServerException` - 5xx errors
- `NetworkException` - Other network errors
- `ParseException` - Invalid response format
### 4. Local Data Source
**File**: `/Users/ssg/project/worker/lib/features/cart/data/datasources/cart_local_datasource.dart`
**Class**: `CartLocalDataSourceImpl`
**Features**:
- Uses Hive for local storage
- Box name: `cartBox` (from `HiveBoxNames.cartBox`)
- Uses `Box<dynamic>` with `.whereType<CartItemModel>()` (best practice)
- Stores items with `productId` as key
**Methods**:
- `saveCartItems()` - Replace all cart items
- `getCartItems()` - Get all cart items
- `addCartItem()` - Add single item (merges if exists)
- `updateCartItem()` - Update single item
- `removeCartItems()` - Remove items by productId
- `clearCart()` - Clear all items
- `getCartItemCount()` - Sum of all quantities
- `getCartTotal()` - Sum of all subtotals
**Best Practice**:
```dart
Box<dynamic> get _cartBox => _hiveService.getBox<dynamic>(HiveBoxNames.cartBox);
Future<List<CartItemModel>> getCartItems() async {
final items = _cartBox.values
.whereType<CartItemModel>()
.toList();
return items;
}
```
### 5. Repository Implementation
**File**: `/Users/ssg/project/worker/lib/features/cart/data/repositories/cart_repository_impl.dart`
**Class**: `CartRepositoryImpl`
**Strategy**: API-first with local fallback
#### Workflow:
1. **Add to Cart**:
- Try API request first
- On success: Sync to local storage
- On network error: Add to local only + queue for sync
- Convert models to entities before returning
2. **Remove from Cart**:
- Try API request first
- On success: Remove from local storage
- On network error: Remove from local only + queue for sync
3. **Get Cart Items**:
- Try API request first
- On success: Sync to local storage
- On network error: Return local data (offline support)
- Convert models to entities before returning
4. **Update Quantity**:
- Uses `addToCart` with new quantity (replaces existing)
5. **Clear Cart**:
- Gets all items and removes them via API
**Error Propagation**:
- Rethrows: `StorageException`, `NetworkException`, `ServerException`, `ValidationException`
- Wraps unknown errors in `UnknownException`
**Helper Methods**:
- `_modelToEntity()` - Convert `CartItemModel` to `CartItem` entity
- `_createCartItemModel()` - Create new model from parameters
### 6. Riverpod Providers
**Remote Data Source Provider**:
```dart
@riverpod
CartRemoteDataSource cartRemoteDataSource(CartRemoteDataSourceRef ref) {
final dioClient = ref.watch(dioClientProvider).requireValue;
return CartRemoteDataSourceImpl(dioClient);
}
```
**Local Data Source Provider**:
```dart
@riverpod
CartLocalDataSource cartLocalDataSource(CartLocalDataSourceRef ref) {
final hiveService = HiveService();
return CartLocalDataSourceImpl(hiveService);
}
```
**Repository Provider**:
```dart
@riverpod
CartRepository cartRepository(CartRepositoryRef ref) {
final remoteDataSource = ref.watch(cartRemoteDataSourceProvider);
final localDataSource = ref.watch(cartLocalDataSourceProvider);
return CartRepositoryImpl(
remoteDataSource: remoteDataSource,
localDataSource: localDataSource,
);
}
```
## Usage Example
### In Cart Provider (Presentation Layer)
```dart
@riverpod
class Cart extends _$Cart {
CartRepository get _repository => ref.read(cartRepositoryProvider);
// Add product to cart via API
Future<void> addProductToCart(Product product, double quantity) async {
try {
// Call repository
final items = await _repository.addToCart(
itemIds: [product.erpnextItemCode ?? product.productId],
quantities: [quantity],
prices: [product.basePrice],
);
// Update UI state
state = state.copyWith(items: _convertToCartItemData(items));
} catch (e) {
// Handle error (show snackbar, etc.)
throw e;
}
}
// Get cart items from API
Future<void> loadCartItems() async {
try {
final items = await _repository.getCartItems();
state = state.copyWith(items: _convertToCartItemData(items));
} catch (e) {
// Handle error
throw e;
}
}
// Remove from cart via API
Future<void> removeProductFromCart(String productId) async {
try {
await _repository.removeFromCart(itemIds: [productId]);
// Update UI state
final updatedItems = state.items
.where((item) => item.product.productId != productId)
.toList();
state = state.copyWith(items: updatedItems);
} catch (e) {
// Handle error
throw e;
}
}
}
```
## API-First Strategy Details
### Online Scenario:
1. User adds item to cart
2. API request sent to backend
3. Backend returns updated cart
4. Local storage synced with API response
5. UI updated with latest data
### Offline Scenario:
1. User adds item to cart
2. API request fails (no internet)
3. Item added to local storage only
4. Request queued for later sync (TODO)
5. UI updated from local data
### Sync on Reconnection:
When internet connection restored:
1. Process offline queue
2. Send queued requests to API
3. Sync local storage with API responses
4. Clear queue on success
## Important Notes
### Product ID Mapping
- **Frontend**: Uses `product.productId` (UUID)
- **API**: Expects `item_id` (ERPNext item code)
- **Mapping**: Use `product.erpnextItemCode` when calling API
- **Fallback**: If `erpnextItemCode` is null, use `productId`
### API Response Fields
- `name` - Cart item ID (ERPNext internal ID)
- `item` or `item_code` - Product ERPNext code
- `quantity` - Item quantity
- `amount` - Unit price
- `conversion_of_sm` - Conversion factor (if applicable)
### Local Storage
- Box: `HiveBoxNames.cartBox`
- Key: `productId` (for easy lookup and updates)
- Type: `CartItemModel` (Hive type ID: 5)
- Strategy: `Box<dynamic>` with `.whereType<CartItemModel>()`
### Error Handling Best Practices
1. Always catch specific exceptions first
2. Rethrow domain-specific exceptions
3. Wrap unknown errors in `UnknownException`
4. Provide user-friendly error messages
5. Log errors for debugging
## Testing Checklist
- [ ] Add single item to cart
- [ ] Add multiple items at once
- [ ] Update item quantity
- [ ] Remove single item
- [ ] Remove multiple items
- [ ] Clear entire cart
- [ ] Get cart items
- [ ] Calculate cart total
- [ ] Calculate item count
- [ ] Offline add (no internet)
- [ ] Offline remove (no internet)
- [ ] Sync after reconnection
- [ ] Handle API errors gracefully
- [ ] Handle timeout errors
- [ ] Handle unauthorized errors
- [ ] Local storage persistence
## Future Enhancements
1. **Offline Queue System**:
- Implement request queue for offline operations
- Auto-sync when connection restored
- Conflict resolution for concurrent edits
2. **Optimistic Updates**:
- Update UI immediately
- Sync with backend in background
- Rollback on failure
3. **Cart Sync Status**:
- Track sync state per item
- Show sync indicators in UI
- Manual sync trigger
4. **Multi-cart Support**:
- Named carts (e.g., "Project A", "Project B")
- Switch between carts
- Merge carts
5. **Cart Analytics**:
- Track add/remove events
- Cart abandonment tracking
- Conversion metrics
## Related Files
- Domain Entity: `/Users/ssg/project/worker/lib/features/cart/domain/entities/cart_item.dart`
- Data Model: `/Users/ssg/project/worker/lib/features/cart/data/models/cart_item_model.dart`
- UI State: `/Users/ssg/project/worker/lib/features/cart/presentation/providers/cart_state.dart`
- Cart Page: `/Users/ssg/project/worker/lib/features/cart/presentation/pages/cart_page.dart`
## Dependencies
- `dio` - HTTP client
- `hive_ce` - Local database
- `riverpod` - State management
- `riverpod_annotation` - Code generation
## Code Generation
To generate Riverpod providers (`.g.dart` files):
```bash
dart run build_runner build --delete-conflicting-outputs
```
Or for watch mode:
```bash
dart run build_runner watch --delete-conflicting-outputs
```
## Summary
This implementation provides:
- ✅ Clean architecture separation
- ✅ API-first with local fallback
- ✅ Offline support
- ✅ Proper error handling
- ✅ Type-safe operations
- ✅ Hive best practices
- ✅ Riverpod integration
- ✅ Scalable and maintainable code
All files follow the existing codebase patterns and are ready for integration with the UI layer.