Files
worker/lib/features/reviews/data/datasources/reviews_remote_datasource.dart
2025-11-17 17:54:32 +07:00

342 lines
10 KiB
Dart

/// Remote Data Source: Reviews
///
/// Handles API calls for review operations.
library;
import 'package:dio/dio.dart';
import 'package:worker/core/errors/exceptions.dart';
import 'package:worker/core/network/dio_client.dart';
import 'package:worker/features/reviews/data/models/review_model.dart';
import 'package:worker/features/reviews/data/models/review_response_model.dart';
import 'package:worker/features/reviews/data/models/review_statistics_model.dart';
/// Reviews remote data source interface
abstract class ReviewsRemoteDataSource {
/// Get reviews for a product (legacy - returns only reviews)
Future<List<ReviewModel>> getProductReviews({
required String itemId,
int limitPageLength = 10,
int limitStart = 0,
});
/// Get complete review response with statistics
Future<ReviewResponseModel> getProductReviewsWithStats({
required String itemId,
int limitPageLength = 10,
int limitStart = 0,
});
/// Submit a review (create or update)
Future<void> submitReview({
required String itemId,
required double rating,
required String comment,
String? name,
});
/// Delete a review
Future<void> deleteReview({required String name});
}
/// Reviews remote data source implementation
class ReviewsRemoteDataSourceImpl implements ReviewsRemoteDataSource {
const ReviewsRemoteDataSourceImpl(this._dioClient);
final DioClient _dioClient;
// API endpoints
static const String _getListEndpoint =
'/api/method/building_material.building_material.api.item_feedback.get_list';
static const String _updateEndpoint =
'/api/method/building_material.building_material.api.item_feedback.update';
static const String _deleteEndpoint =
'/api/method/building_material.building_material.api.item_feedback.delete';
@override
Future<List<ReviewModel>> getProductReviews({
required String itemId,
int limitPageLength = 10,
int limitStart = 0,
}) async {
try {
final response = await _dioClient.post(
_getListEndpoint,
data: {
'limit_page_length': limitPageLength,
'limit_start': limitStart,
'item_id': itemId,
},
);
// Handle response
if (response.statusCode == 200 || response.statusCode == 201) {
final data = response.data;
// Try different response formats
List<dynamic> reviewsJson;
if (data is Map<String, dynamic>) {
// Frappe typically returns: { "message": {...} }
if (data.containsKey('message')) {
final message = data['message'];
if (message is List) {
reviewsJson = message;
} else if (message is Map<String, dynamic>) {
// New API format: { "message": { "feedbacks": [...], "statistics": {...} } }
if (message.containsKey('feedbacks')) {
reviewsJson = message['feedbacks'] as List;
} else if (message.containsKey('data')) {
// Alternative: { "message": { "data": [...] } }
reviewsJson = message['data'] as List;
} else {
throw const ParseException(
'Unexpected response format: message map has no feedbacks or data key',
);
}
} else {
throw const ParseException(
'Unexpected response format: message is not a list or map',
);
}
} else if (data.containsKey('data')) {
// Alternative: { "data": [...] }
reviewsJson = data['data'] as List;
} else if (data.containsKey('feedbacks')) {
// Direct feedbacks key
reviewsJson = data['feedbacks'] as List;
} else {
throw const ParseException(
'Unexpected response format: no message, data, or feedbacks key',
);
}
} else if (data is List) {
// Direct list response
reviewsJson = data;
} else {
throw ParseException(
'Unexpected response format: ${data.runtimeType}',
);
}
// Parse reviews
return reviewsJson
.map((json) => ReviewModel.fromJson(json as Map<String, dynamic>))
.toList();
} else {
throw NetworkException(
'Failed to fetch reviews',
statusCode: response.statusCode,
data: response.data,
);
}
} on DioException catch (e) {
throw _handleDioException(e);
} on ParseException {
rethrow;
} catch (e, stackTrace) {
throw UnknownException(
'Failed to fetch reviews: ${e.toString()}',
e,
stackTrace,
);
}
}
@override
Future<ReviewResponseModel> getProductReviewsWithStats({
required String itemId,
int limitPageLength = 10,
int limitStart = 0,
}) async {
try {
final response = await _dioClient.post(
_getListEndpoint,
data: {
'limit_page_length': limitPageLength,
'limit_start': limitStart,
'item_id': itemId,
},
);
// Handle response
if (response.statusCode == 200 || response.statusCode == 201) {
final data = response.data;
// Parse the message object
if (data is Map<String, dynamic> && data.containsKey('message')) {
final message = data['message'];
if (message is Map<String, dynamic>) {
// New API format with complete response
return ReviewResponseModel.fromJson(message);
} else {
throw const ParseException(
'Unexpected response format: message is not a map',
);
}
} else {
throw const ParseException(
'Unexpected response format: no message key',
);
}
} else {
throw NetworkException(
'Failed to fetch reviews',
statusCode: response.statusCode,
data: response.data,
);
}
} on DioException catch (e) {
throw _handleDioException(e);
} on ParseException {
rethrow;
} catch (e, stackTrace) {
throw UnknownException(
'Failed to fetch reviews: ${e.toString()}',
e,
stackTrace,
);
}
}
@override
Future<void> submitReview({
required String itemId,
required double rating,
required String comment,
String? name,
}) async {
try {
final data = {
'item_id': itemId,
'rating': rating,
'comment': comment,
if (name != null) 'name': name,
};
final response = await _dioClient.post(
_updateEndpoint,
data: data,
);
// Handle response
if (response.statusCode != 200 && response.statusCode != 201) {
throw NetworkException(
'Failed to submit review',
statusCode: response.statusCode,
data: response.data,
);
}
} on DioException catch (e) {
throw _handleDioException(e);
} catch (e, stackTrace) {
throw UnknownException(
'Failed to submit review: ${e.toString()}',
e,
stackTrace,
);
}
}
@override
Future<void> deleteReview({required String name}) async {
try {
final response = await _dioClient.post(
_deleteEndpoint,
data: {'name': name},
);
// Handle response
if (response.statusCode != 200 && response.statusCode != 204) {
throw NetworkException(
'Failed to delete review',
statusCode: response.statusCode,
data: response.data,
);
}
} on DioException catch (e) {
throw _handleDioException(e);
} catch (e, stackTrace) {
throw UnknownException(
'Failed to delete review: ${e.toString()}',
e,
stackTrace,
);
}
}
/// Handle Dio exceptions and convert to app exceptions
Exception _handleDioException(DioException e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return const TimeoutException();
case DioExceptionType.connectionError:
return const NoInternetException();
case DioExceptionType.badResponse:
final statusCode = e.response?.statusCode;
final data = e.response?.data;
// Extract error message from response if available
String? errorMessage;
if (data is Map<String, dynamic>) {
errorMessage = data['message'] as String? ??
data['error'] as String? ??
data['exc'] as String?;
}
switch (statusCode) {
case 400:
return BadRequestException(
errorMessage ?? 'Dữ liệu không hợp lệ',
);
case 401:
return UnauthorizedException(
errorMessage ?? 'Phiên đăng nhập hết hạn. Vui lòng đăng nhập lại.',
);
case 403:
return const ForbiddenException();
case 404:
return NotFoundException(
errorMessage ?? 'Không tìm thấy đánh giá',
);
case 409:
return ConflictException(
errorMessage ?? 'Đánh giá đã tồn tại',
);
case 429:
return const RateLimitException();
case 500:
case 502:
case 503:
case 504:
return ServerException(
errorMessage ?? 'Lỗi máy chủ. Vui lòng thử lại sau.',
statusCode,
);
default:
return NetworkException(
errorMessage ?? 'Lỗi mạng không xác định',
statusCode: statusCode,
data: data,
);
}
case DioExceptionType.cancel:
return const NetworkException('Yêu cầu đã bị hủy');
case DioExceptionType.badCertificate:
return const NetworkException('Lỗi chứng chỉ SSL');
case DioExceptionType.unknown:
default:
return NetworkException(
'Lỗi kết nối: ${e.message ?? "Unknown error"}',
);
}
}
}