Files
worker/docs/REVIEWS_API_INTEGRATION_SUMMARY.md
Phuoc Nguyen 192c322816 a
2025-11-17 17:56:34 +07:00

18 KiB

Review API Integration - Implementation Summary

Overview

Successfully integrated the Review/Feedback API into the Flutter Worker app, replacing mock review data with real API calls from the Frappe/ERPNext backend.

Implementation Date

November 17, 2024


API Endpoints Integrated

1. Get List Reviews

POST /api/method/building_material.building_material.api.item_feedback.get_list

Request Body:
{
  "limit_page_length": 10,
  "limit_start": 0,
  "item_id": "GIB20 G04"
}

2. Create/Update Review

POST /api/method/building_material.building_material.api.item_feedback.update

Request Body:
{
  "item_id": "Gạch ốp Signature SIG.P-8806",
  "rating": 0.5,  // 0-1 scale (0.5 = 2.5 stars out of 5)
  "comment": "Good job 2",
  "name": "ITEM-{item_id}-{user_email}"  // Optional for updates
}

3. Delete Review

POST /api/method/building_material.building_material.api.item_feedback.delete

Request Body:
{
  "name": "ITEM-{item_id}-{user_email}"
}

Rating Scale Conversion

CRITICAL: The API uses a 0-1 rating scale while the UI uses 1-5 stars.

Conversion Formulas

  • API to UI: stars = (apiRating * 5).round()
  • UI to API: apiRating = stars / 5.0

Examples

API Rating Stars (Decimal) Stars (Rounded)
0.2 1.0 1 star
0.4 2.0 2 stars
0.5 2.5 3 stars
0.8 4.0 4 stars
1.0 5.0 5 stars

Implementation:

  • Review.starsRating getter: Returns rounded integer (0-5)
  • Review.starsRatingDecimal getter: Returns exact decimal (0-5)
  • starsToApiRating() helper: Converts UI stars to API rating
  • apiRatingToStars() helper: Converts API rating to UI stars

File Structure Created

lib/features/reviews/
  data/
    datasources/
      reviews_remote_datasource.dart          # API calls with Dio
    models/
      review_model.dart                       # JSON serialization
    repositories/
      reviews_repository_impl.dart            # Repository implementation
  domain/
    entities/
      review.dart                             # Domain entity
    repositories/
      reviews_repository.dart                 # Repository interface
    usecases/
      get_product_reviews.dart                # Fetch reviews use case
      submit_review.dart                      # Submit review use case
      delete_review.dart                      # Delete review use case
  presentation/
    providers/
      reviews_provider.dart                   # Riverpod providers
      reviews_provider.g.dart                 # Generated provider code (manual)

Domain Layer

Review Entity

File: /Users/ssg/project/worker/lib/features/reviews/domain/entities/review.dart

class Review {
  final String id;              // Review ID (format: ITEM-{item_id}-{user_email})
  final String itemId;          // Product item code
  final double rating;          // API rating (0-1 scale)
  final String comment;         // Review text
  final String? reviewerName;   // Reviewer name
  final String? reviewerEmail;  // Reviewer email
  final DateTime? reviewDate;   // Review date

  // Convert API rating (0-1) to stars (0-5)
  int get starsRating => (rating * 5).round();

  // Get exact decimal rating (0-5)
  double get starsRatingDecimal => rating * 5;
}

Repository Interface

File: /Users/ssg/project/worker/lib/features/reviews/domain/repositories/reviews_repository.dart

abstract class ReviewsRepository {
  Future<List<Review>> getProductReviews({
    required String itemId,
    int limitPageLength = 10,
    int limitStart = 0,
  });

  Future<void> submitReview({
    required String itemId,
    required double rating,
    required String comment,
    String? name,
  });

  Future<void> deleteReview({required String name});
}

Use Cases

  1. GetProductReviews: Fetches reviews with pagination
  2. SubmitReview: Creates or updates a review (validates rating 0-1, comment 20-1000 chars)
  3. DeleteReview: Deletes a review by ID

Data Layer

Review Model

File: /Users/ssg/project/worker/lib/features/reviews/data/models/review_model.dart

Features:

  • JSON serialization with fromJson() and toJson()
  • Entity conversion with toEntity() and fromEntity()
  • Email-to-name extraction fallback (e.g., "john.doe@example.com" → "John Doe")
  • DateTime parsing for both ISO 8601 and Frappe formats
  • Handles multiple response field names (owner_full_name, full_name)

