1use crate::application::dto::{
2 CreatePaymentRequest, PaymentResponse, PaymentStatsResponse, RefundPaymentRequest,
3};
4use crate::application::ports::{PaymentMethodRepository, PaymentRepository, PaymentStats};
5use crate::domain::entities::{Payment, TransactionStatus};
6use std::sync::Arc;
7use uuid::Uuid;
8
9pub struct PaymentUseCases {
10 payment_repository: Arc<dyn PaymentRepository>,
11 payment_method_repository: Arc<dyn PaymentMethodRepository>,
12}
13
14impl PaymentUseCases {
15 pub fn new(
16 payment_repository: Arc<dyn PaymentRepository>,
17 payment_method_repository: Arc<dyn PaymentMethodRepository>,
18 ) -> Self {
19 Self {
20 payment_repository,
21 payment_method_repository,
22 }
23 }
24
25 pub async fn create_payment(
30 &self,
31 organization_id: Uuid,
32 request: CreatePaymentRequest,
33 ) -> Result<PaymentResponse, String> {
34 let idempotency_key = format!(
36 "{}-{}-{}-{}",
37 organization_id,
38 request.building_id,
39 request.owner_id,
40 Uuid::new_v4()
41 );
42
43 if let Some(existing_payment) = self
45 .payment_repository
46 .find_by_idempotency_key(organization_id, &idempotency_key)
47 .await?
48 {
49 return Ok(PaymentResponse::from(existing_payment));
51 }
52
53 let payment = Payment::new(
55 organization_id,
56 request.building_id,
57 request.owner_id,
58 request.expense_id,
59 request.amount_cents,
60 request.payment_method_type,
61 idempotency_key,
62 request.description,
63 )?;
64
65 let created = self.payment_repository.create(&payment).await?;
66 Ok(PaymentResponse::from(created))
67 }
68
69 pub async fn get_payment(&self, id: Uuid) -> Result<Option<PaymentResponse>, String> {
71 match self.payment_repository.find_by_id(id).await? {
72 Some(payment) => Ok(Some(PaymentResponse::from(payment))),
73 None => Ok(None),
74 }
75 }
76
77 pub async fn get_payment_by_stripe_intent(
79 &self,
80 stripe_payment_intent_id: &str,
81 ) -> Result<Option<PaymentResponse>, String> {
82 match self
83 .payment_repository
84 .find_by_stripe_payment_intent_id(stripe_payment_intent_id)
85 .await?
86 {
87 Some(payment) => Ok(Some(PaymentResponse::from(payment))),
88 None => Ok(None),
89 }
90 }
91
92 pub async fn list_owner_payments(
94 &self,
95 owner_id: Uuid,
96 ) -> Result<Vec<PaymentResponse>, String> {
97 let payments = self.payment_repository.find_by_owner(owner_id).await?;
98 Ok(payments.into_iter().map(PaymentResponse::from).collect())
99 }
100
101 pub async fn list_building_payments(
103 &self,
104 building_id: Uuid,
105 ) -> Result<Vec<PaymentResponse>, String> {
106 let payments = self
107 .payment_repository
108 .find_by_building(building_id)
109 .await?;
110 Ok(payments.into_iter().map(PaymentResponse::from).collect())
111 }
112
113 pub async fn list_expense_payments(
115 &self,
116 expense_id: Uuid,
117 ) -> Result<Vec<PaymentResponse>, String> {
118 let payments = self.payment_repository.find_by_expense(expense_id).await?;
119 Ok(payments.into_iter().map(PaymentResponse::from).collect())
120 }
121
122 pub async fn list_organization_payments(
124 &self,
125 organization_id: Uuid,
126 ) -> Result<Vec<PaymentResponse>, String> {
127 let payments = self
128 .payment_repository
129 .find_by_organization(organization_id)
130 .await?;
131 Ok(payments.into_iter().map(PaymentResponse::from).collect())
132 }
133
134 pub async fn list_payments_by_status(
136 &self,
137 organization_id: Uuid,
138 status: TransactionStatus,
139 ) -> Result<Vec<PaymentResponse>, String> {
140 let payments = self
141 .payment_repository
142 .find_by_status(organization_id, status)
143 .await?;
144 Ok(payments.into_iter().map(PaymentResponse::from).collect())
145 }
146
147 pub async fn list_pending_payments(
149 &self,
150 organization_id: Uuid,
151 ) -> Result<Vec<PaymentResponse>, String> {
152 let payments = self
153 .payment_repository
154 .find_pending(organization_id)
155 .await?;
156 Ok(payments.into_iter().map(PaymentResponse::from).collect())
157 }
158
159 pub async fn list_failed_payments(
161 &self,
162 organization_id: Uuid,
163 ) -> Result<Vec<PaymentResponse>, String> {
164 let payments = self.payment_repository.find_failed(organization_id).await?;
165 Ok(payments.into_iter().map(PaymentResponse::from).collect())
166 }
167
168 pub async fn mark_processing(&self, id: Uuid) -> Result<PaymentResponse, String> {
170 let mut payment = self
171 .payment_repository
172 .find_by_id(id)
173 .await?
174 .ok_or_else(|| "Payment not found".to_string())?;
175
176 payment.mark_processing()?;
177
178 let updated = self.payment_repository.update(&payment).await?;
179 Ok(PaymentResponse::from(updated))
180 }
181
182 pub async fn mark_requires_action(&self, id: Uuid) -> Result<PaymentResponse, String> {
184 let mut payment = self
185 .payment_repository
186 .find_by_id(id)
187 .await?
188 .ok_or_else(|| "Payment not found".to_string())?;
189
190 payment.mark_requires_action()?;
191
192 let updated = self.payment_repository.update(&payment).await?;
193 Ok(PaymentResponse::from(updated))
194 }
195
196 pub async fn mark_succeeded(&self, id: Uuid) -> Result<PaymentResponse, String> {
198 let mut payment = self
199 .payment_repository
200 .find_by_id(id)
201 .await?
202 .ok_or_else(|| "Payment not found".to_string())?;
203
204 payment.mark_succeeded()?;
205
206 let updated = self.payment_repository.update(&payment).await?;
207 Ok(PaymentResponse::from(updated))
208 }
209
210 pub async fn mark_failed(&self, id: Uuid, reason: String) -> Result<PaymentResponse, String> {
212 let mut payment = self
213 .payment_repository
214 .find_by_id(id)
215 .await?
216 .ok_or_else(|| "Payment not found".to_string())?;
217
218 payment.mark_failed(reason)?;
219
220 let updated = self.payment_repository.update(&payment).await?;
221 Ok(PaymentResponse::from(updated))
222 }
223
224 pub async fn mark_cancelled(&self, id: Uuid) -> Result<PaymentResponse, String> {
226 let mut payment = self
227 .payment_repository
228 .find_by_id(id)
229 .await?
230 .ok_or_else(|| "Payment not found".to_string())?;
231
232 payment.mark_cancelled()?;
233
234 let updated = self.payment_repository.update(&payment).await?;
235 Ok(PaymentResponse::from(updated))
236 }
237
238 pub async fn refund_payment(
240 &self,
241 id: Uuid,
242 request: RefundPaymentRequest,
243 ) -> Result<PaymentResponse, String> {
244 let mut payment = self
245 .payment_repository
246 .find_by_id(id)
247 .await?
248 .ok_or_else(|| "Payment not found".to_string())?;
249
250 payment.refund(request.amount_cents)?;
251
252 let updated = self.payment_repository.update(&payment).await?;
253 Ok(PaymentResponse::from(updated))
254 }
255
256 pub async fn set_stripe_payment_intent_id(
258 &self,
259 id: Uuid,
260 stripe_payment_intent_id: String,
261 ) -> Result<PaymentResponse, String> {
262 let mut payment = self
263 .payment_repository
264 .find_by_id(id)
265 .await?
266 .ok_or_else(|| "Payment not found".to_string())?;
267
268 payment.set_stripe_payment_intent_id(stripe_payment_intent_id);
269
270 let updated = self.payment_repository.update(&payment).await?;
271 Ok(PaymentResponse::from(updated))
272 }
273
274 pub async fn set_stripe_customer_id(
276 &self,
277 id: Uuid,
278 stripe_customer_id: String,
279 ) -> Result<PaymentResponse, String> {
280 let mut payment = self
281 .payment_repository
282 .find_by_id(id)
283 .await?
284 .ok_or_else(|| "Payment not found".to_string())?;
285
286 payment.set_stripe_customer_id(stripe_customer_id);
287
288 let updated = self.payment_repository.update(&payment).await?;
289 Ok(PaymentResponse::from(updated))
290 }
291
292 pub async fn set_payment_method_id(
294 &self,
295 id: Uuid,
296 payment_method_id: Uuid,
297 ) -> Result<PaymentResponse, String> {
298 let _payment_method = self
300 .payment_method_repository
301 .find_by_id(payment_method_id)
302 .await?
303 .ok_or_else(|| "Payment method not found".to_string())?;
304
305 let mut payment = self
306 .payment_repository
307 .find_by_id(id)
308 .await?
309 .ok_or_else(|| "Payment not found".to_string())?;
310
311 payment.set_payment_method_id(payment_method_id);
312
313 let updated = self.payment_repository.update(&payment).await?;
314 Ok(PaymentResponse::from(updated))
315 }
316
317 pub async fn delete_payment(&self, id: Uuid) -> Result<bool, String> {
319 self.payment_repository.delete(id).await
320 }
321
322 pub async fn get_total_paid_for_expense(&self, expense_id: Uuid) -> Result<i64, String> {
324 self.payment_repository
325 .get_total_paid_for_expense(expense_id)
326 .await
327 }
328
329 pub async fn get_total_paid_by_owner(&self, owner_id: Uuid) -> Result<i64, String> {
331 self.payment_repository
332 .get_total_paid_by_owner(owner_id)
333 .await
334 }
335
336 pub async fn get_total_paid_for_building(&self, building_id: Uuid) -> Result<i64, String> {
338 self.payment_repository
339 .get_total_paid_for_building(building_id)
340 .await
341 }
342
343 pub async fn get_owner_payment_stats(
345 &self,
346 owner_id: Uuid,
347 ) -> Result<PaymentStatsResponse, String> {
348 let stats = self
349 .payment_repository
350 .get_owner_payment_stats(owner_id)
351 .await?;
352 Ok(Self::payment_stats_to_response(stats))
353 }
354
355 pub async fn get_building_payment_stats(
357 &self,
358 building_id: Uuid,
359 ) -> Result<PaymentStatsResponse, String> {
360 let stats = self
361 .payment_repository
362 .get_building_payment_stats(building_id)
363 .await?;
364 Ok(Self::payment_stats_to_response(stats))
365 }
366
367 fn payment_stats_to_response(stats: PaymentStats) -> PaymentStatsResponse {
369 PaymentStatsResponse {
370 total_count: stats.total_count,
371 succeeded_count: stats.succeeded_count,
372 failed_count: stats.failed_count,
373 pending_count: stats.pending_count,
374 total_amount_cents: stats.total_amount_cents,
375 total_succeeded_cents: stats.total_succeeded_cents,
376 total_refunded_cents: stats.total_refunded_cents,
377 net_amount_cents: stats.net_amount_cents,
378 }
379 }
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385 use crate::application::ports::{PaymentMethodRepository, PaymentRepository, PaymentStats};
386 use crate::domain::entities::payment_method::{
387 PaymentMethod, PaymentMethodType as PMMethodType,
388 };
389 use crate::domain::entities::{Payment, PaymentMethodType, TransactionStatus};
390 use async_trait::async_trait;
391 use std::collections::HashMap;
392 use std::sync::Mutex;
393 use uuid::Uuid;
394
395 struct MockPaymentRepository {
398 payments: Mutex<HashMap<Uuid, Payment>>,
399 }
400
401 impl MockPaymentRepository {
402 fn new() -> Self {
403 Self {
404 payments: Mutex::new(HashMap::new()),
405 }
406 }
407 }
408
409 #[async_trait]
410 impl PaymentRepository for MockPaymentRepository {
411 async fn create(&self, payment: &Payment) -> Result<Payment, String> {
412 self.payments
413 .lock()
414 .unwrap()
415 .insert(payment.id, payment.clone());
416 Ok(payment.clone())
417 }
418
419 async fn find_by_id(&self, id: Uuid) -> Result<Option<Payment>, String> {
420 Ok(self.payments.lock().unwrap().get(&id).cloned())
421 }
422
423 async fn find_by_stripe_payment_intent_id(
424 &self,
425 stripe_payment_intent_id: &str,
426 ) -> Result<Option<Payment>, String> {
427 Ok(self
428 .payments
429 .lock()
430 .unwrap()
431 .values()
432 .find(|p| p.stripe_payment_intent_id.as_deref() == Some(stripe_payment_intent_id))
433 .cloned())
434 }
435
436 async fn find_by_idempotency_key(
437 &self,
438 organization_id: Uuid,
439 idempotency_key: &str,
440 ) -> Result<Option<Payment>, String> {
441 Ok(self
442 .payments
443 .lock()
444 .unwrap()
445 .values()
446 .find(|p| {
447 p.organization_id == organization_id && p.idempotency_key == idempotency_key
448 })
449 .cloned())
450 }
451
452 async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<Payment>, String> {
453 Ok(self
454 .payments
455 .lock()
456 .unwrap()
457 .values()
458 .filter(|p| p.owner_id == owner_id)
459 .cloned()
460 .collect())
461 }
462
463 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Payment>, String> {
464 Ok(self
465 .payments
466 .lock()
467 .unwrap()
468 .values()
469 .filter(|p| p.building_id == building_id)
470 .cloned()
471 .collect())
472 }
473
474 async fn find_by_expense(&self, expense_id: Uuid) -> Result<Vec<Payment>, String> {
475 Ok(self
476 .payments
477 .lock()
478 .unwrap()
479 .values()
480 .filter(|p| p.expense_id == Some(expense_id))
481 .cloned()
482 .collect())
483 }
484
485 async fn find_by_organization(
486 &self,
487 organization_id: Uuid,
488 ) -> Result<Vec<Payment>, String> {
489 Ok(self
490 .payments
491 .lock()
492 .unwrap()
493 .values()
494 .filter(|p| p.organization_id == organization_id)
495 .cloned()
496 .collect())
497 }
498
499 async fn find_by_status(
500 &self,
501 organization_id: Uuid,
502 status: TransactionStatus,
503 ) -> Result<Vec<Payment>, String> {
504 Ok(self
505 .payments
506 .lock()
507 .unwrap()
508 .values()
509 .filter(|p| p.organization_id == organization_id && p.status == status)
510 .cloned()
511 .collect())
512 }
513
514 async fn find_by_building_and_status(
515 &self,
516 building_id: Uuid,
517 status: TransactionStatus,
518 ) -> Result<Vec<Payment>, String> {
519 Ok(self
520 .payments
521 .lock()
522 .unwrap()
523 .values()
524 .filter(|p| p.building_id == building_id && p.status == status)
525 .cloned()
526 .collect())
527 }
528
529 async fn find_pending(&self, organization_id: Uuid) -> Result<Vec<Payment>, String> {
530 self.find_by_status(organization_id, TransactionStatus::Pending)
531 .await
532 }
533
534 async fn find_failed(&self, organization_id: Uuid) -> Result<Vec<Payment>, String> {
535 self.find_by_status(organization_id, TransactionStatus::Failed)
536 .await
537 }
538
539 async fn update(&self, payment: &Payment) -> Result<Payment, String> {
540 self.payments
541 .lock()
542 .unwrap()
543 .insert(payment.id, payment.clone());
544 Ok(payment.clone())
545 }
546
547 async fn delete(&self, id: Uuid) -> Result<bool, String> {
548 Ok(self.payments.lock().unwrap().remove(&id).is_some())
549 }
550
551 async fn get_total_paid_for_expense(&self, expense_id: Uuid) -> Result<i64, String> {
552 Ok(self
553 .payments
554 .lock()
555 .unwrap()
556 .values()
557 .filter(|p| {
558 p.expense_id == Some(expense_id) && p.status == TransactionStatus::Succeeded
559 })
560 .map(|p| p.amount_cents)
561 .sum())
562 }
563
564 async fn get_total_paid_by_owner(&self, owner_id: Uuid) -> Result<i64, String> {
565 Ok(self
566 .payments
567 .lock()
568 .unwrap()
569 .values()
570 .filter(|p| p.owner_id == owner_id && p.status == TransactionStatus::Succeeded)
571 .map(|p| p.amount_cents)
572 .sum())
573 }
574
575 async fn get_total_paid_for_building(&self, building_id: Uuid) -> Result<i64, String> {
576 Ok(self
577 .payments
578 .lock()
579 .unwrap()
580 .values()
581 .filter(|p| {
582 p.building_id == building_id && p.status == TransactionStatus::Succeeded
583 })
584 .map(|p| p.amount_cents)
585 .sum())
586 }
587
588 async fn get_owner_payment_stats(&self, owner_id: Uuid) -> Result<PaymentStats, String> {
589 let payments: Vec<_> = self
590 .payments
591 .lock()
592 .unwrap()
593 .values()
594 .filter(|p| p.owner_id == owner_id)
595 .cloned()
596 .collect();
597 Ok(compute_stats(&payments))
598 }
599
600 async fn get_building_payment_stats(
601 &self,
602 building_id: Uuid,
603 ) -> Result<PaymentStats, String> {
604 let payments: Vec<_> = self
605 .payments
606 .lock()
607 .unwrap()
608 .values()
609 .filter(|p| p.building_id == building_id)
610 .cloned()
611 .collect();
612 Ok(compute_stats(&payments))
613 }
614 }
615
616 fn compute_stats(payments: &[Payment]) -> PaymentStats {
617 let total_count = payments.len() as i64;
618 let succeeded_count = payments
619 .iter()
620 .filter(|p| p.status == TransactionStatus::Succeeded)
621 .count() as i64;
622 let failed_count = payments
623 .iter()
624 .filter(|p| p.status == TransactionStatus::Failed)
625 .count() as i64;
626 let pending_count = payments
627 .iter()
628 .filter(|p| p.status == TransactionStatus::Pending)
629 .count() as i64;
630 let total_amount_cents: i64 = payments.iter().map(|p| p.amount_cents).sum();
631 let total_succeeded_cents: i64 = payments
632 .iter()
633 .filter(|p| p.status == TransactionStatus::Succeeded)
634 .map(|p| p.amount_cents)
635 .sum();
636 let total_refunded_cents: i64 = payments.iter().map(|p| p.refunded_amount_cents).sum();
637 PaymentStats {
638 total_count,
639 succeeded_count,
640 failed_count,
641 pending_count,
642 total_amount_cents,
643 total_succeeded_cents,
644 total_refunded_cents,
645 net_amount_cents: total_succeeded_cents - total_refunded_cents,
646 }
647 }
648
649 struct MockPaymentMethodRepository {
652 methods: Mutex<HashMap<Uuid, PaymentMethod>>,
653 }
654
655 impl MockPaymentMethodRepository {
656 fn new() -> Self {
657 Self {
658 methods: Mutex::new(HashMap::new()),
659 }
660 }
661
662 fn with_method(method: PaymentMethod) -> Self {
663 let mut map = HashMap::new();
664 map.insert(method.id, method);
665 Self {
666 methods: Mutex::new(map),
667 }
668 }
669 }
670
671 #[async_trait]
672 impl PaymentMethodRepository for MockPaymentMethodRepository {
673 async fn create(&self, pm: &PaymentMethod) -> Result<PaymentMethod, String> {
674 self.methods.lock().unwrap().insert(pm.id, pm.clone());
675 Ok(pm.clone())
676 }
677
678 async fn find_by_id(&self, id: Uuid) -> Result<Option<PaymentMethod>, String> {
679 Ok(self.methods.lock().unwrap().get(&id).cloned())
680 }
681
682 async fn find_by_stripe_payment_method_id(
683 &self,
684 stripe_payment_method_id: &str,
685 ) -> Result<Option<PaymentMethod>, String> {
686 Ok(self
687 .methods
688 .lock()
689 .unwrap()
690 .values()
691 .find(|m| m.stripe_payment_method_id == stripe_payment_method_id)
692 .cloned())
693 }
694
695 async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<PaymentMethod>, String> {
696 Ok(self
697 .methods
698 .lock()
699 .unwrap()
700 .values()
701 .filter(|m| m.owner_id == owner_id)
702 .cloned()
703 .collect())
704 }
705
706 async fn find_active_by_owner(&self, owner_id: Uuid) -> Result<Vec<PaymentMethod>, String> {
707 Ok(self
708 .methods
709 .lock()
710 .unwrap()
711 .values()
712 .filter(|m| m.owner_id == owner_id && m.is_active)
713 .cloned()
714 .collect())
715 }
716
717 async fn find_default_by_owner(
718 &self,
719 owner_id: Uuid,
720 ) -> Result<Option<PaymentMethod>, String> {
721 Ok(self
722 .methods
723 .lock()
724 .unwrap()
725 .values()
726 .find(|m| m.owner_id == owner_id && m.is_default)
727 .cloned())
728 }
729
730 async fn find_by_organization(
731 &self,
732 organization_id: Uuid,
733 ) -> Result<Vec<PaymentMethod>, String> {
734 Ok(self
735 .methods
736 .lock()
737 .unwrap()
738 .values()
739 .filter(|m| m.organization_id == organization_id)
740 .cloned()
741 .collect())
742 }
743
744 async fn find_by_owner_and_type(
745 &self,
746 owner_id: Uuid,
747 method_type: PMMethodType,
748 ) -> Result<Vec<PaymentMethod>, String> {
749 Ok(self
750 .methods
751 .lock()
752 .unwrap()
753 .values()
754 .filter(|m| m.owner_id == owner_id && m.method_type == method_type)
755 .cloned()
756 .collect())
757 }
758
759 async fn update(&self, pm: &PaymentMethod) -> Result<PaymentMethod, String> {
760 self.methods.lock().unwrap().insert(pm.id, pm.clone());
761 Ok(pm.clone())
762 }
763
764 async fn delete(&self, id: Uuid) -> Result<bool, String> {
765 Ok(self.methods.lock().unwrap().remove(&id).is_some())
766 }
767
768 async fn set_as_default(&self, id: Uuid, _owner_id: Uuid) -> Result<PaymentMethod, String> {
769 let mut methods = self.methods.lock().unwrap();
770 let pm = methods
771 .get_mut(&id)
772 .ok_or_else(|| "Not found".to_string())?;
773 pm.is_default = true;
774 Ok(pm.clone())
775 }
776
777 async fn count_active_by_owner(&self, owner_id: Uuid) -> Result<i64, String> {
778 Ok(self
779 .methods
780 .lock()
781 .unwrap()
782 .values()
783 .filter(|m| m.owner_id == owner_id && m.is_active)
784 .count() as i64)
785 }
786
787 async fn has_active_payment_methods(&self, owner_id: Uuid) -> Result<bool, String> {
788 Ok(self.count_active_by_owner(owner_id).await? > 0)
789 }
790 }
791
792 fn make_use_cases(
795 payment_repo: Arc<dyn PaymentRepository>,
796 pm_repo: Arc<dyn PaymentMethodRepository>,
797 ) -> PaymentUseCases {
798 PaymentUseCases::new(payment_repo, pm_repo)
799 }
800
801 fn make_create_request(
802 building_id: Uuid,
803 owner_id: Uuid,
804 amount_cents: i64,
805 ) -> CreatePaymentRequest {
806 CreatePaymentRequest {
807 building_id,
808 owner_id,
809 expense_id: None,
810 amount_cents,
811 payment_method_type: PaymentMethodType::Card,
812 payment_method_id: None,
813 description: Some("Test payment".to_string()),
814 metadata: None,
815 }
816 }
817
818 async fn seed_payment(
820 repo: &Arc<MockPaymentRepository>,
821 org_id: Uuid,
822 building_id: Uuid,
823 owner_id: Uuid,
824 amount_cents: i64,
825 ) -> Payment {
826 let payment = Payment::new(
827 org_id,
828 building_id,
829 owner_id,
830 None,
831 amount_cents,
832 PaymentMethodType::Card,
833 format!("idem-{}-{}", org_id, Uuid::new_v4()),
834 Some("seeded".to_string()),
835 )
836 .unwrap();
837 repo.create(&payment).await.unwrap();
838 payment
839 }
840
841 #[tokio::test]
844 async fn test_create_payment_success() {
845 let payment_repo = Arc::new(MockPaymentRepository::new());
846 let pm_repo = Arc::new(MockPaymentMethodRepository::new());
847 let uc = make_use_cases(payment_repo.clone(), pm_repo);
848
849 let org_id = Uuid::new_v4();
850 let building_id = Uuid::new_v4();
851 let owner_id = Uuid::new_v4();
852
853 let request = make_create_request(building_id, owner_id, 15000);
854 let result = uc.create_payment(org_id, request).await;
855
856 assert!(result.is_ok());
857 let resp = result.unwrap();
858 assert_eq!(resp.amount_cents, 15000);
859 assert_eq!(resp.currency, "EUR");
860 assert_eq!(resp.status, TransactionStatus::Pending);
861 assert_eq!(resp.organization_id, org_id);
862 assert_eq!(resp.building_id, building_id);
863 assert_eq!(resp.owner_id, owner_id);
864 assert_eq!(resp.refunded_amount_cents, 0);
865 assert_eq!(payment_repo.payments.lock().unwrap().len(), 1);
867 }
868
869 #[tokio::test]
870 async fn test_create_payment_invalid_amount_zero() {
871 let payment_repo = Arc::new(MockPaymentRepository::new());
872 let pm_repo = Arc::new(MockPaymentMethodRepository::new());
873 let uc = make_use_cases(payment_repo, pm_repo);
874
875 let request = make_create_request(Uuid::new_v4(), Uuid::new_v4(), 0);
876 let result = uc.create_payment(Uuid::new_v4(), request).await;
877
878 assert!(result.is_err());
879 assert!(result
880 .unwrap_err()
881 .contains("Amount must be greater than 0"));
882 }
883
884 #[tokio::test]
885 async fn test_create_payment_invalid_amount_negative() {
886 let payment_repo = Arc::new(MockPaymentRepository::new());
887 let pm_repo = Arc::new(MockPaymentMethodRepository::new());
888 let uc = make_use_cases(payment_repo, pm_repo);
889
890 let request = make_create_request(Uuid::new_v4(), Uuid::new_v4(), -500);
891 let result = uc.create_payment(Uuid::new_v4(), request).await;
892
893 assert!(result.is_err());
894 assert!(result
895 .unwrap_err()
896 .contains("Amount must be greater than 0"));
897 }
898
899 #[tokio::test]
900 async fn test_status_transition_pending_to_processing_to_succeeded() {
901 let payment_repo = Arc::new(MockPaymentRepository::new());
902 let pm_repo = Arc::new(MockPaymentMethodRepository::new());
903 let uc = make_use_cases(payment_repo.clone(), pm_repo);
904
905 let org_id = Uuid::new_v4();
906 let payment =
907 seed_payment(&payment_repo, org_id, Uuid::new_v4(), Uuid::new_v4(), 10000).await;
908 let pid = payment.id;
909
910 let resp = uc.mark_processing(pid).await.unwrap();
912 assert_eq!(resp.status, TransactionStatus::Processing);
913
914 let resp = uc.mark_succeeded(pid).await.unwrap();
916 assert_eq!(resp.status, TransactionStatus::Succeeded);
917 assert!(resp.succeeded_at.is_some());
918 }
919
920 #[tokio::test]
921 async fn test_status_transition_mark_failed() {
922 let payment_repo = Arc::new(MockPaymentRepository::new());
923 let pm_repo = Arc::new(MockPaymentMethodRepository::new());
924 let uc = make_use_cases(payment_repo.clone(), pm_repo);
925
926 let org_id = Uuid::new_v4();
927 let payment =
928 seed_payment(&payment_repo, org_id, Uuid::new_v4(), Uuid::new_v4(), 5000).await;
929 let pid = payment.id;
930
931 uc.mark_processing(pid).await.unwrap();
933
934 let resp = uc
936 .mark_failed(pid, "Card declined".to_string())
937 .await
938 .unwrap();
939 assert_eq!(resp.status, TransactionStatus::Failed);
940 assert_eq!(resp.failure_reason, Some("Card declined".to_string()));
941 assert!(resp.failed_at.is_some());
942 }
943
944 #[tokio::test]
945 async fn test_mark_processing_not_found() {
946 let payment_repo = Arc::new(MockPaymentRepository::new());
947 let pm_repo = Arc::new(MockPaymentMethodRepository::new());
948 let uc = make_use_cases(payment_repo, pm_repo);
949
950 let result = uc.mark_processing(Uuid::new_v4()).await;
951
952 assert!(result.is_err());
953 assert!(result.unwrap_err().contains("Payment not found"));
954 }
955
956 #[tokio::test]
957 async fn test_refund_partial_success() {
958 let payment_repo = Arc::new(MockPaymentRepository::new());
959 let pm_repo = Arc::new(MockPaymentMethodRepository::new());
960 let uc = make_use_cases(payment_repo.clone(), pm_repo);
961
962 let org_id = Uuid::new_v4();
963 let payment =
964 seed_payment(&payment_repo, org_id, Uuid::new_v4(), Uuid::new_v4(), 20000).await;
965 let pid = payment.id;
966
967 uc.mark_succeeded(pid).await.unwrap();
969
970 let resp = uc
972 .refund_payment(
973 pid,
974 RefundPaymentRequest {
975 amount_cents: 8000,
976 reason: None,
977 },
978 )
979 .await
980 .unwrap();
981 assert_eq!(resp.status, TransactionStatus::Refunded);
982 assert_eq!(resp.refunded_amount_cents, 8000);
983 assert_eq!(resp.net_amount_cents, 12000); }
985
986 #[tokio::test]
987 async fn test_refund_over_refund_prevention() {
988 let payment_repo = Arc::new(MockPaymentRepository::new());
989 let pm_repo = Arc::new(MockPaymentMethodRepository::new());
990 let uc = make_use_cases(payment_repo.clone(), pm_repo);
991
992 let org_id = Uuid::new_v4();
993 let payment =
994 seed_payment(&payment_repo, org_id, Uuid::new_v4(), Uuid::new_v4(), 10000).await;
995 let pid = payment.id;
996
997 uc.mark_succeeded(pid).await.unwrap();
999
1000 uc.refund_payment(
1002 pid,
1003 RefundPaymentRequest {
1004 amount_cents: 6000,
1005 reason: None,
1006 },
1007 )
1008 .await
1009 .unwrap();
1010
1011 let result = uc
1013 .refund_payment(
1014 pid,
1015 RefundPaymentRequest {
1016 amount_cents: 6000,
1017 reason: None,
1018 },
1019 )
1020 .await;
1021 assert!(result.is_err());
1022 assert!(result.unwrap_err().contains("exceeds"));
1023 }
1024
1025 #[tokio::test]
1026 async fn test_refund_pending_payment_fails() {
1027 let payment_repo = Arc::new(MockPaymentRepository::new());
1028 let pm_repo = Arc::new(MockPaymentMethodRepository::new());
1029 let uc = make_use_cases(payment_repo.clone(), pm_repo);
1030
1031 let org_id = Uuid::new_v4();
1032 let payment =
1033 seed_payment(&payment_repo, org_id, Uuid::new_v4(), Uuid::new_v4(), 10000).await;
1034 let pid = payment.id;
1035
1036 let result = uc
1038 .refund_payment(
1039 pid,
1040 RefundPaymentRequest {
1041 amount_cents: 5000,
1042 reason: None,
1043 },
1044 )
1045 .await;
1046 assert!(result.is_err());
1047 assert!(result.unwrap_err().contains("succeeded payments"));
1048 }
1049
1050 #[tokio::test]
1051 async fn test_get_payment_found_and_not_found() {
1052 let payment_repo = Arc::new(MockPaymentRepository::new());
1053 let pm_repo = Arc::new(MockPaymentMethodRepository::new());
1054 let uc = make_use_cases(payment_repo.clone(), pm_repo);
1055
1056 let org_id = Uuid::new_v4();
1057 let payment =
1058 seed_payment(&payment_repo, org_id, Uuid::new_v4(), Uuid::new_v4(), 5000).await;
1059
1060 let result = uc.get_payment(payment.id).await.unwrap();
1062 assert!(result.is_some());
1063 assert_eq!(result.unwrap().amount_cents, 5000);
1064
1065 let result = uc.get_payment(Uuid::new_v4()).await.unwrap();
1067 assert!(result.is_none());
1068 }
1069
1070 #[tokio::test]
1071 async fn test_list_owner_and_building_payments() {
1072 let payment_repo = Arc::new(MockPaymentRepository::new());
1073 let pm_repo = Arc::new(MockPaymentMethodRepository::new());
1074 let uc = make_use_cases(payment_repo.clone(), pm_repo);
1075
1076 let org_id = Uuid::new_v4();
1077 let building_id = Uuid::new_v4();
1078 let owner_id = Uuid::new_v4();
1079
1080 seed_payment(&payment_repo, org_id, building_id, owner_id, 5000).await;
1082 seed_payment(&payment_repo, org_id, building_id, owner_id, 7000).await;
1083
1084 seed_payment(&payment_repo, org_id, building_id, Uuid::new_v4(), 3000).await;
1086
1087 let owner_payments = uc.list_owner_payments(owner_id).await.unwrap();
1088 assert_eq!(owner_payments.len(), 2);
1089
1090 let building_payments = uc.list_building_payments(building_id).await.unwrap();
1091 assert_eq!(building_payments.len(), 3);
1092 }
1093
1094 #[tokio::test]
1095 async fn test_set_payment_method_id_validates_existence() {
1096 let payment_repo = Arc::new(MockPaymentRepository::new());
1097 let pm_repo = Arc::new(MockPaymentMethodRepository::new());
1099 let uc = make_use_cases(payment_repo.clone(), pm_repo);
1100
1101 let org_id = Uuid::new_v4();
1102 let payment =
1103 seed_payment(&payment_repo, org_id, Uuid::new_v4(), Uuid::new_v4(), 5000).await;
1104
1105 let result = uc.set_payment_method_id(payment.id, Uuid::new_v4()).await;
1107 assert!(result.is_err());
1108 assert!(result.unwrap_err().contains("Payment method not found"));
1109 }
1110
1111 #[tokio::test]
1112 async fn test_set_payment_method_id_success() {
1113 let payment_repo = Arc::new(MockPaymentRepository::new());
1114 let pm = PaymentMethod::new(
1115 Uuid::new_v4(),
1116 Uuid::new_v4(),
1117 PMMethodType::Card,
1118 "pm_test_stripe_id_12345".to_string(),
1119 "cus_test_customer_12345".to_string(),
1120 "Visa **** 4242".to_string(),
1121 true,
1122 )
1123 .unwrap();
1124 let pm_id = pm.id;
1125 let pm_repo = Arc::new(MockPaymentMethodRepository::with_method(pm));
1126 let uc = make_use_cases(payment_repo.clone(), pm_repo);
1127
1128 let org_id = Uuid::new_v4();
1129 let payment =
1130 seed_payment(&payment_repo, org_id, Uuid::new_v4(), Uuid::new_v4(), 5000).await;
1131
1132 let resp = uc.set_payment_method_id(payment.id, pm_id).await.unwrap();
1133 assert_eq!(resp.payment_method_id, Some(pm_id));
1134 }
1135
1136 #[tokio::test]
1137 async fn test_delete_payment() {
1138 let payment_repo = Arc::new(MockPaymentRepository::new());
1139 let pm_repo = Arc::new(MockPaymentMethodRepository::new());
1140 let uc = make_use_cases(payment_repo.clone(), pm_repo);
1141
1142 let org_id = Uuid::new_v4();
1143 let payment =
1144 seed_payment(&payment_repo, org_id, Uuid::new_v4(), Uuid::new_v4(), 5000).await;
1145
1146 assert!(uc.delete_payment(payment.id).await.unwrap());
1147 let result = uc.get_payment(payment.id).await.unwrap();
1149 assert!(result.is_none());
1150
1151 assert!(!uc.delete_payment(Uuid::new_v4()).await.unwrap());
1153 }
1154
1155 #[tokio::test]
1156 async fn test_get_owner_payment_stats() {
1157 let payment_repo = Arc::new(MockPaymentRepository::new());
1158 let pm_repo = Arc::new(MockPaymentMethodRepository::new());
1159 let uc = make_use_cases(payment_repo.clone(), pm_repo);
1160
1161 let org_id = Uuid::new_v4();
1162 let building_id = Uuid::new_v4();
1163 let owner_id = Uuid::new_v4();
1164
1165 let p1 = seed_payment(&payment_repo, org_id, building_id, owner_id, 10000).await;
1167 seed_payment(&payment_repo, org_id, building_id, owner_id, 5000).await;
1168
1169 uc.mark_succeeded(p1.id).await.unwrap();
1171
1172 let stats = uc.get_owner_payment_stats(owner_id).await.unwrap();
1173 assert_eq!(stats.total_count, 2);
1174 assert_eq!(stats.succeeded_count, 1);
1175 assert_eq!(stats.pending_count, 1);
1176 assert_eq!(stats.total_amount_cents, 15000);
1177 assert_eq!(stats.total_succeeded_cents, 10000);
1178 }
1179}