1use crate::application::ports::{AccountRepository, ExpenseRepository, JournalEntryRepository};
14use crate::domain::entities::AccountType;
15use serde::Serialize;
16use std::collections::HashMap;
17use std::sync::Arc;
18use uuid::Uuid;
19
20pub struct FinancialReportUseCases {
21 account_repo: Arc<dyn AccountRepository>,
22 #[allow(dead_code)]
23 expense_repo: Arc<dyn ExpenseRepository>,
24 journal_entry_repo: Arc<dyn JournalEntryRepository>,
25}
26
27#[derive(Debug, Serialize)]
28pub struct BalanceSheetReport {
29 pub organization_id: String,
31 pub report_date: String,
33 pub assets: AccountSection,
35 pub liabilities: AccountSection,
37 pub equity: AccountSection,
39 pub total_assets: f64,
41 pub total_liabilities: f64,
43 pub total_equity: f64,
45 pub balance: f64,
47}
48
49#[derive(Debug, Serialize)]
50pub struct IncomeStatementReport {
51 pub organization_id: String,
53 pub report_date: String,
55 pub period_start: String,
57 pub period_end: String,
59 pub expenses: AccountSection,
61 pub revenue: AccountSection,
63 pub total_expenses: f64,
65 pub total_revenue: f64,
67 pub net_result: f64,
69}
70
71#[derive(Debug, Serialize)]
72pub struct AccountSection {
73 pub account_type: String,
75 pub accounts: Vec<AccountLine>,
77 pub total: f64,
79}
80
81#[derive(Debug, Serialize)]
82pub struct AccountLine {
83 pub code: String,
85 pub label: String,
87 pub amount: f64,
89}
90
91impl FinancialReportUseCases {
92 pub fn new(
93 account_repo: Arc<dyn AccountRepository>,
94 expense_repo: Arc<dyn ExpenseRepository>,
95 journal_entry_repo: Arc<dyn JournalEntryRepository>,
96 ) -> Self {
97 Self {
98 account_repo,
99 expense_repo,
100 journal_entry_repo,
101 }
102 }
103
104 pub async fn generate_balance_sheet(
115 &self,
116 organization_id: Uuid,
117 ) -> Result<BalanceSheetReport, String> {
118 let all_accounts = self
120 .account_repo
121 .find_by_organization(organization_id)
122 .await?;
123
124 let account_balances = self.calculate_account_balances(organization_id).await?;
126
127 let mut assets_accounts = Vec::new();
129 let mut liabilities_accounts = Vec::new();
130 let mut expense_accounts = Vec::new();
131 let mut revenue_accounts = Vec::new();
132
133 for account in all_accounts {
134 let amount = account_balances.get(&account.code).cloned().unwrap_or(0.0);
135
136 let line = AccountLine {
137 code: account.code.clone(),
138 label: account.label.clone(),
139 amount,
140 };
141
142 match account.account_type {
143 AccountType::Asset => assets_accounts.push(line),
144 AccountType::Liability => liabilities_accounts.push(line),
145 AccountType::Expense => expense_accounts.push(line),
146 AccountType::Revenue => revenue_accounts.push(line),
147 AccountType::OffBalance => {} }
149 }
150
151 let total_assets: f64 = assets_accounts.iter().map(|a| a.amount).sum();
153 let total_liabilities: f64 = liabilities_accounts.iter().map(|a| a.amount).sum();
154 let total_expenses: f64 = expense_accounts.iter().map(|a| a.amount).sum();
155 let total_revenue: f64 = revenue_accounts.iter().map(|a| a.amount).sum();
156
157 let net_result = total_revenue - total_expenses;
159
160 let equity_accounts = vec![AccountLine {
162 code: "RESULT".to_string(),
163 label: if net_result >= 0.0 {
164 "Résultat de l'exercice (Bénéfice)".to_string()
165 } else {
166 "Résultat de l'exercice (Perte)".to_string()
167 },
168 amount: net_result,
169 }];
170
171 let total_equity = net_result;
172
173 let balance = total_assets - (total_liabilities + total_equity);
175
176 Ok(BalanceSheetReport {
177 organization_id: organization_id.to_string(),
178 report_date: chrono::Utc::now().to_rfc3339(),
179 assets: AccountSection {
180 account_type: "ASSET".to_string(),
181 accounts: assets_accounts,
182 total: total_assets,
183 },
184 liabilities: AccountSection {
185 account_type: "LIABILITY".to_string(),
186 accounts: liabilities_accounts,
187 total: total_liabilities,
188 },
189 equity: AccountSection {
190 account_type: "EQUITY".to_string(),
191 accounts: equity_accounts,
192 total: total_equity,
193 },
194 total_assets,
195 total_liabilities,
196 total_equity,
197 balance,
198 })
199 }
200
201 pub async fn generate_income_statement(
209 &self,
210 organization_id: Uuid,
211 period_start: chrono::DateTime<chrono::Utc>,
212 period_end: chrono::DateTime<chrono::Utc>,
213 ) -> Result<IncomeStatementReport, String> {
214 let all_accounts = self
216 .account_repo
217 .find_by_organization(organization_id)
218 .await?;
219
220 let expense_amounts = self
222 .calculate_account_balances_for_period(organization_id, period_start, period_end)
223 .await?;
224
225 let mut expense_accounts = Vec::new();
227 let mut revenue_accounts = Vec::new();
228
229 for account in all_accounts {
230 let amount = expense_amounts.get(&account.code).cloned().unwrap_or(0.0);
231
232 if amount == 0.0 {
234 continue;
235 }
236
237 let line = AccountLine {
238 code: account.code.clone(),
239 label: account.label.clone(),
240 amount,
241 };
242
243 match account.account_type {
244 AccountType::Expense => expense_accounts.push(line),
245 AccountType::Revenue => revenue_accounts.push(line),
246 _ => {} }
248 }
249
250 let total_expenses: f64 = expense_accounts.iter().map(|a| a.amount).sum();
252 let total_revenue: f64 = revenue_accounts.iter().map(|a| a.amount).sum();
253 let net_result = total_revenue - total_expenses;
254
255 Ok(IncomeStatementReport {
256 organization_id: organization_id.to_string(),
257 report_date: chrono::Utc::now().to_rfc3339(),
258 period_start: period_start.to_rfc3339(),
259 period_end: period_end.to_rfc3339(),
260 expenses: AccountSection {
261 account_type: "EXPENSE".to_string(),
262 accounts: expense_accounts,
263 total: total_expenses,
264 },
265 revenue: AccountSection {
266 account_type: "REVENUE".to_string(),
267 accounts: revenue_accounts,
268 total: total_revenue,
269 },
270 total_expenses,
271 total_revenue,
272 net_result,
273 })
274 }
275
276 async fn calculate_account_balances(
283 &self,
284 organization_id: Uuid,
285 ) -> Result<HashMap<String, f64>, String> {
286 self.journal_entry_repo
289 .calculate_account_balances(organization_id)
290 .await
291 }
292
293 async fn calculate_account_balances_for_period(
297 &self,
298 organization_id: Uuid,
299 period_start: chrono::DateTime<chrono::Utc>,
300 period_end: chrono::DateTime<chrono::Utc>,
301 ) -> Result<HashMap<String, f64>, String> {
302 self.journal_entry_repo
304 .calculate_account_balances_for_period(organization_id, period_start, period_end)
305 .await
306 }
307
308 pub async fn generate_balance_sheet_for_building(
310 &self,
311 organization_id: Uuid,
312 building_id: Uuid,
313 ) -> Result<BalanceSheetReport, String> {
314 let all_accounts = self
316 .account_repo
317 .find_by_organization(organization_id)
318 .await?;
319
320 let account_balances = self
322 .journal_entry_repo
323 .calculate_account_balances_for_building(organization_id, building_id)
324 .await?;
325
326 let mut asset_accounts = Vec::new();
328 let mut liability_accounts = Vec::new();
329
330 for account in all_accounts {
331 let balance = account_balances.get(&account.code).cloned().unwrap_or(0.0);
332 if balance == 0.0 {
333 continue;
334 }
335
336 let line = AccountLine {
337 code: account.code.clone(),
338 label: account.label.clone(),
339 amount: balance.abs(),
340 };
341
342 match account.account_type {
343 AccountType::Asset => asset_accounts.push(line),
344 AccountType::Liability => liability_accounts.push(line),
345 _ => {}
346 }
347 }
348
349 let total_assets: f64 = asset_accounts.iter().map(|a| a.amount).sum();
350 let total_liabilities: f64 = liability_accounts.iter().map(|a| a.amount).sum();
351
352 let total_revenue: f64 = account_balances
354 .iter()
355 .filter(|(code, _)| code.starts_with('7'))
356 .map(|(_, balance)| *balance)
357 .sum();
358 let total_expenses: f64 = account_balances
359 .iter()
360 .filter(|(code, _)| code.starts_with('6'))
361 .map(|(_, balance)| *balance)
362 .sum();
363 let net_result = total_revenue - total_expenses;
364
365 let equity_line = AccountLine {
366 code: "RESULT".to_string(),
367 label: if net_result >= 0.0 {
368 "Résultat de l'exercice (Bénéfice)".to_string()
369 } else {
370 "Résultat de l'exercice (Perte)".to_string()
371 },
372 amount: net_result.abs(),
373 };
374
375 let total_equity = net_result;
376 let balance = total_assets - (total_liabilities + total_equity);
377
378 Ok(BalanceSheetReport {
379 organization_id: organization_id.to_string(),
380 report_date: chrono::Utc::now().to_rfc3339(),
381 assets: AccountSection {
382 account_type: "ASSET".to_string(),
383 accounts: asset_accounts,
384 total: total_assets,
385 },
386 liabilities: AccountSection {
387 account_type: "LIABILITY".to_string(),
388 accounts: liability_accounts,
389 total: total_liabilities,
390 },
391 equity: AccountSection {
392 account_type: "EQUITY".to_string(),
393 accounts: vec![equity_line],
394 total: total_equity,
395 },
396 total_assets,
397 total_liabilities,
398 total_equity,
399 balance,
400 })
401 }
402
403 pub async fn generate_income_statement_for_building(
405 &self,
406 organization_id: Uuid,
407 building_id: Uuid,
408 period_start: chrono::DateTime<chrono::Utc>,
409 period_end: chrono::DateTime<chrono::Utc>,
410 ) -> Result<IncomeStatementReport, String> {
411 let all_accounts = self
413 .account_repo
414 .find_by_organization(organization_id)
415 .await?;
416
417 let account_balances = self
419 .journal_entry_repo
420 .calculate_account_balances_for_building_and_period(
421 organization_id,
422 building_id,
423 period_start,
424 period_end,
425 )
426 .await?;
427
428 let mut expense_accounts = Vec::new();
430 let mut revenue_accounts = Vec::new();
431
432 for account in all_accounts {
433 let amount = account_balances.get(&account.code).cloned().unwrap_or(0.0);
434 if amount == 0.0 {
435 continue;
436 }
437
438 let line = AccountLine {
439 code: account.code.clone(),
440 label: account.label.clone(),
441 amount,
442 };
443
444 match account.account_type {
445 AccountType::Expense => expense_accounts.push(line),
446 AccountType::Revenue => revenue_accounts.push(line),
447 _ => {}
448 }
449 }
450
451 let total_expenses: f64 = expense_accounts.iter().map(|a| a.amount).sum();
452 let total_revenue: f64 = revenue_accounts.iter().map(|a| a.amount).sum();
453 let net_result = total_revenue - total_expenses;
454
455 Ok(IncomeStatementReport {
456 organization_id: organization_id.to_string(),
457 report_date: chrono::Utc::now().to_rfc3339(),
458 period_start: period_start.to_rfc3339(),
459 period_end: period_end.to_rfc3339(),
460 expenses: AccountSection {
461 account_type: "EXPENSE".to_string(),
462 accounts: expense_accounts,
463 total: total_expenses,
464 },
465 revenue: AccountSection {
466 account_type: "REVENUE".to_string(),
467 accounts: revenue_accounts,
468 total: total_revenue,
469 },
470 total_expenses,
471 total_revenue,
472 net_result,
473 })
474 }
475}
476
477#[cfg(test)]
478mod tests {
479 use super::*;
480
481 #[test]
485 fn test_balance_sheet_report_structure() {
486 let report = BalanceSheetReport {
488 organization_id: "test-org".to_string(),
489 report_date: "2024-01-01T00:00:00Z".to_string(),
490 assets: AccountSection {
491 account_type: "ASSET".to_string(),
492 accounts: vec![AccountLine {
493 code: "550".to_string(),
494 label: "Banque".to_string(),
495 amount: 10000.0,
496 }],
497 total: 10000.0,
498 },
499 liabilities: AccountSection {
500 account_type: "LIABILITY".to_string(),
501 accounts: vec![AccountLine {
502 code: "4400".to_string(),
503 label: "Fournisseurs".to_string(),
504 amount: 8000.0,
505 }],
506 total: 8000.0,
507 },
508 equity: AccountSection {
509 account_type: "EQUITY".to_string(),
510 accounts: vec![AccountLine {
511 code: "RESULT".to_string(),
512 label: "Résultat de l'exercice (Bénéfice)".to_string(),
513 amount: 2000.0,
514 }],
515 total: 2000.0,
516 },
517 total_assets: 10000.0,
518 total_liabilities: 8000.0,
519 total_equity: 2000.0,
520 balance: 0.0,
521 };
522
523 assert_eq!(report.total_assets, 10000.0);
524 assert_eq!(report.total_liabilities, 8000.0);
525 assert_eq!(report.total_equity, 2000.0);
526 assert_eq!(
528 report.total_assets,
529 report.total_liabilities + report.total_equity
530 );
531 assert_eq!(report.balance, 0.0);
532 }
533
534 #[test]
535 fn test_income_statement_report_structure() {
536 let report = IncomeStatementReport {
538 organization_id: "test-org".to_string(),
539 report_date: "2024-01-01T00:00:00Z".to_string(),
540 period_start: "2024-01-01T00:00:00Z".to_string(),
541 period_end: "2024-12-31T23:59:59Z".to_string(),
542 expenses: AccountSection {
543 account_type: "EXPENSE".to_string(),
544 accounts: vec![AccountLine {
545 code: "604001".to_string(),
546 label: "Électricité".to_string(),
547 amount: 5000.0,
548 }],
549 total: 5000.0,
550 },
551 revenue: AccountSection {
552 account_type: "REVENUE".to_string(),
553 accounts: vec![AccountLine {
554 code: "700001".to_string(),
555 label: "Appels de fonds".to_string(),
556 amount: 8000.0,
557 }],
558 total: 8000.0,
559 },
560 total_expenses: 5000.0,
561 total_revenue: 8000.0,
562 net_result: 3000.0,
563 };
564
565 assert_eq!(report.total_expenses, 5000.0);
566 assert_eq!(report.total_revenue, 8000.0);
567 assert_eq!(report.net_result, 3000.0); }
569
570 #[test]
571 fn test_income_statement_loss() {
572 let total_expenses = 10000.0;
574 let total_revenue = 7000.0;
575 let net_result = total_revenue - total_expenses;
576
577 assert_eq!(net_result, -3000.0); }
579}