Assumed API Response Format:

{
  "name": "ITEM-GIB20 G04-user@example.com",
  "item_id": "GIB20 G04",
  "rating": 0.8,
  "comment": "Great product!",
  "owner": "user@example.com",
  "owner_full_name": "John Doe",
  "creation": "2024-11-17 10:30:00",
  "modified": "2024-11-17 10:30:00"
}

Remote Data Source

File: /Users/ssg/project/worker/lib/features/reviews/data/datasources/reviews_remote_datasource.dart

Features:

  • DioClient integration with interceptors
  • Comprehensive error handling:
    • Network errors (timeout, no internet, connection)
    • HTTP status codes (400, 401, 403, 404, 409, 429, 5xx)
    • Frappe-specific error extraction from response
  • Multiple response format handling:
    • { "message": [...] }
    • { "message": { "data": [...] } }
    • { "data": [...] }
    • Direct array [...]

Repository Implementation

File: /Users/ssg/project/worker/lib/features/reviews/data/repositories/reviews_repository_impl.dart

Features:

  • Converts models to entities
  • Sorts reviews by date (newest first)
  • Delegates to remote data source

Presentation Layer

Riverpod Providers

File: /Users/ssg/project/worker/lib/features/reviews/presentation/providers/reviews_provider.dart

Data Layer Providers:

  • reviewsRemoteDataSourceProvider: Remote data source instance
  • reviewsRepositoryProvider: Repository instance

Use Case Providers:

  • getProductReviewsProvider: Get reviews use case
  • submitReviewProvider: Submit review use case
  • deleteReviewProvider: Delete review use case

State Providers:

// Fetch reviews for a product
final reviewsAsync = ref.watch(productReviewsProvider(itemId));

// Calculate average rating
final avgRating = ref.watch(productAverageRatingProvider(itemId));

// Get review count
final count = ref.watch(productReviewCountProvider(itemId));

// Check if user can submit review
final canSubmit = ref.watch(canSubmitReviewProvider(itemId));

Helper Functions:

// Convert UI stars to API rating
double apiRating = starsToApiRating(5); // 1.0

// Convert API rating to UI stars
int stars = apiRatingToStars(0.8); // 4

UI Updates

1. ProductTabsSection Widget

File: /Users/ssg/project/worker/lib/features/products/presentation/widgets/product_detail/product_tabs_section.dart

Changes:

  • Changed _ReviewsTab from StatelessWidget to ConsumerWidget
  • Replaced mock reviews with productReviewsProvider
  • Added real-time average rating calculation
  • Implemented loading, error, and empty states
  • Updated _ReviewItem to use Review entity instead of Map
  • Added date formatting function (_formatDate)

States:

  1. Loading: Shows CircularProgressIndicator
  2. Error: Shows error icon and message
  3. Empty: Shows "Chưa có đánh giá nào" with call-to-action
  4. Data: Shows rating overview and review list

Rating Overview:

  • Dynamic average rating display (0-5 scale)
  • Star rendering with full/half/empty stars
  • Review count from actual data

Review Cards:

  • Reviewer name (with fallback to "Người dùng")
  • Relative date formatting (e.g., "2 ngày trước", "1 tuần trước")
  • Star rating (converted from 0-1 to 5 stars)
  • Comment text

2. WriteReviewPage

File: /Users/ssg/project/worker/lib/features/products/presentation/pages/write_review_page.dart

Changes:

  • Added submitReviewProvider usage
  • Implemented real API submission with error handling
  • Added rating conversion (stars → API rating)
  • Invalidates productReviewsProvider after successful submission
  • Shows success/error SnackBars with appropriate icons

Submit Flow:

  1. Validate form (rating 1-5, comment 20-1000 chars)
  2. Convert rating: apiRating = _selectedRating / 5.0
  3. Call API via submitReview use case
  4. On success:
    • Show success SnackBar
    • Invalidate reviews cache (triggers refresh)
    • Navigate back to product detail
  5. On error:
    • Show error SnackBar
    • Keep user on page to retry

API Constants Updated

File: /Users/ssg/project/worker/lib/core/constants/api_constants.dart

Added three new constants:

static const String frappeGetReviews =
  '/building_material.building_material.api.item_feedback.get_list';

static const String frappeUpdateReview =
  '/building_material.building_material.api.item_feedback.update';

static const String frappeDeleteReview =
  '/building_material.building_material.api.item_feedback.delete';

