feat: enhance API error handling and response structure; introduce ApiException for V2 error codes

This commit is contained in:
Achintha Isuru
2026-03-17 14:15:26 -04:00
parent d6ddb7829e
commit b6a655a261
5 changed files with 116 additions and 52 deletions

View File

@@ -8,7 +8,7 @@ class ApiResponse {
this.errors = const <String, dynamic>{},
});
/// The response code (e.g., '200', '404', or custom error code).
/// The response code (e.g., '200', '404', or V2 error code like 'VALIDATION_ERROR').
final String code;
/// A descriptive message about the response.
@@ -19,4 +19,13 @@ class ApiResponse {
/// A map of field-specific error messages, if any.
final Map<String, dynamic> errors;
/// Whether the response indicates success (HTTP 2xx).
bool get isSuccess {
final int? statusCode = int.tryParse(code);
return statusCode != null && statusCode >= 200 && statusCode < 300;
}
/// Whether the response indicates failure.
bool get isFailure => !isSuccess;
}

View File

@@ -1,3 +1,4 @@
import '../../../exceptions/app_exception.dart';
import 'api_response.dart';
import 'base_api_service.dart';
@@ -14,10 +15,13 @@ abstract class BaseCoreService {
/// Standardized wrapper to execute API actions.
///
/// This handles generic error normalization for unexpected non-HTTP errors.
/// Rethrows [AppException] subclasses (domain errors) directly.
/// Wraps unexpected non-HTTP errors into an error [ApiResponse].
Future<ApiResponse> action(Future<ApiResponse> Function() execution) async {
try {
return await execution();
} on AppException {
rethrow;
} catch (e) {
return ApiResponse(
code: 'CORE_INTERNAL_ERROR',

View File

@@ -286,6 +286,54 @@ class NoActiveShiftException extends ShiftException {
// NETWORK/GENERIC EXCEPTIONS
// ============================================================
// ============================================================
// API EXCEPTIONS (mapped from V2 error envelope codes)
// ============================================================
/// Thrown when the V2 API returns a non-success response.
///
/// Carries the full error envelope so callers can inspect details.
class ApiException extends AppException {
/// Creates an [ApiException].
const ApiException({
required String apiCode,
required String apiMessage,
this.statusCode,
this.details,
super.technicalMessage,
}) : super(code: apiCode);
/// The HTTP status code (e.g. 400, 404, 500).
final int? statusCode;
/// The V2 API error code string (e.g. 'VALIDATION_ERROR').
String get apiCode => code;
/// Optional details from the error envelope.
final dynamic details;
@override
String get messageKey {
switch (code) {
case 'VALIDATION_ERROR':
return 'errors.generic.validation_error';
case 'NOT_FOUND':
return 'errors.generic.not_found';
case 'FORBIDDEN':
return 'errors.generic.forbidden';
case 'UNAUTHENTICATED':
return 'errors.auth.not_authenticated';
case 'CONFLICT':
return 'errors.generic.conflict';
default:
if (statusCode != null && statusCode! >= 500) {
return 'errors.generic.server_error';
}
return 'errors.generic.unknown';
}
}
}
/// Thrown when there is no network connection.
class NetworkException extends AppException {
const NetworkException({super.technicalMessage})