Skip to main content

koprogo_api/infrastructure/web/handlers/
dashboard_handlers.rs

1// Infrastructure Web Handlers: Dashboard
2//
3// HTTP handlers for dashboard endpoints
4
5use crate::infrastructure::web::{AppState, AuthenticatedUser};
6use actix_web::{get, web, HttpResponse, Responder};
7
8/// Les rôles qui peuvent lire les chiffres financiers d'une organisation.
9///
10/// ── Ce que la route faisait avant ─────────────────────────────────────────
11///
12/// Elle prenait `AuthenticatedUser` et ne le consultait QUE pour lire
13/// `organization_id`. Tout membre de l'organisation — copropriétaire compris —
14/// obtenait donc le total encaissé, le total en attente, et le nombre de
15/// copropriétaires en retard de paiement.
16///
17/// Ce dernier chiffre est le plus sensible : il dit combien de voisins ne
18/// paient pas. Un copropriétaire n'a aucun titre à le connaître, et le lui
19/// servir est un manquement au cloisonnement, pas une commodité.
20///
21/// C'est un des 87 cas de #864 — des routes qui PRENNENT une identité sans
22/// s'en servir pour décider. Elles sont plus trompeuses que les routes nues :
23/// une route sans `AuthenticatedUser` se voit, une garde la compte, personne
24/// ne la croit protégée. Celle-ci avait l'air gardée.
25const ROLES_FINANCIERS: &[&str] = &[
26    "superadmin",
27    "admin",
28    "syndic",
29    "accountant",
30    "accountant.encodeur",
31    "accountant.emetteur",
32];
33
34/// GET /api/v1/dashboard/accountant/stats
35/// Get accountant dashboard statistics
36#[get("/dashboard/accountant/stats")]
37pub async fn get_accountant_stats(
38    state: web::Data<AppState>,
39    user: AuthenticatedUser,
40) -> impl Responder {
41    // Fail-closed : un rôle inconnu n'obtient rien. Autoriser par défaut
42    // ferait de chaque nouveau rôle un lecteur des comptes sans qu'on l'ait
43    // décidé.
44    if !ROLES_FINANCIERS.contains(&user.role.to_lowercase().as_str()) {
45        return HttpResponse::Forbidden().json(serde_json::json!({
46            "error": "Les chiffres financiers de la copropriété sont réservés \
47                      au syndic, au comptable et à l'administration."
48        }));
49    }
50
51    let organization_id = match user.organization_id {
52        Some(org_id) => org_id,
53        None => {
54            return HttpResponse::BadRequest().body("User does not belong to an organization");
55        }
56    };
57
58    match state
59        .dashboard_use_cases
60        .get_accountant_stats(organization_id)
61        .await
62    {
63        Ok(stats) => HttpResponse::Ok().json(stats),
64        Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
65    }
66}
67
68/// GET /api/v1/dashboard/accountant/transactions?limit=10
69/// Get recent transactions for dashboard
70#[get("/dashboard/accountant/transactions")]
71pub async fn get_recent_transactions(
72    state: web::Data<AppState>,
73    user: AuthenticatedUser,
74    query: web::Query<RecentTransactionsQuery>,
75) -> impl Responder {
76    let organization_id = match user.organization_id {
77        Some(org_id) => org_id,
78        None => {
79            return HttpResponse::BadRequest().body("User does not belong to an organization");
80        }
81    };
82
83    let limit = query.limit.unwrap_or(10).min(50); // Max 50 transactions
84
85    match state
86        .dashboard_use_cases
87        .get_recent_transactions(organization_id, limit)
88        .await
89    {
90        Ok(transactions) => HttpResponse::Ok().json(transactions),
91        Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
92    }
93}
94
95#[derive(serde::Deserialize)]
96pub struct RecentTransactionsQuery {
97    pub limit: Option<usize>,
98}