koprogo_api/domain/entities/
expense.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5/// Catégorie de charges
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
7pub enum ExpenseCategory {
8    Maintenance,    // Entretien
9    Repairs,        // Réparations
10    Insurance,      // Assurance
11    Utilities,      // Charges courantes (eau, électricité)
12    Cleaning,       // Nettoyage
13    Administration, // Administration
14    Works,          // Travaux
15    Other,
16}
17
18/// Statut de paiement
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
20#[serde(rename_all = "snake_case")]
21pub enum PaymentStatus {
22    Pending,
23    Paid,
24    Overdue,
25    Cancelled,
26}
27
28/// Statut d'approbation pour le workflow de validation
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
30#[serde(rename_all = "snake_case")]
31pub enum ApprovalStatus {
32    Draft,           // Brouillon - en cours d'édition
33    PendingApproval, // Soumis pour validation
34    Approved,        // Approuvé par le syndic
35    Rejected,        // Rejeté
36}
37
38/// Représente une charge de copropriété / facture
39///
40/// Conforme au PCMN belge (Plan Comptable Minimum Normalisé).
41/// Chaque charge peut être liée à un compte comptable via account_code.
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
43pub struct Expense {
44    pub id: Uuid,
45    pub organization_id: Uuid,
46    pub building_id: Uuid,
47    pub category: ExpenseCategory,
48    pub description: String,
49
50    // Montants et TVA
51    pub amount: f64,                  // Montant TTC (backward compatibility)
52    pub amount_excl_vat: Option<f64>, // Montant HT
53    pub vat_rate: Option<f64>,        // Taux TVA (ex: 21.0 pour 21%)
54    pub vat_amount: Option<f64>,      // Montant TVA
55    pub amount_incl_vat: Option<f64>, // Montant TTC (explicite)
56
57    // Dates multiples
58    pub expense_date: DateTime<Utc>, // Date originale (backward compatibility)
59    pub invoice_date: Option<DateTime<Utc>>, // Date de la facture
60    pub due_date: Option<DateTime<Utc>>, // Date d'échéance
61    pub paid_date: Option<DateTime<Utc>>, // Date de paiement effectif
62
63    // Workflow de validation
64    pub approval_status: ApprovalStatus,
65    pub submitted_at: Option<DateTime<Utc>>, // Date de soumission pour validation
66    pub approved_by: Option<Uuid>,           // User ID qui a approuvé/rejeté
67    pub approved_at: Option<DateTime<Utc>>,  // Date d'approbation/rejet
68    pub rejection_reason: Option<String>,    // Raison du rejet
69
70    // Statut et métadonnées
71    pub payment_status: PaymentStatus,
72    pub supplier: Option<String>,
73    pub invoice_number: Option<String>,
74    /// Code du compte comptable PCMN (e.g., "604001" for electricity, "611002" for elevator maintenance)
75    /// References: accounts.code column in the database
76    pub account_code: Option<String>,
77    pub created_at: DateTime<Utc>,
78    pub updated_at: DateTime<Utc>,
79}
80
81impl Expense {
82    #[allow(clippy::too_many_arguments)]
83    pub fn new(
84        organization_id: Uuid,
85        building_id: Uuid,
86        category: ExpenseCategory,
87        description: String,
88        amount: f64,
89        expense_date: DateTime<Utc>,
90        supplier: Option<String>,
91        invoice_number: Option<String>,
92        account_code: Option<String>,
93    ) -> Result<Self, String> {
94        if description.is_empty() {
95            return Err("Description cannot be empty".to_string());
96        }
97        if amount <= 0.0 {
98            return Err("Amount must be greater than 0".to_string());
99        }
100
101        // Validate account_code format if provided (Belgian PCMN codes)
102        if let Some(ref code) = account_code {
103            if code.is_empty() {
104                return Err("Account code cannot be empty if provided".to_string());
105            }
106            // Belgian PCMN codes are typically 1-10 characters (e.g., "6", "60", "604001")
107            if code.len() > 40 {
108                return Err("Account code cannot exceed 40 characters".to_string());
109            }
110        }
111
112        let now = Utc::now();
113        Ok(Self {
114            id: Uuid::new_v4(),
115            organization_id,
116            building_id,
117            category,
118            description,
119            amount,
120            amount_excl_vat: None,
121            vat_rate: None,
122            vat_amount: None,
123            amount_incl_vat: Some(amount), // Pour compatibilité, amount = TTC
124            expense_date,
125            invoice_date: None,
126            due_date: None,
127            paid_date: None,
128            approval_status: ApprovalStatus::Draft, // Par défaut en brouillon
129            submitted_at: None,
130            approved_by: None,
131            approved_at: None,
132            rejection_reason: None,
133            payment_status: PaymentStatus::Pending,
134            supplier,
135            invoice_number,
136            account_code,
137            created_at: now,
138            updated_at: now,
139        })
140    }
141
142    /// Crée une facture avec gestion complète de la TVA
143    #[allow(clippy::too_many_arguments)]
144    pub fn new_with_vat(
145        organization_id: Uuid,
146        building_id: Uuid,
147        category: ExpenseCategory,
148        description: String,
149        amount_excl_vat: f64,
150        vat_rate: f64,
151        invoice_date: DateTime<Utc>,
152        due_date: Option<DateTime<Utc>>,
153        supplier: Option<String>,
154        invoice_number: Option<String>,
155        account_code: Option<String>,
156    ) -> Result<Self, String> {
157        if description.is_empty() {
158            return Err("Description cannot be empty".to_string());
159        }
160        if amount_excl_vat <= 0.0 {
161            return Err("Amount (excl. VAT) must be greater than 0".to_string());
162        }
163        if !(0.0..=100.0).contains(&vat_rate) {
164            return Err("VAT rate must be between 0 and 100".to_string());
165        }
166
167        // Calcul automatique de la TVA
168        let vat_amount = (amount_excl_vat * vat_rate) / 100.0;
169        let amount_incl_vat = amount_excl_vat + vat_amount;
170
171        let now = Utc::now();
172        Ok(Self {
173            id: Uuid::new_v4(),
174            organization_id,
175            building_id,
176            category,
177            description,
178            amount: amount_incl_vat, // Backward compatibility
179            amount_excl_vat: Some(amount_excl_vat),
180            vat_rate: Some(vat_rate),
181            vat_amount: Some(vat_amount),
182            amount_incl_vat: Some(amount_incl_vat),
183            expense_date: invoice_date, // Backward compatibility
184            invoice_date: Some(invoice_date),
185            due_date,
186            paid_date: None,
187            approval_status: ApprovalStatus::Draft,
188            submitted_at: None,
189            approved_by: None,
190            approved_at: None,
191            rejection_reason: None,
192            payment_status: PaymentStatus::Pending,
193            supplier,
194            invoice_number,
195            account_code,
196            created_at: now,
197            updated_at: now,
198        })
199    }
200
201    /// Recalcule la TVA si le montant HT ou le taux change
202    pub fn recalculate_vat(&mut self) -> Result<(), String> {
203        if let (Some(amount_excl_vat), Some(vat_rate)) = (self.amount_excl_vat, self.vat_rate) {
204            if amount_excl_vat <= 0.0 {
205                return Err("Amount (excl. VAT) must be greater than 0".to_string());
206            }
207            if !(0.0..=100.0).contains(&vat_rate) {
208                return Err("VAT rate must be between 0 and 100".to_string());
209            }
210
211            let vat_amount = (amount_excl_vat * vat_rate) / 100.0;
212            let amount_incl_vat = amount_excl_vat + vat_amount;
213
214            self.vat_amount = Some(vat_amount);
215            self.amount_incl_vat = Some(amount_incl_vat);
216            self.amount = amount_incl_vat; // Backward compatibility
217            self.updated_at = Utc::now();
218            Ok(())
219        } else {
220            Err("Cannot recalculate VAT: amount_excl_vat or vat_rate is missing".to_string())
221        }
222    }
223
224    /// Soumet la facture pour validation (Draft → PendingApproval)
225    pub fn submit_for_approval(&mut self) -> Result<(), String> {
226        match self.approval_status {
227            ApprovalStatus::Draft => {
228                self.approval_status = ApprovalStatus::PendingApproval;
229                self.submitted_at = Some(Utc::now());
230                self.updated_at = Utc::now();
231                Ok(())
232            }
233            ApprovalStatus::Rejected => {
234                // Permet de re-soumettre une facture rejetée
235                self.approval_status = ApprovalStatus::PendingApproval;
236                self.submitted_at = Some(Utc::now());
237                self.rejection_reason = None; // Efface la raison du rejet précédent
238                self.updated_at = Utc::now();
239                Ok(())
240            }
241            ApprovalStatus::PendingApproval => {
242                Err("Invoice is already pending approval".to_string())
243            }
244            ApprovalStatus::Approved => Err("Cannot submit an approved invoice".to_string()),
245        }
246    }
247
248    /// Approuve la facture (PendingApproval → Approved)
249    pub fn approve(&mut self, approved_by_user_id: Uuid) -> Result<(), String> {
250        match self.approval_status {
251            ApprovalStatus::PendingApproval => {
252                self.approval_status = ApprovalStatus::Approved;
253                self.approved_by = Some(approved_by_user_id);
254                self.approved_at = Some(Utc::now());
255                self.updated_at = Utc::now();
256                Ok(())
257            }
258            ApprovalStatus::Draft => {
259                Err("Cannot approve a draft invoice (must be submitted first)".to_string())
260            }
261            ApprovalStatus::Approved => Err("Invoice is already approved".to_string()),
262            ApprovalStatus::Rejected => {
263                Err("Cannot approve a rejected invoice (resubmit first)".to_string())
264            }
265        }
266    }
267
268    /// Rejette la facture avec une raison (PendingApproval → Rejected)
269    pub fn reject(&mut self, rejected_by_user_id: Uuid, reason: String) -> Result<(), String> {
270        if reason.trim().is_empty() {
271            return Err("Rejection reason cannot be empty".to_string());
272        }
273
274        match self.approval_status {
275            ApprovalStatus::PendingApproval => {
276                self.approval_status = ApprovalStatus::Rejected;
277                self.approved_by = Some(rejected_by_user_id); // Celui qui a rejeté
278                self.approved_at = Some(Utc::now());
279                self.rejection_reason = Some(reason);
280                self.updated_at = Utc::now();
281                Ok(())
282            }
283            ApprovalStatus::Draft => {
284                Err("Cannot reject a draft invoice (not submitted)".to_string())
285            }
286            ApprovalStatus::Approved => Err("Cannot reject an approved invoice".to_string()),
287            ApprovalStatus::Rejected => Err("Invoice is already rejected".to_string()),
288        }
289    }
290
291    /// Vérifie si la facture peut être modifiée (uniquement en Draft ou Rejected)
292    pub fn can_be_modified(&self) -> bool {
293        matches!(
294            self.approval_status,
295            ApprovalStatus::Draft | ApprovalStatus::Rejected
296        )
297    }
298
299    /// Vérifie si la facture est approuvée
300    pub fn is_approved(&self) -> bool {
301        self.approval_status == ApprovalStatus::Approved
302    }
303
304    pub fn mark_as_paid(&mut self) -> Result<(), String> {
305        // Validation critique : une facture ne peut être payée que si elle est approuvée
306        if self.approval_status != ApprovalStatus::Approved {
307            return Err(format!(
308                "Cannot mark expense as paid: invoice must be approved first (current status: {:?})",
309                self.approval_status
310            ));
311        }
312
313        match self.payment_status {
314            PaymentStatus::Pending | PaymentStatus::Overdue => {
315                self.payment_status = PaymentStatus::Paid;
316                self.paid_date = Some(Utc::now()); // Enregistre la date de paiement effectif
317                self.updated_at = Utc::now();
318                Ok(())
319            }
320            PaymentStatus::Paid => Err("Expense is already paid".to_string()),
321            PaymentStatus::Cancelled => Err("Cannot mark a cancelled expense as paid".to_string()),
322        }
323    }
324
325    pub fn mark_as_overdue(&mut self) -> Result<(), String> {
326        match self.payment_status {
327            PaymentStatus::Pending => {
328                self.payment_status = PaymentStatus::Overdue;
329                self.updated_at = Utc::now();
330                Ok(())
331            }
332            PaymentStatus::Overdue => Err("Expense is already overdue".to_string()),
333            PaymentStatus::Paid => Err("Cannot mark a paid expense as overdue".to_string()),
334            PaymentStatus::Cancelled => {
335                Err("Cannot mark a cancelled expense as overdue".to_string())
336            }
337        }
338    }
339
340    pub fn cancel(&mut self) -> Result<(), String> {
341        match self.payment_status {
342            PaymentStatus::Pending | PaymentStatus::Overdue => {
343                self.payment_status = PaymentStatus::Cancelled;
344                self.updated_at = Utc::now();
345                Ok(())
346            }
347            PaymentStatus::Paid => Err("Cannot cancel a paid expense".to_string()),
348            PaymentStatus::Cancelled => Err("Expense is already cancelled".to_string()),
349        }
350    }
351
352    pub fn reactivate(&mut self) -> Result<(), String> {
353        match self.payment_status {
354            PaymentStatus::Cancelled => {
355                self.payment_status = PaymentStatus::Pending;
356                self.updated_at = Utc::now();
357                Ok(())
358            }
359            _ => Err("Can only reactivate cancelled expenses".to_string()),
360        }
361    }
362
363    pub fn unpay(&mut self) -> Result<(), String> {
364        match self.payment_status {
365            PaymentStatus::Paid => {
366                self.payment_status = PaymentStatus::Pending;
367                self.updated_at = Utc::now();
368                Ok(())
369            }
370            _ => Err("Can only unpay paid expenses".to_string()),
371        }
372    }
373
374    pub fn is_paid(&self) -> bool {
375        self.payment_status == PaymentStatus::Paid
376    }
377}
378
379#[cfg(test)]
380mod tests {
381    use super::*;
382
383    #[test]
384    fn test_create_expense_success() {
385        let org_id = Uuid::new_v4();
386        let building_id = Uuid::new_v4();
387        let expense = Expense::new(
388            org_id,
389            building_id,
390            ExpenseCategory::Maintenance,
391            "Entretien ascenseur".to_string(),
392            500.0,
393            Utc::now(),
394            Some("ACME Elevators".to_string()),
395            Some("INV-2024-001".to_string()),
396            Some("611002".to_string()), // Elevator maintenance account (Belgian PCMN)
397        );
398
399        assert!(expense.is_ok());
400        let expense = expense.unwrap();
401        assert_eq!(expense.organization_id, org_id);
402        assert_eq!(expense.amount, 500.0);
403        assert_eq!(expense.payment_status, PaymentStatus::Pending);
404        assert_eq!(expense.account_code, Some("611002".to_string()));
405    }
406
407    #[test]
408    fn test_create_expense_without_account_code() {
409        let org_id = Uuid::new_v4();
410        let building_id = Uuid::new_v4();
411        let expense = Expense::new(
412            org_id,
413            building_id,
414            ExpenseCategory::Other,
415            "Miscellaneous expense".to_string(),
416            100.0,
417            Utc::now(),
418            None,
419            None,
420            None, // No account code
421        );
422
423        assert!(expense.is_ok());
424        let expense = expense.unwrap();
425        assert_eq!(expense.account_code, None);
426    }
427
428    #[test]
429    fn test_create_expense_empty_account_code_fails() {
430        let org_id = Uuid::new_v4();
431        let building_id = Uuid::new_v4();
432        let expense = Expense::new(
433            org_id,
434            building_id,
435            ExpenseCategory::Maintenance,
436            "Test".to_string(),
437            100.0,
438            Utc::now(),
439            None,
440            None,
441            Some("".to_string()), // Empty account code should fail
442        );
443
444        assert!(expense.is_err());
445        assert!(expense
446            .unwrap_err()
447            .contains("Account code cannot be empty"));
448    }
449
450    #[test]
451    fn test_create_expense_long_account_code_fails() {
452        let org_id = Uuid::new_v4();
453        let building_id = Uuid::new_v4();
454        let long_code = "a".repeat(41); // 41 characters, exceeds limit
455        let expense = Expense::new(
456            org_id,
457            building_id,
458            ExpenseCategory::Maintenance,
459            "Test".to_string(),
460            100.0,
461            Utc::now(),
462            None,
463            None,
464            Some(long_code),
465        );
466
467        assert!(expense.is_err());
468        assert!(expense
469            .unwrap_err()
470            .contains("Account code cannot exceed 40 characters"));
471    }
472
473    #[test]
474    fn test_create_expense_negative_amount_fails() {
475        let org_id = Uuid::new_v4();
476        let building_id = Uuid::new_v4();
477        let expense = Expense::new(
478            org_id,
479            building_id,
480            ExpenseCategory::Maintenance,
481            "Test".to_string(),
482            -100.0,
483            Utc::now(),
484            None,
485            None,
486            None,
487        );
488
489        assert!(expense.is_err());
490    }
491
492    #[test]
493    fn test_mark_expense_as_paid() {
494        let org_id = Uuid::new_v4();
495        let building_id = Uuid::new_v4();
496        let syndic_id = Uuid::new_v4();
497        let mut expense = Expense::new(
498            org_id,
499            building_id,
500            ExpenseCategory::Maintenance,
501            "Test".to_string(),
502            100.0,
503            Utc::now(),
504            None,
505            None,
506            None,
507        )
508        .unwrap();
509
510        // Follow approval workflow before payment
511        expense.submit_for_approval().unwrap();
512        expense.approve(syndic_id).unwrap();
513
514        assert!(!expense.is_paid());
515        let result = expense.mark_as_paid();
516        assert!(result.is_ok());
517        assert!(expense.is_paid());
518    }
519
520    #[test]
521    fn test_mark_paid_expense_as_paid_fails() {
522        let org_id = Uuid::new_v4();
523        let building_id = Uuid::new_v4();
524        let syndic_id = Uuid::new_v4();
525        let mut expense = Expense::new(
526            org_id,
527            building_id,
528            ExpenseCategory::Maintenance,
529            "Test".to_string(),
530            100.0,
531            Utc::now(),
532            None,
533            None,
534            None,
535        )
536        .unwrap();
537
538        // Follow approval workflow before payment
539        expense.submit_for_approval().unwrap();
540        expense.approve(syndic_id).unwrap();
541
542        expense.mark_as_paid().unwrap();
543        let result = expense.mark_as_paid();
544        assert!(result.is_err());
545    }
546
547    #[test]
548    fn test_cancel_expense() {
549        let org_id = Uuid::new_v4();
550        let building_id = Uuid::new_v4();
551        let mut expense = Expense::new(
552            org_id,
553            building_id,
554            ExpenseCategory::Maintenance,
555            "Test".to_string(),
556            100.0,
557            Utc::now(),
558            None,
559            None,
560            None,
561        )
562        .unwrap();
563
564        let result = expense.cancel();
565        assert!(result.is_ok());
566        assert_eq!(expense.payment_status, PaymentStatus::Cancelled);
567    }
568
569    #[test]
570    fn test_reactivate_expense() {
571        let org_id = Uuid::new_v4();
572        let building_id = Uuid::new_v4();
573        let mut expense = Expense::new(
574            org_id,
575            building_id,
576            ExpenseCategory::Maintenance,
577            "Test".to_string(),
578            100.0,
579            Utc::now(),
580            None,
581            None,
582            None,
583        )
584        .unwrap();
585
586        expense.cancel().unwrap();
587        let result = expense.reactivate();
588        assert!(result.is_ok());
589        assert_eq!(expense.payment_status, PaymentStatus::Pending);
590    }
591
592    // ========== Tests pour gestion TVA ==========
593
594    #[test]
595    fn test_create_invoice_with_vat_success() {
596        let org_id = Uuid::new_v4();
597        let building_id = Uuid::new_v4();
598        let invoice_date = Utc::now();
599        let due_date = invoice_date + chrono::Duration::days(30);
600
601        let invoice = Expense::new_with_vat(
602            org_id,
603            building_id,
604            ExpenseCategory::Maintenance,
605            "Réparation toiture".to_string(),
606            1000.0, // HT
607            21.0,   // TVA 21%
608            invoice_date,
609            Some(due_date),
610            Some("BatiPro SPRL".to_string()),
611            Some("INV-2025-042".to_string()),
612            None, // account_code
613        );
614
615        assert!(invoice.is_ok());
616        let invoice = invoice.unwrap();
617        assert_eq!(invoice.amount_excl_vat, Some(1000.0));
618        assert_eq!(invoice.vat_rate, Some(21.0));
619        assert_eq!(invoice.vat_amount, Some(210.0));
620        assert_eq!(invoice.amount_incl_vat, Some(1210.0));
621        assert_eq!(invoice.amount, 1210.0); // Backward compatibility
622        assert_eq!(invoice.approval_status, ApprovalStatus::Draft);
623    }
624
625    #[test]
626    fn test_create_invoice_with_vat_6_percent() {
627        let org_id = Uuid::new_v4();
628        let building_id = Uuid::new_v4();
629
630        let invoice = Expense::new_with_vat(
631            org_id,
632            building_id,
633            ExpenseCategory::Works,
634            "Rénovation énergétique".to_string(),
635            5000.0, // HT
636            6.0,    // TVA réduite 6%
637            Utc::now(),
638            None,
639            None,
640            None,
641            None, // account_code
642        )
643        .unwrap();
644
645        assert_eq!(invoice.vat_amount, Some(300.0));
646        assert_eq!(invoice.amount_incl_vat, Some(5300.0));
647    }
648
649    #[test]
650    fn test_create_invoice_negative_vat_rate_fails() {
651        let org_id = Uuid::new_v4();
652        let building_id = Uuid::new_v4();
653
654        let invoice = Expense::new_with_vat(
655            org_id,
656            building_id,
657            ExpenseCategory::Maintenance,
658            "Test".to_string(),
659            100.0,
660            -5.0, // Taux négatif invalide
661            Utc::now(),
662            None,
663            None,
664            None,
665            None, // account_code
666        );
667
668        assert!(invoice.is_err());
669        assert_eq!(invoice.unwrap_err(), "VAT rate must be between 0 and 100");
670    }
671
672    #[test]
673    fn test_create_invoice_vat_rate_above_100_fails() {
674        let org_id = Uuid::new_v4();
675        let building_id = Uuid::new_v4();
676
677        let invoice = Expense::new_with_vat(
678            org_id,
679            building_id,
680            ExpenseCategory::Maintenance,
681            "Test".to_string(),
682            100.0,
683            150.0, // Taux > 100% invalide
684            Utc::now(),
685            None,
686            None,
687            None,
688            None, // account_code
689        );
690
691        assert!(invoice.is_err());
692    }
693
694    #[test]
695    fn test_recalculate_vat_success() {
696        let org_id = Uuid::new_v4();
697        let building_id = Uuid::new_v4();
698
699        let mut invoice = Expense::new_with_vat(
700            org_id,
701            building_id,
702            ExpenseCategory::Maintenance,
703            "Test".to_string(),
704            1000.0,
705            21.0,
706            Utc::now(),
707            None,
708            None,
709            None,
710            None, // account_code
711        )
712        .unwrap();
713
714        // Modifier le montant HT
715        invoice.amount_excl_vat = Some(1500.0);
716        let result = invoice.recalculate_vat();
717
718        assert!(result.is_ok());
719        assert_eq!(invoice.vat_amount, Some(315.0)); // 1500 * 21% = 315
720        assert_eq!(invoice.amount_incl_vat, Some(1815.0));
721    }
722
723    #[test]
724    fn test_recalculate_vat_without_vat_data_fails() {
725        let org_id = Uuid::new_v4();
726        let building_id = Uuid::new_v4();
727
728        // Créer une expense classique sans TVA
729        let mut expense = Expense::new(
730            org_id,
731            building_id,
732            ExpenseCategory::Maintenance,
733            "Test".to_string(),
734            100.0,
735            Utc::now(),
736            None,
737            None,
738            None, // account_code
739        )
740        .unwrap();
741
742        let result = expense.recalculate_vat();
743        assert!(result.is_err());
744    }
745
746    // ========== Tests pour workflow de validation ==========
747
748    #[test]
749    fn test_submit_draft_invoice_for_approval() {
750        let org_id = Uuid::new_v4();
751        let building_id = Uuid::new_v4();
752
753        let mut invoice = Expense::new_with_vat(
754            org_id,
755            building_id,
756            ExpenseCategory::Maintenance,
757            "Test".to_string(),
758            1000.0,
759            21.0,
760            Utc::now(),
761            None,
762            None,
763            None,
764            None, // account_code
765        )
766        .unwrap();
767
768        assert_eq!(invoice.approval_status, ApprovalStatus::Draft);
769        assert!(invoice.submitted_at.is_none());
770
771        let result = invoice.submit_for_approval();
772        assert!(result.is_ok());
773        assert_eq!(invoice.approval_status, ApprovalStatus::PendingApproval);
774        assert!(invoice.submitted_at.is_some());
775    }
776
777    #[test]
778    fn test_submit_already_pending_invoice_fails() {
779        let org_id = Uuid::new_v4();
780        let building_id = Uuid::new_v4();
781
782        let mut invoice = Expense::new_with_vat(
783            org_id,
784            building_id,
785            ExpenseCategory::Maintenance,
786            "Test".to_string(),
787            1000.0,
788            21.0,
789            Utc::now(),
790            None,
791            None,
792            None,
793            None, // account_code
794        )
795        .unwrap();
796
797        invoice.submit_for_approval().unwrap();
798        let result = invoice.submit_for_approval();
799
800        assert!(result.is_err());
801        assert_eq!(result.unwrap_err(), "Invoice is already pending approval");
802    }
803
804    #[test]
805    fn test_resubmit_rejected_invoice() {
806        let org_id = Uuid::new_v4();
807        let building_id = Uuid::new_v4();
808        let user_id = Uuid::new_v4();
809
810        let mut invoice = Expense::new_with_vat(
811            org_id,
812            building_id,
813            ExpenseCategory::Maintenance,
814            "Test".to_string(),
815            1000.0,
816            21.0,
817            Utc::now(),
818            None,
819            None,
820            None,
821            None, // account_code
822        )
823        .unwrap();
824
825        invoice.submit_for_approval().unwrap();
826        invoice
827            .reject(user_id, "Montant incorrect".to_string())
828            .unwrap();
829        assert_eq!(invoice.approval_status, ApprovalStatus::Rejected);
830
831        // Re-soumettre une facture rejetée devrait fonctionner
832        let result = invoice.submit_for_approval();
833        assert!(result.is_ok());
834        assert_eq!(invoice.approval_status, ApprovalStatus::PendingApproval);
835        assert!(invoice.rejection_reason.is_none()); // Raison effacée
836    }
837
838    #[test]
839    fn test_approve_pending_invoice() {
840        let org_id = Uuid::new_v4();
841        let building_id = Uuid::new_v4();
842        let syndic_id = Uuid::new_v4();
843
844        let mut invoice = Expense::new_with_vat(
845            org_id,
846            building_id,
847            ExpenseCategory::Maintenance,
848            "Test".to_string(),
849            1000.0,
850            21.0,
851            Utc::now(),
852            None,
853            None,
854            None,
855            None, // account_code
856        )
857        .unwrap();
858
859        invoice.submit_for_approval().unwrap();
860        let result = invoice.approve(syndic_id);
861
862        assert!(result.is_ok());
863        assert_eq!(invoice.approval_status, ApprovalStatus::Approved);
864        assert_eq!(invoice.approved_by, Some(syndic_id));
865        assert!(invoice.approved_at.is_some());
866        assert!(invoice.is_approved());
867    }
868
869    #[test]
870    fn test_approve_draft_invoice_fails() {
871        let org_id = Uuid::new_v4();
872        let building_id = Uuid::new_v4();
873        let syndic_id = Uuid::new_v4();
874
875        let mut invoice = Expense::new_with_vat(
876            org_id,
877            building_id,
878            ExpenseCategory::Maintenance,
879            "Test".to_string(),
880            1000.0,
881            21.0,
882            Utc::now(),
883            None,
884            None,
885            None,
886            None, // account_code
887        )
888        .unwrap();
889
890        // Ne PAS soumettre, tenter d'approuver directement
891        let result = invoice.approve(syndic_id);
892
893        assert!(result.is_err());
894        assert!(result.unwrap_err().contains("must be submitted first"));
895    }
896
897    #[test]
898    fn test_reject_pending_invoice_with_reason() {
899        let org_id = Uuid::new_v4();
900        let building_id = Uuid::new_v4();
901        let syndic_id = Uuid::new_v4();
902
903        let mut invoice = Expense::new_with_vat(
904            org_id,
905            building_id,
906            ExpenseCategory::Maintenance,
907            "Test".to_string(),
908            1000.0,
909            21.0,
910            Utc::now(),
911            None,
912            None,
913            None,
914            None, // account_code
915        )
916        .unwrap();
917
918        invoice.submit_for_approval().unwrap();
919        let result = invoice.reject(
920            syndic_id,
921            "Le montant ne correspond pas au devis".to_string(),
922        );
923
924        assert!(result.is_ok());
925        assert_eq!(invoice.approval_status, ApprovalStatus::Rejected);
926        assert_eq!(invoice.approved_by, Some(syndic_id));
927        assert_eq!(
928            invoice.rejection_reason,
929            Some("Le montant ne correspond pas au devis".to_string())
930        );
931    }
932
933    #[test]
934    fn test_reject_invoice_without_reason_fails() {
935        let org_id = Uuid::new_v4();
936        let building_id = Uuid::new_v4();
937        let syndic_id = Uuid::new_v4();
938
939        let mut invoice = Expense::new_with_vat(
940            org_id,
941            building_id,
942            ExpenseCategory::Maintenance,
943            "Test".to_string(),
944            1000.0,
945            21.0,
946            Utc::now(),
947            None,
948            None,
949            None,
950            None, // account_code
951        )
952        .unwrap();
953
954        invoice.submit_for_approval().unwrap();
955        let result = invoice.reject(syndic_id, "".to_string());
956
957        assert!(result.is_err());
958        assert_eq!(result.unwrap_err(), "Rejection reason cannot be empty");
959    }
960
961    #[test]
962    fn test_can_be_modified_draft() {
963        let org_id = Uuid::new_v4();
964        let building_id = Uuid::new_v4();
965
966        let invoice = Expense::new_with_vat(
967            org_id,
968            building_id,
969            ExpenseCategory::Maintenance,
970            "Test".to_string(),
971            1000.0,
972            21.0,
973            Utc::now(),
974            None,
975            None,
976            None,
977            None, // account_code
978        )
979        .unwrap();
980
981        assert!(invoice.can_be_modified()); // Draft peut être modifié
982    }
983
984    #[test]
985    fn test_can_be_modified_rejected() {
986        let org_id = Uuid::new_v4();
987        let building_id = Uuid::new_v4();
988        let syndic_id = Uuid::new_v4();
989
990        let mut invoice = Expense::new_with_vat(
991            org_id,
992            building_id,
993            ExpenseCategory::Maintenance,
994            "Test".to_string(),
995            1000.0,
996            21.0,
997            Utc::now(),
998            None,
999            None,
1000            None,
1001            None, // account_code
1002        )
1003        .unwrap();
1004
1005        invoice.submit_for_approval().unwrap();
1006        invoice.reject(syndic_id, "Erreur".to_string()).unwrap();
1007
1008        assert!(invoice.can_be_modified()); // Rejected peut être modifié
1009    }
1010
1011    #[test]
1012    fn test_cannot_modify_approved_invoice() {
1013        let org_id = Uuid::new_v4();
1014        let building_id = Uuid::new_v4();
1015        let syndic_id = Uuid::new_v4();
1016
1017        let mut invoice = Expense::new_with_vat(
1018            org_id,
1019            building_id,
1020            ExpenseCategory::Maintenance,
1021            "Test".to_string(),
1022            1000.0,
1023            21.0,
1024            Utc::now(),
1025            None,
1026            None,
1027            None,
1028            None, // account_code
1029        )
1030        .unwrap();
1031
1032        invoice.submit_for_approval().unwrap();
1033        invoice.approve(syndic_id).unwrap();
1034
1035        assert!(!invoice.can_be_modified()); // Approved ne peut PAS être modifié
1036    }
1037
1038    #[test]
1039    fn test_mark_as_paid_sets_paid_date() {
1040        let org_id = Uuid::new_v4();
1041        let building_id = Uuid::new_v4();
1042        let syndic_id = Uuid::new_v4();
1043
1044        let mut expense = Expense::new(
1045            org_id,
1046            building_id,
1047            ExpenseCategory::Maintenance,
1048            "Test".to_string(),
1049            100.0,
1050            Utc::now(),
1051            None,
1052            None,
1053            None, // account_code
1054        )
1055        .unwrap();
1056
1057        // Follow approval workflow: Draft → Submit → Approve → Pay
1058        expense.submit_for_approval().unwrap();
1059        expense.approve(syndic_id).unwrap();
1060
1061        assert!(expense.paid_date.is_none());
1062        expense.mark_as_paid().unwrap();
1063        assert!(expense.paid_date.is_some());
1064        assert!(expense.is_paid());
1065    }
1066
1067    #[test]
1068    fn test_workflow_complete_cycle() {
1069        // Test du cycle complet : Draft → Submit → Approve → Pay
1070        let org_id = Uuid::new_v4();
1071        let building_id = Uuid::new_v4();
1072        let syndic_id = Uuid::new_v4();
1073
1074        let mut invoice = Expense::new_with_vat(
1075            org_id,
1076            building_id,
1077            ExpenseCategory::Maintenance,
1078            "Entretien annuel".to_string(),
1079            2000.0,
1080            21.0,
1081            Utc::now(),
1082            Some(Utc::now() + chrono::Duration::days(30)),
1083            Some("MaintenancePro".to_string()),
1084            Some("INV-2025-100".to_string()),
1085            None, // account_code
1086        )
1087        .unwrap();
1088
1089        // Étape 1: Draft
1090        assert_eq!(invoice.approval_status, ApprovalStatus::Draft);
1091        assert!(invoice.can_be_modified());
1092
1093        // Étape 2: Soumettre
1094        invoice.submit_for_approval().unwrap();
1095        assert_eq!(invoice.approval_status, ApprovalStatus::PendingApproval);
1096        assert!(!invoice.can_be_modified());
1097
1098        // Étape 3: Approuver
1099        invoice.approve(syndic_id).unwrap();
1100        assert_eq!(invoice.approval_status, ApprovalStatus::Approved);
1101        assert!(invoice.is_approved());
1102
1103        // Étape 4: Payer
1104        invoice.mark_as_paid().unwrap();
1105        assert!(invoice.is_paid());
1106        assert!(invoice.paid_date.is_some());
1107    }
1108}