416 lines
11 KiB
Markdown
416 lines
11 KiB
Markdown
# 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.
|