koprogo_api/application/use_cases/
payment_use_cases.rs

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    /// Create a new payment
26    ///
27    /// Generates a unique idempotency key to prevent duplicate charges.
28    /// Checks for existing payment with same idempotency key (prevents retries from creating duplicates).
29    pub async fn create_payment(
30        &self,
31        organization_id: Uuid,
32        request: CreatePaymentRequest,
33    ) -> Result<PaymentResponse, String> {
34        // Generate idempotency key (organization_id + building_id + owner_id + timestamp + random)
35        let idempotency_key = format!(
36            "{}-{}-{}-{}",
37            organization_id,
38            request.building_id,
39            request.owner_id,
40            Uuid::new_v4()
41        );
42
43        // Check if payment with same idempotency key already exists
44        if let Some(existing_payment) = self
45            .payment_repository
46            .find_by_idempotency_key(organization_id, &idempotency_key)
47            .await?
48        {
49            // Return existing payment (idempotent)
50            return Ok(PaymentResponse::from(existing_payment));
51        }
52
53        // Create new payment
54        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    /// Get payment by ID
70    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    /// Get payment by Stripe payment intent ID
78    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    /// List payments for an owner
93    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    /// List payments for a building
102    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    /// List payments for an expense
114    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    /// List payments for an organization
123    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    /// List payments by status
135    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    /// List pending payments (for background processing)
148    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    /// List failed payments (for retry or analysis)
160    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    /// Mark payment as processing
169    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    /// Mark payment as requiring action (e.g., 3D Secure)
183    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    /// Mark payment as succeeded
197    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    /// Mark payment as failed
211    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    /// Mark payment as cancelled
225    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    /// Refund payment (partial or full)
239    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    /// Set Stripe payment intent ID
257    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    /// Set Stripe customer ID
275    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    /// Set payment method ID
293    pub async fn set_payment_method_id(
294        &self,
295        id: Uuid,
296        payment_method_id: Uuid,
297    ) -> Result<PaymentResponse, String> {
298        // Verify payment method exists
299        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    /// Delete payment
318    pub async fn delete_payment(&self, id: Uuid) -> Result<bool, String> {
319        self.payment_repository.delete(id).await
320    }
321
322    /// Get total paid for expense
323    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    /// Get total paid by owner
330    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    /// Get total paid for building
337    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    /// Get payment statistics for owner
344    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    /// Get payment statistics for building
356    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    /// Convert PaymentStats to PaymentStatsResponse
368    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    // ─── Mock PaymentRepository ───────────────────────────────────────
396
397    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    // ─── Mock PaymentMethodRepository ─────────────────────────────────
650
651    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    // ─── Helpers ──────────────────────────────────────────────────────
793
794    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    /// Insert a payment directly into the mock repo and return it.
819    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    // ─── Tests ────────────────────────────────────────────────────────
842
843    #[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        // Verify it was persisted
866        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        // Pending -> Processing
911        let resp = uc.mark_processing(pid).await.unwrap();
912        assert_eq!(resp.status, TransactionStatus::Processing);
913
914        // Processing -> Succeeded
915        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        // Pending -> Processing
932        uc.mark_processing(pid).await.unwrap();
933
934        // Processing -> Failed
935        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        // Move to Succeeded first
968        uc.mark_succeeded(pid).await.unwrap();
969
970        // Partial refund: 8000 out of 20000
971        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); // 20000 - 8000
984    }
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        // Move to Succeeded
998        uc.mark_succeeded(pid).await.unwrap();
999
1000        // Partial refund: 6000
1001        uc.refund_payment(
1002            pid,
1003            RefundPaymentRequest {
1004                amount_cents: 6000,
1005                reason: None,
1006            },
1007        )
1008        .await
1009        .unwrap();
1010
1011        // Try to refund 6000 more (total would be 12000 > 10000)
1012        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        // Payment is still Pending, refund should fail
1037        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        // Found
1061        let result = uc.get_payment(payment.id).await.unwrap();
1062        assert!(result.is_some());
1063        assert_eq!(result.unwrap().amount_cents, 5000);
1064
1065        // Not found
1066        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 2 payments for the same owner and building
1081        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 1 payment for a different owner
1085        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        // Empty payment method repo -- no methods exist
1098        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        // Setting a non-existent payment method should fail
1106        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        // After deletion, get should return None
1148        let result = uc.get_payment(payment.id).await.unwrap();
1149        assert!(result.is_none());
1150
1151        // Deleting non-existent returns false
1152        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        // Seed 2 payments, mark one succeeded
1166        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        // Mark p1 as succeeded via the use case
1170        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}