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