update review api.

This commit is contained in:
Phuoc Nguyen
2025-11-17 17:54:32 +07:00
parent 0798b28db5
commit 0841e3bf3d
23 changed files with 4856 additions and 209 deletions

View File

@@ -0,0 +1,116 @@
/// Domain Entity: Review
///
/// Represents a product review with rating and comment.
library;
/// Review entity
///
/// Contains user feedback for a product including:
/// - Unique review ID (name field from API)
/// - Product item code
/// - Rating (0-1 from API, converted to 0-5 stars for display)
/// - Review comment text
/// - Reviewer information
/// - Review date
class Review {
const Review({
required this.id,
required this.itemId,
required this.rating,
required this.comment,
this.reviewerName,
this.reviewerEmail,
this.reviewDate,
});
/// Unique review identifier (format: ITEM-{item_id}-{user_email})
final String id;
/// Product item code being reviewed
final String itemId;
/// Rating from API (0-5 scale)
/// Note: API already provides rating on 0-5 scale, no conversion needed
final double rating;
/// Review comment text
final String comment;
/// Name of the reviewer (if available)
final String? reviewerName;
/// Email of the reviewer (if available)
final String? reviewerEmail;
/// Date when the review was created (if available)
final DateTime? reviewDate;
/// Get star rating rounded to nearest integer (0-5)
///
/// Examples:
/// - API rating 0.5 = 1 star
/// - API rating 2.25 = 2 stars
/// - API rating 4.0 = 4 stars
int get starsRating => rating.round();
/// Get rating as exact decimal (0-5 scale)
///
/// This is useful for average rating calculations and display
/// API already returns this on 0-5 scale
double get starsRatingDecimal => rating;
/// Copy with method for creating modified copies
Review copyWith({
String? id,
String? itemId,
double? rating,
String? comment,
String? reviewerName,
String? reviewerEmail,
DateTime? reviewDate,
}) {
return Review(
id: id ?? this.id,
itemId: itemId ?? this.itemId,
rating: rating ?? this.rating,
comment: comment ?? this.comment,
reviewerName: reviewerName ?? this.reviewerName,
reviewerEmail: reviewerEmail ?? this.reviewerEmail,
reviewDate: reviewDate ?? this.reviewDate,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Review &&
other.id == id &&
other.itemId == itemId &&
other.rating == rating &&
other.comment == comment &&
other.reviewerName == reviewerName &&
other.reviewerEmail == reviewerEmail &&
other.reviewDate == reviewDate;
}
@override
int get hashCode {
return Object.hash(
id,
itemId,
rating,
comment,
reviewerName,
reviewerEmail,
reviewDate,
);
}
@override
String toString() {
return 'Review(id: $id, itemId: $itemId, rating: $rating, '
'starsRating: $starsRating, comment: ${comment.substring(0, comment.length > 30 ? 30 : comment.length)}..., '
'reviewerName: $reviewerName, reviewDate: $reviewDate)';
}
}

View File

@@ -0,0 +1,49 @@
/// Domain Entity: Review Statistics
///
/// Aggregate statistics for product reviews.
library;
/// Review statistics entity
///
/// Contains aggregate data about reviews:
/// - Total number of feedbacks
/// - Average rating (0-5 scale)
class ReviewStatistics {
const ReviewStatistics({
required this.totalFeedback,
required this.averageRating,
});
/// Total number of reviews/feedbacks
final int totalFeedback;
/// Average rating (0-5 scale)
/// Note: This is already on 0-5 scale from API, no conversion needed
final double averageRating;
/// Check if there are any reviews
bool get hasReviews => totalFeedback > 0;
/// Get star rating rounded to nearest 0.5
/// Used for displaying star icons
double get displayRating {
return (averageRating * 2).round() / 2;
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ReviewStatistics &&
other.totalFeedback == totalFeedback &&
other.averageRating == averageRating;
}
@override
int get hashCode => Object.hash(totalFeedback, averageRating);
@override
String toString() {
return 'ReviewStatistics(totalFeedback: $totalFeedback, averageRating: $averageRating)';
}
}

View File

@@ -0,0 +1,82 @@
/// Domain Repository Interface: Reviews
///
/// Defines the contract for review data operations.
library;
import 'package:worker/features/reviews/domain/entities/review.dart';
import 'package:worker/features/reviews/domain/entities/review_statistics.dart';
/// Reviews repository interface
///
/// Defines methods for managing product reviews:
/// - Fetching reviews for a product
/// - Submitting new reviews
/// - Updating existing reviews
/// - Deleting reviews
abstract class ReviewsRepository {
/// Get reviews for a specific product
///
/// [itemId] - Product item code
/// [limitPageLength] - Number of reviews per page (default: 10)
/// [limitStart] - Pagination offset (default: 0)
///
/// Returns a list of [Review] entities
///
/// Throws:
/// - [NetworkException] on network errors
/// - [ServerException] on server errors
/// - [ParseException] on JSON parsing errors
Future<List<Review>> getProductReviews({
required String itemId,
int limitPageLength = 10,
int limitStart = 0,
});
/// Get review statistics for a product
///
/// [itemId] - Product item code
///
/// Returns [ReviewStatistics] with total count and average rating
///
/// Throws:
/// - [NetworkException] on network errors
/// - [ServerException] on server errors
Future<ReviewStatistics> getProductReviewStatistics({
required String itemId,
});
/// Submit a new review or update an existing one
///
/// [itemId] - Product item code
/// [rating] - Rating value (0-1 scale for API)
/// [comment] - Review comment text
/// [name] - Optional review ID for updates (format: ITEM-{item_id}-{user_email})
///
/// If [name] is provided, the review will be updated.
/// If [name] is null, a new review will be created.
///
/// Throws:
/// - [NetworkException] on network errors
/// - [ServerException] on server errors
/// - [ValidationException] on invalid data
/// - [AuthException] if not authenticated
Future<void> submitReview({
required String itemId,
required double rating,
required String comment,
String? name,
});
/// Delete a review
///
/// [name] - Review ID to delete (format: ITEM-{item_id}-{user_email})
///
/// Throws:
/// - [NetworkException] on network errors
/// - [ServerException] on server errors
/// - [NotFoundException] if review doesn't exist
/// - [AuthException] if not authenticated or not authorized
Future<void> deleteReview({
required String name,
});
}

View File

@@ -0,0 +1,24 @@
/// Use Case: Delete Review
///
/// Deletes a product review.
library;
import 'package:worker/features/reviews/domain/repositories/reviews_repository.dart';
/// Use case for deleting a product review
class DeleteReview {
const DeleteReview(this._repository);
final ReviewsRepository _repository;
/// Execute the use case
///
/// [name] - Review ID to delete (format: ITEM-{item_id}-{user_email})
Future<void> call({required String name}) async {
if (name.trim().isEmpty) {
throw ArgumentError('Review ID cannot be empty');
}
await _repository.deleteReview(name: name);
}
}

View File

@@ -0,0 +1,33 @@
/// Use Case: Get Product Reviews
///
/// Fetches reviews for a specific product with pagination.
library;
import 'package:worker/features/reviews/domain/entities/review.dart';
import 'package:worker/features/reviews/domain/repositories/reviews_repository.dart';
/// Use case for getting product reviews
class GetProductReviews {
const GetProductReviews(this._repository);
final ReviewsRepository _repository;
/// Execute the use case
///
/// [itemId] - Product item code
/// [limitPageLength] - Number of reviews per page (default: 10)
/// [limitStart] - Pagination offset (default: 0)
///
/// Returns a list of [Review] entities sorted by date (newest first)
Future<List<Review>> call({
required String itemId,
int limitPageLength = 10,
int limitStart = 0,
}) async {
return await _repository.getProductReviews(
itemId: itemId,
limitPageLength: limitPageLength,
limitStart: limitStart,
);
}
}

View File

@@ -0,0 +1,61 @@
/// Use Case: Submit Review
///
/// Submits a new product review or updates an existing one.
library;
import 'package:worker/features/reviews/domain/repositories/reviews_repository.dart';
/// Use case for submitting a product review
class SubmitReview {
const SubmitReview(this._repository);
final ReviewsRepository _repository;
/// Execute the use case
///
/// [itemId] - Product item code
/// [rating] - Rating value (0-1 scale for API)
/// [comment] - Review comment text
/// [name] - Optional review ID for updates
///
/// Note: The rating should be in 0-1 scale for the API.
/// If you have a 1-5 star rating, convert it first: `stars / 5.0`
Future<void> call({
required String itemId,
required double rating,
required String comment,
String? name,
}) async {
// Validate rating range (0-1)
if (rating < 0 || rating > 1) {
throw ArgumentError(
'Rating must be between 0 and 1. Got: $rating. '
'If you have a 1-5 star rating, convert it first: stars / 5.0',
);
}
// Validate comment length
if (comment.trim().isEmpty) {
throw ArgumentError('Review comment cannot be empty');
}
if (comment.trim().length < 20) {
throw ArgumentError(
'Review comment must be at least 20 characters. Got: ${comment.trim().length}',
);
}
if (comment.length > 1000) {
throw ArgumentError(
'Review comment must not exceed 1000 characters. Got: ${comment.length}',
);
}
await _repository.submitReview(
itemId: itemId,
rating: rating,
comment: comment.trim(),
name: name,
);
}
}