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    struct MockUnitOwnerRepository {
307        /// Stores active building ownerships: building_id -> Vec<(unit_id, owner_id, percentage)>
308        building_ownerships: Mutex<HashMap<Uuid, Vec<(Uuid, Uuid, f64)>>>,
309    }
310
311    impl MockUnitOwnerRepository {
312        fn new() -> Self {
313            Self {
314                building_ownerships: Mutex::new(HashMap::new()),
315            }
316        }
317
318        fn with_building_ownerships(building_id: Uuid, ownerships: Vec<(Uuid, Uuid, f64)>) -> Self {
319            let mut map = HashMap::new();
320            map.insert(building_id, ownerships);
321            Self {
322                building_ownerships: Mutex::new(map),
323            }
324        }
325    }
326
327    #[async_trait]
328    impl UnitOwnerRepository for MockUnitOwnerRepository {
329        async fn create(&self, _unit_owner: &UnitOwner) -> Result<UnitOwner, String> {
330            unimplemented!("not needed for charge distribution tests")
331        }
332
333        async fn find_by_id(&self, _id: Uuid) -> Result<Option<UnitOwner>, String> {
334            unimplemented!("not needed for charge distribution tests")
335        }
336
337        async fn find_current_owners_by_unit(
338            &self,
339            _unit_id: Uuid,
340        ) -> Result<Vec<UnitOwner>, String> {
341            unimplemented!("not needed for charge distribution tests")
342        }
343
344        async fn find_current_units_by_owner(
345            &self,
346            _owner_id: Uuid,
347        ) -> Result<Vec<UnitOwner>, String> {
348            unimplemented!("not needed for charge distribution tests")
349        }
350
351        async fn find_all_owners_by_unit(&self, _unit_id: Uuid) -> Result<Vec<UnitOwner>, String> {
352            unimplemented!("not needed for charge distribution tests")
353        }
354
355        async fn find_all_units_by_owner(&self, _owner_id: Uuid) -> Result<Vec<UnitOwner>, String> {
356            unimplemented!("not needed for charge distribution tests")
357        }
358
359        async fn update(&self, _unit_owner: &UnitOwner) -> Result<UnitOwner, String> {
360            unimplemented!("not needed for charge distribution tests")
361        }
362
363        async fn delete(&self, _id: Uuid) -> Result<(), String> {
364            unimplemented!("not needed for charge distribution tests")
365        }
366
367        async fn has_active_owners(&self, _unit_id: Uuid) -> Result<bool, String> {
368            unimplemented!("not needed for charge distribution tests")
369        }
370
371        async fn get_total_ownership_percentage(&self, _unit_id: Uuid) -> Result<f64, String> {
372            unimplemented!("not needed for charge distribution tests")
373        }
374
375        async fn find_active_by_unit_and_owner(
376            &self,
377            _unit_id: Uuid,
378            _owner_id: Uuid,
379        ) -> Result<Option<UnitOwner>, String> {
380            unimplemented!("not needed for charge distribution tests")
381        }
382
383        async fn find_active_by_building(
384            &self,
385            building_id: Uuid,
386        ) -> Result<Vec<(Uuid, Uuid, f64)>, String> {
387            let ownerships = self.building_ownerships.lock().unwrap();
388            Ok(ownerships.get(&building_id).cloned().unwrap_or_default())
389        }
390    }
391
392    // ========== Helpers ==========
393
394    fn make_approved_expense(building_id: Uuid, amount_incl_vat: f64) -> Expense {
395        let now = Utc::now();
396        Expense {
397            id: Uuid::new_v4(),
398            organization_id: Uuid::new_v4(),
399            building_id,
400            category: ExpenseCategory::Maintenance,
401            description: "Elevator maintenance".to_string(),
402            amount: amount_incl_vat,
403            amount_excl_vat: Some(amount_incl_vat / 1.21),
404            vat_rate: Some(21.0),
405            vat_amount: Some(amount_incl_vat - amount_incl_vat / 1.21),
406            amount_incl_vat: Some(amount_incl_vat),
407            expense_date: now,
408            invoice_date: Some(now),
409            due_date: None,
410            paid_date: None,
411            approval_status: ApprovalStatus::Approved,
412            submitted_at: Some(now),
413            approved_by: Some(Uuid::new_v4()),
414            approved_at: Some(now),
415            rejection_reason: None,
416            payment_status: PaymentStatus::Pending,
417            supplier: Some("Schindler SA".to_string()),
418            invoice_number: Some("INV-001".to_string()),
419            account_code: Some("611002".to_string()),
420            contractor_report_id: None,
421            created_at: now,
422            updated_at: now,
423        }
424    }
425
426    fn make_draft_expense(building_id: Uuid) -> Expense {
427        let now = Utc::now();
428        Expense {
429            id: Uuid::new_v4(),
430            organization_id: Uuid::new_v4(),
431            building_id,
432            category: ExpenseCategory::Maintenance,
433            description: "Draft expense".to_string(),
434            amount: 1000.0,
435            amount_excl_vat: None,
436            vat_rate: None,
437            vat_amount: None,
438            amount_incl_vat: None,
439            expense_date: now,
440            invoice_date: None,
441            due_date: None,
442            paid_date: None,
443            approval_status: ApprovalStatus::Draft,
444            submitted_at: None,
445            approved_by: None,
446            approved_at: None,
447            rejection_reason: None,
448            payment_status: PaymentStatus::Pending,
449            supplier: None,
450            invoice_number: None,
451            account_code: None,
452            contractor_report_id: None,
453            created_at: now,
454            updated_at: now,
455        }
456    }
457
458    fn make_use_cases(
459        dist_repo: MockChargeDistributionRepository,
460        expense_repo: MockExpenseRepository,
461        unit_owner_repo: MockUnitOwnerRepository,
462    ) -> ChargeDistributionUseCases {
463        ChargeDistributionUseCases::new(
464            Arc::new(dist_repo),
465            Arc::new(expense_repo),
466            Arc::new(unit_owner_repo),
467        )
468    }
469
470    // ========== Tests ==========
471
472    #[tokio::test]
473    async fn test_calculate_and_save_distribution_success() {
474        let building_id = Uuid::new_v4();
475        let unit1_id = Uuid::new_v4();
476        let unit2_id = Uuid::new_v4();
477        let owner1_id = Uuid::new_v4();
478        let owner2_id = Uuid::new_v4();
479
480        let expense = make_approved_expense(building_id, 1000.0);
481        let expense_id = expense.id;
482
483        let ownerships = vec![
484            (unit1_id, owner1_id, 0.60), // 60%
485            (unit2_id, owner2_id, 0.40), // 40%
486        ];
487
488        let dist_repo = MockChargeDistributionRepository::new();
489        let expense_repo = MockExpenseRepository::with_expense(expense);
490        let unit_owner_repo =
491            MockUnitOwnerRepository::with_building_ownerships(building_id, ownerships);
492
493        let uc = make_use_cases(dist_repo, expense_repo, unit_owner_repo);
494
495        let result = uc.calculate_and_save_distribution(expense_id).await;
496        assert!(result.is_ok());
497
498        let distributions = result.unwrap();
499        assert_eq!(distributions.len(), 2);
500
501        // Verify amounts: 60% of 1000 = 600, 40% of 1000 = 400
502        let total_amount: f64 = distributions.iter().map(|d| d.amount_due).sum();
503        assert!((total_amount - 1000.0).abs() < 0.01);
504
505        // Verify all distributions reference the correct expense
506        assert!(distributions
507            .iter()
508            .all(|d| d.expense_id == expense_id.to_string()));
509    }
510
511    #[tokio::test]
512    async fn test_calculate_and_save_distribution_non_approved_expense() {
513        let building_id = Uuid::new_v4();
514        let expense = make_draft_expense(building_id);
515        let expense_id = expense.id;
516
517        let dist_repo = MockChargeDistributionRepository::new();
518        let expense_repo = MockExpenseRepository::with_expense(expense);
519        let unit_owner_repo = MockUnitOwnerRepository::new();
520
521        let uc = make_use_cases(dist_repo, expense_repo, unit_owner_repo);
522
523        let result = uc.calculate_and_save_distribution(expense_id).await;
524        assert!(result.is_err());
525        let err = result.unwrap_err();
526        assert!(err.contains("non-approved invoice"));
527    }
528
529    #[tokio::test]
530    async fn test_calculate_and_save_distribution_expense_not_found() {
531        let dist_repo = MockChargeDistributionRepository::new();
532        let expense_repo = MockExpenseRepository::new(); // empty
533        let unit_owner_repo = MockUnitOwnerRepository::new();
534
535        let uc = make_use_cases(dist_repo, expense_repo, unit_owner_repo);
536
537        let result = uc.calculate_and_save_distribution(Uuid::new_v4()).await;
538        assert!(result.is_err());
539        assert_eq!(result.unwrap_err(), "Expense/Invoice not found");
540    }
541
542    #[tokio::test]
543    async fn test_calculate_and_save_distribution_no_active_owners() {
544        let building_id = Uuid::new_v4();
545        let expense = make_approved_expense(building_id, 1000.0);
546        let expense_id = expense.id;
547
548        let dist_repo = MockChargeDistributionRepository::new();
549        let expense_repo = MockExpenseRepository::with_expense(expense);
550        // Empty ownerships for the building
551        let unit_owner_repo =
552            MockUnitOwnerRepository::with_building_ownerships(building_id, vec![]);
553
554        let uc = make_use_cases(dist_repo, expense_repo, unit_owner_repo);
555
556        let result = uc.calculate_and_save_distribution(expense_id).await;
557        assert!(result.is_err());
558        assert_eq!(
559            result.unwrap_err(),
560            "No active unit-owner relationships found for this building"
561        );
562    }
563
564    #[tokio::test]
565    async fn test_get_distribution_by_expense() {
566        let building_id = Uuid::new_v4();
567        let unit1_id = Uuid::new_v4();
568        let owner1_id = Uuid::new_v4();
569
570        let expense = make_approved_expense(building_id, 500.0);
571        let expense_id = expense.id;
572
573        let ownerships = vec![(unit1_id, owner1_id, 1.0)]; // 100%
574
575        let dist_repo = MockChargeDistributionRepository::new();
576        let expense_repo = MockExpenseRepository::with_expense(expense);
577        let unit_owner_repo =
578            MockUnitOwnerRepository::with_building_ownerships(building_id, ownerships);
579
580        let uc = make_use_cases(dist_repo, expense_repo, unit_owner_repo);
581
582        // First calculate and save
583        uc.calculate_and_save_distribution(expense_id)
584            .await
585            .unwrap();
586
587        // Then retrieve by expense
588        let result = uc.get_distribution_by_expense(expense_id).await;
589        assert!(result.is_ok());
590        let distributions = result.unwrap();
591        assert_eq!(distributions.len(), 1);
592        assert_eq!(distributions[0].expense_id, expense_id.to_string());
593        assert_eq!(distributions[0].quota_percentage, 1.0);
594        assert!((distributions[0].amount_due - 500.0).abs() < 0.01);
595    }
596
597    #[tokio::test]
598    async fn test_get_distribution_by_expense_empty() {
599        let dist_repo = MockChargeDistributionRepository::new();
600        let expense_repo = MockExpenseRepository::new();
601        let unit_owner_repo = MockUnitOwnerRepository::new();
602
603        let uc = make_use_cases(dist_repo, expense_repo, unit_owner_repo);
604
605        let result = uc.get_distribution_by_expense(Uuid::new_v4()).await;
606        assert!(result.is_ok());
607        assert!(result.unwrap().is_empty());
608    }
609
610    #[tokio::test]
611    async fn test_get_total_due_by_owner() {
612        let building_id = Uuid::new_v4();
613        let unit1_id = Uuid::new_v4();
614        let unit2_id = Uuid::new_v4();
615        let owner1_id = Uuid::new_v4();
616        let owner2_id = Uuid::new_v4();
617
618        // Create 2 approved expenses
619        let expense1 = make_approved_expense(building_id, 1000.0);
620        let expense1_id = expense1.id;
621        let expense2 = make_approved_expense(building_id, 2000.0);
622        let expense2_id = expense2.id;
623
624        let ownerships = vec![
625            (unit1_id, owner1_id, 0.60), // 60%
626            (unit2_id, owner2_id, 0.40), // 40%
627        ];
628
629        let dist_repo = MockChargeDistributionRepository::new();
630        let mut expense_map = HashMap::new();
631        expense_map.insert(expense1.id, expense1);
632        expense_map.insert(expense2.id, expense2);
633        let expense_repo = MockExpenseRepository {
634            expenses: Mutex::new(expense_map),
635        };
636        let unit_owner_repo =
637            MockUnitOwnerRepository::with_building_ownerships(building_id, ownerships);
638
639        let uc = make_use_cases(dist_repo, expense_repo, unit_owner_repo);
640
641        // Calculate distributions for both expenses
642        uc.calculate_and_save_distribution(expense1_id)
643            .await
644            .unwrap();
645        uc.calculate_and_save_distribution(expense2_id)
646            .await
647            .unwrap();
648
649        // Owner 1 owes 60% of 1000 + 60% of 2000 = 600 + 1200 = 1800
650        let total = uc.get_total_due_by_owner(owner1_id).await.unwrap();
651        assert!((total - 1800.0).abs() < 0.01);
652
653        // Owner 2 owes 40% of 1000 + 40% of 2000 = 400 + 800 = 1200
654        let total = uc.get_total_due_by_owner(owner2_id).await.unwrap();
655        assert!((total - 1200.0).abs() < 0.01);
656    }
657
658    #[tokio::test]
659    async fn test_get_total_due_by_owner_no_distributions() {
660        let dist_repo = MockChargeDistributionRepository::new();
661        let expense_repo = MockExpenseRepository::new();
662        let unit_owner_repo = MockUnitOwnerRepository::new();
663
664        let uc = make_use_cases(dist_repo, expense_repo, unit_owner_repo);
665
666        let total = uc.get_total_due_by_owner(Uuid::new_v4()).await.unwrap();
667        assert_eq!(total, 0.0);
668    }
669
670    #[tokio::test]
671    async fn test_calculate_distribution_uses_amount_incl_vat() {
672        let building_id = Uuid::new_v4();
673        let unit_id = Uuid::new_v4();
674        let owner_id = Uuid::new_v4();
675
676        // Expense with amount_incl_vat = 1210, amount = 1000 (backward compat)
677        let now = Utc::now();
678        let expense = Expense {
679            id: Uuid::new_v4(),
680            organization_id: Uuid::new_v4(),
681            building_id,
682            category: ExpenseCategory::Utilities,
683            description: "Electricity".to_string(),
684            amount: 1000.0,
685            amount_excl_vat: Some(1000.0),
686            vat_rate: Some(21.0),
687            vat_amount: Some(210.0),
688            amount_incl_vat: Some(1210.0),
689            expense_date: now,
690            invoice_date: Some(now),
691            due_date: None,
692            paid_date: None,
693            approval_status: ApprovalStatus::Approved,
694            submitted_at: Some(now),
695            approved_by: Some(Uuid::new_v4()),
696            approved_at: Some(now),
697            rejection_reason: None,
698            payment_status: PaymentStatus::Pending,
699            supplier: None,
700            invoice_number: None,
701            account_code: None,
702            contractor_report_id: None,
703            created_at: now,
704            updated_at: now,
705        };
706        let expense_id = expense.id;
707
708        let ownerships = vec![(unit_id, owner_id, 1.0)]; // 100%
709
710        let dist_repo = MockChargeDistributionRepository::new();
711        let expense_repo = MockExpenseRepository::with_expense(expense);
712        let unit_owner_repo =
713            MockUnitOwnerRepository::with_building_ownerships(building_id, ownerships);
714
715        let uc = make_use_cases(dist_repo, expense_repo, unit_owner_repo);
716
717        let result = uc
718            .calculate_and_save_distribution(expense_id)
719            .await
720            .unwrap();
721
722        // Should use amount_incl_vat (1210), not amount (1000)
723        assert_eq!(result.len(), 1);
724        assert!((result[0].amount_due - 1210.0).abs() < 0.01);
725    }
726}