1use chrono::{DateTime, Utc};
6use rust_decimal::Decimal;
7use rust_decimal_macros::dec;
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
13pub enum ExpenseCategory {
14 Maintenance, Repairs, Insurance, Utilities, Cleaning, Administration, Works, Other,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
26#[serde(rename_all = "snake_case")]
27pub enum PaymentStatus {
28 Pending,
29 Paid,
30 Overdue,
31 Cancelled,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
36#[serde(rename_all = "snake_case")]
37pub enum ApprovalStatus {
38 Draft, PendingApproval, Approved, Rejected, }
43
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
49pub struct Expense {
50 pub id: Uuid,
51 pub acp_id: Uuid,
59 pub organization_id: Uuid,
66 pub building_id: Uuid,
67 pub category: ExpenseCategory,
68 pub description: String,
69
70 pub amount: Decimal, pub amount_excl_vat: Option<Decimal>, pub vat_rate: Option<Decimal>, pub vat_amount: Option<Decimal>, pub amount_incl_vat: Option<Decimal>, 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,
85 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,
92 pub supplier: Option<String>,
93 pub invoice_number: Option<String>,
94 pub account_code: Option<String>,
97 pub contractor_report_id: Option<Uuid>,
100 pub created_at: DateTime<Utc>,
101 pub updated_at: DateTime<Utc>,
102}
103
104impl Expense {
105 #[allow(clippy::too_many_arguments)]
106 pub fn new(
107 acp_id: Uuid,
108 organization_id: Uuid,
109 building_id: Uuid,
110 category: ExpenseCategory,
111 description: String,
112 amount: Decimal,
113 expense_date: DateTime<Utc>,
114 supplier: Option<String>,
115 invoice_number: Option<String>,
116 account_code: Option<String>,
117 ) -> Result<Self, String> {
118 if description.is_empty() {
119 return Err("Description cannot be empty".to_string());
120 }
121 if amount <= Decimal::ZERO {
122 return Err("Amount must be greater than 0".to_string());
123 }
124
125 if let Some(ref code) = account_code {
127 if code.is_empty() {
128 return Err("Account code cannot be empty if provided".to_string());
129 }
130 if code.len() > 40 {
132 return Err("Account code cannot exceed 40 characters".to_string());
133 }
134 }
135
136 let now = Utc::now();
137 Ok(Self {
138 id: Uuid::new_v4(),
139 acp_id,
140 organization_id,
141 building_id,
142 category,
143 description,
144 amount,
145 amount_excl_vat: None,
146 vat_rate: None,
147 vat_amount: None,
148 amount_incl_vat: Some(amount), expense_date,
150 invoice_date: None,
151 due_date: None,
152 paid_date: None,
153 approval_status: ApprovalStatus::Draft, submitted_at: None,
155 approved_by: None,
156 approved_at: None,
157 rejection_reason: None,
158 payment_status: PaymentStatus::Pending,
159 supplier,
160 invoice_number,
161 account_code,
162 contractor_report_id: None,
163 created_at: now,
164 updated_at: now,
165 })
166 }
167
168 #[allow(clippy::too_many_arguments)]
170 pub fn new_with_vat(
171 acp_id: Uuid,
172 organization_id: Uuid,
173 building_id: Uuid,
174 category: ExpenseCategory,
175 description: String,
176 amount_excl_vat: Decimal,
177 vat_rate: Decimal,
178 invoice_date: DateTime<Utc>,
179 due_date: Option<DateTime<Utc>>,
180 supplier: Option<String>,
181 invoice_number: Option<String>,
182 account_code: Option<String>,
183 ) -> Result<Self, String> {
184 if description.is_empty() {
185 return Err("Description cannot be empty".to_string());
186 }
187 if amount_excl_vat <= Decimal::ZERO {
188 return Err("Amount (excl. VAT) must be greater than 0".to_string());
189 }
190 if vat_rate < Decimal::ZERO || vat_rate > dec!(100) {
191 return Err("VAT rate must be between 0 and 100".to_string());
192 }
193
194 let vat_amount = (amount_excl_vat * vat_rate) / dec!(100);
196 let amount_incl_vat = amount_excl_vat + vat_amount;
197
198 let now = Utc::now();
199 Ok(Self {
200 id: Uuid::new_v4(),
201 acp_id,
202 organization_id,
203 building_id,
204 category,
205 description,
206 amount: amount_incl_vat, amount_excl_vat: Some(amount_excl_vat),
208 vat_rate: Some(vat_rate),
209 vat_amount: Some(vat_amount),
210 amount_incl_vat: Some(amount_incl_vat),
211 expense_date: invoice_date, invoice_date: Some(invoice_date),
213 due_date,
214 paid_date: None,
215 approval_status: ApprovalStatus::Draft,
216 submitted_at: None,
217 approved_by: None,
218 approved_at: None,
219 rejection_reason: None,
220 payment_status: PaymentStatus::Pending,
221 supplier,
222 invoice_number,
223 account_code,
224 contractor_report_id: None,
225 created_at: now,
226 updated_at: now,
227 })
228 }
229
230 pub fn recalculate_vat(&mut self) -> Result<(), String> {
232 if let (Some(amount_excl_vat), Some(vat_rate)) = (self.amount_excl_vat, self.vat_rate) {
233 if amount_excl_vat <= Decimal::ZERO {
234 return Err("Amount (excl. VAT) must be greater than 0".to_string());
235 }
236 if vat_rate < Decimal::ZERO || vat_rate > dec!(100) {
237 return Err("VAT rate must be between 0 and 100".to_string());
238 }
239
240 let vat_amount = (amount_excl_vat * vat_rate) / dec!(100);
241 let amount_incl_vat = amount_excl_vat + vat_amount;
242
243 self.vat_amount = Some(vat_amount);
244 self.amount_incl_vat = Some(amount_incl_vat);
245 self.amount = amount_incl_vat; self.updated_at = Utc::now();
247 Ok(())
248 } else {
249 Err("Cannot recalculate VAT: amount_excl_vat or vat_rate is missing".to_string())
250 }
251 }
252
253 pub fn submit_for_approval(&mut self) -> Result<(), String> {
255 match self.approval_status {
256 ApprovalStatus::Draft => {
257 self.approval_status = ApprovalStatus::PendingApproval;
258 self.submitted_at = Some(Utc::now());
259 self.updated_at = Utc::now();
260 Ok(())
261 }
262 ApprovalStatus::Rejected => {
263 self.approval_status = ApprovalStatus::PendingApproval;
265 self.submitted_at = Some(Utc::now());
266 self.rejection_reason = None; self.updated_at = Utc::now();
268 Ok(())
269 }
270 ApprovalStatus::PendingApproval => {
271 Err("Invoice is already pending approval".to_string())
272 }
273 ApprovalStatus::Approved => Err("Cannot submit an approved invoice".to_string()),
274 }
275 }
276
277 pub fn approve(&mut self, approved_by_user_id: Uuid) -> Result<(), String> {
280 if self.category == ExpenseCategory::Works && self.contractor_report_id.is_none() {
282 return Err(
283 "Work expenses require a validated contractor report before approval".to_string(),
284 );
285 }
286
287 match self.approval_status {
288 ApprovalStatus::PendingApproval => {
289 self.approval_status = ApprovalStatus::Approved;
290 self.approved_by = Some(approved_by_user_id);
291 self.approved_at = Some(Utc::now());
292 self.updated_at = Utc::now();
293 Ok(())
294 }
295 ApprovalStatus::Draft => {
296 Err("Cannot approve a draft invoice (must be submitted first)".to_string())
297 }
298 ApprovalStatus::Approved => Err("Invoice is already approved".to_string()),
299 ApprovalStatus::Rejected => {
300 Err("Cannot approve a rejected invoice (resubmit first)".to_string())
301 }
302 }
303 }
304
305 pub fn reject(&mut self, rejected_by_user_id: Uuid, reason: String) -> Result<(), String> {
307 if reason.trim().is_empty() {
308 return Err("Rejection reason cannot be empty".to_string());
309 }
310
311 match self.approval_status {
312 ApprovalStatus::PendingApproval => {
313 self.approval_status = ApprovalStatus::Rejected;
314 self.approved_by = Some(rejected_by_user_id); self.approved_at = Some(Utc::now());
316 self.rejection_reason = Some(reason);
317 self.updated_at = Utc::now();
318 Ok(())
319 }
320 ApprovalStatus::Draft => {
321 Err("Cannot reject a draft invoice (not submitted)".to_string())
322 }
323 ApprovalStatus::Approved => Err("Cannot reject an approved invoice".to_string()),
324 ApprovalStatus::Rejected => Err("Invoice is already rejected".to_string()),
325 }
326 }
327
328 pub fn can_be_modified(&self) -> bool {
330 matches!(
331 self.approval_status,
332 ApprovalStatus::Draft | ApprovalStatus::Rejected
333 )
334 }
335
336 pub fn is_approved(&self) -> bool {
338 self.approval_status == ApprovalStatus::Approved
339 }
340
341 pub fn mark_as_paid(&mut self) -> Result<(), String> {
342 if self.approval_status != ApprovalStatus::Approved {
344 return Err(format!(
345 "Cannot mark expense as paid: invoice must be approved first (current status: {:?})",
346 self.approval_status
347 ));
348 }
349
350 match self.payment_status {
351 PaymentStatus::Pending | PaymentStatus::Overdue => {
352 self.payment_status = PaymentStatus::Paid;
353 self.paid_date = Some(Utc::now()); self.updated_at = Utc::now();
355 Ok(())
356 }
357 PaymentStatus::Paid => Err("Expense is already paid".to_string()),
358 PaymentStatus::Cancelled => Err("Cannot mark a cancelled expense as paid".to_string()),
359 }
360 }
361
362 pub fn mark_as_overdue(&mut self) -> Result<(), String> {
363 match self.payment_status {
364 PaymentStatus::Pending => {
365 self.payment_status = PaymentStatus::Overdue;
366 self.updated_at = Utc::now();
367 Ok(())
368 }
369 PaymentStatus::Overdue => Err("Expense is already overdue".to_string()),
370 PaymentStatus::Paid => Err("Cannot mark a paid expense as overdue".to_string()),
371 PaymentStatus::Cancelled => {
372 Err("Cannot mark a cancelled expense as overdue".to_string())
373 }
374 }
375 }
376
377 pub fn cancel(&mut self) -> Result<(), String> {
378 match self.payment_status {
379 PaymentStatus::Pending | PaymentStatus::Overdue => {
380 self.payment_status = PaymentStatus::Cancelled;
381 self.updated_at = Utc::now();
382 Ok(())
383 }
384 PaymentStatus::Paid => Err("Cannot cancel a paid expense".to_string()),
385 PaymentStatus::Cancelled => Err("Expense is already cancelled".to_string()),
386 }
387 }
388
389 pub fn set_contractor_report(&mut self, contractor_report_id: Uuid) -> Result<(), String> {
391 if self.category != ExpenseCategory::Works {
392 return Err(
393 "Contractor report can only be linked to Works category expenses".to_string(),
394 );
395 }
396 self.contractor_report_id = Some(contractor_report_id);
397 self.updated_at = Utc::now();
398 Ok(())
399 }
400
401 pub fn reactivate(&mut self) -> Result<(), String> {
402 match self.payment_status {
403 PaymentStatus::Cancelled => {
404 self.payment_status = PaymentStatus::Pending;
405 self.updated_at = Utc::now();
406 Ok(())
407 }
408 _ => Err("Can only reactivate cancelled expenses".to_string()),
409 }
410 }
411
412 pub fn unpay(&mut self) -> Result<(), String> {
413 match self.payment_status {
414 PaymentStatus::Paid => {
415 self.payment_status = PaymentStatus::Pending;
416 self.updated_at = Utc::now();
417 Ok(())
418 }
419 _ => Err("Can only unpay paid expenses".to_string()),
420 }
421 }
422
423 pub fn is_paid(&self) -> bool {
424 self.payment_status == PaymentStatus::Paid
425 }
426}
427
428impl crate::domain::services::PieceDeGestion for Expense {
429 fn acp_id(&self) -> Uuid {
430 self.acp_id
431 }
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437
438 #[test]
439 fn test_create_expense_success() {
440 let org_id = Uuid::new_v4();
441 let building_id = Uuid::new_v4();
442 let expense = Expense::new(
443 Uuid::new_v4(), org_id,
445 building_id,
446 ExpenseCategory::Maintenance,
447 "Entretien ascenseur".to_string(),
448 dec!(500),
449 Utc::now(),
450 Some("ACME Elevators".to_string()),
451 Some("INV-2024-001".to_string()),
452 Some("611002".to_string()), );
454
455 assert!(expense.is_ok());
456 let expense = expense.unwrap();
457 assert_eq!(expense.organization_id, org_id);
458 assert_eq!(expense.amount, dec!(500));
459 assert_eq!(expense.payment_status, PaymentStatus::Pending);
460 assert_eq!(expense.account_code, Some("611002".to_string()));
461 }
462
463 #[test]
464 fn test_create_expense_without_account_code() {
465 let org_id = Uuid::new_v4();
466 let building_id = Uuid::new_v4();
467 let expense = Expense::new(
468 Uuid::new_v4(), org_id,
470 building_id,
471 ExpenseCategory::Other,
472 "Miscellaneous expense".to_string(),
473 dec!(100),
474 Utc::now(),
475 None,
476 None,
477 None, );
479
480 assert!(expense.is_ok());
481 let expense = expense.unwrap();
482 assert_eq!(expense.account_code, None);
483 }
484
485 #[test]
486 fn test_create_expense_empty_account_code_fails() {
487 let org_id = Uuid::new_v4();
488 let building_id = Uuid::new_v4();
489 let expense = Expense::new(
490 Uuid::new_v4(), org_id,
492 building_id,
493 ExpenseCategory::Maintenance,
494 "Test".to_string(),
495 dec!(100),
496 Utc::now(),
497 None,
498 None,
499 Some("".to_string()), );
501
502 assert!(expense.is_err());
503 assert!(expense
504 .unwrap_err()
505 .contains("Account code cannot be empty"));
506 }
507
508 #[test]
509 fn test_create_expense_long_account_code_fails() {
510 let org_id = Uuid::new_v4();
511 let building_id = Uuid::new_v4();
512 let long_code = "a".repeat(41); let expense = Expense::new(
514 Uuid::new_v4(), org_id,
516 building_id,
517 ExpenseCategory::Maintenance,
518 "Test".to_string(),
519 dec!(100),
520 Utc::now(),
521 None,
522 None,
523 Some(long_code),
524 );
525
526 assert!(expense.is_err());
527 assert!(expense
528 .unwrap_err()
529 .contains("Account code cannot exceed 40 characters"));
530 }
531
532 #[test]
533 fn test_create_expense_negative_amount_fails() {
534 let org_id = Uuid::new_v4();
535 let building_id = Uuid::new_v4();
536 let expense = Expense::new(
537 Uuid::new_v4(), org_id,
539 building_id,
540 ExpenseCategory::Maintenance,
541 "Test".to_string(),
542 dec!(-100),
543 Utc::now(),
544 None,
545 None,
546 None,
547 );
548
549 assert!(expense.is_err());
550 }
551
552 #[test]
553 fn test_mark_expense_as_paid() {
554 let org_id = Uuid::new_v4();
555 let building_id = Uuid::new_v4();
556 let syndic_id = Uuid::new_v4();
557 let mut expense = Expense::new(
558 Uuid::new_v4(), org_id,
560 building_id,
561 ExpenseCategory::Maintenance,
562 "Test".to_string(),
563 dec!(100),
564 Utc::now(),
565 None,
566 None,
567 None,
568 )
569 .unwrap();
570
571 expense.submit_for_approval().unwrap();
573 expense.approve(syndic_id).unwrap();
574
575 assert!(!expense.is_paid());
576 let result = expense.mark_as_paid();
577 assert!(result.is_ok());
578 assert!(expense.is_paid());
579 }
580
581 #[test]
582 fn test_mark_paid_expense_as_paid_fails() {
583 let org_id = Uuid::new_v4();
584 let building_id = Uuid::new_v4();
585 let syndic_id = Uuid::new_v4();
586 let mut expense = Expense::new(
587 Uuid::new_v4(), org_id,
589 building_id,
590 ExpenseCategory::Maintenance,
591 "Test".to_string(),
592 dec!(100),
593 Utc::now(),
594 None,
595 None,
596 None,
597 )
598 .unwrap();
599
600 expense.submit_for_approval().unwrap();
602 expense.approve(syndic_id).unwrap();
603
604 expense.mark_as_paid().unwrap();
605 let result = expense.mark_as_paid();
606 assert!(result.is_err());
607 }
608
609 #[test]
610 fn test_cancel_expense() {
611 let org_id = Uuid::new_v4();
612 let building_id = Uuid::new_v4();
613 let mut expense = Expense::new(
614 Uuid::new_v4(), org_id,
616 building_id,
617 ExpenseCategory::Maintenance,
618 "Test".to_string(),
619 dec!(100),
620 Utc::now(),
621 None,
622 None,
623 None,
624 )
625 .unwrap();
626
627 let result = expense.cancel();
628 assert!(result.is_ok());
629 assert_eq!(expense.payment_status, PaymentStatus::Cancelled);
630 }
631
632 #[test]
633 fn test_reactivate_expense() {
634 let org_id = Uuid::new_v4();
635 let building_id = Uuid::new_v4();
636 let mut expense = Expense::new(
637 Uuid::new_v4(), org_id,
639 building_id,
640 ExpenseCategory::Maintenance,
641 "Test".to_string(),
642 dec!(100),
643 Utc::now(),
644 None,
645 None,
646 None,
647 )
648 .unwrap();
649
650 expense.cancel().unwrap();
651 let result = expense.reactivate();
652 assert!(result.is_ok());
653 assert_eq!(expense.payment_status, PaymentStatus::Pending);
654 }
655
656 #[test]
659 fn test_create_invoice_with_vat_success() {
660 let org_id = Uuid::new_v4();
661 let building_id = Uuid::new_v4();
662 let invoice_date = Utc::now();
663 let due_date = invoice_date + chrono::Duration::days(30);
664
665 let invoice = Expense::new_with_vat(
666 Uuid::new_v4(), org_id,
668 building_id,
669 ExpenseCategory::Maintenance,
670 "Réparation toiture".to_string(),
671 dec!(1000), dec!(21), invoice_date,
674 Some(due_date),
675 Some("BatiPro SPRL".to_string()),
676 Some("INV-2025-042".to_string()),
677 None, );
679
680 assert!(invoice.is_ok());
681 let invoice = invoice.unwrap();
682 assert_eq!(invoice.amount_excl_vat, Some(dec!(1000)));
683 assert_eq!(invoice.vat_rate, Some(dec!(21)));
684 assert_eq!(invoice.vat_amount, Some(dec!(210)));
685 assert_eq!(invoice.amount_incl_vat, Some(dec!(1210)));
686 assert_eq!(invoice.amount, dec!(1210)); assert_eq!(invoice.approval_status, ApprovalStatus::Draft);
688 }
689
690 #[test]
691 fn test_create_invoice_with_vat_6_percent() {
692 let org_id = Uuid::new_v4();
693 let building_id = Uuid::new_v4();
694
695 let invoice = Expense::new_with_vat(
696 Uuid::new_v4(), org_id,
698 building_id,
699 ExpenseCategory::Works,
700 "Rénovation énergétique".to_string(),
701 dec!(5000), dec!(6), Utc::now(),
704 None,
705 None,
706 None,
707 None, )
709 .unwrap();
710
711 assert_eq!(invoice.vat_amount, Some(dec!(300)));
712 assert_eq!(invoice.amount_incl_vat, Some(dec!(5300)));
713 }
714
715 #[test]
716 fn test_create_invoice_negative_vat_rate_fails() {
717 let org_id = Uuid::new_v4();
718 let building_id = Uuid::new_v4();
719
720 let invoice = Expense::new_with_vat(
721 Uuid::new_v4(), org_id,
723 building_id,
724 ExpenseCategory::Maintenance,
725 "Test".to_string(),
726 dec!(100),
727 dec!(-5), Utc::now(),
729 None,
730 None,
731 None,
732 None, );
734
735 assert!(invoice.is_err());
736 assert_eq!(invoice.unwrap_err(), "VAT rate must be between 0 and 100");
737 }
738
739 #[test]
740 fn test_create_invoice_vat_rate_above_100_fails() {
741 let org_id = Uuid::new_v4();
742 let building_id = Uuid::new_v4();
743
744 let invoice = Expense::new_with_vat(
745 Uuid::new_v4(), org_id,
747 building_id,
748 ExpenseCategory::Maintenance,
749 "Test".to_string(),
750 dec!(100),
751 dec!(150), Utc::now(),
753 None,
754 None,
755 None,
756 None, );
758
759 assert!(invoice.is_err());
760 }
761
762 #[test]
763 fn test_recalculate_vat_success() {
764 let org_id = Uuid::new_v4();
765 let building_id = Uuid::new_v4();
766
767 let mut invoice = Expense::new_with_vat(
768 Uuid::new_v4(), org_id,
770 building_id,
771 ExpenseCategory::Maintenance,
772 "Test".to_string(),
773 dec!(1000),
774 dec!(21),
775 Utc::now(),
776 None,
777 None,
778 None,
779 None, )
781 .unwrap();
782
783 invoice.amount_excl_vat = Some(dec!(1500));
785 let result = invoice.recalculate_vat();
786
787 assert!(result.is_ok());
788 assert_eq!(invoice.vat_amount, Some(dec!(315))); assert_eq!(invoice.amount_incl_vat, Some(dec!(1815)));
790 }
791
792 #[test]
793 fn test_recalculate_vat_without_vat_data_fails() {
794 let org_id = Uuid::new_v4();
795 let building_id = Uuid::new_v4();
796
797 let mut expense = Expense::new(
799 Uuid::new_v4(), org_id,
801 building_id,
802 ExpenseCategory::Maintenance,
803 "Test".to_string(),
804 dec!(100),
805 Utc::now(),
806 None,
807 None,
808 None, )
810 .unwrap();
811
812 let result = expense.recalculate_vat();
813 assert!(result.is_err());
814 }
815
816 #[test]
819 fn test_submit_draft_invoice_for_approval() {
820 let org_id = Uuid::new_v4();
821 let building_id = Uuid::new_v4();
822
823 let mut invoice = Expense::new_with_vat(
824 Uuid::new_v4(), org_id,
826 building_id,
827 ExpenseCategory::Maintenance,
828 "Test".to_string(),
829 dec!(1000),
830 dec!(21),
831 Utc::now(),
832 None,
833 None,
834 None,
835 None, )
837 .unwrap();
838
839 assert_eq!(invoice.approval_status, ApprovalStatus::Draft);
840 assert!(invoice.submitted_at.is_none());
841
842 let result = invoice.submit_for_approval();
843 assert!(result.is_ok());
844 assert_eq!(invoice.approval_status, ApprovalStatus::PendingApproval);
845 assert!(invoice.submitted_at.is_some());
846 }
847
848 #[test]
849 fn test_submit_already_pending_invoice_fails() {
850 let org_id = Uuid::new_v4();
851 let building_id = Uuid::new_v4();
852
853 let mut invoice = Expense::new_with_vat(
854 Uuid::new_v4(), org_id,
856 building_id,
857 ExpenseCategory::Maintenance,
858 "Test".to_string(),
859 dec!(1000),
860 dec!(21),
861 Utc::now(),
862 None,
863 None,
864 None,
865 None, )
867 .unwrap();
868
869 invoice.submit_for_approval().unwrap();
870 let result = invoice.submit_for_approval();
871
872 assert!(result.is_err());
873 assert_eq!(result.unwrap_err(), "Invoice is already pending approval");
874 }
875
876 #[test]
877 fn test_resubmit_rejected_invoice() {
878 let org_id = Uuid::new_v4();
879 let building_id = Uuid::new_v4();
880 let user_id = Uuid::new_v4();
881
882 let mut invoice = Expense::new_with_vat(
883 Uuid::new_v4(), org_id,
885 building_id,
886 ExpenseCategory::Maintenance,
887 "Test".to_string(),
888 dec!(1000),
889 dec!(21),
890 Utc::now(),
891 None,
892 None,
893 None,
894 None, )
896 .unwrap();
897
898 invoice.submit_for_approval().unwrap();
899 invoice
900 .reject(user_id, "Montant incorrect".to_string())
901 .unwrap();
902 assert_eq!(invoice.approval_status, ApprovalStatus::Rejected);
903
904 let result = invoice.submit_for_approval();
906 assert!(result.is_ok());
907 assert_eq!(invoice.approval_status, ApprovalStatus::PendingApproval);
908 assert!(invoice.rejection_reason.is_none()); }
910
911 #[test]
912 fn test_approve_pending_invoice() {
913 let org_id = Uuid::new_v4();
914 let building_id = Uuid::new_v4();
915 let syndic_id = Uuid::new_v4();
916
917 let mut invoice = Expense::new_with_vat(
918 Uuid::new_v4(), org_id,
920 building_id,
921 ExpenseCategory::Maintenance,
922 "Test".to_string(),
923 dec!(1000),
924 dec!(21),
925 Utc::now(),
926 None,
927 None,
928 None,
929 None, )
931 .unwrap();
932
933 invoice.submit_for_approval().unwrap();
934 let result = invoice.approve(syndic_id);
935
936 assert!(result.is_ok());
937 assert_eq!(invoice.approval_status, ApprovalStatus::Approved);
938 assert_eq!(invoice.approved_by, Some(syndic_id));
939 assert!(invoice.approved_at.is_some());
940 assert!(invoice.is_approved());
941 }
942
943 #[test]
944 fn test_approve_draft_invoice_fails() {
945 let org_id = Uuid::new_v4();
946 let building_id = Uuid::new_v4();
947 let syndic_id = Uuid::new_v4();
948
949 let mut invoice = Expense::new_with_vat(
950 Uuid::new_v4(), org_id,
952 building_id,
953 ExpenseCategory::Maintenance,
954 "Test".to_string(),
955 dec!(1000),
956 dec!(21),
957 Utc::now(),
958 None,
959 None,
960 None,
961 None, )
963 .unwrap();
964
965 let result = invoice.approve(syndic_id);
967
968 assert!(result.is_err());
969 assert!(result.unwrap_err().contains("must be submitted first"));
970 }
971
972 #[test]
973 fn test_reject_pending_invoice_with_reason() {
974 let org_id = Uuid::new_v4();
975 let building_id = Uuid::new_v4();
976 let syndic_id = Uuid::new_v4();
977
978 let mut invoice = Expense::new_with_vat(
979 Uuid::new_v4(), org_id,
981 building_id,
982 ExpenseCategory::Maintenance,
983 "Test".to_string(),
984 dec!(1000),
985 dec!(21),
986 Utc::now(),
987 None,
988 None,
989 None,
990 None, )
992 .unwrap();
993
994 invoice.submit_for_approval().unwrap();
995 let result = invoice.reject(
996 syndic_id,
997 "Le montant ne correspond pas au devis".to_string(),
998 );
999
1000 assert!(result.is_ok());
1001 assert_eq!(invoice.approval_status, ApprovalStatus::Rejected);
1002 assert_eq!(invoice.approved_by, Some(syndic_id));
1003 assert_eq!(
1004 invoice.rejection_reason,
1005 Some("Le montant ne correspond pas au devis".to_string())
1006 );
1007 }
1008
1009 #[test]
1010 fn test_reject_invoice_without_reason_fails() {
1011 let org_id = Uuid::new_v4();
1012 let building_id = Uuid::new_v4();
1013 let syndic_id = Uuid::new_v4();
1014
1015 let mut invoice = Expense::new_with_vat(
1016 Uuid::new_v4(), org_id,
1018 building_id,
1019 ExpenseCategory::Maintenance,
1020 "Test".to_string(),
1021 dec!(1000),
1022 dec!(21),
1023 Utc::now(),
1024 None,
1025 None,
1026 None,
1027 None, )
1029 .unwrap();
1030
1031 invoice.submit_for_approval().unwrap();
1032 let result = invoice.reject(syndic_id, "".to_string());
1033
1034 assert!(result.is_err());
1035 assert_eq!(result.unwrap_err(), "Rejection reason cannot be empty");
1036 }
1037
1038 #[test]
1039 fn test_can_be_modified_draft() {
1040 let org_id = Uuid::new_v4();
1041 let building_id = Uuid::new_v4();
1042
1043 let invoice = Expense::new_with_vat(
1044 Uuid::new_v4(), org_id,
1046 building_id,
1047 ExpenseCategory::Maintenance,
1048 "Test".to_string(),
1049 dec!(1000),
1050 dec!(21),
1051 Utc::now(),
1052 None,
1053 None,
1054 None,
1055 None, )
1057 .unwrap();
1058
1059 assert!(invoice.can_be_modified()); }
1061
1062 #[test]
1063 fn test_can_be_modified_rejected() {
1064 let org_id = Uuid::new_v4();
1065 let building_id = Uuid::new_v4();
1066 let syndic_id = Uuid::new_v4();
1067
1068 let mut invoice = Expense::new_with_vat(
1069 Uuid::new_v4(), org_id,
1071 building_id,
1072 ExpenseCategory::Maintenance,
1073 "Test".to_string(),
1074 dec!(1000),
1075 dec!(21),
1076 Utc::now(),
1077 None,
1078 None,
1079 None,
1080 None, )
1082 .unwrap();
1083
1084 invoice.submit_for_approval().unwrap();
1085 invoice.reject(syndic_id, "Erreur".to_string()).unwrap();
1086
1087 assert!(invoice.can_be_modified()); }
1089
1090 #[test]
1091 fn test_cannot_modify_approved_invoice() {
1092 let org_id = Uuid::new_v4();
1093 let building_id = Uuid::new_v4();
1094 let syndic_id = Uuid::new_v4();
1095
1096 let mut invoice = Expense::new_with_vat(
1097 Uuid::new_v4(), org_id,
1099 building_id,
1100 ExpenseCategory::Maintenance,
1101 "Test".to_string(),
1102 dec!(1000),
1103 dec!(21),
1104 Utc::now(),
1105 None,
1106 None,
1107 None,
1108 None, )
1110 .unwrap();
1111
1112 invoice.submit_for_approval().unwrap();
1113 invoice.approve(syndic_id).unwrap();
1114
1115 assert!(!invoice.can_be_modified()); }
1117
1118 #[test]
1119 fn test_mark_as_paid_sets_paid_date() {
1120 let org_id = Uuid::new_v4();
1121 let building_id = Uuid::new_v4();
1122 let syndic_id = Uuid::new_v4();
1123
1124 let mut expense = Expense::new(
1125 Uuid::new_v4(), org_id,
1127 building_id,
1128 ExpenseCategory::Maintenance,
1129 "Test".to_string(),
1130 dec!(100),
1131 Utc::now(),
1132 None,
1133 None,
1134 None, )
1136 .unwrap();
1137
1138 expense.submit_for_approval().unwrap();
1140 expense.approve(syndic_id).unwrap();
1141
1142 assert!(expense.paid_date.is_none());
1143 expense.mark_as_paid().unwrap();
1144 assert!(expense.paid_date.is_some());
1145 assert!(expense.is_paid());
1146 }
1147
1148 #[test]
1149 fn test_workflow_complete_cycle() {
1150 let org_id = Uuid::new_v4();
1152 let building_id = Uuid::new_v4();
1153 let syndic_id = Uuid::new_v4();
1154
1155 let mut invoice = Expense::new_with_vat(
1156 Uuid::new_v4(), org_id,
1158 building_id,
1159 ExpenseCategory::Maintenance,
1160 "Entretien annuel".to_string(),
1161 dec!(2000),
1162 dec!(21),
1163 Utc::now(),
1164 Some(Utc::now() + chrono::Duration::days(30)),
1165 Some("MaintenancePro".to_string()),
1166 Some("INV-2025-100".to_string()),
1167 None, )
1169 .unwrap();
1170
1171 assert_eq!(invoice.approval_status, ApprovalStatus::Draft);
1173 assert!(invoice.can_be_modified());
1174
1175 invoice.submit_for_approval().unwrap();
1177 assert_eq!(invoice.approval_status, ApprovalStatus::PendingApproval);
1178 assert!(!invoice.can_be_modified());
1179
1180 invoice.approve(syndic_id).unwrap();
1182 assert_eq!(invoice.approval_status, ApprovalStatus::Approved);
1183 assert!(invoice.is_approved());
1184
1185 invoice.mark_as_paid().unwrap();
1187 assert!(invoice.is_paid());
1188 assert!(invoice.paid_date.is_some());
1189 }
1190
1191 #[test]
1192 fn test_approve_works_expense_without_contractor_report_fails() {
1193 let org_id = Uuid::new_v4();
1195 let building_id = Uuid::new_v4();
1196 let syndic_id = Uuid::new_v4();
1197
1198 let mut expense = Expense::new(
1199 Uuid::new_v4(), org_id,
1201 building_id,
1202 ExpenseCategory::Works,
1203 "Réparation toiture".to_string(),
1204 dec!(5000),
1205 Utc::now(),
1206 Some("Construction SPRL".to_string()),
1207 Some("DV-2025-001".to_string()),
1208 None,
1209 )
1210 .unwrap();
1211
1212 expense.submit_for_approval().unwrap();
1213
1214 let result = expense.approve(syndic_id);
1216 assert!(result.is_err());
1217 assert!(result.unwrap_err().contains("contractor report"));
1218 }
1219
1220 #[test]
1221 fn test_set_contractor_report_for_works_expense() {
1222 let org_id = Uuid::new_v4();
1224 let building_id = Uuid::new_v4();
1225 let contractor_report_id = Uuid::new_v4();
1226
1227 let mut expense = Expense::new(
1228 Uuid::new_v4(), org_id,
1230 building_id,
1231 ExpenseCategory::Works,
1232 "Réparation toiture".to_string(),
1233 dec!(5000),
1234 Utc::now(),
1235 Some("Construction SPRL".to_string()),
1236 Some("DV-2025-001".to_string()),
1237 None,
1238 )
1239 .unwrap();
1240
1241 let result = expense.set_contractor_report(contractor_report_id);
1243 assert!(result.is_ok());
1244 assert_eq!(expense.contractor_report_id, Some(contractor_report_id));
1245 }
1246
1247 #[test]
1248 fn test_set_contractor_report_for_non_works_fails() {
1249 let org_id = Uuid::new_v4();
1251 let building_id = Uuid::new_v4();
1252 let contractor_report_id = Uuid::new_v4();
1253
1254 let mut expense = Expense::new(
1255 Uuid::new_v4(), org_id,
1257 building_id,
1258 ExpenseCategory::Maintenance,
1259 "Maintenance".to_string(),
1260 dec!(1000),
1261 Utc::now(),
1262 None,
1263 None,
1264 None,
1265 )
1266 .unwrap();
1267
1268 let result = expense.set_contractor_report(contractor_report_id);
1270 assert!(result.is_err());
1271 assert!(result.unwrap_err().contains("Works category"));
1272 }
1273
1274 #[test]
1275 fn test_approve_works_expense_with_contractor_report_succeeds() {
1276 let org_id = Uuid::new_v4();
1278 let building_id = Uuid::new_v4();
1279 let syndic_id = Uuid::new_v4();
1280 let contractor_report_id = Uuid::new_v4();
1281
1282 let mut expense = Expense::new(
1283 Uuid::new_v4(), org_id,
1285 building_id,
1286 ExpenseCategory::Works,
1287 "Réparation toiture".to_string(),
1288 dec!(5000),
1289 Utc::now(),
1290 Some("Construction SPRL".to_string()),
1291 Some("DV-2025-001".to_string()),
1292 None,
1293 )
1294 .unwrap();
1295
1296 expense.set_contractor_report(contractor_report_id).unwrap();
1298
1299 expense.submit_for_approval().unwrap();
1301 let result = expense.approve(syndic_id);
1302
1303 assert!(result.is_ok());
1304 assert_eq!(expense.approval_status, ApprovalStatus::Approved);
1305 }
1306}