koprogo_api/domain/services/
pcn_mapper.rs1use crate::domain::entities::{Expense, ExpenseCategory};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub struct PcnAccount {
15 pub code: String,
17 pub label_nl: String,
19 pub label_fr: String,
21 pub label_de: String,
23 pub label_en: String,
25}
26
27impl PcnAccount {
28 pub fn new(
29 code: impl Into<String>,
30 label_nl: impl Into<String>,
31 label_fr: impl Into<String>,
32 label_de: impl Into<String>,
33 label_en: impl Into<String>,
34 ) -> Self {
35 Self {
36 code: code.into(),
37 label_nl: label_nl.into(),
38 label_fr: label_fr.into(),
39 label_de: label_de.into(),
40 label_en: label_en.into(),
41 }
42 }
43}
44
45pub struct PcnMapper;
47
48#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
50pub struct PcnReportLine {
51 pub account: PcnAccount,
53 pub total_amount: f64,
55 pub entry_count: usize,
57}
58
59impl PcnMapper {
60 pub fn map_expense_to_pcn(category: &ExpenseCategory) -> PcnAccount {
63 match category {
64 ExpenseCategory::Works => PcnAccount::new(
65 "610",
66 "Werken en grote herstellingen",
67 "Travaux et grosses réparations",
68 "Arbeiten und große Reparaturen",
69 "Works and major repairs",
70 ),
71 ExpenseCategory::Maintenance => PcnAccount::new(
72 "611",
73 "Onderhoud en kleine herstellingen",
74 "Entretien et petites réparations",
75 "Wartung und kleine Reparaturen",
76 "Maintenance and minor repairs",
77 ),
78 ExpenseCategory::Repairs => PcnAccount::new(
79 "612",
80 "Gewone herstellingen",
81 "Réparations ordinaires",
82 "Gewöhnliche Reparaturen",
83 "Ordinary repairs",
84 ),
85 ExpenseCategory::Insurance => PcnAccount::new(
86 "613",
87 "Verzekeringen",
88 "Assurances",
89 "Versicherungen",
90 "Insurance",
91 ),
92 ExpenseCategory::Cleaning => PcnAccount::new(
93 "614",
94 "Schoonmaak en lopend onderhoud",
95 "Nettoyage et entretien courant",
96 "Reinigung und laufende Wartung",
97 "Cleaning and routine maintenance",
98 ),
99 ExpenseCategory::Utilities => PcnAccount::new(
100 "615",
101 "Water, energie en verwarming",
102 "Eau, énergie et chauffage",
103 "Wasser, Energie und Heizung",
104 "Water, energy and heating",
105 ),
106 ExpenseCategory::Administration => PcnAccount::new(
107 "620",
108 "Beheer- en administratiekosten",
109 "Frais de gestion et d'administration",
110 "Verwaltungs- und Verwaltungskosten",
111 "Management and administration costs",
112 ),
113 ExpenseCategory::Other => PcnAccount::new(
114 "619",
115 "Overige diverse kosten",
116 "Autres charges diverses",
117 "Sonstige verschiedene Kosten",
118 "Other miscellaneous expenses",
119 ),
120 }
121 }
122
123 pub fn generate_report(expenses: &[Expense]) -> Vec<PcnReportLine> {
127 let mut aggregated: HashMap<String, (PcnAccount, f64, usize)> = HashMap::new();
128
129 for expense in expenses {
131 let account = Self::map_expense_to_pcn(&expense.category);
132 let entry = aggregated
133 .entry(account.code.clone())
134 .or_insert((account, 0.0, 0));
135 entry.1 += expense.amount;
136 entry.2 += 1;
137 }
138
139 let mut report: Vec<PcnReportLine> = aggregated
141 .into_iter()
142 .map(|(_, (account, total_amount, entry_count))| PcnReportLine {
143 account,
144 total_amount,
145 entry_count,
146 })
147 .collect();
148
149 report.sort_by(|a, b| a.account.code.cmp(&b.account.code));
150 report
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn test_map_maintenance_to_pcn_611() {
160 let account = PcnMapper::map_expense_to_pcn(&ExpenseCategory::Maintenance);
161
162 assert_eq!(account.code, "611");
163 assert!(account.label_fr.contains("Entretien"));
164 assert!(!account.label_nl.is_empty());
165 }
166
167 #[test]
168 fn test_map_utilities_to_pcn_615() {
169 let account = PcnMapper::map_expense_to_pcn(&ExpenseCategory::Utilities);
170
171 assert_eq!(account.code, "615");
172 assert!(account.label_fr.contains("Eau") || account.label_fr.contains("Énergie"));
173 }
174
175 #[test]
176 fn test_map_insurance_to_pcn_613() {
177 let account = PcnMapper::map_expense_to_pcn(&ExpenseCategory::Insurance);
178
179 assert_eq!(account.code, "613");
180 assert!(account.label_fr.contains("Assurance"));
181 }
182
183 #[test]
184 fn test_map_repairs_to_pcn_612() {
185 let account = PcnMapper::map_expense_to_pcn(&ExpenseCategory::Repairs);
186
187 assert_eq!(account.code, "612");
188 assert!(account.label_fr.contains("Réparation"));
189 }
190
191 #[test]
192 fn test_map_cleaning_to_pcn_614() {
193 let account = PcnMapper::map_expense_to_pcn(&ExpenseCategory::Cleaning);
194
195 assert_eq!(account.code, "614");
196 assert!(account.label_fr.contains("Nettoyage"));
197 }
198
199 #[test]
200 fn test_map_administration_to_pcn_620() {
201 let account = PcnMapper::map_expense_to_pcn(&ExpenseCategory::Administration);
202
203 assert_eq!(account.code, "620");
204 assert!(
205 account.label_fr.to_lowercase().contains("administration")
206 || account.label_fr.to_lowercase().contains("gestion")
207 );
208 }
209
210 #[test]
211 fn test_map_works_to_pcn_610() {
212 let account = PcnMapper::map_expense_to_pcn(&ExpenseCategory::Works);
213
214 assert_eq!(account.code, "610");
215 assert!(account.label_fr.contains("Travaux"));
216 }
217
218 #[test]
219 fn test_map_other_to_pcn_619() {
220 let account = PcnMapper::map_expense_to_pcn(&ExpenseCategory::Other);
221
222 assert_eq!(account.code, "619");
223 assert!(account.label_fr.contains("Divers") || account.label_fr.contains("Autre"));
224 }
225
226 fn create_test_expense(category: ExpenseCategory, amount: f64) -> Expense {
228 use chrono::Utc;
229 use uuid::Uuid;
230
231 let description = format!("Test expense for {:?}", category);
232 Expense::new(
233 Uuid::new_v4(), Uuid::new_v4(), category,
236 description,
237 amount,
238 Utc::now(),
239 Some("Test Supplier".to_string()),
240 Some("INV-001".to_string()),
241 )
242 .unwrap()
243 }
244
245 #[test]
246 fn test_generate_report_empty_expenses() {
247 let expenses: Vec<Expense> = vec![];
248 let report = PcnMapper::generate_report(&expenses);
249
250 assert!(report.is_empty());
251 }
252
253 #[test]
254 fn test_generate_report_single_category() {
255 let expenses = vec![
256 create_test_expense(ExpenseCategory::Maintenance, 100.0),
257 create_test_expense(ExpenseCategory::Maintenance, 150.0),
258 ];
259
260 let report = PcnMapper::generate_report(&expenses);
261
262 assert_eq!(report.len(), 1);
263 assert_eq!(report[0].account.code, "611");
264 assert_eq!(report[0].total_amount, 250.0);
265 assert_eq!(report[0].entry_count, 2);
266 }
267
268 #[test]
269 fn test_generate_report_multiple_categories() {
270 let expenses = vec![
271 create_test_expense(ExpenseCategory::Maintenance, 100.0),
272 create_test_expense(ExpenseCategory::Utilities, 50.0),
273 create_test_expense(ExpenseCategory::Maintenance, 150.0),
274 create_test_expense(ExpenseCategory::Insurance, 200.0),
275 ];
276
277 let report = PcnMapper::generate_report(&expenses);
278
279 assert_eq!(report.len(), 3);
280
281 let maintenance_line = report.iter().find(|l| l.account.code == "611").unwrap();
283 assert_eq!(maintenance_line.total_amount, 250.0);
284 assert_eq!(maintenance_line.entry_count, 2);
285
286 let utilities_line = report.iter().find(|l| l.account.code == "615").unwrap();
287 assert_eq!(utilities_line.total_amount, 50.0);
288 assert_eq!(utilities_line.entry_count, 1);
289
290 let insurance_line = report.iter().find(|l| l.account.code == "613").unwrap();
291 assert_eq!(insurance_line.total_amount, 200.0);
292 assert_eq!(insurance_line.entry_count, 1);
293 }
294
295 #[test]
296 fn test_generate_report_sorted_by_account_code() {
297 let expenses = vec![
298 create_test_expense(ExpenseCategory::Administration, 100.0), create_test_expense(ExpenseCategory::Works, 200.0), create_test_expense(ExpenseCategory::Utilities, 50.0), ];
302
303 let report = PcnMapper::generate_report(&expenses);
304
305 assert_eq!(report.len(), 3);
306 assert_eq!(report[0].account.code, "610");
308 assert_eq!(report[1].account.code, "615");
309 assert_eq!(report[2].account.code, "620");
310 }
311}