Skip to main content

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