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 serde::Deserialize;
16
17#[derive(Debug, Deserialize)]
18pub struct IncomeStatementQuery {
19 pub period_start: String,
21 pub period_end: String,
23}
24
25#[get("/reports/balance-sheet")]
39pub async fn generate_balance_sheet(
40 state: web::Data<AppState>,
41 user: AuthenticatedUser,
42) -> impl Responder {
43 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 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 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#[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 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 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 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 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 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}