koprogo_api/application/use_cases/
charge_distribution_use_cases.rs

1use crate::application::dto::ChargeDistributionResponseDto;
2use crate::application::ports::{
3    ChargeDistributionRepository, ExpenseRepository, UnitOwnerRepository,
4};
5use crate::domain::entities::{ApprovalStatus, ChargeDistribution};
6use std::sync::Arc;
7use uuid::Uuid;
8
9pub struct ChargeDistributionUseCases {
10    distribution_repository: Arc<dyn ChargeDistributionRepository>,
11    expense_repository: Arc<dyn ExpenseRepository>,
12    unit_owner_repository: Arc<dyn UnitOwnerRepository>,
13}
14
15impl ChargeDistributionUseCases {
16    pub fn new(
17        distribution_repository: Arc<dyn ChargeDistributionRepository>,
18        expense_repository: Arc<dyn ExpenseRepository>,
19        unit_owner_repository: Arc<dyn UnitOwnerRepository>,
20    ) -> Self {
21        Self {
22            distribution_repository,
23            expense_repository,
24            unit_owner_repository,
25        }
26    }
27
28    /// Calculer et sauvegarder la répartition des charges pour une facture approuvée
29    pub async fn calculate_and_save_distribution(
30        &self,
31        expense_id: Uuid,
32    ) -> Result<Vec<ChargeDistributionResponseDto>, String> {
33        // 1. Récupérer la facture
34        let expense = self
35            .expense_repository
36            .find_by_id(expense_id)
37            .await?
38            .ok_or_else(|| "Expense/Invoice not found".to_string())?;
39
40        // 2. Vérifier que la facture est approuvée
41        if expense.approval_status != ApprovalStatus::Approved {
42            return Err(format!(
43                "Cannot calculate distribution for non-approved invoice (status: {:?})",
44                expense.approval_status
45            ));
46        }
47
48        // 3. Récupérer le montant TTC à répartir
49        let total_amount = expense.amount_incl_vat.unwrap_or(expense.amount);
50
51        // 4. Récupérer toutes les relations unit-owner actives pour ce bâtiment
52        let unit_ownerships = self
53            .unit_owner_repository
54            .find_active_by_building(expense.building_id)
55            .await?;
56
57        if unit_ownerships.is_empty() {
58            return Err("No active unit-owner relationships found for this building".to_string());
59        }
60
61        // 5. Calculer les distributions
62        let distributions =
63            ChargeDistribution::calculate_distributions(expense_id, total_amount, unit_ownerships)?;
64
65        // 6. Sauvegarder en masse
66        let saved_distributions = self
67            .distribution_repository
68            .create_bulk(&distributions)
69            .await?;
70
71        // 7. Convertir en DTOs
72        Ok(saved_distributions
73            .iter()
74            .map(|d| self.to_response_dto(d))
75            .collect())
76    }
77
78    /// Récupérer la répartition d'une facture
79    pub async fn get_distribution_by_expense(
80        &self,
81        expense_id: Uuid,
82    ) -> Result<Vec<ChargeDistributionResponseDto>, String> {
83        let distributions = self
84            .distribution_repository
85            .find_by_expense(expense_id)
86            .await?;
87        Ok(distributions
88            .iter()
89            .map(|d| self.to_response_dto(d))
90            .collect())
91    }
92
93    /// Récupérer toutes les distributions pour un propriétaire
94    pub async fn get_distributions_by_owner(
95        &self,
96        owner_id: Uuid,
97    ) -> Result<Vec<ChargeDistributionResponseDto>, String> {
98        let distributions = self.distribution_repository.find_by_owner(owner_id).await?;
99        Ok(distributions
100            .iter()
101            .map(|d| self.to_response_dto(d))
102            .collect())
103    }
104
105    /// Récupérer le montant total dû par un propriétaire
106    pub async fn get_total_due_by_owner(&self, owner_id: Uuid) -> Result<f64, String> {
107        self.distribution_repository
108            .get_total_due_by_owner(owner_id)
109            .await
110    }
111
112    /// Supprimer les distributions d'une facture (si annulée)
113    pub async fn delete_distribution_by_expense(&self, expense_id: Uuid) -> Result<(), String> {
114        self.distribution_repository
115            .delete_by_expense(expense_id)
116            .await
117    }
118
119    fn to_response_dto(&self, distribution: &ChargeDistribution) -> ChargeDistributionResponseDto {
120        ChargeDistributionResponseDto {
121            id: distribution.id.to_string(),
122            expense_id: distribution.expense_id.to_string(),
123            unit_id: distribution.unit_id.to_string(),
124            owner_id: distribution.owner_id.to_string(),
125            quota_percentage: distribution.quota_percentage,
126            amount_due: distribution.amount_due,
127            created_at: distribution.created_at.to_rfc3339(),
128        }
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use crate::application::dto::{ExpenseFilters, PageRequest};
136    use crate::application::ports::{
137        ChargeDistributionRepository, ExpenseRepository, UnitOwnerRepository,
138    };
139    use crate::domain::entities::{
140        ApprovalStatus, ChargeDistribution, Expense, ExpenseCategory, PaymentStatus, UnitOwner,
141    };
142    use async_trait::async_trait;
143    use chrono::Utc;
144    use std::collections::HashMap;
145    use std::sync::Mutex;
146
147    // ========== Mock ChargeDistributionRepository ==========
148
149    struct MockChargeDistributionRepository {
150        distributions: Mutex<HashMap<Uuid, ChargeDistribution>>,
151    }
152
153    impl MockChargeDistributionRepository {
154        fn new() -> Self {
155            Self {
156                distributions: Mutex::new(HashMap::new()),
157            }
158        }
159    }
160
161    #[async_trait]
162    impl ChargeDistributionRepository for MockChargeDistributionRepository {
163        async fn create(
164            &self,
165            distribution: &ChargeDistribution,
166        ) -> Result<ChargeDistribution, String> {
167            let mut distributions = self.distributions.lock().unwrap();
168            distributions.insert(distribution.id, distribution.clone());
169            Ok(distribution.clone())
170        }
171
172        async fn create_bulk(
173            &self,
174            distributions: &[ChargeDistribution],
175        ) -> Result<Vec<ChargeDistribution>, String> {
176            let mut store = self.distributions.lock().unwrap();
177            let mut result = Vec::new();
178            for d in distributions {
179                store.insert(d.id, d.clone());
180                result.push(d.clone());
181            }
182            Ok(result)
183        }
184
185        async fn find_by_id(&self, id: Uuid) -> Result<Option<ChargeDistribution>, String> {
186            let distributions = self.distributions.lock().unwrap();
187            Ok(distributions.get(&id).cloned())
188        }
189
190        async fn find_by_expense(
191            &self,
192            expense_id: Uuid,
193        ) -> Result<Vec<ChargeDistribution>, String> {
194            let distributions = self.distributions.lock().unwrap();
195            Ok(distributions
196                .values()
197                .filter(|d| d.expense_id == expense_id)
198                .cloned()
199                .collect())
200        }
201
202        async fn find_by_unit(&self, unit_id: Uuid) -> Result<Vec<ChargeDistribution>, String> {
203            let distributions = self.distributions.lock().unwrap();
204            Ok(distributions
205                .values()
206                .filter(|d| d.unit_id == unit_id)
207                .cloned()
208                .collect())
209        }
210
211        async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<ChargeDistribution>, String> {
212            let distributions = self.distributions.lock().unwrap();
213            Ok(distributions
214                .values()
215                .filter(|d| d.owner_id == owner_id)
216                .cloned()
217                .collect())
218        }
219
220        async fn delete_by_expense(&self, expense_id: Uuid) -> Result<(), String> {
221            let mut distributions = self.distributions.lock().unwrap();
222            distributions.retain(|_, d| d.expense_id != expense_id);
223            Ok(())
224        }
225
226        async fn get_total_due_by_owner(&self, owner_id: Uuid) -> Result<f64, String> {
227            let distributions = self.distributions.lock().unwrap();
228            let total = distributions
229                .values()
230                .filter(|d| d.owner_id == owner_id)
231                .map(|d| d.amount_due)
232                .sum();
233            Ok(total)
234        }
235    }
236
237    // ========== Mock ExpenseRepository ==========
238
239    struct MockExpenseRepository {
240        expenses: Mutex<HashMap<Uuid, Expense>>,
241    }
242
243    impl MockExpenseRepository {
244        fn new() -> Self {
245            Self {
246                expenses: Mutex::new(HashMap::new()),
247            }
248        }
249
250        fn with_expense(expense: Expense) -> Self {
251            let mut map = HashMap::new();
252            map.insert(expense.id, expense);
253            Self {
254                expenses: Mutex::new(map),
255            }
256        }
257    }
258
259    #[async_trait]
260    impl ExpenseRepository for MockExpenseRepository {
261        async fn create(&self, expense: &Expense) -> Result<Expense, String> {
262            let mut expenses = self.expenses.lock().unwrap();
263            expenses.insert(expense.id, expense.clone());
264            Ok(expense.clone())
265        }
266
267        async fn find_by_id(&self, id: Uuid) -> Result<Option<Expense>, String> {
268            let expenses = self.expenses.lock().unwrap();
269            Ok(expenses.get(&id).cloned())
270        }
271
272        async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Expense>, String> {
273            let expenses = self.expenses.lock().unwrap();
274            Ok(expenses
275                .values()
276                .filter(|e| e.building_id == building_id)
277                .cloned()
278                .collect())
279        }
280
281        async fn find_all_paginated(
282            &self,
283            _page_request: &PageRequest,
284            _filters: &ExpenseFilters,
285        ) -> Result<(Vec<Expense>, i64), String> {
286            let expenses = self.expenses.lock().unwrap();
287            let all: Vec<Expense> = expenses.values().cloned().collect();
288            let count = all.len() as i64;
289            Ok((all, count))
290        }
291
292        async fn update(&self, expense: &Expense) -> Result<Expense, String> {
293            let mut expenses = self.expenses.lock().unwrap();
294            expenses.insert(expense.id, expense.clone());
295            Ok(expense.clone())
296        }
297
298        async fn delete(&self, id: Uuid) -> Result<bool, String> {
299            let mut expenses = self.expenses.lock().unwrap();
300            Ok(expenses.remove(&id).is_some())
301        }
302    }
303
304    // ========== Mock UnitOwnerRepository ==========
305
306    /// (unit_id, owner_id, percentage) tuples grouped by building_id
307    type BuildingOwnerships = HashMap<Uuid, Vec<(Uuid, Uuid, f64)>>;
308
309    struct MockUnitOwnerRepository {
310        /// Stores active building ownerships: building_id -> Vec<(unit_id, owner_id, percentage)>
311        building_ownerships: Mutex<BuildingOwnerships>,
312    }
313
314    impl MockUnitOwnerRepository {
315        fn new() -> Self {
316            Self {
317                building_ownerships: Mutex::new(HashMap::new()),
318            }
319        }
320
321        fn with_building_ownerships(building_id: Uuid, ownerships: Vec<(Uuid, Uuid, f64)>) -> Self {
322            let mut map = HashMap::new();
323            map.insert(building_id, ownerships);
324            Self {
325                building_ownerships: Mutex::new(map),
326            }
327        }
328    }
329
330    #[async_trait]
331    impl UnitOwnerRepository for MockUnitOwnerRepository {
332        async fn create(&self, _unit_owner: &UnitOwner) -> Result<UnitOwner, String> {
333            unimplemented!("not needed for charge distribution tests")
334        }
335
336        async fn find_by_id(&self, _id: Uuid) -> Result<Option<UnitOwner>, String> {
337            unimplemented!("not needed for charge distribution tests")
338        }
339
340        async fn find_current_owners_by_unit(
341            &self,
342            _unit_id: Uuid,
343        ) -> Result<Vec<UnitOwner>, String> {
344            unimplemented!("not needed for charge distribution tests")
345        }
346
347        async fn find_current_units_by_owner(
348            &self,
349            _owner_id: Uuid,
350        ) -> Result<Vec<UnitOwner>, String> {
351            unimplemented!("not needed for charge distribution tests")
352        }
353
354        async fn find_all_owners_by_unit(&self, _unit_id: Uuid) -> Result<Vec<UnitOwner>, String> {
355            unimplemented!("not needed for charge distribution tests")
356        }
357
358        async fn find_all_units_by_owner(&self, _owner_id: Uuid) -> Result<Vec<UnitOwner>, String> {
359            unimplemented!("not needed for charge distribution tests")
360        }
361
362        async fn update(&self, _unit_owner: &UnitOwner) -> Result<UnitOwner, String> {
363            unimplemented!("not needed for charge distribution tests")
364        }
365
366        async fn delete(&self, _id: Uuid) -> Result<(), String> {
367            unimplemented!("not needed for charge distribution tests")
368        }
369
370        async fn has_active_owners(&self, _unit_id: Uuid) -> Result<bool, String> {
371            unimplemented!("not needed for charge distribution tests")
372        }
373
374        async fn get_total_ownership_percentage(&self, _unit_id: Uuid) -> Result<f64, String> {
375            unimplemented!("not needed for charge distribution tests")
376        }
377
378        async fn find_active_by_unit_and_owner(
379            &self,
380            _unit_id: Uuid,
381            _owner_id: Uuid,
382        ) -> Result<Option<UnitOwner>, String> {
383            unimplemented!("not needed for charge distribution tests")
384        }
385
386        async fn find_active_by_building(
387            &self,
388            building_id: Uuid,
389        ) -> Result<Vec<(Uuid, Uuid, f64)>, String> {
390            let ownerships = self.building_ownerships.lock().unwrap();
391            Ok(ownerships.get(&building_id).cloned().unwrap_or_default())
392        }
393    }
394
395    // ========== Helpers ==========
396
397    fn make_approved_expense(building_id: Uuid, amount_incl_vat: f64) -> Expense {
398        let now = Utc::now();
399        Expense {
400            id: Uuid::new_v4(),
401            organization_id: Uuid::new_v4(),
402            building_id,
403            category: ExpenseCategory::Maintenance,
404            description: "Elevator maintenance".to_string(),
405            amount: amount_incl_vat,
406            amount_excl_vat: Some(amount_incl_vat / 1.21),
407            vat_rate: Some(21.0),
408            vat_amount: Some(amount_incl_vat - amount_incl_vat / 1.21),
409            amount_incl_vat: Some(amount_incl_vat),
410            expense_date: now,
411            invoice_date: Some(now),
412            due_date: None,
413            paid_date: None,
414            approval_status: ApprovalStatus::Approved,
415            submitted_at: Some(now),
416            approved_by: Some(Uuid::new_v4()),
417            approved_at: Some(now),
418            rejection_reason: None,
419            payment_status: PaymentStatus::Pending,
420            supplier: Some("Schindler SA".to_string()),
421            invoice_number: Some("INV-001".to_string()),
422            account_code: Some("611002".to_string()),
423            contractor_report_id: None,
424            created_at: now,
425            updated_at: now,
426        }
427    }
428
429    fn make_draft_expense(building_id: Uuid) -> Expense {
430        let now = Utc::now();
431        Expense {
432            id: Uuid::new_v4(),
433            organization_id: Uuid::new_v4(),
434            building_id,
435            category: ExpenseCategory::Maintenance,
436            description: "Draft expense".to_string(),
437            amount: 1000.0,
438            amount_excl_vat: None,
439            vat_rate: None,
440            vat_amount: None,
441            amount_incl_vat: None,
442            expense_date: now,
443            invoice_date: None,
444            due_date: None,
445            paid_date: None,
446            approval_status: ApprovalStatus::Draft,
447            submitted_at: None,
448            approved_by: None,
449            approved_at: None,
450            rejection_reason: None,
451            payment_status: PaymentStatus::Pending,
452            supplier: None,
453            invoice_number: None,
454            account_code: None,
455            contractor_report_id: None,
456            created_at: now,
457            updated_at: now,
458        }
459    }
460
461    fn make_use_cases(
462        dist_repo: MockChargeDistributionRepository,
463        expense_repo: MockExpenseRepository,
464        unit_owner_repo: MockUnitOwnerRepository,
465    ) -> ChargeDistributionUseCases {
466        ChargeDistributionUseCases::new(
467            Arc::new(dist_repo),
468            Arc::new(expense_repo),
469            Arc::new(unit_owner_repo),
470        )
471    }
472
473    // ========== Tests ==========
474
475    #[tokio::test]
476    async fn test_calculate_and_save_distribution_success() {
477        let building_id = Uuid::new_v4();
478        let unit1_id = Uuid::new_v4();
479        let unit2_id = Uuid::new_v4();
480        let owner1_id = Uuid::new_v4();
481        let owner2_id = Uuid::new_v4();
482
483        let expense = make_approved_expense(building_id, 1000.0);
484        let expense_id = expense.id;
485
486        let ownerships = vec![
487            (unit1_id, owner1_id, 0.60), // 60%
488            (unit2_id, owner2_id, 0.40), // 40%
489        ];
490
491        let dist_repo = MockChargeDistributionRepository::new();
492        let expense_repo = MockExpenseRepository::with_expense(expense);
493        let unit_owner_repo =
494            MockUnitOwnerRepository::with_building_ownerships(building_id, ownerships);
495
496        let uc = make_use_cases(dist_repo, expense_repo, unit_owner_repo);
497
498        let result = uc.calculate_and_save_distribution(expense_id).await;
499        assert!(result.is_ok());
500
501        let distributions = result.unwrap();
502        assert_eq!(distributions.len(), 2);
503
504        // Verify amounts: 60% of 1000 = 600, 40% of 1000 = 400
505        let total_amount: f64 = distributions.iter().map(|d| d.amount_due).sum();
506        assert!((total_amount - 1000.0).abs() < 0.01);
507
508        // Verify all distributions reference the correct expense
509        assert!(distributions
510            .iter()
511            .all(|d| d.expense_id == expense_id.to_string()));
512    }
513
514    #[tokio::test]
515    async fn test_calculate_and_save_distribution_non_approved_expense() {
516        let building_id = Uuid::new_v4();
517        let expense = make_draft_expense(building_id);
518        let expense_id = expense.id;
519
520        let dist_repo = MockChargeDistributionRepository::new();
521        let expense_repo = MockExpenseRepository::with_expense(expense);
522        let unit_owner_repo = MockUnitOwnerRepository::new();
523
524        let uc = make_use_cases(dist_repo, expense_repo, unit_owner_repo);
525
526        let result = uc.calculate_and_save_distribution(expense_id).await;
527        assert!(result.is_err());
528        let err = result.unwrap_err();
529        assert!(err.contains("non-approved invoice"));
530    }
531
532    #[tokio::test]
533    async fn test_calculate_and_save_distribution_expense_not_found() {
534        let dist_repo = MockChargeDistributionRepository::new();
535        let expense_repo = MockExpenseRepository::new(); // empty
536        let unit_owner_repo = MockUnitOwnerRepository::new();
537
538        let uc = make_use_cases(dist_repo, expense_repo, unit_owner_repo);
539
540        let result = uc.calculate_and_save_distribution(Uuid::new_v4()).await;
541        assert!(result.is_err());
542        assert_eq!(result.unwrap_err(), "Expense/Invoice not found");
543    }
544
545    #[tokio::test]
546    async fn test_calculate_and_save_distribution_no_active_owners() {
547        let building_id = Uuid::new_v4();
548        let expense = make_approved_expense(building_id, 1000.0);
549        let expense_id = expense.id;
550
551        let dist_repo = MockChargeDistributionRepository::new();
552        let expense_repo = MockExpenseRepository::with_expense(expense);
553        // Empty ownerships for the building
554        let unit_owner_repo =
555            MockUnitOwnerRepository::with_building_ownerships(building_id, vec![]);
556
557        let uc = make_use_cases(dist_repo, expense_repo, unit_owner_repo);
558
559        let result = uc.calculate_and_save_distribution(expense_id).await;
560        assert!(result.is_err());
561        assert_eq!(
562            result.unwrap_err(),
563            "No active unit-owner relationships found for this building"
564        );
565    }
566
567    #[tokio::test]
568    async fn test_get_distribution_by_expense() {
569        let building_id = Uuid::new_v4();
570        let unit1_id = Uuid::new_v4();
571        let owner1_id = Uuid::new_v4();
572
573        let expense = make_approved_expense(building_id, 500.0);
574        let expense_id = expense.id;
575
576        let ownerships = vec![(unit1_id, owner1_id, 1.0)]; // 100%
577
578        let dist_repo = MockChargeDistributionRepository::new();
579        let expense_repo = MockExpenseRepository::with_expense(expense);
580        let unit_owner_repo =
581            MockUnitOwnerRepository::with_building_ownerships(building_id, ownerships);
582
583        let uc = make_use_cases(dist_repo, expense_repo, unit_owner_repo);
584
585        // First calculate and save
586        uc.calculate_and_save_distribution(expense_id)
587            .await
588            .unwrap();
589
590        // Then retrieve by expense
591        let result = uc.get_distribution_by_expense(expense_id).await;
592        assert!(result.is_ok());
593        let distributions = result.unwrap();
594        assert_eq!(distributions.len(), 1);
595        assert_eq!(distributions[0].expense_id, expense_id.to_string());
596        assert_eq!(distributions[0].quota_percentage, 1.0);
597        assert!((distributions[0].amount_due - 500.0).abs() < 0.01);
598    }
599
600    #[tokio::test]
601    async fn test_get_distribution_by_expense_empty() {
602        let dist_repo = MockChargeDistributionRepository::new();
603        let expense_repo = MockExpenseRepository::new();
604        let unit_owner_repo = MockUnitOwnerRepository::new();
605
606        let uc = make_use_cases(dist_repo, expense_repo, unit_owner_repo);
607
608        let result = uc.get_distribution_by_expense(Uuid::new_v4()).await;
609        assert!(result.is_ok());
610        assert!(result.unwrap().is_empty());
611    }
612
613    #[tokio::test]
614    async fn test_get_total_due_by_owner() {
615        let building_id = Uuid::new_v4();
616        let unit1_id = Uuid::new_v4();
617        let unit2_id = Uuid::new_v4();
618        let owner1_id = Uuid::new_v4();
619        let owner2_id = Uuid::new_v4();
620
621        // Create 2 approved expenses
622        let expense1 = make_approved_expense(building_id, 1000.0);
623        let expense1_id = expense1.id;
624        let expense2 = make_approved_expense(building_id, 2000.0);
625        let expense2_id = expense2.id;
626
627        let ownerships = vec![
628            (unit1_id, owner1_id, 0.60), // 60%
629            (unit2_id, owner2_id, 0.40), // 40%
630        ];
631
632        let dist_repo = MockChargeDistributionRepository::new();
633        let mut expense_map = HashMap::new();
634        expense_map.insert(expense1.id, expense1);
635        expense_map.insert(expense2.id, expense2);
636        let expense_repo = MockExpenseRepository {
637            expenses: Mutex::new(expense_map),
638        };
639        let unit_owner_repo =
640            MockUnitOwnerRepository::with_building_ownerships(building_id, ownerships);
641
642        let uc = make_use_cases(dist_repo, expense_repo, unit_owner_repo);
643
644        // Calculate distributions for both expenses
645        uc.calculate_and_save_distribution(expense1_id)
646            .await
647            .unwrap();
648        uc.calculate_and_save_distribution(expense2_id)
649            .await
650            .unwrap();
651
652        // Owner 1 owes 60% of 1000 + 60% of 2000 = 600 + 1200 = 1800
653        let total = uc.get_total_due_by_owner(owner1_id).await.unwrap();
654        assert!((total - 1800.0).abs() < 0.01);
655
656        // Owner 2 owes 40% of 1000 + 40% of 2000 = 400 + 800 = 1200
657        let total = uc.get_total_due_by_owner(owner2_id).await.unwrap();
658        assert!((total - 1200.0).abs() < 0.01);
659    }
660
661    #[tokio::test]
662    async fn test_get_total_due_by_owner_no_distributions() {
663        let dist_repo = MockChargeDistributionRepository::new();
664        let expense_repo = MockExpenseRepository::new();
665        let unit_owner_repo = MockUnitOwnerRepository::new();
666
667        let uc = make_use_cases(dist_repo, expense_repo, unit_owner_repo);
668
669        let total = uc.get_total_due_by_owner(Uuid::new_v4()).await.unwrap();
670        assert_eq!(total, 0.0);
671    }
672
673    #[tokio::test]
674    async fn test_calculate_distribution_uses_amount_incl_vat() {
675        let building_id = Uuid::new_v4();
676        let unit_id = Uuid::new_v4();
677        let owner_id = Uuid::new_v4();
678
679        // Expense with amount_incl_vat = 1210, amount = 1000 (backward compat)
680        let now = Utc::now();
681        let expense = Expense {
682            id: Uuid::new_v4(),
683            organization_id: Uuid::new_v4(),
684            building_id,
685            category: ExpenseCategory::Utilities,
686            description: "Electricity".to_string(),
687            amount: 1000.0,
688            amount_excl_vat: Some(1000.0),
689            vat_rate: Some(21.0),
690            vat_amount: Some(210.0),
691            amount_incl_vat: Some(1210.0),
692            expense_date: now,
693            invoice_date: Some(now),
694            due_date: None,
695            paid_date: None,
696            approval_status: ApprovalStatus::Approved,
697            submitted_at: Some(now),
698            approved_by: Some(Uuid::new_v4()),
699            approved_at: Some(now),
700            rejection_reason: None,
701            payment_status: PaymentStatus::Pending,
702            supplier: None,
703            invoice_number: None,
704            account_code: None,
705            contractor_report_id: None,
706            created_at: now,
707            updated_at: now,
708        };
709        let expense_id = expense.id;
710
711        let ownerships = vec![(unit_id, owner_id, 1.0)]; // 100%
712
713        let dist_repo = MockChargeDistributionRepository::new();
714        let expense_repo = MockExpenseRepository::with_expense(expense);
715        let unit_owner_repo =
716            MockUnitOwnerRepository::with_building_ownerships(building_id, ownerships);
717
718        let uc = make_use_cases(dist_repo, expense_repo, unit_owner_repo);
719
720        let result = uc
721            .calculate_and_save_distribution(expense_id)
722            .await
723            .unwrap();
724
725        // Should use amount_incl_vat (1210), not amount (1000)
726        assert_eq!(result.len(), 1);
727        assert!((result[0].amount_due - 1210.0).abs() < 0.01);
728    }
729}