koprogo_api/infrastructure/web/handlers/
financial_report_handlers.rs

1// Web Handlers: Financial Reports
2//
3// CREDITS & ATTRIBUTION:
4// This implementation is inspired by the Noalyss project (https://gitlab.com/noalyss/noalyss)
5// Noalyss is a free accounting software for Belgian and French accounting
6// License: GPL-2.0-or-later (GNU General Public License version 2 or later)
7// Copyright: (C) 1989, 1991 Free Software Foundation, Inc.
8// Copyright: Dany De Bontridder <dany@alchimerys.eu>
9//
10// API endpoints for generating Belgian PCMN financial reports
11
12use crate::infrastructure::audit::{AuditEventType, AuditLogEntry};
13use crate::infrastructure::web::{AppState, AuthenticatedUser};
14use actix_web::{get, web, HttpResponse, Responder};
15use serde::Deserialize;
16
17#[derive(Debug, Deserialize)]
18pub struct IncomeStatementQuery {
19    /// Period start date (ISO 8601 format, e.g., "2024-01-01T00:00:00Z")
20    pub period_start: String,
21    /// Period end date (ISO 8601 format, e.g., "2024-12-31T23:59:59Z")
22    pub period_end: String,
23}
24
25/// Generate balance sheet report for an organization
26///
27/// **Access:** Accountant, SuperAdmin
28///
29/// **Belgian PCMN Balance Sheet:**
30/// - Assets (Classes 2-5): Buildings, receivables, bank, cash
31/// - Liabilities (Class 1): Capital, reserves, provisions, payables
32///
33/// **Example:**
34/// ```
35/// GET /api/v1/reports/balance-sheet
36/// Authorization: Bearer <token>
37/// ```
38#[get("/reports/balance-sheet")]
39pub async fn generate_balance_sheet(
40    state: web::Data<AppState>,
41    user: AuthenticatedUser,
42) -> impl Responder {
43    // Only Accountant and SuperAdmin can generate financial reports
44    if !matches!(user.role.as_str(), "accountant" | "superadmin") {
45        return HttpResponse::Forbidden().json(serde_json::json!({
46            "error": "Only accountants and superadmins can generate financial reports"
47        }));
48    }
49
50    let organization_id = match user.require_organization() {
51        Ok(org_id) => org_id,
52        Err(e) => {
53            return HttpResponse::Unauthorized().json(serde_json::json!({
54                "error": e.to_string()
55            }))
56        }
57    };
58
59    match state
60        .financial_report_use_cases
61        .generate_balance_sheet(organization_id)
62        .await
63    {
64        Ok(report) => {
65            // Audit log: balance sheet generated
66            AuditLogEntry::new(
67                AuditEventType::ReportGenerated,
68                Some(user.user_id),
69                Some(organization_id),
70            )
71            .with_metadata(serde_json::json!({
72                "report_type": "balance_sheet"
73            }))
74            .log();
75
76            HttpResponse::Ok().json(report)
77        }
78        Err(err) => {
79            // Audit log: failed to generate balance sheet
80            AuditLogEntry::new(
81                AuditEventType::ReportGenerated,
82                Some(user.user_id),
83                Some(organization_id),
84            )
85            .with_metadata(serde_json::json!({
86                "report_type": "balance_sheet"
87            }))
88            .with_error(err.clone())
89            .log();
90
91            HttpResponse::InternalServerError().json(serde_json::json!({
92                "error": err
93            }))
94        }
95    }
96}
97
98/// Generate income statement (profit & loss) report for a time period
99///
100/// **Access:** Accountant, SuperAdmin
101///
102/// **Belgian PCMN Income Statement:**
103/// - Expenses (Class 6): Operating costs, maintenance, utilities
104/// - Revenue (Class 7): Regular fees, extraordinary fees, interest income
105///
106/// **Query Parameters:**
107/// - `period_start`: ISO 8601 datetime (e.g., "2024-01-01T00:00:00Z")
108/// - `period_end`: ISO 8601 datetime (e.g., "2024-12-31T23:59:59Z")
109///
110/// **Example:**
111/// ```
112/// GET /api/v1/reports/income-statement?period_start=2024-01-01T00:00:00Z&period_end=2024-12-31T23:59:59Z
113/// Authorization: Bearer <token>
114/// ```
115#[get("/reports/income-statement")]
116pub async fn generate_income_statement(
117    state: web::Data<AppState>,
118    user: AuthenticatedUser,
119    query: web::Query<IncomeStatementQuery>,
120) -> impl Responder {
121    // Only Accountant and SuperAdmin can generate financial reports
122    if !matches!(user.role.as_str(), "accountant" | "superadmin") {
123        return HttpResponse::Forbidden().json(serde_json::json!({
124            "error": "Only accountants and superadmins can generate financial reports"
125        }));
126    }
127
128    let organization_id = match user.require_organization() {
129        Ok(org_id) => org_id,
130        Err(e) => {
131            return HttpResponse::Unauthorized().json(serde_json::json!({
132                "error": e.to_string()
133            }))
134        }
135    };
136
137    // Parse dates
138    let period_start =
139        match chrono::DateTime::parse_from_rfc3339(&query.period_start) {
140            Ok(dt) => dt.with_timezone(&chrono::Utc),
141            Err(_) => return HttpResponse::BadRequest().json(serde_json::json!({
142                "error": "Invalid period_start format. Use ISO 8601 (e.g., 2024-01-01T00:00:00Z)"
143            })),
144        };
145
146    let period_end = match chrono::DateTime::parse_from_rfc3339(&query.period_end) {
147        Ok(dt) => dt.with_timezone(&chrono::Utc),
148        Err(_) => {
149            return HttpResponse::BadRequest().json(serde_json::json!({
150                "error": "Invalid period_end format. Use ISO 8601 (e.g., 2024-12-31T23:59:59Z)"
151            }))
152        }
153    };
154
155    // Validate date range
156    if period_start >= period_end {
157        return HttpResponse::BadRequest().json(serde_json::json!({
158            "error": "period_start must be before period_end"
159        }));
160    }
161
162    match state
163        .financial_report_use_cases
164        .generate_income_statement(organization_id, period_start, period_end)
165        .await
166    {
167        Ok(report) => {
168            // Audit log: income statement generated
169            AuditLogEntry::new(
170                AuditEventType::ReportGenerated,
171                Some(user.user_id),
172                Some(organization_id),
173            )
174            .with_metadata(serde_json::json!({
175                "report_type": "income_statement",
176                "period_start": &query.period_start,
177                "period_end": &query.period_end
178            }))
179            .log();
180
181            HttpResponse::Ok().json(report)
182        }
183        Err(err) => {
184            // Audit log: failed to generate income statement
185            AuditLogEntry::new(
186                AuditEventType::ReportGenerated,
187                Some(user.user_id),
188                Some(organization_id),
189            )
190            .with_metadata(serde_json::json!({
191                "report_type": "income_statement"
192            }))
193            .with_error(err.clone())
194            .log();
195
196            HttpResponse::InternalServerError().json(serde_json::json!({
197                "error": err
198            }))
199        }
200    }
201}