1use crate::application::ports::{AccountRepository, ExpenseRepository, JournalEntryRepository};
14use crate::domain::entities::AccountType;
15use rust_decimal::Decimal;
16use serde::Serialize;
17use std::collections::HashMap;
18use std::sync::Arc;
19use uuid::Uuid;
20
21pub struct FinancialReportUseCases {
22 account_repo: Arc<dyn AccountRepository>,
23 #[allow(dead_code)]
24 expense_repo: Arc<dyn ExpenseRepository>,
25 journal_entry_repo: Arc<dyn JournalEntryRepository>,
26}
27
28#[derive(Debug, Serialize)]
29pub struct BalanceSheetReport {
30 pub organization_id: String,
32 pub report_date: String,
34 pub assets: AccountSection,
36 pub liabilities: AccountSection,
38 pub equity: AccountSection,
40 pub total_assets: Decimal,
42 pub total_liabilities: Decimal,
44 pub total_equity: Decimal,
46 pub balance: Decimal,
48}
49
50#[derive(Debug, Serialize)]
51pub struct IncomeStatementReport {
52 pub organization_id: String,
54 pub report_date: String,
56 pub period_start: String,
58 pub period_end: String,
60 pub expenses: AccountSection,
62 pub revenue: AccountSection,
64 pub total_expenses: Decimal,
66 pub total_revenue: Decimal,
68 pub net_result: Decimal,
70}
71
72#[derive(Debug, Serialize)]
73pub struct AccountSection {
74 pub account_type: String,
76 pub accounts: Vec<AccountLine>,
78 pub total: Decimal,
80}
81
82#[derive(Debug, Serialize)]
83pub struct AccountLine {
84 pub code: String,
86 pub label: String,
88 pub amount: Decimal,
90}
91
92impl FinancialReportUseCases {
93 pub fn new(
94 account_repo: Arc<dyn AccountRepository>,
95 expense_repo: Arc<dyn ExpenseRepository>,
96 journal_entry_repo: Arc<dyn JournalEntryRepository>,
97 ) -> Self {
98 Self {
99 account_repo,
100 expense_repo,
101 journal_entry_repo,
102 }
103 }
104
105 pub async fn generate_balance_sheet(
116 &self,
117 organization_id: Uuid,
118 ) -> Result<BalanceSheetReport, String> {
119 let all_accounts = self
121 .account_repo
122 .find_by_organization(organization_id)
123 .await?;
124
125 let account_balances = self.calculate_account_balances(organization_id).await?;
127
128 let mut assets_accounts = Vec::new();
130 let mut liabilities_accounts = Vec::new();
131 let mut expense_accounts = Vec::new();
132 let mut revenue_accounts = Vec::new();
133
134 for account in all_accounts {
135 let amount = account_balances
136 .get(&account.code)
137 .cloned()
138 .unwrap_or(Decimal::ZERO);
139
140 let line = AccountLine {
141 code: account.code.clone(),
142 label: account.label.clone(),
143 amount,
144 };
145
146 match account.account_type {
147 AccountType::Asset => assets_accounts.push(line),
148 AccountType::Liability => liabilities_accounts.push(line),
149 AccountType::Expense => expense_accounts.push(line),
150 AccountType::Revenue => revenue_accounts.push(line),
151 AccountType::OffBalance => {} }
153 }
154
155 let total_assets: Decimal = assets_accounts.iter().map(|a| a.amount).sum();
157 let total_liabilities: Decimal = liabilities_accounts.iter().map(|a| a.amount).sum();
158 let total_expenses: Decimal = expense_accounts.iter().map(|a| a.amount).sum();
159 let total_revenue: Decimal = revenue_accounts.iter().map(|a| a.amount).sum();
160
161 let net_result = total_revenue - total_expenses;
163
164 let equity_accounts = vec![AccountLine {
166 code: "RESULT".to_string(),
167 label: if net_result >= Decimal::ZERO {
168 "Résultat de l'exercice (Bénéfice)".to_string()
169 } else {
170 "Résultat de l'exercice (Perte)".to_string()
171 },
172 amount: net_result,
173 }];
174
175 let total_equity = net_result;
176
177 let balance = total_assets - (total_liabilities + total_equity);
179
180 Ok(BalanceSheetReport {
181 organization_id: organization_id.to_string(),
182 report_date: chrono::Utc::now().to_rfc3339(),
183 assets: AccountSection {
184 account_type: "ASSET".to_string(),
185 accounts: assets_accounts,
186 total: total_assets,
187 },
188 liabilities: AccountSection {
189 account_type: "LIABILITY".to_string(),
190 accounts: liabilities_accounts,
191 total: total_liabilities,
192 },
193 equity: AccountSection {
194 account_type: "EQUITY".to_string(),
195 accounts: equity_accounts,
196 total: total_equity,
197 },
198 total_assets,
199 total_liabilities,
200 total_equity,
201 balance,
202 })
203 }
204
205 pub async fn generate_income_statement(
213 &self,
214 organization_id: Uuid,
215 period_start: chrono::DateTime<chrono::Utc>,
216 period_end: chrono::DateTime<chrono::Utc>,
217 ) -> Result<IncomeStatementReport, String> {
218 let all_accounts = self
220 .account_repo
221 .find_by_organization(organization_id)
222 .await?;
223
224 let expense_amounts = self
226 .calculate_account_balances_for_period(organization_id, period_start, period_end)
227 .await?;
228
229 let mut expense_accounts = Vec::new();
231 let mut revenue_accounts = Vec::new();
232
233 for account in all_accounts {
234 let amount = expense_amounts
235 .get(&account.code)
236 .cloned()
237 .unwrap_or(Decimal::ZERO);
238
239 if amount == Decimal::ZERO {
241 continue;
242 }
243
244 let line = AccountLine {
245 code: account.code.clone(),
246 label: account.label.clone(),
247 amount,
248 };
249
250 match account.account_type {
251 AccountType::Expense => expense_accounts.push(line),
252 AccountType::Revenue => revenue_accounts.push(line),
253 _ => {} }
255 }
256
257 let total_expenses: Decimal = expense_accounts.iter().map(|a| a.amount).sum();
259 let total_revenue: Decimal = revenue_accounts.iter().map(|a| a.amount).sum();
260 let net_result = total_revenue - total_expenses;
261
262 Ok(IncomeStatementReport {
263 organization_id: organization_id.to_string(),
264 report_date: chrono::Utc::now().to_rfc3339(),
265 period_start: period_start.to_rfc3339(),
266 period_end: period_end.to_rfc3339(),
267 expenses: AccountSection {
268 account_type: "EXPENSE".to_string(),
269 accounts: expense_accounts,
270 total: total_expenses,
271 },
272 revenue: AccountSection {
273 account_type: "REVENUE".to_string(),
274 accounts: revenue_accounts,
275 total: total_revenue,
276 },
277 total_expenses,
278 total_revenue,
279 net_result,
280 })
281 }
282
283 async fn calculate_account_balances(
290 &self,
291 organization_id: Uuid,
292 ) -> Result<HashMap<String, Decimal>, String> {
293 self.journal_entry_repo
296 .calculate_account_balances(organization_id)
297 .await
298 }
299
300 async fn calculate_account_balances_for_period(
304 &self,
305 organization_id: Uuid,
306 period_start: chrono::DateTime<chrono::Utc>,
307 period_end: chrono::DateTime<chrono::Utc>,
308 ) -> Result<HashMap<String, Decimal>, String> {
309 self.journal_entry_repo
311 .calculate_account_balances_for_period(organization_id, period_start, period_end)
312 .await
313 }
314
315 pub async fn generate_balance_sheet_for_building(
317 &self,
318 organization_id: Uuid,
319 building_id: Uuid,
320 ) -> Result<BalanceSheetReport, String> {
321 let all_accounts = self
323 .account_repo
324 .find_by_organization(organization_id)
325 .await?;
326
327 let account_balances = self
329 .journal_entry_repo
330 .calculate_account_balances_for_building(organization_id, building_id)
331 .await?;
332
333 let mut asset_accounts = Vec::new();
335 let mut liability_accounts = Vec::new();
336
337 for account in all_accounts {
338 let balance = account_balances
339 .get(&account.code)
340 .cloned()
341 .unwrap_or(Decimal::ZERO);
342 if balance == Decimal::ZERO {
343 continue;
344 }
345
346 let line = AccountLine {
347 code: account.code.clone(),
348 label: account.label.clone(),
349 amount: balance.abs(),
350 };
351
352 match account.account_type {
353 AccountType::Asset => asset_accounts.push(line),
354 AccountType::Liability => liability_accounts.push(line),
355 _ => {}
356 }
357 }
358
359 let total_assets: Decimal = asset_accounts.iter().map(|a| a.amount).sum();
360 let total_liabilities: Decimal = liability_accounts.iter().map(|a| a.amount).sum();
361
362 let total_revenue: Decimal = account_balances
364 .iter()
365 .filter(|(code, _)| code.starts_with('7'))
366 .map(|(_, balance)| *balance)
367 .sum();
368 let total_expenses: Decimal = account_balances
369 .iter()
370 .filter(|(code, _)| code.starts_with('6'))
371 .map(|(_, balance)| *balance)
372 .sum();
373 let net_result = total_revenue - total_expenses;
374
375 let equity_line = AccountLine {
376 code: "RESULT".to_string(),
377 label: if net_result >= Decimal::ZERO {
378 "Résultat de l'exercice (Bénéfice)".to_string()
379 } else {
380 "Résultat de l'exercice (Perte)".to_string()
381 },
382 amount: net_result.abs(),
383 };
384
385 let total_equity = net_result;
386 let balance = total_assets - (total_liabilities + total_equity);
387
388 Ok(BalanceSheetReport {
389 organization_id: organization_id.to_string(),
390 report_date: chrono::Utc::now().to_rfc3339(),
391 assets: AccountSection {
392 account_type: "ASSET".to_string(),
393 accounts: asset_accounts,
394 total: total_assets,
395 },
396 liabilities: AccountSection {
397 account_type: "LIABILITY".to_string(),
398 accounts: liability_accounts,
399 total: total_liabilities,
400 },
401 equity: AccountSection {
402 account_type: "EQUITY".to_string(),
403 accounts: vec![equity_line],
404 total: total_equity,
405 },
406 total_assets,
407 total_liabilities,
408 total_equity,
409 balance,
410 })
411 }
412
413 pub async fn generate_income_statement_for_building(
415 &self,
416 organization_id: Uuid,
417 building_id: Uuid,
418 period_start: chrono::DateTime<chrono::Utc>,
419 period_end: chrono::DateTime<chrono::Utc>,
420 ) -> Result<IncomeStatementReport, String> {
421 let all_accounts = self
423 .account_repo
424 .find_by_organization(organization_id)
425 .await?;
426
427 let account_balances = self
429 .journal_entry_repo
430 .calculate_account_balances_for_building_and_period(
431 organization_id,
432 building_id,
433 period_start,
434 period_end,
435 )
436 .await?;
437
438 let mut expense_accounts = Vec::new();
440 let mut revenue_accounts = Vec::new();
441
442 for account in all_accounts {
443 let amount = account_balances
444 .get(&account.code)
445 .cloned()
446 .unwrap_or(Decimal::ZERO);
447 if amount == Decimal::ZERO {
448 continue;
449 }
450
451 let line = AccountLine {
452 code: account.code.clone(),
453 label: account.label.clone(),
454 amount,
455 };
456
457 match account.account_type {
458 AccountType::Expense => expense_accounts.push(line),
459 AccountType::Revenue => revenue_accounts.push(line),
460 _ => {}
461 }
462 }
463
464 let total_expenses: Decimal = expense_accounts.iter().map(|a| a.amount).sum();
465 let total_revenue: Decimal = revenue_accounts.iter().map(|a| a.amount).sum();
466 let net_result = total_revenue - total_expenses;
467
468 Ok(IncomeStatementReport {
469 organization_id: organization_id.to_string(),
470 report_date: chrono::Utc::now().to_rfc3339(),
471 period_start: period_start.to_rfc3339(),
472 period_end: period_end.to_rfc3339(),
473 expenses: AccountSection {
474 account_type: "EXPENSE".to_string(),
475 accounts: expense_accounts,
476 total: total_expenses,
477 },
478 revenue: AccountSection {
479 account_type: "REVENUE".to_string(),
480 accounts: revenue_accounts,
481 total: total_revenue,
482 },
483 total_expenses,
484 total_revenue,
485 net_result,
486 })
487 }
488}
489
490#[cfg(test)]
491mod tests {
492 use super::*;
493
494 use rust_decimal_macros::dec;
498
499 #[test]
500 fn test_balance_sheet_report_structure() {
501 let report = BalanceSheetReport {
503 organization_id: "test-org".to_string(),
504 report_date: "2024-01-01T00:00:00Z".to_string(),
505 assets: AccountSection {
506 account_type: "ASSET".to_string(),
507 accounts: vec![AccountLine {
508 code: "550".to_string(),
509 label: "Banque".to_string(),
510 amount: dec!(10000),
511 }],
512 total: dec!(10000),
513 },
514 liabilities: AccountSection {
515 account_type: "LIABILITY".to_string(),
516 accounts: vec![AccountLine {
517 code: "4400".to_string(),
518 label: "Fournisseurs".to_string(),
519 amount: dec!(8000),
520 }],
521 total: dec!(8000),
522 },
523 equity: AccountSection {
524 account_type: "EQUITY".to_string(),
525 accounts: vec![AccountLine {
526 code: "RESULT".to_string(),
527 label: "Résultat de l'exercice (Bénéfice)".to_string(),
528 amount: dec!(2000),
529 }],
530 total: dec!(2000),
531 },
532 total_assets: dec!(10000),
533 total_liabilities: dec!(8000),
534 total_equity: dec!(2000),
535 balance: Decimal::ZERO,
536 };
537
538 assert_eq!(report.total_assets, dec!(10000));
539 assert_eq!(report.total_liabilities, dec!(8000));
540 assert_eq!(report.total_equity, dec!(2000));
541 assert_eq!(
543 report.total_assets,
544 report.total_liabilities + report.total_equity
545 );
546 assert_eq!(report.balance, Decimal::ZERO);
547 }
548
549 #[test]
550 fn test_income_statement_report_structure() {
551 let report = IncomeStatementReport {
553 organization_id: "test-org".to_string(),
554 report_date: "2024-01-01T00:00:00Z".to_string(),
555 period_start: "2024-01-01T00:00:00Z".to_string(),
556 period_end: "2024-12-31T23:59:59Z".to_string(),
557 expenses: AccountSection {
558 account_type: "EXPENSE".to_string(),
559 accounts: vec![AccountLine {
560 code: "604001".to_string(),
561 label: "Électricité".to_string(),
562 amount: dec!(5000),
563 }],
564 total: dec!(5000),
565 },
566 revenue: AccountSection {
567 account_type: "REVENUE".to_string(),
568 accounts: vec![AccountLine {
569 code: "700001".to_string(),
570 label: "Appels de fonds".to_string(),
571 amount: dec!(8000),
572 }],
573 total: dec!(8000),
574 },
575 total_expenses: dec!(5000),
576 total_revenue: dec!(8000),
577 net_result: dec!(3000),
578 };
579
580 assert_eq!(report.total_expenses, dec!(5000));
581 assert_eq!(report.total_revenue, dec!(8000));
582 assert_eq!(report.net_result, dec!(3000)); }
584
585 #[test]
586 fn test_income_statement_loss() {
587 let total_expenses = dec!(10000);
589 let total_revenue = dec!(7000);
590 let net_result = total_revenue - total_expenses;
591
592 assert_eq!(net_result, dec!(-3000)); }
594}