koprogo_api/application/use_cases/
pcn_use_cases.rs1use crate::application::dto::{PcnReportLineDto, PcnReportRequest, PcnReportResponse};
2use crate::application::ports::ExpenseRepository;
3use crate::domain::services::{PcnExporter, PcnMapper};
4use chrono::Utc;
5use std::sync::Arc;
6
7pub struct PcnUseCases {
8 expense_repo: Arc<dyn ExpenseRepository>,
9}
10
11impl PcnUseCases {
12 pub fn new(expense_repo: Arc<dyn ExpenseRepository>) -> Self {
13 Self { expense_repo }
14 }
15
16 pub async fn generate_report(
19 &self,
20 request: PcnReportRequest,
21 ) -> Result<PcnReportResponse, String> {
22 let all_expenses = self
24 .expense_repo
25 .find_by_building(request.building_id)
26 .await?;
27
28 let expenses: Vec<_> = all_expenses
30 .into_iter()
31 .filter(|e| {
32 let after_start = request
33 .start_date
34 .map(|start| e.expense_date >= start)
35 .unwrap_or(true);
36 let before_end = request
37 .end_date
38 .map(|end| e.expense_date <= end)
39 .unwrap_or(true);
40 after_start && before_end
41 })
42 .collect();
43
44 let report_lines = PcnMapper::generate_report(&expenses);
46
47 let total_amount: rust_decimal::Decimal = report_lines.iter().map(|l| l.total_amount).sum();
49 let total_entries: usize = report_lines.iter().map(|l| l.entry_count).sum();
50
51 let lines: Vec<PcnReportLineDto> = report_lines
53 .into_iter()
54 .map(PcnReportLineDto::from)
55 .collect();
56
57 Ok(PcnReportResponse {
58 building_id: request.building_id,
59 generated_at: Utc::now(),
60 period_start: request.start_date,
61 period_end: request.end_date,
62 lines,
63 total_amount,
64 total_entries,
65 })
66 }
67
68 pub async fn export_pdf(
70 &self,
71 building_name: &str,
72 request: PcnReportRequest,
73 ) -> Result<Vec<u8>, String> {
74 let report_response = self.generate_report(request).await?;
76
77 let report_lines: Vec<_> = report_response
79 .lines
80 .iter()
81 .map(|dto| crate::domain::services::PcnReportLine {
82 account: crate::domain::services::PcnAccount {
83 code: dto.account_code.clone(),
84 label_nl: dto.account_label_nl.clone(),
85 label_fr: dto.account_label_fr.clone(),
86 label_de: dto.account_label_de.clone(),
87 label_en: dto.account_label_en.clone(),
88 },
89 total_amount: dto.total_amount,
90 entry_count: dto.entry_count,
91 })
92 .collect();
93
94 PcnExporter::export_to_pdf(building_name, &report_lines, report_response.total_amount)
95 }
96
97 pub async fn export_excel(
99 &self,
100 building_name: &str,
101 request: PcnReportRequest,
102 ) -> Result<Vec<u8>, String> {
103 let report_response = self.generate_report(request).await?;
105
106 let report_lines: Vec<_> = report_response
108 .lines
109 .iter()
110 .map(|dto| crate::domain::services::PcnReportLine {
111 account: crate::domain::services::PcnAccount {
112 code: dto.account_code.clone(),
113 label_nl: dto.account_label_nl.clone(),
114 label_fr: dto.account_label_fr.clone(),
115 label_de: dto.account_label_de.clone(),
116 label_en: dto.account_label_en.clone(),
117 },
118 total_amount: dto.total_amount,
119 entry_count: dto.entry_count,
120 })
121 .collect();
122
123 PcnExporter::export_to_excel(building_name, &report_lines, report_response.total_amount)
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use crate::application::dto::{ExpenseFilters, PageRequest};
131 use crate::application::ports::ExpenseRepository;
132 use crate::domain::entities::{Expense, ExpenseCategory};
133 use async_trait::async_trait;
134 use chrono::Utc;
135 use uuid::Uuid;
136
137 struct MockExpenseRepository {
138 expenses: Vec<Expense>,
139 }
140
141 #[async_trait]
142 impl ExpenseRepository for MockExpenseRepository {
143 async fn enregistrer_lignes_de_facture(
144 &self,
145 _expense_id: Uuid,
146 _lignes: &[crate::application::ports::expense_repository::LigneDeFacture],
147 ) -> Result<(), String> {
148 Ok(())
152 }
153
154 async fn create(&self, _expense: &Expense) -> Result<Expense, String> {
155 unimplemented!()
156 }
157
158 async fn find_by_id(&self, _id: Uuid) -> Result<Option<Expense>, String> {
159 unimplemented!()
160 }
161
162 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Expense>, String> {
163 Ok(self
164 .expenses
165 .iter()
166 .filter(|e| e.building_id == building_id)
167 .cloned()
168 .collect())
169 }
170
171 async fn update(&self, _expense: &Expense) -> Result<Expense, String> {
172 unimplemented!()
173 }
174
175 async fn delete(&self, _id: Uuid) -> Result<bool, String> {
176 unimplemented!()
177 }
178
179 async fn find_all_paginated(
180 &self,
181 _page_request: &PageRequest,
182 _filters: &ExpenseFilters,
183 ) -> Result<(Vec<Expense>, i64), String> {
184 unimplemented!()
185 }
186 }
187
188 fn create_test_expense(
189 organization_id: Uuid,
190 building_id: Uuid,
191 category: ExpenseCategory,
192 amount: rust_decimal::Decimal,
193 ) -> Expense {
194 Expense::new(
195 Uuid::new_v4(), organization_id,
197 building_id,
198 category,
199 "Test expense".to_string(),
200 amount,
201 Utc::now(),
202 Some("Supplier".to_string()),
203 Some("INV-001".to_string()),
204 None, )
206 .unwrap()
207 }
208
209 #[tokio::test]
210 async fn test_generate_report_success() {
211 let org_id = Uuid::new_v4();
212 let building_id = Uuid::new_v4();
213 let expenses = vec![
214 create_test_expense(
215 org_id,
216 building_id,
217 ExpenseCategory::Maintenance,
218 rust_decimal_macros::dec!(100),
219 ),
220 create_test_expense(
221 org_id,
222 building_id,
223 ExpenseCategory::Maintenance,
224 rust_decimal_macros::dec!(150),
225 ),
226 create_test_expense(
227 org_id,
228 building_id,
229 ExpenseCategory::Utilities,
230 rust_decimal_macros::dec!(50),
231 ),
232 ];
233
234 let repo = Arc::new(MockExpenseRepository { expenses });
235 let use_cases = PcnUseCases::new(repo);
236
237 let request = PcnReportRequest {
238 building_id,
239 start_date: None,
240 end_date: None,
241 };
242
243 let result = use_cases.generate_report(request).await;
244 assert!(result.is_ok());
245
246 let response = result.unwrap();
247 assert_eq!(response.building_id, building_id);
248 assert_eq!(response.lines.len(), 2); assert_eq!(response.total_amount, rust_decimal_macros::dec!(300));
250 assert_eq!(response.total_entries, 3);
251
252 let maintenance = response
254 .lines
255 .iter()
256 .find(|l| l.account_code == "611")
257 .unwrap();
258 assert_eq!(maintenance.total_amount, rust_decimal_macros::dec!(250));
259 assert_eq!(maintenance.entry_count, 2);
260 }
261
262 #[tokio::test]
263 async fn test_generate_report_empty() {
264 let building_id = Uuid::new_v4();
265 let repo = Arc::new(MockExpenseRepository { expenses: vec![] });
266 let use_cases = PcnUseCases::new(repo);
267
268 let request = PcnReportRequest {
269 building_id,
270 start_date: None,
271 end_date: None,
272 };
273
274 let result = use_cases.generate_report(request).await;
275 assert!(result.is_ok());
276
277 let response = result.unwrap();
278 assert_eq!(response.lines.len(), 0);
279 assert_eq!(response.total_amount, rust_decimal::Decimal::ZERO);
280 assert_eq!(response.total_entries, 0);
281 }
282}