1use crate::application::dto::{
2 ApproveInvoiceDto, CreateExpenseDto, CreateInvoiceDraftDto, ExpenseFilters, ExpenseResponseDto,
3 InvoiceResponseDto, PageRequest, PendingInvoicesListDto, RejectInvoiceDto, SortOrder,
4 SubmitForApprovalDto, UpdateInvoiceDraftDto,
5};
6use crate::application::ports::ExpenseRepository;
7use crate::application::services::expense_accounting_service::ExpenseAccountingService;
8use crate::domain::entities::{ApprovalStatus, Expense};
9use chrono::DateTime;
10use std::sync::Arc;
11use uuid::Uuid;
12
13pub struct ExpenseUseCases {
14 repository: Arc<dyn ExpenseRepository>,
15 accounting_service: Option<Arc<ExpenseAccountingService>>,
16}
17
18impl ExpenseUseCases {
19 pub fn new(repository: Arc<dyn ExpenseRepository>) -> Self {
20 Self {
21 repository,
22 accounting_service: None,
23 }
24 }
25
26 pub fn with_accounting_service(
27 repository: Arc<dyn ExpenseRepository>,
28 accounting_service: Arc<ExpenseAccountingService>,
29 ) -> Self {
30 Self {
31 repository,
32 accounting_service: Some(accounting_service),
33 }
34 }
35
36 pub async fn create_expense(
37 &self,
38 dto: CreateExpenseDto,
39 ) -> Result<ExpenseResponseDto, String> {
40 let organization_id = Uuid::parse_str(&dto.organization_id)
41 .map_err(|_| "Invalid organization_id format".to_string())?;
42 let building_id = Uuid::parse_str(&dto.building_id)
43 .map_err(|_| "Invalid building ID format".to_string())?;
44
45 let expense_date = DateTime::parse_from_rfc3339(&dto.expense_date)
46 .map_err(|_| "Invalid date format".to_string())?
47 .with_timezone(&chrono::Utc);
48
49 let expense = Expense::new(
50 organization_id,
51 building_id,
52 dto.category,
53 dto.description,
54 dto.amount,
55 expense_date,
56 dto.supplier,
57 dto.invoice_number,
58 dto.account_code,
59 )?;
60
61 let created = self.repository.create(&expense).await?;
62 Ok(self.to_response_dto(&created))
63 }
64
65 pub async fn get_expense(&self, id: Uuid) -> Result<Option<ExpenseResponseDto>, String> {
66 let expense = self.repository.find_by_id(id).await?;
67 Ok(expense.map(|e| self.to_response_dto(&e)))
68 }
69
70 pub async fn list_expenses_by_building(
71 &self,
72 building_id: Uuid,
73 ) -> Result<Vec<ExpenseResponseDto>, String> {
74 let expenses = self.repository.find_by_building(building_id).await?;
75 Ok(expenses.iter().map(|e| self.to_response_dto(e)).collect())
76 }
77
78 pub async fn list_expenses_paginated(
79 &self,
80 page_request: &PageRequest,
81 organization_id: Option<Uuid>,
82 ) -> Result<(Vec<ExpenseResponseDto>, i64), String> {
83 let filters = ExpenseFilters {
84 organization_id,
85 ..Default::default()
86 };
87
88 let (expenses, total) = self
89 .repository
90 .find_all_paginated(page_request, &filters)
91 .await?;
92
93 let dtos = expenses.iter().map(|e| self.to_response_dto(e)).collect();
94 Ok((dtos, total))
95 }
96
97 pub async fn mark_as_paid(&self, id: Uuid) -> Result<ExpenseResponseDto, String> {
101 let mut expense = self
102 .repository
103 .find_by_id(id)
104 .await?
105 .ok_or_else(|| "Expense not found".to_string())?;
106
107 expense.mark_as_paid()?;
108
109 let updated = self.repository.update(&expense).await?;
110
111 if let Some(ref accounting_service) = self.accounting_service {
113 if let Err(e) = accounting_service
114 .generate_payment_entry(&updated, None, None)
115 .await
116 {
117 log::warn!(
118 "Failed to generate payment journal entry for expense {}: {}",
119 updated.id,
120 e
121 );
122 }
125 }
126
127 Ok(self.to_response_dto(&updated))
128 }
129
130 pub async fn mark_as_overdue(&self, id: Uuid) -> Result<ExpenseResponseDto, String> {
131 let mut expense = self
132 .repository
133 .find_by_id(id)
134 .await?
135 .ok_or_else(|| "Expense not found".to_string())?;
136
137 expense.mark_as_overdue()?;
138
139 let updated = self.repository.update(&expense).await?;
140 Ok(self.to_response_dto(&updated))
141 }
142
143 pub async fn cancel_expense(&self, id: Uuid) -> Result<ExpenseResponseDto, String> {
144 let mut expense = self
145 .repository
146 .find_by_id(id)
147 .await?
148 .ok_or_else(|| "Expense not found".to_string())?;
149
150 expense.cancel()?;
151
152 let updated = self.repository.update(&expense).await?;
153 Ok(self.to_response_dto(&updated))
154 }
155
156 pub async fn reactivate_expense(&self, id: Uuid) -> Result<ExpenseResponseDto, String> {
157 let mut expense = self
158 .repository
159 .find_by_id(id)
160 .await?
161 .ok_or_else(|| "Expense not found".to_string())?;
162
163 expense.reactivate()?;
164
165 let updated = self.repository.update(&expense).await?;
166 Ok(self.to_response_dto(&updated))
167 }
168
169 pub async fn unpay_expense(&self, id: Uuid) -> Result<ExpenseResponseDto, String> {
170 let mut expense = self
171 .repository
172 .find_by_id(id)
173 .await?
174 .ok_or_else(|| "Expense not found".to_string())?;
175
176 expense.unpay()?;
177
178 let updated = self.repository.update(&expense).await?;
179 Ok(self.to_response_dto(&updated))
180 }
181
182 pub async fn create_invoice_draft(
186 &self,
187 dto: CreateInvoiceDraftDto,
188 ) -> Result<InvoiceResponseDto, String> {
189 let organization_id = Uuid::parse_str(&dto.organization_id)
190 .map_err(|_| "Invalid organization_id format".to_string())?;
191 let building_id = Uuid::parse_str(&dto.building_id)
192 .map_err(|_| "Invalid building ID format".to_string())?;
193
194 let invoice_date = DateTime::parse_from_rfc3339(&dto.invoice_date)
195 .map_err(|_| "Invalid invoice_date format".to_string())?
196 .with_timezone(&chrono::Utc);
197
198 let due_date = dto
199 .due_date
200 .map(|d| {
201 DateTime::parse_from_rfc3339(&d)
202 .map_err(|_| "Invalid due_date format".to_string())
203 .map(|dt| dt.with_timezone(&chrono::Utc))
204 })
205 .transpose()?;
206
207 let invoice = Expense::new_with_vat(
208 organization_id,
209 building_id,
210 dto.category,
211 dto.description,
212 dto.amount_excl_vat,
213 dto.vat_rate,
214 invoice_date,
215 due_date,
216 dto.supplier,
217 dto.invoice_number,
218 None, )?;
220
221 let created = self.repository.create(&invoice).await?;
222 Ok(self.to_invoice_response_dto(&created))
223 }
224
225 pub async fn update_invoice_draft(
227 &self,
228 invoice_id: Uuid,
229 dto: UpdateInvoiceDraftDto,
230 ) -> Result<InvoiceResponseDto, String> {
231 let mut invoice = self
232 .repository
233 .find_by_id(invoice_id)
234 .await?
235 .ok_or_else(|| "Invoice not found".to_string())?;
236
237 if !invoice.can_be_modified() {
239 return Err(format!(
240 "Invoice cannot be modified (status: {:?})",
241 invoice.approval_status
242 ));
243 }
244
245 if let Some(desc) = dto.description {
247 invoice.description = desc;
248 }
249 if let Some(cat) = dto.category {
250 invoice.category = cat;
251 }
252 if let Some(amount_ht) = dto.amount_excl_vat {
253 invoice.amount_excl_vat = Some(amount_ht);
254 }
255 if let Some(vat_rate) = dto.vat_rate {
256 invoice.vat_rate = Some(vat_rate);
257 }
258
259 if dto.amount_excl_vat.is_some() || dto.vat_rate.is_some() {
261 invoice.recalculate_vat()?;
262 }
263
264 if let Some(inv_date) = dto.invoice_date {
265 let parsed_date = DateTime::parse_from_rfc3339(&inv_date)
266 .map_err(|_| "Invalid invoice_date format".to_string())?
267 .with_timezone(&chrono::Utc);
268 invoice.invoice_date = Some(parsed_date);
269 }
270
271 if let Some(due_date_str) = dto.due_date {
272 let parsed_date = DateTime::parse_from_rfc3339(&due_date_str)
273 .map_err(|_| "Invalid due_date format".to_string())?
274 .with_timezone(&chrono::Utc);
275 invoice.due_date = Some(parsed_date);
276 }
277
278 if dto.supplier.is_some() {
279 invoice.supplier = dto.supplier;
280 }
281 if dto.invoice_number.is_some() {
282 invoice.invoice_number = dto.invoice_number;
283 }
284
285 invoice.updated_at = chrono::Utc::now();
286
287 let updated = self.repository.update(&invoice).await?;
288 Ok(self.to_invoice_response_dto(&updated))
289 }
290
291 pub async fn submit_for_approval(
293 &self,
294 invoice_id: Uuid,
295 _dto: SubmitForApprovalDto,
296 ) -> Result<InvoiceResponseDto, String> {
297 let mut invoice = self
298 .repository
299 .find_by_id(invoice_id)
300 .await?
301 .ok_or_else(|| "Invoice not found".to_string())?;
302
303 invoice.submit_for_approval()?;
304
305 let updated = self.repository.update(&invoice).await?;
306 Ok(self.to_invoice_response_dto(&updated))
307 }
308
309 pub async fn approve_invoice(
313 &self,
314 invoice_id: Uuid,
315 dto: ApproveInvoiceDto,
316 ) -> Result<InvoiceResponseDto, String> {
317 let mut invoice = self
318 .repository
319 .find_by_id(invoice_id)
320 .await?
321 .ok_or_else(|| "Invoice not found".to_string())?;
322
323 let approved_by_user_id = Uuid::parse_str(&dto.approved_by_user_id)
324 .map_err(|_| "Invalid approved_by_user_id format".to_string())?;
325
326 invoice.approve(approved_by_user_id)?;
327
328 let updated = self.repository.update(&invoice).await?;
329
330 if let Some(ref accounting_service) = self.accounting_service {
332 if let Err(e) = accounting_service
333 .generate_journal_entry_for_expense(&updated, Some(approved_by_user_id))
334 .await
335 {
336 log::warn!(
337 "Failed to generate journal entry for approved expense {}: {}",
338 updated.id,
339 e
340 );
341 }
344 }
345
346 Ok(self.to_invoice_response_dto(&updated))
347 }
348
349 pub async fn reject_invoice(
351 &self,
352 invoice_id: Uuid,
353 dto: RejectInvoiceDto,
354 ) -> Result<InvoiceResponseDto, String> {
355 let mut invoice = self
356 .repository
357 .find_by_id(invoice_id)
358 .await?
359 .ok_or_else(|| "Invoice not found".to_string())?;
360
361 let rejected_by_user_id = Uuid::parse_str(&dto.rejected_by_user_id)
362 .map_err(|_| "Invalid rejected_by_user_id format".to_string())?;
363
364 invoice.reject(rejected_by_user_id, dto.rejection_reason)?;
365
366 let updated = self.repository.update(&invoice).await?;
367 Ok(self.to_invoice_response_dto(&updated))
368 }
369
370 pub async fn get_pending_invoices(
372 &self,
373 organization_id: Uuid,
374 ) -> Result<PendingInvoicesListDto, String> {
375 let filters = ExpenseFilters {
376 organization_id: Some(organization_id),
377 approval_status: Some(ApprovalStatus::PendingApproval),
378 ..Default::default()
379 };
380
381 let page_request = PageRequest {
383 page: 1,
384 per_page: 1000, sort_by: None,
386 order: SortOrder::default(),
387 };
388
389 let (expenses, _total) = self
390 .repository
391 .find_all_paginated(&page_request, &filters)
392 .await?;
393
394 let invoices: Vec<InvoiceResponseDto> = expenses
395 .iter()
396 .map(|e| self.to_invoice_response_dto(e))
397 .collect();
398
399 Ok(PendingInvoicesListDto {
400 count: invoices.len(),
401 invoices,
402 })
403 }
404
405 pub async fn get_invoice(&self, id: Uuid) -> Result<Option<InvoiceResponseDto>, String> {
407 let expense = self.repository.find_by_id(id).await?;
408 Ok(expense.map(|e| self.to_invoice_response_dto(&e)))
409 }
410
411 fn to_response_dto(&self, expense: &Expense) -> ExpenseResponseDto {
414 ExpenseResponseDto {
415 id: expense.id.to_string(),
416 building_id: expense.building_id.to_string(),
417 category: expense.category.clone(),
418 description: expense.description.clone(),
419 amount: expense.amount,
420 expense_date: expense.expense_date.to_rfc3339(),
421 payment_status: expense.payment_status.clone(),
422 approval_status: expense.approval_status.clone(),
423 supplier: expense.supplier.clone(),
424 invoice_number: expense.invoice_number.clone(),
425 account_code: expense.account_code.clone(),
426 contractor_report_id: expense.contractor_report_id.map(|id| id.to_string()),
427 }
428 }
429
430 fn to_invoice_response_dto(&self, expense: &Expense) -> InvoiceResponseDto {
431 InvoiceResponseDto {
432 id: expense.id.to_string(),
433 organization_id: expense.organization_id.to_string(),
434 building_id: expense.building_id.to_string(),
435 category: expense.category.clone(),
436 description: expense.description.clone(),
437
438 amount: expense.amount,
440 amount_excl_vat: expense.amount_excl_vat,
441 vat_rate: expense.vat_rate,
442 vat_amount: expense.vat_amount,
443 amount_incl_vat: expense.amount_incl_vat,
444
445 expense_date: expense.expense_date.to_rfc3339(),
447 invoice_date: expense.invoice_date.map(|d| d.to_rfc3339()),
448 due_date: expense.due_date.map(|d| d.to_rfc3339()),
449 paid_date: expense.paid_date.map(|d| d.to_rfc3339()),
450
451 approval_status: expense.approval_status.clone(),
453 submitted_at: expense.submitted_at.map(|d| d.to_rfc3339()),
454 approved_by: expense.approved_by.map(|u| u.to_string()),
455 approved_at: expense.approved_at.map(|d| d.to_rfc3339()),
456 rejection_reason: expense.rejection_reason.clone(),
457
458 payment_status: expense.payment_status.clone(),
460 supplier: expense.supplier.clone(),
461 invoice_number: expense.invoice_number.clone(),
462
463 contractor_report_id: expense.contractor_report_id.map(|id| id.to_string()),
464
465 created_at: expense.created_at.to_rfc3339(),
466 updated_at: expense.updated_at.to_rfc3339(),
467 }
468 }
469}
470
471#[cfg(test)]
472mod tests {
473 use super::*;
474 use crate::application::dto::{ExpenseFilters, PageRequest};
475 use crate::application::ports::ExpenseRepository;
476 use crate::domain::entities::{ApprovalStatus, ExpenseCategory, PaymentStatus};
477 use async_trait::async_trait;
478 use std::collections::HashMap;
479 use std::sync::Mutex;
480
481 struct MockExpenseRepository {
484 expenses: Mutex<HashMap<Uuid, Expense>>,
485 }
486
487 impl MockExpenseRepository {
488 fn new() -> Self {
489 Self {
490 expenses: Mutex::new(HashMap::new()),
491 }
492 }
493 }
494
495 #[async_trait]
496 impl ExpenseRepository for MockExpenseRepository {
497 async fn create(&self, expense: &Expense) -> Result<Expense, String> {
498 let mut expenses = self.expenses.lock().unwrap();
499 expenses.insert(expense.id, expense.clone());
500 Ok(expense.clone())
501 }
502
503 async fn find_by_id(&self, id: Uuid) -> Result<Option<Expense>, String> {
504 let expenses = self.expenses.lock().unwrap();
505 Ok(expenses.get(&id).cloned())
506 }
507
508 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Expense>, String> {
509 let expenses = self.expenses.lock().unwrap();
510 Ok(expenses
511 .values()
512 .filter(|e| e.building_id == building_id)
513 .cloned()
514 .collect())
515 }
516
517 async fn find_all_paginated(
518 &self,
519 _page_request: &PageRequest,
520 filters: &ExpenseFilters,
521 ) -> Result<(Vec<Expense>, i64), String> {
522 let expenses = self.expenses.lock().unwrap();
523 let filtered: Vec<Expense> = expenses
524 .values()
525 .filter(|e| {
526 if let Some(org_id) = filters.organization_id {
527 if e.organization_id != org_id {
528 return false;
529 }
530 }
531 if let Some(ref status) = filters.approval_status {
532 if e.approval_status != *status {
533 return false;
534 }
535 }
536 true
537 })
538 .cloned()
539 .collect();
540 let count = filtered.len() as i64;
541 Ok((filtered, count))
542 }
543
544 async fn update(&self, expense: &Expense) -> Result<Expense, String> {
545 let mut expenses = self.expenses.lock().unwrap();
546 expenses.insert(expense.id, expense.clone());
547 Ok(expense.clone())
548 }
549
550 async fn delete(&self, id: Uuid) -> Result<bool, String> {
551 let mut expenses = self.expenses.lock().unwrap();
552 Ok(expenses.remove(&id).is_some())
553 }
554 }
555
556 fn make_use_cases(repo: MockExpenseRepository) -> ExpenseUseCases {
559 ExpenseUseCases::new(Arc::new(repo))
560 }
561
562 fn valid_create_dto(org_id: Uuid, building_id: Uuid) -> CreateExpenseDto {
563 CreateExpenseDto {
564 organization_id: org_id.to_string(),
565 building_id: building_id.to_string(),
566 category: ExpenseCategory::Maintenance,
567 description: "Elevator maintenance Q1".to_string(),
568 amount: 1500.0,
569 expense_date: "2026-01-15T10:00:00Z".to_string(),
570 supplier: Some("Schindler SA".to_string()),
571 invoice_number: Some("INV-2026-001".to_string()),
572 account_code: Some("611002".to_string()),
573 }
574 }
575
576 fn valid_invoice_draft_dto(org_id: Uuid, building_id: Uuid) -> CreateInvoiceDraftDto {
577 CreateInvoiceDraftDto {
578 organization_id: org_id.to_string(),
579 building_id: building_id.to_string(),
580 category: ExpenseCategory::Utilities,
581 description: "Electricity bill January".to_string(),
582 amount_excl_vat: 1000.0,
583 vat_rate: 21.0,
584 invoice_date: "2026-01-31T10:00:00Z".to_string(),
585 due_date: Some("2026-02-28T10:00:00Z".to_string()),
586 supplier: Some("Engie Electrabel".to_string()),
587 invoice_number: Some("ELEC-2026-001".to_string()),
588 }
589 }
590
591 #[tokio::test]
594 async fn test_create_expense_success() {
595 let repo = MockExpenseRepository::new();
596 let uc = make_use_cases(repo);
597 let org_id = Uuid::new_v4();
598 let building_id = Uuid::new_v4();
599
600 let result = uc
601 .create_expense(valid_create_dto(org_id, building_id))
602 .await;
603
604 assert!(result.is_ok());
605 let dto = result.unwrap();
606 assert_eq!(dto.building_id, building_id.to_string());
607 assert_eq!(dto.description, "Elevator maintenance Q1");
608 assert_eq!(dto.amount, 1500.0);
609 assert_eq!(dto.payment_status, PaymentStatus::Pending);
610 assert_eq!(dto.approval_status, ApprovalStatus::Draft);
611 assert_eq!(dto.supplier, Some("Schindler SA".to_string()));
612 assert_eq!(dto.account_code, Some("611002".to_string()));
613 }
614
615 #[tokio::test]
616 async fn test_create_expense_invalid_building_id() {
617 let repo = MockExpenseRepository::new();
618 let uc = make_use_cases(repo);
619
620 let mut dto = valid_create_dto(Uuid::new_v4(), Uuid::new_v4());
621 dto.building_id = "not-a-uuid".to_string();
622
623 let result = uc.create_expense(dto).await;
624 assert!(result.is_err());
625 assert_eq!(result.unwrap_err(), "Invalid building ID format");
626 }
627
628 #[tokio::test]
629 async fn test_submit_for_approval_success() {
630 let repo = MockExpenseRepository::new();
631 let uc = make_use_cases(repo);
632 let org_id = Uuid::new_v4();
633 let building_id = Uuid::new_v4();
634
635 let created = uc
637 .create_expense(valid_create_dto(org_id, building_id))
638 .await
639 .unwrap();
640 let expense_id = Uuid::parse_str(&created.id).unwrap();
641
642 let result = uc
644 .submit_for_approval(expense_id, SubmitForApprovalDto {})
645 .await;
646
647 assert!(result.is_ok());
648 let invoice = result.unwrap();
649 assert_eq!(invoice.approval_status, ApprovalStatus::PendingApproval);
650 assert!(invoice.submitted_at.is_some());
651 }
652
653 #[tokio::test]
654 async fn test_approve_invoice_success() {
655 let repo = MockExpenseRepository::new();
656 let uc = make_use_cases(repo);
657 let org_id = Uuid::new_v4();
658 let building_id = Uuid::new_v4();
659 let approver_id = Uuid::new_v4();
660
661 let created = uc
663 .create_expense(valid_create_dto(org_id, building_id))
664 .await
665 .unwrap();
666 let expense_id = Uuid::parse_str(&created.id).unwrap();
667 uc.submit_for_approval(expense_id, SubmitForApprovalDto {})
668 .await
669 .unwrap();
670
671 let result = uc
673 .approve_invoice(
674 expense_id,
675 ApproveInvoiceDto {
676 approved_by_user_id: approver_id.to_string(),
677 },
678 )
679 .await;
680
681 assert!(result.is_ok());
682 let invoice = result.unwrap();
683 assert_eq!(invoice.approval_status, ApprovalStatus::Approved);
684 assert_eq!(invoice.approved_by, Some(approver_id.to_string()));
685 assert!(invoice.approved_at.is_some());
686 }
687
688 #[tokio::test]
689 async fn test_reject_invoice_success() {
690 let repo = MockExpenseRepository::new();
691 let uc = make_use_cases(repo);
692 let org_id = Uuid::new_v4();
693 let building_id = Uuid::new_v4();
694 let rejector_id = Uuid::new_v4();
695
696 let created = uc
698 .create_expense(valid_create_dto(org_id, building_id))
699 .await
700 .unwrap();
701 let expense_id = Uuid::parse_str(&created.id).unwrap();
702 uc.submit_for_approval(expense_id, SubmitForApprovalDto {})
703 .await
704 .unwrap();
705
706 let result = uc
708 .reject_invoice(
709 expense_id,
710 RejectInvoiceDto {
711 rejected_by_user_id: rejector_id.to_string(),
712 rejection_reason: "Missing supporting documents".to_string(),
713 },
714 )
715 .await;
716
717 assert!(result.is_ok());
718 let invoice = result.unwrap();
719 assert_eq!(invoice.approval_status, ApprovalStatus::Rejected);
720 assert_eq!(
721 invoice.rejection_reason,
722 Some("Missing supporting documents".to_string())
723 );
724 }
725
726 #[tokio::test]
727 async fn test_mark_as_paid_requires_approval() {
728 let repo = MockExpenseRepository::new();
729 let uc = make_use_cases(repo);
730 let org_id = Uuid::new_v4();
731 let building_id = Uuid::new_v4();
732
733 let created = uc
735 .create_expense(valid_create_dto(org_id, building_id))
736 .await
737 .unwrap();
738 let expense_id = Uuid::parse_str(&created.id).unwrap();
739
740 let result = uc.mark_as_paid(expense_id).await;
742 assert!(result.is_err());
743 assert!(result
744 .unwrap_err()
745 .contains("invoice must be approved first"));
746 }
747
748 #[tokio::test]
749 async fn test_mark_as_paid_after_approval() {
750 let repo = MockExpenseRepository::new();
751 let uc = make_use_cases(repo);
752 let org_id = Uuid::new_v4();
753 let building_id = Uuid::new_v4();
754 let approver_id = Uuid::new_v4();
755
756 let created = uc
758 .create_expense(valid_create_dto(org_id, building_id))
759 .await
760 .unwrap();
761 let expense_id = Uuid::parse_str(&created.id).unwrap();
762 uc.submit_for_approval(expense_id, SubmitForApprovalDto {})
763 .await
764 .unwrap();
765 uc.approve_invoice(
766 expense_id,
767 ApproveInvoiceDto {
768 approved_by_user_id: approver_id.to_string(),
769 },
770 )
771 .await
772 .unwrap();
773
774 let result = uc.mark_as_paid(expense_id).await;
776 assert!(result.is_ok());
777 let dto = result.unwrap();
778 assert_eq!(dto.payment_status, PaymentStatus::Paid);
779 }
780
781 #[tokio::test]
782 async fn test_find_by_building() {
783 let repo = MockExpenseRepository::new();
784 let uc = make_use_cases(repo);
785 let org_id = Uuid::new_v4();
786 let building_a = Uuid::new_v4();
787 let building_b = Uuid::new_v4();
788
789 let mut dto_a = valid_create_dto(org_id, building_a);
791 dto_a.description = "Building A expense".to_string();
792 uc.create_expense(dto_a).await.unwrap();
793
794 let mut dto_b = valid_create_dto(org_id, building_b);
795 dto_b.description = "Building B expense".to_string();
796 uc.create_expense(dto_b).await.unwrap();
797
798 let mut dto_a2 = valid_create_dto(org_id, building_a);
800 dto_a2.description = "Building A expense 2".to_string();
801 uc.create_expense(dto_a2).await.unwrap();
802
803 let result = uc.list_expenses_by_building(building_a).await;
805 assert!(result.is_ok());
806 let expenses = result.unwrap();
807 assert_eq!(expenses.len(), 2);
808 assert!(expenses
809 .iter()
810 .all(|e| e.building_id == building_a.to_string()));
811 }
812
813 #[tokio::test]
814 async fn test_update_invoice_draft_blocked_after_approval() {
815 let repo = MockExpenseRepository::new();
816 let uc = make_use_cases(repo);
817 let org_id = Uuid::new_v4();
818 let building_id = Uuid::new_v4();
819 let approver_id = Uuid::new_v4();
820
821 let created = uc
823 .create_invoice_draft(valid_invoice_draft_dto(org_id, building_id))
824 .await
825 .unwrap();
826 let invoice_id = Uuid::parse_str(&created.id).unwrap();
827 uc.submit_for_approval(invoice_id, SubmitForApprovalDto {})
828 .await
829 .unwrap();
830 uc.approve_invoice(
831 invoice_id,
832 ApproveInvoiceDto {
833 approved_by_user_id: approver_id.to_string(),
834 },
835 )
836 .await
837 .unwrap();
838
839 let update_dto = UpdateInvoiceDraftDto {
841 description: Some("Changed description".to_string()),
842 category: None,
843 amount_excl_vat: None,
844 vat_rate: None,
845 invoice_date: None,
846 due_date: None,
847 supplier: None,
848 invoice_number: None,
849 };
850
851 let result = uc.update_invoice_draft(invoice_id, update_dto).await;
852 assert!(result.is_err());
853 assert!(result.unwrap_err().contains("cannot be modified"));
854 }
855
856 #[tokio::test]
857 async fn test_create_invoice_draft_vat_calculations() {
858 let repo = MockExpenseRepository::new();
859 let uc = make_use_cases(repo);
860 let org_id = Uuid::new_v4();
861 let building_id = Uuid::new_v4();
862
863 let result = uc
865 .create_invoice_draft(valid_invoice_draft_dto(org_id, building_id))
866 .await;
867
868 assert!(result.is_ok());
869 let invoice = result.unwrap();
870
871 assert_eq!(invoice.amount_excl_vat, Some(1000.0));
873 assert_eq!(invoice.vat_rate, Some(21.0));
874 assert_eq!(invoice.vat_amount, Some(210.0));
875 assert_eq!(invoice.amount_incl_vat, Some(1210.0));
876 assert_eq!(invoice.amount, 1210.0);
878 }
879
880 #[tokio::test]
881 async fn test_reject_then_resubmit() {
882 let repo = MockExpenseRepository::new();
883 let uc = make_use_cases(repo);
884 let org_id = Uuid::new_v4();
885 let building_id = Uuid::new_v4();
886 let rejector_id = Uuid::new_v4();
887
888 let created = uc
890 .create_expense(valid_create_dto(org_id, building_id))
891 .await
892 .unwrap();
893 let expense_id = Uuid::parse_str(&created.id).unwrap();
894 uc.submit_for_approval(expense_id, SubmitForApprovalDto {})
895 .await
896 .unwrap();
897 uc.reject_invoice(
898 expense_id,
899 RejectInvoiceDto {
900 rejected_by_user_id: rejector_id.to_string(),
901 rejection_reason: "Incorrect amount".to_string(),
902 },
903 )
904 .await
905 .unwrap();
906
907 let rejected = uc.get_invoice(expense_id).await.unwrap().unwrap();
909 assert_eq!(rejected.approval_status, ApprovalStatus::Rejected);
910 assert_eq!(
911 rejected.rejection_reason,
912 Some("Incorrect amount".to_string())
913 );
914
915 let result = uc
917 .submit_for_approval(expense_id, SubmitForApprovalDto {})
918 .await;
919 assert!(result.is_ok());
920 let resubmitted = result.unwrap();
921 assert_eq!(resubmitted.approval_status, ApprovalStatus::PendingApproval);
922 assert_eq!(resubmitted.rejection_reason, None);
924 }
925}