Skip to main content

koprogo_api/infrastructure/web/handlers/
charge_distribution_handlers.rs

1use crate::infrastructure::web::handlers::conformity_response::try_build_conformity_response;
2use crate::infrastructure::web::middleware::scope_guard::verify_expense_org_access;
3use crate::infrastructure::web::{AppState, AuthenticatedUser};
4use actix_web::{get, post, web, HttpResponse, Responder, ResponseError};
5use uuid::Uuid;
6
7/// POST /invoices/{id}/calculate-distribution - Calculate and save charge distribution
8/// Automatically called after invoice approval, or can be triggered manually
9/// Only accountant, syndic, or superadmin can calculate distribution
10#[post("/invoices/{expense_id}/calculate-distribution")]
11pub async fn calculate_and_save_distribution(
12    state: web::Data<AppState>,
13    user: AuthenticatedUser,
14    expense_id: web::Path<Uuid>,
15) -> impl Responder {
16    // Check permissions
17    if user.role != "accountant" && user.role != "syndic" && !user.is_superadmin() {
18        return HttpResponse::Forbidden().json(serde_json::json!({
19            "error": "Only accountant, syndic, or superadmin can calculate charge distributions"
20        }));
21    }
22
23    match state
24        .charge_distribution_use_cases
25        .calculate_and_save_distribution(*expense_id)
26        .await
27    {
28        Ok(distributions) => HttpResponse::Ok().json(serde_json::json!({
29            "message": "Charge distribution calculated successfully",
30            "count": distributions.len(),
31            "distributions": distributions
32        })),
33        Err(err) => {
34            // Track H Story H2 — pre-check validate-before-compute → 422 narratif
35            if let Some(resp) = try_build_conformity_response(&err) {
36                return resp;
37            }
38            HttpResponse::BadRequest().json(serde_json::json!({
39                "error": err
40            }))
41        }
42    }
43}
44
45/// GET /invoices/{id}/distribution - Get charge distribution for an invoice
46#[get("/invoices/{expense_id}/distribution")]
47pub async fn get_distribution_by_expense(
48    state: web::Data<AppState>,
49    user: AuthenticatedUser,
50    expense_id: web::Path<Uuid>,
51) -> impl Responder {
52    // Cloisonnement : la distribution d'une charge dit ce que CHAQUE
53    // copropriétaire doit pour cette dépense, nominativement et au centime.
54    // L'identité était prise puis ignorée — `_user` (#772).
55    if let Err(err) = verify_expense_org_access(
56        &user,
57        *expense_id,
58        &state.expense_use_cases,
59        &state.building_use_cases,
60        &state.acp_use_cases,
61    )
62    .await
63    {
64        return err.error_response();
65    }
66
67    match state
68        .charge_distribution_use_cases
69        .get_distribution_by_expense(*expense_id)
70        .await
71    {
72        Ok(distributions) => HttpResponse::Ok().json(distributions),
73        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
74            "error": err
75        })),
76    }
77}
78
79/// GET /owners/{id}/distributions - Get all charge distributions for an owner
80#[get("/owners/{owner_id}/distributions")]
81pub async fn get_distributions_by_owner(
82    state: web::Data<AppState>,
83    owner_id: web::Path<Uuid>,
84    user: AuthenticatedUser,
85) -> impl Responder {
86    // Route imbriquee non gardee au releve du 2026-09-06 (issue #772) : elle
87    // servait la situation financiere NOMINATIVE d'une personne a quiconque
88    // connaissait son identifiant.
89    if let Err(err) = crate::infrastructure::web::middleware::scope_guard::verify_owner_org_access(
90        &user,
91        *owner_id,
92        &state.owner_use_cases,
93    )
94    .await
95    {
96        return err.error_response();
97    }
98
99    match state
100        .charge_distribution_use_cases
101        .get_distributions_by_owner(*owner_id)
102        .await
103    {
104        Ok(distributions) => HttpResponse::Ok().json(distributions),
105        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
106            "error": err
107        })),
108    }
109}
110
111/// GET /owners/{id}/total-due - Get total amount due for an owner
112#[get("/owners/{owner_id}/total-due")]
113pub async fn get_total_due_by_owner(
114    state: web::Data<AppState>,
115    owner_id: web::Path<Uuid>,
116    user: AuthenticatedUser,
117) -> impl Responder {
118    // Route imbriquee non gardee au releve du 2026-09-06 (issue #772) : elle
119    // servait la situation financiere NOMINATIVE d'une personne a quiconque
120    // connaissait son identifiant.
121    if let Err(err) = crate::infrastructure::web::middleware::scope_guard::verify_owner_org_access(
122        &user,
123        *owner_id,
124        &state.owner_use_cases,
125    )
126    .await
127    {
128        return err.error_response();
129    }
130
131    match state
132        .charge_distribution_use_cases
133        .get_total_due_by_owner(*owner_id)
134        .await
135    {
136        Ok(total_due) => HttpResponse::Ok().json(serde_json::json!({
137            "owner_id": owner_id.to_string(),
138            "total_due": total_due
139        })),
140        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
141            "error": err
142        })),
143    }
144}