1use actix_web::{HttpResponse, ResponseError};
2use arc_core::command_bus::CommandBusError;
3use arc_core::event_store::EventStoreError;
4
5#[derive(Debug, thiserror::Error)]
6pub enum AppError {
7 #[error("Command failed: {0}")]
8 CommandFailed(#[from] CommandBusError),
9 #[error("Audit failed: {message}")]
10 AuditFailed {
11 status: actix_web::http::StatusCode,
12 message: String,
13 },
14}
15
16impl ResponseError for AppError {
18 fn error_response(&self) -> HttpResponse {
19 match self {
20 AppError::AuditFailed { status, message } => {
21 HttpResponse::build(*status).json(serde_json::json!({
22 "error": "AuditFailed",
23 "message": message
24 }))
25 }
26 AppError::CommandFailed(err) => match err {
27 CommandBusError::HandleFailed { message, .. } => {
28 HttpResponse::UnprocessableEntity().json(serde_json::json!({
29 "error": "ValidationFailed",
30 "message": message
31 }))
32 }
33 CommandBusError::AppendFailed { source, .. } => match source {
34 EventStoreError::ConcurrencyConflict { .. } => {
35 HttpResponse::Conflict().json(serde_json::json!({
36 "error": "ConcurrencyConflict",
37 "message": "Resource was modified by another request. Please try again."
38 }))
39 }
40 _ => HttpResponse::InternalServerError().json(serde_json::json!({
41 "error": "InternalServerError",
42 "message": "Storage error."
43 })),
44 },
45 CommandBusError::LoadFailed { .. } => {
46 HttpResponse::NotFound().json(serde_json::json!({
47 "error": "NotFound",
48 "message": "Resource not found."
49 }))
50 }
51 CommandBusError::InvalidAudit { .. } => {
52 HttpResponse::BadRequest().json(serde_json::json!({
53 "error": "InvalidAudit",
54 "message": "Audit metadata for this request is missing or malformed."
55 }))
56 }
57 _ => HttpResponse::InternalServerError().json(serde_json::json!({
58 "error": "InternalServerError",
59 "message": "An unexpected error occurred."
60 })),
61 },
62 }
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69 use actix_web::http::StatusCode;
70 use arc_core::audit::AuditError;
71
72 #[test]
73 fn test_invalid_audit_maps_to_bad_request() {
74 let err = AppError::CommandFailed(CommandBusError::InvalidAudit {
75 aggregate_id: "u-1".to_string(),
76 source: AuditError::EmptyActorId,
77 });
78 let resp = err.error_response();
79 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
80 }
81
82 #[test]
83 fn test_handle_failed_maps_to_unprocessable_entity() {
84 let err = AppError::CommandFailed(CommandBusError::handle_failed("u-1", "bad email"));
85 assert_eq!(
86 err.error_response().status(),
87 StatusCode::UNPROCESSABLE_ENTITY
88 );
89 }
90
91 #[test]
92 fn test_concurrency_conflict_maps_to_conflict() {
93 let err = AppError::CommandFailed(CommandBusError::AppendFailed {
94 aggregate_id: "u-1".to_string(),
95 source: EventStoreError::ConcurrencyConflict {
96 aggregate_id: "u-1".to_string(),
97 expected: 1,
98 actual: 2,
99 },
100 });
101 assert_eq!(err.error_response().status(), StatusCode::CONFLICT);
102 }
103}