koprogo_api/domain/entities/
payment_method.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5/// Payment method entity - Represents a stored payment method
6///
7/// Belgian property management context:
8/// - Store payment methods for recurring charges
9/// - Support cards (Stripe) and SEPA mandates (Belgian bank accounts)
10/// - PCI-DSS compliant: Never store raw card data, only Stripe tokens
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct PaymentMethod {
13    pub id: Uuid,
14    /// Organization (multi-tenant isolation)
15    pub organization_id: Uuid,
16    /// Owner who owns this payment method
17    pub owner_id: Uuid,
18    /// Payment method type
19    pub method_type: PaymentMethodType,
20    /// Stripe payment method ID (pm_xxx for cards, sepa_debit_xxx for SEPA)
21    pub stripe_payment_method_id: String,
22    /// Stripe customer ID (links payment method to customer)
23    pub stripe_customer_id: String,
24    /// Display label for UI (e.g., "Visa •••• 4242", "SEPA BE68 5390 0754")
25    pub display_label: String,
26    /// Is this the default payment method for the owner?
27    pub is_default: bool,
28    /// Is this payment method active? (can be deactivated)
29    pub is_active: bool,
30    /// Card/SEPA specific metadata (JSON) - stores last4, brand, expiry, etc.
31    pub metadata: Option<String>,
32    /// Expiry date for cards (not applicable for SEPA)
33    pub expires_at: Option<DateTime<Utc>>,
34    pub created_at: DateTime<Utc>,
35    pub updated_at: DateTime<Utc>,
36}
37
38/// Payment method type (aligned with Payment entity)
39#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
40#[serde(rename_all = "snake_case")]
41pub enum PaymentMethodType {
42    /// Credit/debit card via Stripe
43    Card,
44    /// SEPA Direct Debit (Belgian bank transfer)
45    SepaDebit,
46}
47
48impl PaymentMethod {
49    /// Create a new payment method
50    ///
51    /// # Arguments
52    /// * `organization_id` - Organization ID (multi-tenant)
53    /// * `owner_id` - Owner who owns this payment method
54    /// * `method_type` - Payment method type (Card or SepaDebit)
55    /// * `stripe_payment_method_id` - Stripe payment method ID
56    /// * `stripe_customer_id` - Stripe customer ID
57    /// * `display_label` - Display label for UI
58    /// * `is_default` - Is this the default payment method?
59    ///
60    /// # Returns
61    /// * `Ok(PaymentMethod)` - New payment method
62    /// * `Err(String)` - Validation error
63    pub fn new(
64        organization_id: Uuid,
65        owner_id: Uuid,
66        method_type: PaymentMethodType,
67        stripe_payment_method_id: String,
68        stripe_customer_id: String,
69        display_label: String,
70        is_default: bool,
71    ) -> Result<Self, String> {
72        // Validate Stripe IDs
73        if stripe_payment_method_id.trim().is_empty() {
74            return Err("Stripe payment method ID cannot be empty".to_string());
75        }
76        if stripe_customer_id.trim().is_empty() {
77            return Err("Stripe customer ID cannot be empty".to_string());
78        }
79
80        // Validate display label
81        if display_label.trim().is_empty() {
82            return Err("Display label cannot be empty".to_string());
83        }
84
85        let now = Utc::now();
86
87        Ok(Self {
88            id: Uuid::new_v4(),
89            organization_id,
90            owner_id,
91            method_type,
92            stripe_payment_method_id,
93            stripe_customer_id,
94            display_label,
95            is_default,
96            is_active: true, // Active by default
97            metadata: None,
98            expires_at: None,
99            created_at: now,
100            updated_at: now,
101        })
102    }
103
104    /// Set as default payment method
105    pub fn set_default(&mut self) {
106        self.is_default = true;
107        self.updated_at = Utc::now();
108    }
109
110    /// Unset as default payment method
111    pub fn unset_default(&mut self) {
112        self.is_default = false;
113        self.updated_at = Utc::now();
114    }
115
116    /// Deactivate payment method (soft delete)
117    pub fn deactivate(&mut self) -> Result<(), String> {
118        if !self.is_active {
119            return Err("Payment method is already inactive".to_string());
120        }
121
122        self.is_active = false;
123        self.updated_at = Utc::now();
124        Ok(())
125    }
126
127    /// Reactivate payment method
128    pub fn reactivate(&mut self) -> Result<(), String> {
129        if self.is_active {
130            return Err("Payment method is already active".to_string());
131        }
132
133        self.is_active = true;
134        self.updated_at = Utc::now();
135        Ok(())
136    }
137
138    /// Set metadata (JSON)
139    pub fn set_metadata(&mut self, metadata: String) {
140        self.metadata = Some(metadata);
141        self.updated_at = Utc::now();
142    }
143
144    /// Set expiry date (for cards only)
145    pub fn set_expiry(&mut self, expires_at: DateTime<Utc>) -> Result<(), String> {
146        if self.method_type != PaymentMethodType::Card {
147            return Err("Only cards have expiry dates".to_string());
148        }
149
150        self.expires_at = Some(expires_at);
151        self.updated_at = Utc::now();
152        Ok(())
153    }
154
155    /// Check if payment method is expired (cards only)
156    pub fn is_expired(&self) -> bool {
157        if let Some(expires_at) = self.expires_at {
158            expires_at < Utc::now()
159        } else {
160            false
161        }
162    }
163
164    /// Check if payment method is usable (active and not expired)
165    pub fn is_usable(&self) -> bool {
166        self.is_active && !self.is_expired()
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    fn create_test_card() -> PaymentMethod {
175        PaymentMethod::new(
176            Uuid::new_v4(),
177            Uuid::new_v4(),
178            PaymentMethodType::Card,
179            "pm_test_card_123456789".to_string(),
180            "cus_test_123456789".to_string(),
181            "Visa •••• 4242".to_string(),
182            true,
183        )
184        .unwrap()
185    }
186
187    fn create_test_sepa() -> PaymentMethod {
188        PaymentMethod::new(
189            Uuid::new_v4(),
190            Uuid::new_v4(),
191            PaymentMethodType::SepaDebit,
192            "sepa_debit_test_123456789".to_string(),
193            "cus_test_123456789".to_string(),
194            "SEPA BE68 5390 0754".to_string(),
195            false,
196        )
197        .unwrap()
198    }
199
200    #[test]
201    fn test_create_card_success() {
202        let card = create_test_card();
203        assert_eq!(card.method_type, PaymentMethodType::Card);
204        assert_eq!(card.display_label, "Visa •••• 4242");
205        assert!(card.is_default);
206        assert!(card.is_active);
207        assert!(card.is_usable());
208    }
209
210    #[test]
211    fn test_create_sepa_success() {
212        let sepa = create_test_sepa();
213        assert_eq!(sepa.method_type, PaymentMethodType::SepaDebit);
214        assert_eq!(sepa.display_label, "SEPA BE68 5390 0754");
215        assert!(!sepa.is_default);
216        assert!(sepa.is_active);
217        assert!(sepa.is_usable());
218    }
219
220    #[test]
221    fn test_create_invalid_stripe_id() {
222        let result = PaymentMethod::new(
223            Uuid::new_v4(),
224            Uuid::new_v4(),
225            PaymentMethodType::Card,
226            "".to_string(), // Empty Stripe ID
227            "cus_123".to_string(),
228            "Visa 4242".to_string(),
229            false,
230        );
231        assert!(result.is_err());
232        assert!(result.unwrap_err().contains("payment method ID"));
233    }
234
235    #[test]
236    fn test_create_invalid_display_label() {
237        let result = PaymentMethod::new(
238            Uuid::new_v4(),
239            Uuid::new_v4(),
240            PaymentMethodType::Card,
241            "pm_123".to_string(),
242            "cus_123".to_string(),
243            "".to_string(), // Empty label
244            false,
245        );
246        assert!(result.is_err());
247        assert!(result.unwrap_err().contains("Display label"));
248    }
249
250    #[test]
251    fn test_set_unset_default() {
252        let mut card = create_test_card();
253        assert!(card.is_default);
254
255        card.unset_default();
256        assert!(!card.is_default);
257
258        card.set_default();
259        assert!(card.is_default);
260    }
261
262    #[test]
263    fn test_deactivate_reactivate() {
264        let mut card = create_test_card();
265        assert!(card.is_active);
266        assert!(card.is_usable());
267
268        // Deactivate
269        assert!(card.deactivate().is_ok());
270        assert!(!card.is_active);
271        assert!(!card.is_usable());
272
273        // Try deactivating again (should fail)
274        assert!(card.deactivate().is_err());
275
276        // Reactivate
277        assert!(card.reactivate().is_ok());
278        assert!(card.is_active);
279        assert!(card.is_usable());
280    }
281
282    #[test]
283    fn test_card_expiry() {
284        let mut card = create_test_card();
285        assert!(!card.is_expired());
286
287        // Set expiry in the past
288        let past = Utc::now() - chrono::Duration::days(30);
289        assert!(card.set_expiry(past).is_ok());
290        assert!(card.is_expired());
291        assert!(!card.is_usable()); // Not usable because expired
292
293        // Set expiry in the future
294        let future = Utc::now() + chrono::Duration::days(365);
295        assert!(card.set_expiry(future).is_ok());
296        assert!(!card.is_expired());
297        assert!(card.is_usable());
298    }
299
300    #[test]
301    fn test_sepa_no_expiry() {
302        let mut sepa = create_test_sepa();
303
304        // SEPA should not have expiry
305        let future = Utc::now() + chrono::Duration::days(365);
306        let result = sepa.set_expiry(future);
307        assert!(result.is_err());
308        assert!(result.unwrap_err().contains("Only cards"));
309    }
310
311    #[test]
312    fn test_set_metadata() {
313        let mut card = create_test_card();
314        assert!(card.metadata.is_none());
315
316        let metadata =
317            r#"{"brand": "visa", "last4": "4242", "exp_month": 12, "exp_year": 2025}"#.to_string();
318        card.set_metadata(metadata.clone());
319        assert_eq!(card.metadata, Some(metadata));
320    }
321}