Error Handling

Network Errors

  • NoInternetException: "Không có kết nối internet"
  • TimeoutException: "Kết nối quá lâu. Vui lòng thử lại."
  • ServerException: "Lỗi máy chủ. Vui lòng thử lại sau."

HTTP Status Codes

  • 400: BadRequestException - "Dữ liệu không hợp lệ"
  • 401: UnauthorizedException - "Phiên đăng nhập hết hạn"
  • 403: ForbiddenException - "Không có quyền truy cập"
  • 404: NotFoundException - "Không tìm thấy đánh giá"
  • 409: ConflictException - "Đánh giá đã tồn tại"
  • 429: RateLimitException - "Quá nhiều yêu cầu"
  • 500+: ServerException - Custom message from API

Validation Errors

  • Rating must be 0-1 (API scale)
  • Comment must be 20-1000 characters
  • Comment cannot be empty

Authentication Requirements

All review API endpoints require:

  1. Cookie: sid={session_id} (from auth flow)
  2. Header: X-Frappe-Csrf-Token: {csrf_token} (from auth flow)

These are handled automatically by the AuthInterceptor in the DioClient configuration.


Review ID Format

The review ID (name field) follows this pattern:

ITEM-{item_id}-{user_email}

Examples:

  • ITEM-GIB20 G04-john.doe@example.com
  • ITEM-Gạch ốp Signature SIG.P-8806-user@company.com

This ID is used for:

  • Identifying reviews in the system
  • Updating existing reviews (pass as name parameter)
  • Deleting reviews

Pagination Support

The getProductReviews endpoint supports pagination:

// Fetch first 10 reviews
final reviews = await repository.getProductReviews(
  itemId: 'GIB20 G04',
  limitPageLength: 10,
  limitStart: 0,
);

// Fetch next 10 reviews
final moreReviews = await repository.getProductReviews(
  itemId: 'GIB20 G04',
  limitPageLength: 10,
  limitStart: 10,
);

Current Implementation: Fetches 50 reviews at once (can be extended with infinite scroll)


Testing Checklist

API Integration

  • Reviews load correctly in ProductTabsSection
  • Rating scale conversion works (0-1 ↔ 1-5 stars)
  • Submit review works and refreshes list
  • Average rating calculated correctly
  • Empty state shown when no reviews
  • Error handling for API failures
  • Loading states shown during API calls

UI/UX

  • Review cards display correct information
  • Date formatting works correctly (relative dates)
  • Star ratings display correctly
  • Write review button navigates correctly
  • Submit button disabled during submission
  • Success/error messages shown appropriately

Edge Cases

  • Handle missing reviewer name (fallback to email extraction)
  • Handle missing review date
  • Handle empty review list
  • Handle API errors gracefully
  • Handle network connectivity issues

Known Issues and Limitations

1. Build Runner

Issue: Cannot run dart run build_runner build due to Dart SDK version mismatch

  • Required: Dart 3.10.0
  • Available: Dart 3.9.2

Workaround: Manually created reviews_provider.g.dart file

Solution: Upgrade Dart SDK to 3.10.0 and regenerate

2. API Response Format

Issue: Actual API response structure not fully documented

Assumption: Based on common Frappe patterns:

{
  "message": [
    {
      "name": "...",
      "item_id": "...",
      "rating": 0.5,
      "comment": "...",
      "owner": "...",
      "creation": "..."
    }
  ]
}

Recommendation: Test with actual API and adjust ReviewModel.fromJson() if needed

3. One Review Per User

Current: Users can submit multiple reviews for the same product

Future Enhancement:

  • Check if user already reviewed product
  • Update canSubmitReviewProvider to enforce one-review-per-user
  • Show "Edit Review" instead of "Write Review" for existing reviews

4. Review Deletion

Current: Delete functionality implemented but not exposed in UI

Future Enhancement:

  • Add "Delete" button for user's own reviews
  • Require confirmation dialog
  • Refresh list after deletion

Next Steps

Immediate

  1. Test with Real API: Verify actual response format and adjust model if needed
  2. Upgrade Dart SDK: To 3.10.0 for proper code generation
  3. Run Build Runner: Regenerate provider code automatically

Short-term

  1. Add Review Editing: Allow users to edit their own reviews
  2. Add Review Deletion UI: Show delete button for user's reviews
  3. Implement Pagination: Add "Load More" button for reviews
  4. Add Helpful Button: Allow users to mark reviews as helpful
  5. Add Review Images: Support photo uploads in reviews

