1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
7pub enum ExpenseCategory {
8 Maintenance, Repairs, Insurance, Utilities, Cleaning, Administration, Works, Other,
16}
17
18#[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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
30#[serde(rename_all = "snake_case")]
31pub enum ApprovalStatus {
32 Draft, PendingApproval, Approved, Rejected, }
37
38#[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 pub amount: f64, pub amount_excl_vat: Option<f64>, pub vat_rate: Option<f64>, pub vat_amount: Option<f64>, pub amount_incl_vat: Option<f64>, pub expense_date: DateTime<Utc>, pub invoice_date: Option<DateTime<Utc>>, pub due_date: Option<DateTime<Utc>>, pub paid_date: Option<DateTime<Utc>>, pub approval_status: ApprovalStatus,
65 pub submitted_at: Option<DateTime<Utc>>, pub approved_by: Option<Uuid>, pub approved_at: Option<DateTime<Utc>>, pub rejection_reason: Option<String>, pub payment_status: PaymentStatus,
72 pub supplier: Option<String>,
73 pub invoice_number: Option<String>,
74 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 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 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), expense_date,
125 invoice_date: None,
126 due_date: None,
127 paid_date: None,
128 approval_status: ApprovalStatus::Draft, 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 #[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 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, 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, 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 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; 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 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 self.approval_status = ApprovalStatus::PendingApproval;
236 self.submitted_at = Some(Utc::now());
237 self.rejection_reason = None; 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 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 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); 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 pub fn can_be_modified(&self) -> bool {
293 matches!(
294 self.approval_status,
295 ApprovalStatus::Draft | ApprovalStatus::Rejected
296 )
297 }
298
299 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 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()); 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()), );
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, );
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()), );
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); 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 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 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 #[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, 21.0, invoice_date,
609 Some(due_date),
610 Some("BatiPro SPRL".to_string()),
611 Some("INV-2025-042".to_string()),
612 None, );
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); 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, 6.0, Utc::now(),
638 None,
639 None,
640 None,
641 None, )
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, Utc::now(),
662 None,
663 None,
664 None,
665 None, );
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, Utc::now(),
685 None,
686 None,
687 None,
688 None, );
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, )
712 .unwrap();
713
714 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)); 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 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, )
740 .unwrap();
741
742 let result = expense.recalculate_vat();
743 assert!(result.is_err());
744 }
745
746 #[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, )
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, )
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, )
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 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()); }
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, )
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, )
888 .unwrap();
889
890 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, )
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, )
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, )
979 .unwrap();
980
981 assert!(invoice.can_be_modified()); }
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, )
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()); }
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, )
1030 .unwrap();
1031
1032 invoice.submit_for_approval().unwrap();
1033 invoice.approve(syndic_id).unwrap();
1034
1035 assert!(!invoice.can_be_modified()); }
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, )
1055 .unwrap();
1056
1057 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 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, )
1087 .unwrap();
1088
1089 assert_eq!(invoice.approval_status, ApprovalStatus::Draft);
1091 assert!(invoice.can_be_modified());
1092
1093 invoice.submit_for_approval().unwrap();
1095 assert_eq!(invoice.approval_status, ApprovalStatus::PendingApproval);
1096 assert!(!invoice.can_be_modified());
1097
1098 invoice.approve(syndic_id).unwrap();
1100 assert_eq!(invoice.approval_status, ApprovalStatus::Approved);
1101 assert!(invoice.is_approved());
1102
1103 invoice.mark_as_paid().unwrap();
1105 assert!(invoice.is_paid());
1106 assert!(invoice.paid_date.is_some());
1107 }
1108}