koprogo_api/infrastructure/web/handlers/
financial_report_handlers.rs1use crate::infrastructure::audit::{AuditEventType, AuditLogEntry};
13use crate::infrastructure::web::{AppState, AuthenticatedUser};
14use actix_web::{get, web, HttpResponse, Responder};
15use chrono::{DateTime, NaiveDate, Utc};
16use serde::Deserialize;
17
18pub fn parse_date_flexible(input: &str) -> Option<DateTime<Utc>> {
20 if let Ok(dt) = DateTime::parse_from_rfc3339(input) {
22 return Some(dt.with_timezone(&Utc));
23 }
24 if let Ok(nd) = NaiveDate::parse_from_str(input, "%Y-%m-%d") {
26 return nd.and_hms_opt(0, 0, 0).map(|ndt| ndt.and_utc());
27 }
28 None
29}
30
31#[derive(Debug, Deserialize)]
32pub struct IncomeStatementQuery {
33 pub period_start: String,
35 pub period_end: String,
37}
38
39#[get("/reports/balance-sheet")]
53pub async fn generate_balance_sheet(
54 state: web::Data<AppState>,
55 user: AuthenticatedUser,
56) -> impl Responder {
57 if !matches!(user.role.as_str(), "accountant" | "superadmin") {
59 return HttpResponse::Forbidden().json(serde_json::json!({
60 "error": "Only accountants and superadmins can generate financial reports"
61 }));
62 }
63
64 let organization_id = match user.require_organization() {
65 Ok(org_id) => org_id,
66 Err(e) => {
67 return HttpResponse::Unauthorized().json(serde_json::json!({
68 "error": e.to_string()
69 }))
70 }
71 };
72
73 match state
74 .financial_report_use_cases
75 .generate_balance_sheet(organization_id)
76 .await
77 {
78 Ok(report) => {
79 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 .log();
89
90 HttpResponse::Ok().json(report)
91 }
92 Err(err) => {
93 AuditLogEntry::new(
95 AuditEventType::ReportGenerated,
96 Some(user.user_id),
97 Some(organization_id),
98 )
99 .with_metadata(serde_json::json!({
100 "report_type": "balance_sheet"
101 }))
102 .with_error(err.clone())
103 .log();
104
105 HttpResponse::InternalServerError().json(serde_json::json!({
106 "error": err
107 }))
108 }
109 }
110}
111
112#[get("/reports/income-statement")]
130pub async fn generate_income_statement(
131 state: web::Data<AppState>,
132 user: AuthenticatedUser,
133 query: web::Query<IncomeStatementQuery>,
134) -> impl Responder {
135 if !matches!(user.role.as_str(), "accountant" | "superadmin") {
137 return HttpResponse::Forbidden().json(serde_json::json!({
138 "error": "Only accountants and superadmins can generate financial reports"
139 }));
140 }
141
142 let organization_id = match user.require_organization() {
143 Ok(org_id) => org_id,
144 Err(e) => {
145 return HttpResponse::Unauthorized().json(serde_json::json!({
146 "error": e.to_string()
147 }))
148 }
149 };
150
151 let period_start = match parse_date_flexible(&query.period_start) {
153 Some(dt) => dt,
154 None => return HttpResponse::BadRequest().json(serde_json::json!({
155 "error": "Invalid period_start format. Use ISO 8601 (e.g., 2024-01-01 or 2024-01-01T00:00:00Z)"
156 })),
157 };
158
159 let period_end = match parse_date_flexible(&query.period_end) {
160 Some(dt) => dt,
161 None => {
162 return HttpResponse::BadRequest().json(serde_json::json!({
163 "error": "Invalid period_end format. Use ISO 8601 (e.g., 2024-12-31 or 2024-12-31T23:59:59Z)"
164 }))
165 }
166 };
167
168 if period_start >= period_end {
170 return HttpResponse::BadRequest().json(serde_json::json!({
171 "error": "period_start must be before period_end"
172 }));
173 }
174
175 match state
176 .financial_report_use_cases
177 .generate_income_statement(organization_id, period_start, period_end)
178 .await
179 {
180 Ok(report) => {
181 AuditLogEntry::new(
183 AuditEventType::ReportGenerated,
184 Some(user.user_id),
185 Some(organization_id),
186 )
187 .with_metadata(serde_json::json!({
188 "report_type": "income_statement",
189 "period_start": &query.period_start,
190 "period_end": &query.period_end
191 }))
192 .log();
193
194 HttpResponse::Ok().json(report)
195 }
196 Err(err) => {
197 AuditLogEntry::new(
199 AuditEventType::ReportGenerated,
200 Some(user.user_id),
201 Some(organization_id),
202 )
203 .with_metadata(serde_json::json!({
204 "report_type": "income_statement"
205 }))
206 .with_error(err.clone())
207 .log();
208
209 HttpResponse::InternalServerError().json(serde_json::json!({
210 "error": err
211 }))
212 }
213 }
214}