Files
worker/lib/features/cart/CART_API_INTEGRATION.md
Phuoc Nguyen aae3c9d080 update cart
2025-11-14 16:19:25 +07:00

11 KiB

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

// 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

// 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:

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:

@riverpod
CartRemoteDataSource cartRemoteDataSource(CartRemoteDataSourceRef ref) {
  final dioClient = ref.watch(dioClientProvider).requireValue;
  return CartRemoteDataSourceImpl(dioClient);
}

Local Data Source Provider:

@riverpod
CartLocalDataSource cartLocalDataSource(CartLocalDataSourceRef ref) {
  final hiveService = HiveService();
  return CartLocalDataSourceImpl(hiveService);
}

Repository Provider:

@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)

@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
  • 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):

dart run build_runner build --delete-conflicting-outputs

Or for watch mode:

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.