koprogo_api/application/use_cases/
pcn_use_cases.rs

1use 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    /// Generate PCN report for a building
17    /// Aggregates expenses by PCN account and returns structured report
18    pub async fn generate_report(
19        &self,
20        request: PcnReportRequest,
21    ) -> Result<PcnReportResponse, String> {
22        // Fetch expenses for the building
23        let all_expenses = self
24            .expense_repo
25            .find_by_building(request.building_id)
26            .await?;
27
28        // Filter by date range if provided
29        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        // Generate PCN report using domain service
45        let report_lines = PcnMapper::generate_report(&expenses);
46
47        // Calculate totals
48        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        // Convert to DTOs
52        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    /// Export PCN report as PDF bytes
69    pub async fn export_pdf(
70        &self,
71        building_name: &str,
72        request: PcnReportRequest,
73    ) -> Result<Vec<u8>, String> {
74        // Generate report first
75        let report_response = self.generate_report(request).await?;
76
77        // Convert DTOs back to domain entities for export
78        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    /// Export PCN report as Excel bytes
98    pub async fn export_excel(
99        &self,
100        building_name: &str,
101        request: PcnReportRequest,
102    ) -> Result<Vec<u8>, String> {
103        // Generate report first
104        let report_response = self.generate_report(request).await?;
105
106        // Convert DTOs back to domain entities for export
107        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); // Maintenance + Utilities
221        assert_eq!(response.total_amount, 300.0);
222        assert_eq!(response.total_entries, 3);
223
224        // Verify Maintenance account (611)
225        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}