koprogo_api/domain/entities/
payment_method.rs1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct PaymentMethod {
13 pub id: Uuid,
14 pub organization_id: Uuid,
16 pub owner_id: Uuid,
18 pub method_type: PaymentMethodType,
20 pub stripe_payment_method_id: String,
22 pub stripe_customer_id: String,
24 pub display_label: String,
26 pub is_default: bool,
28 pub is_active: bool,
30 pub metadata: Option<String>,
32 pub expires_at: Option<DateTime<Utc>>,
34 pub created_at: DateTime<Utc>,
35 pub updated_at: DateTime<Utc>,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
40#[serde(rename_all = "snake_case")]
41pub enum PaymentMethodType {
42 Card,
44 SepaDebit,
46}
47
48impl PaymentMethod {
49 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 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 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, metadata: None,
98 expires_at: None,
99 created_at: now,
100 updated_at: now,
101 })
102 }
103
104 pub fn set_default(&mut self) {
106 self.is_default = true;
107 self.updated_at = Utc::now();
108 }
109
110 pub fn unset_default(&mut self) {
112 self.is_default = false;
113 self.updated_at = Utc::now();
114 }
115
116 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 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 pub fn set_metadata(&mut self, metadata: String) {
140 self.metadata = Some(metadata);
141 self.updated_at = Utc::now();
142 }
143
144 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 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 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(), "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(), 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 assert!(card.deactivate().is_ok());
270 assert!(!card.is_active);
271 assert!(!card.is_usable());
272
273 assert!(card.deactivate().is_err());
275
276 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 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()); 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 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}