Long-term

  1. Review Moderation: Admin panel for reviewing flagged reviews
  2. Verified Purchase Badge: Show badge for reviews from verified purchases
  3. Review Sorting: Sort by date, rating, helpful votes
  4. Review Filtering: Filter by star rating
  5. Review Analytics: Show rating distribution graph

File Paths Reference

All file paths are absolute for easy navigation:

Domain Layer:

  • /Users/ssg/project/worker/lib/features/reviews/domain/entities/review.dart
  • /Users/ssg/project/worker/lib/features/reviews/domain/repositories/reviews_repository.dart
  • /Users/ssg/project/worker/lib/features/reviews/domain/usecases/get_product_reviews.dart
  • /Users/ssg/project/worker/lib/features/reviews/domain/usecases/submit_review.dart
  • /Users/ssg/project/worker/lib/features/reviews/domain/usecases/delete_review.dart

Data Layer:

  • /Users/ssg/project/worker/lib/features/reviews/data/models/review_model.dart
  • /Users/ssg/project/worker/lib/features/reviews/data/datasources/reviews_remote_datasource.dart
  • /Users/ssg/project/worker/lib/features/reviews/data/repositories/reviews_repository_impl.dart

Presentation Layer:

  • /Users/ssg/project/worker/lib/features/reviews/presentation/providers/reviews_provider.dart
  • /Users/ssg/project/worker/lib/features/reviews/presentation/providers/reviews_provider.g.dart

Updated Files:

  • /Users/ssg/project/worker/lib/features/products/presentation/widgets/product_detail/product_tabs_section.dart
  • /Users/ssg/project/worker/lib/features/products/presentation/pages/write_review_page.dart
  • /Users/ssg/project/worker/lib/core/constants/api_constants.dart

Code Examples

Fetching Reviews in a Widget

@override
Widget build(BuildContext context, WidgetRef ref) {
  final reviewsAsync = ref.watch(productReviewsProvider('PRODUCT_ID'));

  return reviewsAsync.when(
    data: (reviews) {
      if (reviews.isEmpty) {
        return Text('No reviews yet');
      }
      return ListView.builder(
        itemCount: reviews.length,
        itemBuilder: (context, index) {
          final review = reviews[index];
          return ListTile(
            title: Text(review.reviewerName ?? 'Anonymous'),
            subtitle: Text(review.comment),
            leading: Row(
              mainAxisSize: MainAxisSize.min,
              children: List.generate(
                5,
                (i) => Icon(
                  i < review.starsRating
                      ? Icons.star
                      : Icons.star_border,
                ),
              ),
            ),
          );
        },
      );
    },
    loading: () => CircularProgressIndicator(),
    error: (error, stack) => Text('Error: $error'),
  );
}

Submitting a Review

Future<void> submitReview(WidgetRef ref, String productId, int stars, String comment) async {
  try {
    final submitUseCase = ref.read(submitReviewProvider);

    // Convert stars (1-5) to API rating (0-1)
    final apiRating = stars / 5.0;

    await submitUseCase(
      itemId: productId,
      rating: apiRating,
      comment: comment,
    );

    // Refresh reviews list
    ref.invalidate(productReviewsProvider(productId));

    // Show success message
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Review submitted successfully!')),
    );
  } catch (e) {
    // Show error message
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Error: $e')),
    );
  }
}

Getting Average Rating

@override
Widget build(BuildContext context, WidgetRef ref) {
  final avgRatingAsync = ref.watch(productAverageRatingProvider('PRODUCT_ID'));

  return avgRatingAsync.when(
    data: (avgRating) => Text(
      'Average: ${avgRating.toStringAsFixed(1)} stars',
    ),
    loading: () => Text('Loading...'),
    error: (_, __) => Text('No ratings yet'),
  );
}

Conclusion

The review API integration is complete and ready for testing with the real backend. The implementation follows clean architecture principles, uses Riverpod for state management, and includes comprehensive error handling.

Key Achievements:

  • Complete clean architecture implementation (domain, data, presentation layers)
  • Type-safe API client with comprehensive error handling
  • Rating scale conversion (0-1 ↔ 1-5 stars)
  • Real-time UI updates with Riverpod
  • Loading, error, and empty states
  • Form validation and user feedback
  • Date formatting and name extraction
  • Pagination support

Next Action: Test with real API endpoints and adjust response parsing if needed.