koprogo_api/infrastructure/web/handlers/
conformity_response.rs1use actix_web::HttpResponse;
33use serde_json::json;
34
35const PREFIX: &str = "BUILDING_NOT_CONFORMANT:";
38const ACP_PREFIX: &str = "ACP_NOT_CONFORMANT:";
40
41pub fn try_build_conformity_response(err: &str) -> Option<HttpResponse> {
50 let trimmed = err.trim();
51
52 if let Some(rest) = trimmed.strip_prefix(ACP_PREFIX) {
55 return Some(build_acp_response(rest.trim()));
56 }
57
58 if !trimmed.starts_with(PREFIX) {
59 return None;
60 }
61
62 let rest = trimmed.trim_start_matches(PREFIX).trim();
63 let mut building_id: Option<String> = None;
64 let mut units_delta: Option<i32> = None;
65 let mut quota_delta: Option<String> = None;
66 let mut quota_basis: Option<i32> = None;
67
68 let tokens: Vec<&str> = rest.split_whitespace().collect();
71 let mut i = 0;
72 while i < tokens.len() {
73 let t = tokens[i];
74 if t == "building" && i + 1 < tokens.len() {
75 building_id = Some(tokens[i + 1].to_string());
76 i += 2;
77 } else if let Some(v) = t.strip_prefix("units_delta=") {
78 units_delta = v.parse().ok();
79 i += 1;
80 } else if let Some(v) = t.strip_prefix("quota_delta=") {
81 quota_delta = Some(v.to_string());
82 i += 1;
83 } else if let Some(v) = t.strip_prefix("quota_basis=") {
84 quota_basis = v.parse().ok();
85 i += 1;
86 } else {
87 i += 1;
88 }
89 }
90
91 let body = json!({
95 "error": "L'immeuble n'est pas conforme à son acte de base",
96 "kind": "building_not_conformant",
97 "details": {
98 "code": "BUILDING_NOT_CONFORMANT",
99 "building_id": building_id.unwrap_or_default(),
100 "units_delta": units_delta.unwrap_or(0),
101 "quota_delta": quota_delta.unwrap_or_else(|| "0".to_string()),
102 "quota_basis": quota_basis.unwrap_or(1000),
103 }
104 });
105 Some(HttpResponse::UnprocessableEntity().json(body))
106}
107
108fn build_acp_response(rest: &str) -> HttpResponse {
112 let mut acp_id: Option<String> = None;
113 let mut units_delta: Option<i32> = None;
114 let mut quota_delta: Option<String> = None;
115 let mut quota_basis: Option<i32> = None;
116
117 let tokens: Vec<&str> = rest.split_whitespace().collect();
118 let mut i = 0;
119 while i < tokens.len() {
120 let t = tokens[i];
121 if t == "acp" && i + 1 < tokens.len() {
122 acp_id = Some(tokens[i + 1].to_string());
123 i += 2;
124 } else if let Some(v) = t.strip_prefix("units_delta=") {
125 units_delta = v.parse().ok();
126 i += 1;
127 } else if let Some(v) = t.strip_prefix("quota_delta=") {
128 quota_delta = Some(v.to_string());
129 i += 1;
130 } else if let Some(v) = t.strip_prefix("quota_basis=") {
131 quota_basis = v.parse().ok();
132 i += 1;
133 } else {
134 i += 1;
135 }
136 }
137
138 let body = json!({
139 "error": "La copropriété n'est pas conforme à son acte de base",
140 "kind": "acp_not_conformant",
141 "details": {
142 "code": "ACP_NOT_CONFORMANT",
143 "acp_id": acp_id.unwrap_or_default(),
144 "units_delta": units_delta.unwrap_or(0),
145 "quota_delta": quota_delta.unwrap_or_else(|| "0".to_string()),
146 "quota_basis": quota_basis.unwrap_or(1000),
147 }
148 });
149 HttpResponse::UnprocessableEntity().json(body)
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
158 fn happy_parses_full_string_to_422_payload() {
159 let err = "BUILDING_NOT_CONFORMANT: building 00000000-0000-0000-0000-000000000001 \
160 units_delta=1 quota_delta=2.5 quota_basis=1000";
161 let resp = try_build_conformity_response(err).expect("matches prefix");
162 assert_eq!(resp.status(), 422);
163 }
164
165 #[test]
167 fn edge_quota_basis_10000() {
168 let err = "BUILDING_NOT_CONFORMANT: building 00000000-0000-0000-0000-000000000002 \
169 units_delta=1 quota_delta=25 quota_basis=10000";
170 let resp = try_build_conformity_response(err).expect("matches");
171 assert_eq!(resp.status(), 422);
172 }
173
174 #[test]
176 fn security_non_conformity_error_returns_none() {
177 let err = "Some other error from somewhere";
178 assert!(try_build_conformity_response(err).is_none());
179 }
180
181 #[test]
183 fn security_close_but_not_prefix_returns_none() {
184 let err = "Almost BUILDING_NOT_CONFORMANT but not";
185 assert!(try_build_conformity_response(err).is_none());
186 }
187
188 #[test]
190 fn negative_partial_payload_still_returns_422() {
191 let err = "BUILDING_NOT_CONFORMANT: building xxx";
192 let resp = try_build_conformity_response(err).expect("matches prefix");
193 assert_eq!(resp.status(), 422);
194 }
195
196 #[test]
198 fn negative_empty_string_returns_none() {
199 assert!(try_build_conformity_response("").is_none());
200 }
201}