1use crate::application::dto::{
2 CreatePaymentRequest, PaymentResponse, PaymentStatsResponse, RefundPaymentRequest,
3};
4use crate::application::ports::{
5 OwnerContributionRepository, PaymentMethodRepository, PaymentRepository, PaymentStats,
6};
7use crate::application::services::expense_accounting_service::ExpenseAccountingService;
8use crate::domain::entities::{ContributionPaymentMethod, Payment, TransactionStatus};
9use std::sync::Arc;
10use uuid::Uuid;
11
12pub struct PaymentUseCases {
13 payment_repository: Arc<dyn PaymentRepository>,
14 payment_method_repository: Arc<dyn PaymentMethodRepository>,
15 owner_contribution_repository: Arc<dyn OwnerContributionRepository>,
17 accounting_service: Option<Arc<ExpenseAccountingService>>,
20}
21
22impl PaymentUseCases {
23 pub fn new(
24 payment_repository: Arc<dyn PaymentRepository>,
25 payment_method_repository: Arc<dyn PaymentMethodRepository>,
26 owner_contribution_repository: Arc<dyn OwnerContributionRepository>,
27 ) -> Self {
28 Self {
29 payment_repository,
30 payment_method_repository,
31 owner_contribution_repository,
32 accounting_service: None,
33 }
34 }
35
36 pub fn with_accounting(mut self, accounting_service: Arc<ExpenseAccountingService>) -> Self {
37 self.accounting_service = Some(accounting_service);
38 self
39 }
40
41 pub async fn create_payment(
46 &self,
47 organization_id: Uuid,
48 request: CreatePaymentRequest,
49 ) -> Result<PaymentResponse, String> {
50 let idempotency_key = format!(
52 "{}-{}-{}-{}",
53 organization_id,
54 request.building_id,
55 request.owner_id,
56 Uuid::new_v4()
57 );
58
59 if let Some(existing_payment) = self
61 .payment_repository
62 .find_by_idempotency_key(organization_id, &idempotency_key)
63 .await?
64 {
65 return Ok(PaymentResponse::from(existing_payment));
67 }
68
69 let payment = Payment::new(
71 organization_id,
72 request.building_id,
73 request.owner_id,
74 request.expense_id,
75 request.contribution_id,
76 request.amount_cents,
77 request.payment_method_type,
78 idempotency_key,
79 request.description,
80 )?;
81
82 let created = self.payment_repository.create(&payment).await?;
83 Ok(PaymentResponse::from(created))
84 }
85
86 pub async fn get_payment(&self, id: Uuid) -> Result<Option<PaymentResponse>, String> {
88 match self.payment_repository.find_by_id(id).await? {
89 Some(payment) => Ok(Some(PaymentResponse::from(payment))),
90 None => Ok(None),
91 }
92 }
93
94 pub async fn get_payment_by_stripe_intent(
96 &self,
97 stripe_payment_intent_id: &str,
98 ) -> Result<Option<PaymentResponse>, String> {
99 match self
100 .payment_repository
101 .find_by_stripe_payment_intent_id(stripe_payment_intent_id)
102 .await?
103 {
104 Some(payment) => Ok(Some(PaymentResponse::from(payment))),
105 None => Ok(None),
106 }
107 }
108
109 pub async fn list_owner_payments(
111 &self,
112 owner_id: Uuid,
113 ) -> Result<Vec<PaymentResponse>, String> {
114 let payments = self.payment_repository.find_by_owner(owner_id).await?;
115 Ok(payments.into_iter().map(PaymentResponse::from).collect())
116 }
117
118 pub async fn list_building_payments(
120 &self,
121 building_id: Uuid,
122 ) -> Result<Vec<PaymentResponse>, String> {
123 let payments = self
124 .payment_repository
125 .find_by_building(building_id)
126 .await?;
127 Ok(payments.into_iter().map(PaymentResponse::from).collect())
128 }
129
130 pub async fn list_expense_payments(
132 &self,
133 expense_id: Uuid,
134 ) -> Result<Vec<PaymentResponse>, String> {
135 let payments = self.payment_repository.find_by_expense(expense_id).await?;
136 Ok(payments.into_iter().map(PaymentResponse::from).collect())
137 }
138
139 pub async fn list_organization_payments(
141 &self,
142 organization_id: Uuid,
143 ) -> Result<Vec<PaymentResponse>, String> {
144 let payments = self
145 .payment_repository
146 .find_by_organization(organization_id)
147 .await?;
148 Ok(payments.into_iter().map(PaymentResponse::from).collect())
149 }
150
151 pub async fn list_payments_by_status(
153 &self,
154 organization_id: Uuid,
155 status: TransactionStatus,
156 ) -> Result<Vec<PaymentResponse>, String> {
157 let payments = self
158 .payment_repository
159 .find_by_status(organization_id, status)
160 .await?;
161 Ok(payments.into_iter().map(PaymentResponse::from).collect())
162 }
163
164 pub async fn list_pending_payments(
166 &self,
167 organization_id: Uuid,
168 ) -> Result<Vec<PaymentResponse>, String> {
169 let payments = self
170 .payment_repository
171 .find_pending(organization_id)
172 .await?;
173 Ok(payments.into_iter().map(PaymentResponse::from).collect())
174 }
175
176 pub async fn list_failed_payments(
178 &self,
179 organization_id: Uuid,
180 ) -> Result<Vec<PaymentResponse>, String> {
181 let payments = self.payment_repository.find_failed(organization_id).await?;
182 Ok(payments.into_iter().map(PaymentResponse::from).collect())
183 }
184
185 pub async fn mark_processing(&self, id: Uuid) -> Result<PaymentResponse, String> {
187 let mut payment = self
188 .payment_repository
189 .find_by_id(id)
190 .await?
191 .ok_or_else(|| "Payment not found".to_string())?;
192
193 payment.mark_processing()?;
194
195 let updated = self.payment_repository.update(&payment).await?;
196 Ok(PaymentResponse::from(updated))
197 }
198
199 pub async fn mark_requires_action(&self, id: Uuid) -> Result<PaymentResponse, String> {
201 let mut payment = self
202 .payment_repository
203 .find_by_id(id)
204 .await?
205 .ok_or_else(|| "Payment not found".to_string())?;
206
207 payment.mark_requires_action()?;
208
209 let updated = self.payment_repository.update(&payment).await?;
210 Ok(PaymentResponse::from(updated))
211 }
212
213 pub async fn mark_succeeded(&self, id: Uuid) -> Result<PaymentResponse, String> {
222 let mut payment = self
223 .payment_repository
224 .find_by_id(id)
225 .await?
226 .ok_or_else(|| "Payment not found".to_string())?;
227
228 payment.mark_succeeded()?;
229
230 let updated = self.payment_repository.update(&payment).await?;
231 self.settle_linked_contribution(&updated).await;
232 Ok(PaymentResponse::from(updated))
233 }
234
235 async fn settle_linked_contribution(&self, payment: &Payment) {
243 let Some(contribution_id) = payment.contribution_id else {
244 return;
245 };
246
247 let contribution = match self
248 .owner_contribution_repository
249 .find_by_id(contribution_id)
250 .await
251 {
252 Ok(Some(c)) => c,
253 Ok(None) => {
254 tracing::warn!(
255 payment_id = %payment.id,
256 contribution_id = %contribution_id,
257 "paiement reussi rattache a une contribution introuvable",
258 );
259 return;
260 }
261 Err(err) => {
262 tracing::error!(
263 payment_id = %payment.id,
264 contribution_id = %contribution_id,
265 error = %err,
266 "lecture de la contribution impossible apres un paiement reussi",
267 );
268 return;
269 }
270 };
271
272 if contribution.is_paid() {
276 return;
277 }
278
279 let mut contribution = contribution;
280 contribution.mark_as_paid(
281 payment.succeeded_at.unwrap_or(payment.updated_at),
282 ContributionPaymentMethod::from(payment.payment_method_type.clone()),
286 Some(payment.id.to_string()),
287 );
288
289 if let Err(err) = self
290 .owner_contribution_repository
291 .update(&contribution)
292 .await
293 {
294 tracing::error!(
295 payment_id = %payment.id,
296 contribution_id = %contribution_id,
297 error = %err,
298 "quote-part non soldee malgre un paiement reussi",
299 );
300 return;
301 }
302
303 if let Some(ref accounting) = self.accounting_service {
309 if let Err(err) = accounting
310 .generate_contribution_receipt_entry(
311 &contribution,
312 Some(payment.building_id),
316 None,
317 None,
318 )
319 .await
320 {
321 tracing::warn!(
322 payment_id = %payment.id,
323 contribution_id = %contribution_id,
324 error = %err,
325 "ecriture d'encaissement non generee",
326 );
327 }
328 }
329 }
330
331 pub async fn mark_failed(&self, id: Uuid, reason: String) -> Result<PaymentResponse, String> {
333 let mut payment = self
334 .payment_repository
335 .find_by_id(id)
336 .await?
337 .ok_or_else(|| "Payment not found".to_string())?;
338
339 payment.mark_failed(reason)?;
340
341 let updated = self.payment_repository.update(&payment).await?;
342 Ok(PaymentResponse::from(updated))
343 }
344
345 pub async fn mark_cancelled(&self, id: Uuid) -> Result<PaymentResponse, String> {
347 let mut payment = self
348 .payment_repository
349 .find_by_id(id)
350 .await?
351 .ok_or_else(|| "Payment not found".to_string())?;
352
353 payment.mark_cancelled()?;
354
355 let updated = self.payment_repository.update(&payment).await?;
356 Ok(PaymentResponse::from(updated))
357 }
358
359 pub async fn refund_payment(
361 &self,
362 id: Uuid,
363 request: RefundPaymentRequest,
364 ) -> Result<PaymentResponse, String> {
365 let mut payment = self
366 .payment_repository
367 .find_by_id(id)
368 .await?
369 .ok_or_else(|| "Payment not found".to_string())?;
370
371 payment.refund(request.amount_cents)?;
372
373 let updated = self.payment_repository.update(&payment).await?;
374 Ok(PaymentResponse::from(updated))
375 }
376
377 pub async fn set_stripe_payment_intent_id(
379 &self,
380 id: Uuid,
381 stripe_payment_intent_id: String,
382 ) -> Result<PaymentResponse, String> {
383 let mut payment = self
384 .payment_repository
385 .find_by_id(id)
386 .await?
387 .ok_or_else(|| "Payment not found".to_string())?;
388
389 payment.set_stripe_payment_intent_id(stripe_payment_intent_id);
390
391 let updated = self.payment_repository.update(&payment).await?;
392 Ok(PaymentResponse::from(updated))
393 }
394
395 pub async fn set_stripe_customer_id(
397 &self,
398 id: Uuid,
399 stripe_customer_id: String,
400 ) -> Result<PaymentResponse, String> {
401 let mut payment = self
402 .payment_repository
403 .find_by_id(id)
404 .await?
405 .ok_or_else(|| "Payment not found".to_string())?;
406
407 payment.set_stripe_customer_id(stripe_customer_id);
408
409 let updated = self.payment_repository.update(&payment).await?;
410 Ok(PaymentResponse::from(updated))
411 }
412
413 pub async fn set_payment_method_id(
415 &self,
416 id: Uuid,
417 payment_method_id: Uuid,
418 ) -> Result<PaymentResponse, String> {
419 let _payment_method = self
421 .payment_method_repository
422 .find_by_id(payment_method_id)
423 .await?
424 .ok_or_else(|| "Payment method not found".to_string())?;
425
426 let mut payment = self
427 .payment_repository
428 .find_by_id(id)
429 .await?
430 .ok_or_else(|| "Payment not found".to_string())?;
431
432 payment.set_payment_method_id(payment_method_id);
433
434 let updated = self.payment_repository.update(&payment).await?;
435 Ok(PaymentResponse::from(updated))
436 }
437
438 pub async fn delete_payment(&self, id: Uuid) -> Result<bool, String> {
440 self.payment_repository.delete(id).await
441 }
442
443 pub async fn get_total_paid_for_expense(&self, expense_id: Uuid) -> Result<i64, String> {
445 self.payment_repository
446 .get_total_paid_for_expense(expense_id)
447 .await
448 }
449
450 pub async fn get_total_paid_by_owner(&self, owner_id: Uuid) -> Result<i64, String> {
452 self.payment_repository
453 .get_total_paid_by_owner(owner_id)
454 .await
455 }
456
457 pub async fn get_total_paid_for_building(&self, building_id: Uuid) -> Result<i64, String> {
459 self.payment_repository
460 .get_total_paid_for_building(building_id)
461 .await
462 }
463
464 pub async fn get_owner_payment_stats(
466 &self,
467 owner_id: Uuid,
468 ) -> Result<PaymentStatsResponse, String> {
469 let stats = self
470 .payment_repository
471 .get_owner_payment_stats(owner_id)
472 .await?;
473 Ok(Self::payment_stats_to_response(stats))
474 }
475
476 pub async fn get_building_payment_stats(
478 &self,
479 building_id: Uuid,
480 ) -> Result<PaymentStatsResponse, String> {
481 let stats = self
482 .payment_repository
483 .get_building_payment_stats(building_id)
484 .await?;
485 Ok(Self::payment_stats_to_response(stats))
486 }
487
488 fn payment_stats_to_response(stats: PaymentStats) -> PaymentStatsResponse {
490 PaymentStatsResponse {
491 total_count: stats.total_count,
492 succeeded_count: stats.succeeded_count,
493 failed_count: stats.failed_count,
494 pending_count: stats.pending_count,
495 total_amount_cents: stats.total_amount_cents,
496 total_succeeded_cents: stats.total_succeeded_cents,
497 total_refunded_cents: stats.total_refunded_cents,
498 net_amount_cents: stats.net_amount_cents,
499 }
500 }
501}
502
503#[cfg(test)]
504mod tests {
505 use super::*;
506 use crate::application::ports::OwnerContributionRepository;
507 use crate::application::ports::{PaymentMethodRepository, PaymentRepository, PaymentStats};
508 use crate::domain::entities::payment_method::{
509 PaymentMethod, PaymentMethodType as PMMethodType,
510 };
511 use crate::domain::entities::{
512 ContributionPaymentStatus, ContributionType, OwnerContribution, Payment, PaymentMethodType,
513 TransactionStatus,
514 };
515 use async_trait::async_trait;
516 use chrono::Utc;
517 use rust_decimal_macros::dec;
518 use std::collections::HashMap;
519 use std::sync::Mutex;
520 use uuid::Uuid;
521
522 struct MockPaymentRepository {
525 payments: Mutex<HashMap<Uuid, Payment>>,
526 }
527
528 impl MockPaymentRepository {
529 fn new() -> Self {
530 Self {
531 payments: Mutex::new(HashMap::new()),
532 }
533 }
534 }
535
536 #[async_trait]
537 impl PaymentRepository for MockPaymentRepository {
538 async fn create(&self, payment: &Payment) -> Result<Payment, String> {
539 self.payments
540 .lock()
541 .unwrap()
542 .insert(payment.id, payment.clone());
543 Ok(payment.clone())
544 }
545
546 async fn find_by_id(&self, id: Uuid) -> Result<Option<Payment>, String> {
547 Ok(self.payments.lock().unwrap().get(&id).cloned())
548 }
549
550 async fn find_by_stripe_payment_intent_id(
551 &self,
552 stripe_payment_intent_id: &str,
553 ) -> Result<Option<Payment>, String> {
554 Ok(self
555 .payments
556 .lock()
557 .unwrap()
558 .values()
559 .find(|p| p.stripe_payment_intent_id.as_deref() == Some(stripe_payment_intent_id))
560 .cloned())
561 }
562
563 async fn find_by_idempotency_key(
564 &self,
565 organization_id: Uuid,
566 idempotency_key: &str,
567 ) -> Result<Option<Payment>, String> {
568 Ok(self
569 .payments
570 .lock()
571 .unwrap()
572 .values()
573 .find(|p| {
574 p.organization_id == organization_id && p.idempotency_key == idempotency_key
575 })
576 .cloned())
577 }
578
579 async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<Payment>, String> {
580 Ok(self
581 .payments
582 .lock()
583 .unwrap()
584 .values()
585 .filter(|p| p.owner_id == owner_id)
586 .cloned()
587 .collect())
588 }
589
590 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Payment>, String> {
591 Ok(self
592 .payments
593 .lock()
594 .unwrap()
595 .values()
596 .filter(|p| p.building_id == building_id)
597 .cloned()
598 .collect())
599 }
600
601 async fn find_by_expense(&self, expense_id: Uuid) -> Result<Vec<Payment>, String> {
602 Ok(self
603 .payments
604 .lock()
605 .unwrap()
606 .values()
607 .filter(|p| p.expense_id == Some(expense_id))
608 .cloned()
609 .collect())
610 }
611
612 async fn find_by_organization(
613 &self,
614 organization_id: Uuid,
615 ) -> Result<Vec<Payment>, String> {
616 Ok(self
617 .payments
618 .lock()
619 .unwrap()
620 .values()
621 .filter(|p| p.organization_id == organization_id)
622 .cloned()
623 .collect())
624 }
625
626 async fn find_by_status(
627 &self,
628 organization_id: Uuid,
629 status: TransactionStatus,
630 ) -> Result<Vec<Payment>, String> {
631 Ok(self
632 .payments
633 .lock()
634 .unwrap()
635 .values()
636 .filter(|p| p.organization_id == organization_id && p.status == status)
637 .cloned()
638 .collect())
639 }
640
641 async fn find_by_building_and_status(
642 &self,
643 building_id: Uuid,
644 status: TransactionStatus,
645 ) -> Result<Vec<Payment>, String> {
646 Ok(self
647 .payments
648 .lock()
649 .unwrap()
650 .values()
651 .filter(|p| p.building_id == building_id && p.status == status)
652 .cloned()
653 .collect())
654 }
655
656 async fn find_pending(&self, organization_id: Uuid) -> Result<Vec<Payment>, String> {
657 self.find_by_status(organization_id, TransactionStatus::Pending)
658 .await
659 }
660
661 async fn find_failed(&self, organization_id: Uuid) -> Result<Vec<Payment>, String> {
662 self.find_by_status(organization_id, TransactionStatus::Failed)
663 .await
664 }
665
666 async fn update(&self, payment: &Payment) -> Result<Payment, String> {
667 self.payments
668 .lock()
669 .unwrap()
670 .insert(payment.id, payment.clone());
671 Ok(payment.clone())
672 }
673
674 async fn delete(&self, id: Uuid) -> Result<bool, String> {
675 Ok(self.payments.lock().unwrap().remove(&id).is_some())
676 }
677
678 async fn get_total_paid_for_expense(&self, expense_id: Uuid) -> Result<i64, String> {
679 Ok(self
680 .payments
681 .lock()
682 .unwrap()
683 .values()
684 .filter(|p| {
685 p.expense_id == Some(expense_id) && p.status == TransactionStatus::Succeeded
686 })
687 .map(|p| p.amount_cents)
688 .sum())
689 }
690
691 async fn get_total_paid_by_owner(&self, owner_id: Uuid) -> Result<i64, String> {
692 Ok(self
693 .payments
694 .lock()
695 .unwrap()
696 .values()
697 .filter(|p| p.owner_id == owner_id && p.status == TransactionStatus::Succeeded)
698 .map(|p| p.amount_cents)
699 .sum())
700 }
701
702 async fn get_total_paid_for_building(&self, building_id: Uuid) -> Result<i64, String> {
703 Ok(self
704 .payments
705 .lock()
706 .unwrap()
707 .values()
708 .filter(|p| {
709 p.building_id == building_id && p.status == TransactionStatus::Succeeded
710 })
711 .map(|p| p.amount_cents)
712 .sum())
713 }
714
715 async fn get_owner_payment_stats(&self, owner_id: Uuid) -> Result<PaymentStats, String> {
716 let payments: Vec<_> = self
717 .payments
718 .lock()
719 .unwrap()
720 .values()
721 .filter(|p| p.owner_id == owner_id)
722 .cloned()
723 .collect();
724 Ok(compute_stats(&payments))
725 }
726
727 async fn get_building_payment_stats(
728 &self,
729 building_id: Uuid,
730 ) -> Result<PaymentStats, String> {
731 let payments: Vec<_> = self
732 .payments
733 .lock()
734 .unwrap()
735 .values()
736 .filter(|p| p.building_id == building_id)
737 .cloned()
738 .collect();
739 Ok(compute_stats(&payments))
740 }
741 }
742
743 fn compute_stats(payments: &[Payment]) -> PaymentStats {
744 let total_count = payments.len() as i64;
745 let succeeded_count = payments
746 .iter()
747 .filter(|p| p.status == TransactionStatus::Succeeded)
748 .count() as i64;
749 let failed_count = payments
750 .iter()
751 .filter(|p| p.status == TransactionStatus::Failed)
752 .count() as i64;
753 let pending_count = payments
754 .iter()
755 .filter(|p| p.status == TransactionStatus::Pending)
756 .count() as i64;
757 let total_amount_cents: i64 = payments.iter().map(|p| p.amount_cents).sum();
758 let total_succeeded_cents: i64 = payments
759 .iter()
760 .filter(|p| p.status == TransactionStatus::Succeeded)
761 .map(|p| p.amount_cents)
762 .sum();
763 let total_refunded_cents: i64 = payments.iter().map(|p| p.refunded_amount_cents).sum();
764 PaymentStats {
765 total_count,
766 succeeded_count,
767 failed_count,
768 pending_count,
769 total_amount_cents,
770 total_succeeded_cents,
771 total_refunded_cents,
772 net_amount_cents: total_succeeded_cents - total_refunded_cents,
773 }
774 }
775
776 struct MockPaymentMethodRepository {
779 methods: Mutex<HashMap<Uuid, PaymentMethod>>,
780 }
781
782 impl MockPaymentMethodRepository {
783 fn new() -> Self {
784 Self {
785 methods: Mutex::new(HashMap::new()),
786 }
787 }
788
789 fn with_method(method: PaymentMethod) -> Self {
790 let mut map = HashMap::new();
791 map.insert(method.id, method);
792 Self {
793 methods: Mutex::new(map),
794 }
795 }
796 }
797
798 #[async_trait]
799 impl PaymentMethodRepository for MockPaymentMethodRepository {
800 async fn create(&self, pm: &PaymentMethod) -> Result<PaymentMethod, String> {
801 self.methods.lock().unwrap().insert(pm.id, pm.clone());
802 Ok(pm.clone())
803 }
804
805 async fn find_by_id(&self, id: Uuid) -> Result<Option<PaymentMethod>, String> {
806 Ok(self.methods.lock().unwrap().get(&id).cloned())
807 }
808
809 async fn find_by_stripe_payment_method_id(
810 &self,
811 stripe_payment_method_id: &str,
812 ) -> Result<Option<PaymentMethod>, String> {
813 Ok(self
814 .methods
815 .lock()
816 .unwrap()
817 .values()
818 .find(|m| m.stripe_payment_method_id == stripe_payment_method_id)
819 .cloned())
820 }
821
822 async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<PaymentMethod>, String> {
823 Ok(self
824 .methods
825 .lock()
826 .unwrap()
827 .values()
828 .filter(|m| m.owner_id == owner_id)
829 .cloned()
830 .collect())
831 }
832
833 async fn find_active_by_owner(&self, owner_id: Uuid) -> Result<Vec<PaymentMethod>, String> {
834 Ok(self
835 .methods
836 .lock()
837 .unwrap()
838 .values()
839 .filter(|m| m.owner_id == owner_id && m.is_active)
840 .cloned()
841 .collect())
842 }
843
844 async fn find_default_by_owner(
845 &self,
846 owner_id: Uuid,
847 ) -> Result<Option<PaymentMethod>, String> {
848 Ok(self
849 .methods
850 .lock()
851 .unwrap()
852 .values()
853 .find(|m| m.owner_id == owner_id && m.is_default)
854 .cloned())
855 }
856
857 async fn find_by_organization(
858 &self,
859 organization_id: Uuid,
860 ) -> Result<Vec<PaymentMethod>, String> {
861 Ok(self
862 .methods
863 .lock()
864 .unwrap()
865 .values()
866 .filter(|m| m.organization_id == organization_id)
867 .cloned()
868 .collect())
869 }
870
871 async fn find_by_owner_and_type(
872 &self,
873 owner_id: Uuid,
874 method_type: PMMethodType,
875 ) -> Result<Vec<PaymentMethod>, String> {
876 Ok(self
877 .methods
878 .lock()
879 .unwrap()
880 .values()
881 .filter(|m| m.owner_id == owner_id && m.method_type == method_type)
882 .cloned()
883 .collect())
884 }
885
886 async fn update(&self, pm: &PaymentMethod) -> Result<PaymentMethod, String> {
887 self.methods.lock().unwrap().insert(pm.id, pm.clone());
888 Ok(pm.clone())
889 }
890
891 async fn delete(&self, id: Uuid) -> Result<bool, String> {
892 Ok(self.methods.lock().unwrap().remove(&id).is_some())
893 }
894
895 async fn set_as_default(&self, id: Uuid, _owner_id: Uuid) -> Result<PaymentMethod, String> {
896 let mut methods = self.methods.lock().unwrap();
897 let pm = methods
898 .get_mut(&id)
899 .ok_or_else(|| "Not found".to_string())?;
900 pm.is_default = true;
901 Ok(pm.clone())
902 }
903
904 async fn count_active_by_owner(&self, owner_id: Uuid) -> Result<i64, String> {
905 Ok(self
906 .methods
907 .lock()
908 .unwrap()
909 .values()
910 .filter(|m| m.owner_id == owner_id && m.is_active)
911 .count() as i64)
912 }
913
914 async fn has_active_payment_methods(&self, owner_id: Uuid) -> Result<bool, String> {
915 Ok(self.count_active_by_owner(owner_id).await? > 0)
916 }
917 }
918
919 fn make_use_cases(
922 payment_repo: Arc<dyn PaymentRepository>,
923 pm_repo: Arc<dyn PaymentMethodRepository>,
924 ) -> PaymentUseCases {
925 PaymentUseCases::new(
926 payment_repo,
927 pm_repo,
928 Arc::new(MockOwnerContributionRepository::new()),
929 )
930 }
931
932 fn make_use_cases_with_contributions(
933 payment_repo: Arc<dyn PaymentRepository>,
934 pm_repo: Arc<dyn PaymentMethodRepository>,
935 contribution_repo: Arc<MockOwnerContributionRepository>,
936 ) -> PaymentUseCases {
937 PaymentUseCases::new(payment_repo, pm_repo, contribution_repo)
938 }
939
940 struct MockOwnerContributionRepository {
941 contributions: Mutex<HashMap<Uuid, OwnerContribution>>,
942 }
943
944 impl MockOwnerContributionRepository {
945 fn new() -> Self {
946 Self {
947 contributions: Mutex::new(HashMap::new()),
948 }
949 }
950
951 fn with_contribution(contribution: OwnerContribution) -> Self {
952 let mut map = HashMap::new();
953 map.insert(contribution.id, contribution);
954 Self {
955 contributions: Mutex::new(map),
956 }
957 }
958
959 fn get(&self, id: Uuid) -> Option<OwnerContribution> {
960 self.contributions.lock().unwrap().get(&id).cloned()
961 }
962 }
963
964 #[async_trait]
965 impl OwnerContributionRepository for MockOwnerContributionRepository {
966 async fn create(&self, c: &OwnerContribution) -> Result<OwnerContribution, String> {
967 self.contributions.lock().unwrap().insert(c.id, c.clone());
968 Ok(c.clone())
969 }
970 async fn find_by_id(&self, id: Uuid) -> Result<Option<OwnerContribution>, String> {
971 Ok(self.contributions.lock().unwrap().get(&id).cloned())
972 }
973 async fn find_by_organization(
974 &self,
975 organization_id: Uuid,
976 ) -> Result<Vec<OwnerContribution>, String> {
977 Ok(self
978 .contributions
979 .lock()
980 .unwrap()
981 .values()
982 .filter(|c| c.organization_id == organization_id)
983 .cloned()
984 .collect())
985 }
986 async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<OwnerContribution>, String> {
987 Ok(self
988 .contributions
989 .lock()
990 .unwrap()
991 .values()
992 .filter(|c| c.owner_id == owner_id)
993 .cloned()
994 .collect())
995 }
996 async fn update(&self, c: &OwnerContribution) -> Result<OwnerContribution, String> {
997 self.contributions.lock().unwrap().insert(c.id, c.clone());
998 Ok(c.clone())
999 }
1000 }
1001
1002 fn seed_contribution(organization_id: Uuid, owner_id: Uuid) -> OwnerContribution {
1003 OwnerContribution::new(
1004 Uuid::new_v4(), organization_id,
1006 owner_id,
1007 None,
1008 "Charges Q3 2026".to_string(),
1009 dec!(2000.00),
1010 ContributionType::Regular,
1011 Utc::now(),
1012 Some("7000".to_string()),
1013 )
1014 .expect("contribution de test valide")
1015 }
1016
1017 fn make_create_request(
1018 building_id: Uuid,
1019 owner_id: Uuid,
1020 amount_cents: i64,
1021 ) -> CreatePaymentRequest {
1022 CreatePaymentRequest {
1023 building_id,
1024 owner_id,
1025 expense_id: None,
1026 contribution_id: None,
1027 amount_cents,
1028 payment_method_type: PaymentMethodType::Card,
1029 payment_method_id: None,
1030 description: Some("Test payment".to_string()),
1031 metadata: None,
1032 }
1033 }
1034
1035 async fn seed_payment(
1037 repo: &Arc<MockPaymentRepository>,
1038 org_id: Uuid,
1039 building_id: Uuid,
1040 owner_id: Uuid,
1041 amount_cents: i64,
1042 ) -> Payment {
1043 let payment = Payment::new(
1044 org_id,
1045 building_id,
1046 owner_id,
1047 None,
1048 None,
1049 amount_cents,
1050 PaymentMethodType::Card,
1051 format!("idem-{}-{}", org_id, Uuid::new_v4()),
1052 Some("seeded".to_string()),
1053 )
1054 .unwrap();
1055 repo.create(&payment).await.unwrap();
1056 payment
1057 }
1058
1059 #[tokio::test]
1062 async fn test_create_payment_success() {
1063 let payment_repo = Arc::new(MockPaymentRepository::new());
1064 let pm_repo = Arc::new(MockPaymentMethodRepository::new());
1065 let uc = make_use_cases(payment_repo.clone(), pm_repo);
1066
1067 let org_id = Uuid::new_v4();
1068 let building_id = Uuid::new_v4();
1069 let owner_id = Uuid::new_v4();
1070
1071 let request = make_create_request(building_id, owner_id, 15000);
1072 let result = uc.create_payment(org_id, request).await;
1073
1074 assert!(result.is_ok());
1075 let resp = result.unwrap();
1076 assert_eq!(resp.amount_cents, 15000);
1077 assert_eq!(resp.currency, "EUR");
1078 assert_eq!(resp.status, TransactionStatus::Pending);
1079 assert_eq!(resp.organization_id, org_id);
1080 assert_eq!(resp.building_id, building_id);
1081 assert_eq!(resp.owner_id, owner_id);
1082 assert_eq!(resp.refunded_amount_cents, 0);
1083 assert_eq!(payment_repo.payments.lock().unwrap().len(), 1);
1085 }
1086
1087 #[tokio::test]
1088 async fn test_create_payment_invalid_amount_zero() {
1089 let payment_repo = Arc::new(MockPaymentRepository::new());
1090 let pm_repo = Arc::new(MockPaymentMethodRepository::new());
1091 let uc = make_use_cases(payment_repo, pm_repo);
1092
1093 let request = make_create_request(Uuid::new_v4(), Uuid::new_v4(), 0);
1094 let result = uc.create_payment(Uuid::new_v4(), request).await;
1095
1096 assert!(result.is_err());
1097 assert!(result
1098 .unwrap_err()
1099 .contains("Amount must be greater than 0"));
1100 }
1101
1102 #[tokio::test]
1103 async fn test_create_payment_invalid_amount_negative() {
1104 let payment_repo = Arc::new(MockPaymentRepository::new());
1105 let pm_repo = Arc::new(MockPaymentMethodRepository::new());
1106 let uc = make_use_cases(payment_repo, pm_repo);
1107
1108 let request = make_create_request(Uuid::new_v4(), Uuid::new_v4(), -500);
1109 let result = uc.create_payment(Uuid::new_v4(), request).await;
1110
1111 assert!(result.is_err());
1112 assert!(result
1113 .unwrap_err()
1114 .contains("Amount must be greater than 0"));
1115 }
1116
1117 #[tokio::test]
1125 async fn test_paiement_reussi_solde_la_quote_part_liee() {
1126 let org_id = Uuid::new_v4();
1127 let owner_id = Uuid::new_v4();
1128 let contribution = seed_contribution(org_id, owner_id);
1129 let contribution_id = contribution.id;
1130 assert_eq!(
1131 contribution.payment_status,
1132 ContributionPaymentStatus::Pending,
1133 "prealable : la contribution part impayee"
1134 );
1135
1136 let payment_repo = Arc::new(MockPaymentRepository::new());
1137 let contrib_repo = Arc::new(MockOwnerContributionRepository::with_contribution(
1138 contribution,
1139 ));
1140 let uc = make_use_cases_with_contributions(
1141 payment_repo.clone(),
1142 Arc::new(MockPaymentMethodRepository::new()),
1143 contrib_repo.clone(),
1144 );
1145
1146 let mut request = make_create_request(Uuid::new_v4(), owner_id, 200_000);
1147 request.contribution_id = Some(contribution_id);
1148 let created = uc.create_payment(org_id, request).await.unwrap();
1149
1150 assert_eq!(created.contribution_id, Some(contribution_id));
1152 assert_eq!(
1156 contrib_repo.get(contribution_id).unwrap().payment_status,
1157 ContributionPaymentStatus::Pending,
1158 "une intention de paiement ne doit pas marquer la quote-part payee"
1159 );
1160
1161 uc.mark_processing(created.id).await.unwrap();
1162 uc.mark_succeeded(created.id).await.unwrap();
1163
1164 let apres = contrib_repo.get(contribution_id).unwrap();
1165 assert_eq!(
1166 apres.payment_status,
1167 ContributionPaymentStatus::Paid,
1168 "la quote-part doit etre soldee des que le paiement aboutit"
1169 );
1170 assert!(
1171 apres.payment_date.is_some(),
1172 "la date de paiement est posee"
1173 );
1174 assert_eq!(
1175 apres.payment_reference,
1176 Some(created.id.to_string()),
1177 "la reference doit permettre de remonter au paiement"
1178 );
1179 assert_eq!(
1181 apres.payment_method,
1182 Some(ContributionPaymentMethod::BankTransfer)
1183 );
1184 }
1185
1186 #[tokio::test]
1188 async fn test_paiement_sans_quote_part_ne_solde_rien() {
1189 let org_id = Uuid::new_v4();
1190 let owner_id = Uuid::new_v4();
1191 let contribution = seed_contribution(org_id, owner_id);
1192 let contribution_id = contribution.id;
1193
1194 let payment_repo = Arc::new(MockPaymentRepository::new());
1195 let contrib_repo = Arc::new(MockOwnerContributionRepository::with_contribution(
1196 contribution,
1197 ));
1198 let uc = make_use_cases_with_contributions(
1199 payment_repo.clone(),
1200 Arc::new(MockPaymentMethodRepository::new()),
1201 contrib_repo.clone(),
1202 );
1203
1204 let request = make_create_request(Uuid::new_v4(), owner_id, 200_000);
1205 let created = uc.create_payment(org_id, request).await.unwrap();
1206 uc.mark_processing(created.id).await.unwrap();
1207 uc.mark_succeeded(created.id).await.unwrap();
1208
1209 assert_eq!(
1210 contrib_repo.get(contribution_id).unwrap().payment_status,
1211 ContributionPaymentStatus::Pending,
1212 "un paiement non rattache ne doit solder aucune quote-part"
1213 );
1214 }
1215
1216 #[tokio::test]
1221 async fn test_quote_part_deja_soldee_nest_pas_ecrasee() {
1222 let org_id = Uuid::new_v4();
1223 let owner_id = Uuid::new_v4();
1224 let mut contribution = seed_contribution(org_id, owner_id);
1225 let contribution_id = contribution.id;
1226 let date_origine = Utc::now() - chrono::Duration::days(10);
1227 contribution.mark_as_paid(
1228 date_origine,
1229 ContributionPaymentMethod::Cash,
1230 Some("recu-papier-42".to_string()),
1231 );
1232
1233 let payment_repo = Arc::new(MockPaymentRepository::new());
1234 let contrib_repo = Arc::new(MockOwnerContributionRepository::with_contribution(
1235 contribution,
1236 ));
1237 let uc = make_use_cases_with_contributions(
1238 payment_repo.clone(),
1239 Arc::new(MockPaymentMethodRepository::new()),
1240 contrib_repo.clone(),
1241 );
1242
1243 let mut request = make_create_request(Uuid::new_v4(), owner_id, 200_000);
1244 request.contribution_id = Some(contribution_id);
1245 let created = uc.create_payment(org_id, request).await.unwrap();
1246 uc.mark_processing(created.id).await.unwrap();
1247 uc.mark_succeeded(created.id).await.unwrap();
1248
1249 let apres = contrib_repo.get(contribution_id).unwrap();
1250 assert_eq!(
1251 apres.payment_reference,
1252 Some("recu-papier-42".to_string()),
1253 "le paiement d'origine doit rester la reference"
1254 );
1255 assert_eq!(apres.payment_method, Some(ContributionPaymentMethod::Cash));
1256 }
1257
1258 #[tokio::test]
1263 async fn test_quote_part_introuvable_ne_fait_pas_echouer_le_paiement() {
1264 let org_id = Uuid::new_v4();
1265 let payment_repo = Arc::new(MockPaymentRepository::new());
1266 let uc = make_use_cases_with_contributions(
1267 payment_repo.clone(),
1268 Arc::new(MockPaymentMethodRepository::new()),
1269 Arc::new(MockOwnerContributionRepository::new()),
1270 );
1271
1272 let mut request = make_create_request(Uuid::new_v4(), Uuid::new_v4(), 200_000);
1273 request.contribution_id = Some(Uuid::new_v4()); let created = uc.create_payment(org_id, request).await.unwrap();
1275 uc.mark_processing(created.id).await.unwrap();
1276
1277 let resultat = uc.mark_succeeded(created.id).await;
1278 assert!(
1279 resultat.is_ok(),
1280 "le paiement reste reussi meme si la quote-part est introuvable"
1281 );
1282 assert_eq!(resultat.unwrap().status, TransactionStatus::Succeeded);
1283 }
1284
1285 #[tokio::test]
1286 async fn test_status_transition_pending_to_processing_to_succeeded() {
1287 let payment_repo = Arc::new(MockPaymentRepository::new());
1288 let pm_repo = Arc::new(MockPaymentMethodRepository::new());
1289 let uc = make_use_cases(payment_repo.clone(), pm_repo);
1290
1291 let org_id = Uuid::new_v4();
1292 let payment =
1293 seed_payment(&payment_repo, org_id, Uuid::new_v4(), Uuid::new_v4(), 10000).await;
1294 let pid = payment.id;
1295
1296 let resp = uc.mark_processing(pid).await.unwrap();
1298 assert_eq!(resp.status, TransactionStatus::Processing);
1299
1300 let resp = uc.mark_succeeded(pid).await.unwrap();
1302 assert_eq!(resp.status, TransactionStatus::Succeeded);
1303 assert!(resp.succeeded_at.is_some());
1304 }
1305
1306 #[tokio::test]
1307 async fn test_status_transition_mark_failed() {
1308 let payment_repo = Arc::new(MockPaymentRepository::new());
1309 let pm_repo = Arc::new(MockPaymentMethodRepository::new());
1310 let uc = make_use_cases(payment_repo.clone(), pm_repo);
1311
1312 let org_id = Uuid::new_v4();
1313 let payment =
1314 seed_payment(&payment_repo, org_id, Uuid::new_v4(), Uuid::new_v4(), 5000).await;
1315 let pid = payment.id;
1316
1317 uc.mark_processing(pid).await.unwrap();
1319
1320 let resp = uc
1322 .mark_failed(pid, "Card declined".to_string())
1323 .await
1324 .unwrap();
1325 assert_eq!(resp.status, TransactionStatus::Failed);
1326 assert_eq!(resp.failure_reason, Some("Card declined".to_string()));
1327 assert!(resp.failed_at.is_some());
1328 }
1329
1330 #[tokio::test]
1331 async fn test_mark_processing_not_found() {
1332 let payment_repo = Arc::new(MockPaymentRepository::new());
1333 let pm_repo = Arc::new(MockPaymentMethodRepository::new());
1334 let uc = make_use_cases(payment_repo, pm_repo);
1335
1336 let result = uc.mark_processing(Uuid::new_v4()).await;
1337
1338 assert!(result.is_err());
1339 assert!(result.unwrap_err().contains("Payment not found"));
1340 }
1341
1342 #[tokio::test]
1343 async fn test_refund_partial_success() {
1344 let payment_repo = Arc::new(MockPaymentRepository::new());
1345 let pm_repo = Arc::new(MockPaymentMethodRepository::new());
1346 let uc = make_use_cases(payment_repo.clone(), pm_repo);
1347
1348 let org_id = Uuid::new_v4();
1349 let payment =
1350 seed_payment(&payment_repo, org_id, Uuid::new_v4(), Uuid::new_v4(), 20000).await;
1351 let pid = payment.id;
1352
1353 uc.mark_succeeded(pid).await.unwrap();
1355
1356 let resp = uc
1358 .refund_payment(
1359 pid,
1360 RefundPaymentRequest {
1361 amount_cents: 8000,
1362 reason: None,
1363 },
1364 )
1365 .await
1366 .unwrap();
1367 assert_eq!(resp.status, TransactionStatus::Refunded);
1368 assert_eq!(resp.refunded_amount_cents, 8000);
1369 assert_eq!(resp.net_amount_cents, 12000); }
1371
1372 #[tokio::test]
1373 async fn test_refund_over_refund_prevention() {
1374 let payment_repo = Arc::new(MockPaymentRepository::new());
1375 let pm_repo = Arc::new(MockPaymentMethodRepository::new());
1376 let uc = make_use_cases(payment_repo.clone(), pm_repo);
1377
1378 let org_id = Uuid::new_v4();
1379 let payment =
1380 seed_payment(&payment_repo, org_id, Uuid::new_v4(), Uuid::new_v4(), 10000).await;
1381 let pid = payment.id;
1382
1383 uc.mark_succeeded(pid).await.unwrap();
1385
1386 uc.refund_payment(
1388 pid,
1389 RefundPaymentRequest {
1390 amount_cents: 6000,
1391 reason: None,
1392 },
1393 )
1394 .await
1395 .unwrap();
1396
1397 let result = uc
1399 .refund_payment(
1400 pid,
1401 RefundPaymentRequest {
1402 amount_cents: 6000,
1403 reason: None,
1404 },
1405 )
1406 .await;
1407 assert!(result.is_err());
1408 assert!(result.unwrap_err().contains("exceeds"));
1409 }
1410
1411 #[tokio::test]
1412 async fn test_refund_pending_payment_fails() {
1413 let payment_repo = Arc::new(MockPaymentRepository::new());
1414 let pm_repo = Arc::new(MockPaymentMethodRepository::new());
1415 let uc = make_use_cases(payment_repo.clone(), pm_repo);
1416
1417 let org_id = Uuid::new_v4();
1418 let payment =
1419 seed_payment(&payment_repo, org_id, Uuid::new_v4(), Uuid::new_v4(), 10000).await;
1420 let pid = payment.id;
1421
1422 let result = uc
1424 .refund_payment(
1425 pid,
1426 RefundPaymentRequest {
1427 amount_cents: 5000,
1428 reason: None,
1429 },
1430 )
1431 .await;
1432 assert!(result.is_err());
1433 assert!(result.unwrap_err().contains("succeeded payments"));
1434 }
1435
1436 #[tokio::test]
1437 async fn test_get_payment_found_and_not_found() {
1438 let payment_repo = Arc::new(MockPaymentRepository::new());
1439 let pm_repo = Arc::new(MockPaymentMethodRepository::new());
1440 let uc = make_use_cases(payment_repo.clone(), pm_repo);
1441
1442 let org_id = Uuid::new_v4();
1443 let payment =
1444 seed_payment(&payment_repo, org_id, Uuid::new_v4(), Uuid::new_v4(), 5000).await;
1445
1446 let result = uc.get_payment(payment.id).await.unwrap();
1448 assert!(result.is_some());
1449 assert_eq!(result.unwrap().amount_cents, 5000);
1450
1451 let result = uc.get_payment(Uuid::new_v4()).await.unwrap();
1453 assert!(result.is_none());
1454 }
1455
1456 #[tokio::test]
1457 async fn test_list_owner_and_building_payments() {
1458 let payment_repo = Arc::new(MockPaymentRepository::new());
1459 let pm_repo = Arc::new(MockPaymentMethodRepository::new());
1460 let uc = make_use_cases(payment_repo.clone(), pm_repo);
1461
1462 let org_id = Uuid::new_v4();
1463 let building_id = Uuid::new_v4();
1464 let owner_id = Uuid::new_v4();
1465
1466 seed_payment(&payment_repo, org_id, building_id, owner_id, 5000).await;
1468 seed_payment(&payment_repo, org_id, building_id, owner_id, 7000).await;
1469
1470 seed_payment(&payment_repo, org_id, building_id, Uuid::new_v4(), 3000).await;
1472
1473 let owner_payments = uc.list_owner_payments(owner_id).await.unwrap();
1474 assert_eq!(owner_payments.len(), 2);
1475
1476 let building_payments = uc.list_building_payments(building_id).await.unwrap();
1477 assert_eq!(building_payments.len(), 3);
1478 }
1479
1480 #[tokio::test]
1481 async fn test_set_payment_method_id_validates_existence() {
1482 let payment_repo = Arc::new(MockPaymentRepository::new());
1483 let pm_repo = Arc::new(MockPaymentMethodRepository::new());
1485 let uc = make_use_cases(payment_repo.clone(), pm_repo);
1486
1487 let org_id = Uuid::new_v4();
1488 let payment =
1489 seed_payment(&payment_repo, org_id, Uuid::new_v4(), Uuid::new_v4(), 5000).await;
1490
1491 let result = uc.set_payment_method_id(payment.id, Uuid::new_v4()).await;
1493 assert!(result.is_err());
1494 assert!(result.unwrap_err().contains("Payment method not found"));
1495 }
1496
1497 #[tokio::test]
1498 async fn test_set_payment_method_id_success() {
1499 let payment_repo = Arc::new(MockPaymentRepository::new());
1500 let pm = PaymentMethod::new(
1501 Uuid::new_v4(),
1502 Uuid::new_v4(),
1503 PMMethodType::Card,
1504 "pm_test_stripe_id_12345".to_string(),
1505 "cus_test_customer_12345".to_string(),
1506 "Visa **** 4242".to_string(),
1507 true,
1508 )
1509 .unwrap();
1510 let pm_id = pm.id;
1511 let pm_repo = Arc::new(MockPaymentMethodRepository::with_method(pm));
1512 let uc = make_use_cases(payment_repo.clone(), pm_repo);
1513
1514 let org_id = Uuid::new_v4();
1515 let payment =
1516 seed_payment(&payment_repo, org_id, Uuid::new_v4(), Uuid::new_v4(), 5000).await;
1517
1518 let resp = uc.set_payment_method_id(payment.id, pm_id).await.unwrap();
1519 assert_eq!(resp.payment_method_id, Some(pm_id));
1520 }
1521
1522 #[tokio::test]
1523 async fn test_delete_payment() {
1524 let payment_repo = Arc::new(MockPaymentRepository::new());
1525 let pm_repo = Arc::new(MockPaymentMethodRepository::new());
1526 let uc = make_use_cases(payment_repo.clone(), pm_repo);
1527
1528 let org_id = Uuid::new_v4();
1529 let payment =
1530 seed_payment(&payment_repo, org_id, Uuid::new_v4(), Uuid::new_v4(), 5000).await;
1531
1532 assert!(uc.delete_payment(payment.id).await.unwrap());
1533 let result = uc.get_payment(payment.id).await.unwrap();
1535 assert!(result.is_none());
1536
1537 assert!(!uc.delete_payment(Uuid::new_v4()).await.unwrap());
1539 }
1540
1541 #[tokio::test]
1542 async fn test_get_owner_payment_stats() {
1543 let payment_repo = Arc::new(MockPaymentRepository::new());
1544 let pm_repo = Arc::new(MockPaymentMethodRepository::new());
1545 let uc = make_use_cases(payment_repo.clone(), pm_repo);
1546
1547 let org_id = Uuid::new_v4();
1548 let building_id = Uuid::new_v4();
1549 let owner_id = Uuid::new_v4();
1550
1551 let p1 = seed_payment(&payment_repo, org_id, building_id, owner_id, 10000).await;
1553 seed_payment(&payment_repo, org_id, building_id, owner_id, 5000).await;
1554
1555 uc.mark_succeeded(p1.id).await.unwrap();
1557
1558 let stats = uc.get_owner_payment_stats(owner_id).await.unwrap();
1559 assert_eq!(stats.total_count, 2);
1560 assert_eq!(stats.succeeded_count, 1);
1561 assert_eq!(stats.pending_count, 1);
1562 assert_eq!(stats.total_amount_cents, 15000);
1563 assert_eq!(stats.total_succeeded_cents, 10000);
1564 }
1565}