koprogo_api/infrastructure/web/handlers/
legal_handlers.rs1use actix_web::{get, web, HttpResponse, Responder};
2use serde_json::{json, Value};
3
4const LEGAL_INDEX: &str = include_str!("../../legal_index.json");
6
7#[utoipa::path(
36 get,
37 path = "/api/v1/legal/rules",
38 tag = "Legal Reference",
39 params(
40 ("role" = Option<String>, Query, description = "Filter by role (e.g., syndic, coproprietaire)"),
41 ("category" = Option<String>, Query, description = "Filter by category (e.g., assemblee-generale, travaux)")
42 ),
43 responses(
44 (status = 200, description = "Array of legal rules", body = Vec<serde_json::Value>),
45 (status = 500, description = "Server error")
46 )
47)]
48#[get("/legal/rules")]
49pub async fn list_legal_rules(
50 query: web::Query<std::collections::HashMap<String, String>>,
51) -> impl Responder {
52 match serde_json::from_str::<Value>(LEGAL_INDEX) {
53 Ok(index) => {
54 if let Some(rules) = index.get("rules").and_then(|r| r.as_array()) {
55 let role_filter = query.get("role").map(|r| r.as_str());
56 let category_filter = query.get("category").map(|c| c.as_str());
57
58 let filtered: Vec<&Value> = rules
59 .iter()
60 .filter(|rule| {
61 if let Some(role) = role_filter {
63 if let Some(roles) = rule.get("roles").and_then(|r| r.as_array()) {
64 let matches = roles
65 .iter()
66 .any(|r| r.as_str().map(|s| s == role).unwrap_or(false));
67 if !matches {
68 return false;
69 }
70 } else {
71 return false;
72 }
73 }
74
75 if let Some(category) = category_filter {
77 if let Some(cat) = rule.get("category").and_then(|c| c.as_str()) {
78 if cat != category {
79 return false;
80 }
81 } else {
82 return false;
83 }
84 }
85
86 true
87 })
88 .collect();
89
90 HttpResponse::Ok().json(filtered)
91 } else {
92 HttpResponse::InternalServerError().json(json!({
93 "error": "Malformed legal index: missing 'rules' array"
94 }))
95 }
96 }
97 Err(e) => HttpResponse::InternalServerError().json(json!({
98 "error": format!("Failed to parse legal index: {}", e)
99 })),
100 }
101}
102
103#[utoipa::path(
130 get,
131 path = "/api/v1/legal/rules/{code}",
132 tag = "Legal Reference",
133 params(
134 ("code" = String, Path, description = "Legal rule code (e.g., AG01, T03)")
135 ),
136 responses(
137 (status = 200, description = "Legal rule details", body = serde_json::Value),
138 (status = 404, description = "Rule not found"),
139 (status = 500, description = "Server error")
140 )
141)]
142#[get("/legal/rules/{code}")]
143pub async fn get_legal_rule(code: web::Path<String>) -> impl Responder {
144 let code = code.into_inner();
145
146 match serde_json::from_str::<Value>(LEGAL_INDEX) {
147 Ok(index) => {
148 if let Some(rules) = index.get("rules").and_then(|r| r.as_array()) {
149 if let Some(rule) = rules.iter().find(|r| {
150 r.get("code")
151 .and_then(|c| c.as_str())
152 .map(|c| c == code)
153 .unwrap_or(false)
154 }) {
155 HttpResponse::Ok().json(rule)
156 } else {
157 HttpResponse::NotFound().json(json!({
158 "error": format!("Legal rule not found: {}", code)
159 }))
160 }
161 } else {
162 HttpResponse::InternalServerError().json(json!({
163 "error": "Malformed legal index: missing 'rules' array"
164 }))
165 }
166 }
167 Err(e) => HttpResponse::InternalServerError().json(json!({
168 "error": format!("Failed to parse legal index: {}", e)
169 })),
170 }
171}
172
173#[utoipa::path(
207 get,
208 path = "/api/v1/legal/ag-sequence",
209 tag = "Legal Reference",
210 responses(
211 (status = 200, description = "AG sequence steps", body = Vec<serde_json::Value>),
212 (status = 500, description = "Server error")
213 )
214)]
215#[get("/legal/ag-sequence")]
216pub async fn get_ag_sequence() -> impl Responder {
217 match serde_json::from_str::<Value>(LEGAL_INDEX) {
218 Ok(index) => {
219 if let Some(sequence) = index.get("ag_sequence") {
220 HttpResponse::Ok().json(sequence)
221 } else {
222 HttpResponse::InternalServerError().json(json!({
223 "error": "Malformed legal index: missing 'ag_sequence'"
224 }))
225 }
226 }
227 Err(e) => HttpResponse::InternalServerError().json(json!({
228 "error": format!("Failed to parse legal index: {}", e)
229 })),
230 }
231}
232
233#[utoipa::path(
259 get,
260 path = "/api/v1/legal/majority-for/{decision_type}",
261 tag = "Legal Reference",
262 params(
263 ("decision_type" = String, Path, description = "Decision type (ordinary, qualified_two_thirds, qualified_four_fifths, unanimity, proxy_limit)")
264 ),
265 responses(
266 (status = 200, description = "Majority rules for decision type", body = serde_json::Value),
267 (status = 404, description = "Decision type not found"),
268 (status = 500, description = "Server error")
269 )
270)]
271#[get("/legal/majority-for/{decision_type}")]
272pub async fn get_majority_for(decision_type: web::Path<String>) -> impl Responder {
273 let decision_type = decision_type.into_inner();
274
275 match serde_json::from_str::<Value>(LEGAL_INDEX) {
276 Ok(index) => {
277 if let Some(majorities) = index.get("majority_types").and_then(|m| m.as_array()) {
278 if let Some(majority) = majorities.iter().find(|m| {
279 m.get("decision_type")
280 .and_then(|dt| dt.as_str())
281 .map(|dt| dt == decision_type)
282 .unwrap_or(false)
283 }) {
284 HttpResponse::Ok().json(majority)
285 } else {
286 HttpResponse::NotFound().json(json!({
287 "error": format!("Decision type not found: {}", decision_type),
288 "valid_types": ["ordinary", "qualified_two_thirds", "qualified_four_fifths", "unanimity", "proxy_limit"]
289 }))
290 }
291 } else {
292 HttpResponse::InternalServerError().json(json!({
293 "error": "Malformed legal index: missing 'majority_types'"
294 }))
295 }
296 }
297 Err(e) => HttpResponse::InternalServerError().json(json!({
298 "error": format!("Failed to parse legal index: {}", e)
299 })),
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306
307 #[test]
308 fn test_legal_index_is_valid_json() {
309 let result = serde_json::from_str::<Value>(LEGAL_INDEX);
311 assert!(result.is_ok(), "Legal index JSON is malformed");
312
313 let index = result.unwrap();
314 assert!(
315 index.get("legal_rules").is_some(),
316 "Missing 'legal_rules' field"
317 );
318 assert!(
319 index.get("jurisdiction").is_some(),
320 "Missing 'jurisdiction' field"
321 );
322 assert!(index.get("metadata").is_some(), "Missing 'metadata' field");
323 assert!(index.get("version").is_some(), "Missing 'version' field");
324 }
325
326 #[test]
327 fn test_legal_rules_have_required_fields() {
328 let index: Value = serde_json::from_str(LEGAL_INDEX).unwrap();
329 let rules = index.get("legal_rules").unwrap().as_array().unwrap();
330
331 assert!(!rules.is_empty(), "legal_rules should not be empty");
332
333 for rule in rules {
334 assert!(rule.get("id").is_some(), "Rule missing 'id' field");
335 assert!(rule.get("title").is_some(), "Rule missing 'title' field");
336 assert!(
337 rule.get("reference").is_some(),
338 "Rule missing 'reference' field"
339 );
340 assert!(
341 rule.get("summary").is_some(),
342 "Rule missing 'summary' field"
343 );
344 assert!(
345 rule.get("key_points").is_some(),
346 "Rule missing 'key_points' field"
347 );
348 }
349 }
350
351 #[test]
352 fn test_majority_rules_exist() {
353 let index: Value = serde_json::from_str(LEGAL_INDEX).unwrap();
354 let rules = index.get("legal_rules").unwrap().as_array().unwrap();
355
356 let majority_ids = vec!["art_3_88_1", "art_3_88_2", "art_3_88_3"];
358 for id in majority_ids {
359 let found = rules.iter().any(|r| {
360 r.get("id")
361 .and_then(|i| i.as_str())
362 .map(|i| i == id)
363 .unwrap_or(false)
364 });
365 assert!(found, "Expected majority rule id {} not found", id);
366 }
367 }
368
369 #[test]
370 fn test_ag_related_rules_exist() {
371 let index: Value = serde_json::from_str(LEGAL_INDEX).unwrap();
372 let rules = index.get("legal_rules").unwrap().as_array().unwrap();
373
374 let ag_ids = vec![
376 "art_3_87_3",
377 "art_3_87_5",
378 "bc15_ag_session",
379 "bc17_age_concertation",
380 ];
381 for id in ag_ids {
382 let found = rules.iter().any(|r| {
383 r.get("id")
384 .and_then(|i| i.as_str())
385 .map(|i| i == id)
386 .unwrap_or(false)
387 });
388 assert!(found, "Expected AG rule id {} not found", id);
389 }
390 }
391
392 #[test]
393 fn test_sample_rules_exist() {
394 let index: Value = serde_json::from_str(LEGAL_INDEX).unwrap();
395 let rules = index.get("legal_rules").unwrap().as_array().unwrap();
396
397 let ids = vec![
398 "art_3_84",
399 "art_3_87_3",
400 "ar_12_07_2012",
401 "gdpr_art_15",
402 "quotas_distribution",
403 ];
404 for id in ids {
405 let found = rules.iter().any(|r| {
406 r.get("id")
407 .and_then(|i| i.as_str())
408 .map(|i| i == id)
409 .unwrap_or(false)
410 });
411 assert!(found, "Expected rule id {} not found", id);
412 }
413 }
414}