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: f64 = 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 create(&self, _expense: &Expense) -> Result<Expense, String> {
144 unimplemented!()
145 }
146
147 async fn find_by_id(&self, _id: Uuid) -> Result<Option<Expense>, String> {
148 unimplemented!()
149 }
150
151 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Expense>, String> {
152 Ok(self
153 .expenses
154 .iter()
155 .filter(|e| e.building_id == building_id)
156 .cloned()
157 .collect())
158 }
159
160 async fn update(&self, _expense: &Expense) -> Result<Expense, String> {
161 unimplemented!()
162 }
163
164 async fn delete(&self, _id: Uuid) -> Result<bool, String> {
165 unimplemented!()
166 }
167
168 async fn find_all_paginated(
169 &self,
170 _page_request: &PageRequest,
171 _filters: &ExpenseFilters,
172 ) -> Result<(Vec<Expense>, i64), String> {
173 unimplemented!()
174 }
175 }
176
177 fn create_test_expense(
178 organization_id: Uuid,
179 building_id: Uuid,
180 category: ExpenseCategory,
181 amount: f64,
182 ) -> Expense {
183 Expense::new(
184 organization_id,
185 building_id,
186 category,
187 "Test expense".to_string(),
188 amount,
189 Utc::now(),
190 Some("Supplier".to_string()),
191 Some("INV-001".to_string()),
192 )
193 .unwrap()
194 }
195
196 #[tokio::test]
197 async fn test_generate_report_success() {
198 let org_id = Uuid::new_v4();
199 let building_id = Uuid::new_v4();
200 let expenses = vec![
201 create_test_expense(org_id, building_id, ExpenseCategory::Maintenance, 100.0),
202 create_test_expense(org_id, building_id, ExpenseCategory::Maintenance, 150.0),
203 create_test_expense(org_id, building_id, ExpenseCategory::Utilities, 50.0),
204 ];
205
206 let repo = Arc::new(MockExpenseRepository { expenses });
207 let use_cases = PcnUseCases::new(repo);
208
209 let request = PcnReportRequest {
210 building_id,
211 start_date: None,
212 end_date: None,
213 };
214
215 let result = use_cases.generate_report(request).await;
216 assert!(result.is_ok());
217
218 let response = result.unwrap();
219 assert_eq!(response.building_id, building_id);
220 assert_eq!(response.lines.len(), 2); assert_eq!(response.total_amount, 300.0);
222 assert_eq!(response.total_entries, 3);
223
224 let maintenance = response
226 .lines
227 .iter()
228 .find(|l| l.account_code == "611")
229 .unwrap();
230 assert_eq!(maintenance.total_amount, 250.0);
231 assert_eq!(maintenance.entry_count, 2);
232 }
233
234 #[tokio::test]
235 async fn test_generate_report_empty() {
236 let building_id = Uuid::new_v4();
237 let repo = Arc::new(MockExpenseRepository { expenses: vec![] });
238 let use_cases = PcnUseCases::new(repo);
239
240 let request = PcnReportRequest {
241 building_id,
242 start_date: None,
243 end_date: None,
244 };
245
246 let result = use_cases.generate_report(request).await;
247 assert!(result.is_ok());
248
249 let response = result.unwrap();
250 assert_eq!(response.lines.len(), 0);
251 assert_eq!(response.total_amount, 0.0);
252 assert_eq!(response.total_entries, 0);
253 }
254}