koprogo_api/domain/comptabilite/
payment_method.rs1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
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, utoipa::ToSchema)]
60#[schema(as = StoredPaymentMethodType)]
61#[serde(rename_all = "snake_case")]
62pub enum PaymentMethodType {
63 Card,
65 SepaDebit,
67}
68
69impl PaymentMethod {
70 pub fn new(
85 organization_id: Uuid,
86 owner_id: Uuid,
87 method_type: PaymentMethodType,
88 stripe_payment_method_id: String,
89 stripe_customer_id: String,
90 display_label: String,
91 is_default: bool,
92 ) -> Result<Self, String> {
93 if stripe_payment_method_id.trim().is_empty() {
95 return Err("Stripe payment method ID cannot be empty".to_string());
96 }
97 if stripe_customer_id.trim().is_empty() {
98 return Err("Stripe customer ID cannot be empty".to_string());
99 }
100
101 if display_label.trim().is_empty() {
103 return Err("Display label cannot be empty".to_string());
104 }
105
106 let now = Utc::now();
107
108 Ok(Self {
109 id: Uuid::new_v4(),
110 organization_id,
111 owner_id,
112 method_type,
113 stripe_payment_method_id,
114 stripe_customer_id,
115 display_label,
116 is_default,
117 is_active: true, metadata: None,
119 expires_at: None,
120 created_at: now,
121 updated_at: now,
122 })
123 }
124
125 pub fn set_default(&mut self) {
127 self.is_default = true;
128 self.updated_at = Utc::now();
129 }
130
131 pub fn unset_default(&mut self) {
133 self.is_default = false;
134 self.updated_at = Utc::now();
135 }
136
137 pub fn deactivate(&mut self) -> Result<(), String> {
139 if !self.is_active {
140 return Err("Payment method is already inactive".to_string());
141 }
142
143 self.is_active = false;
144 self.updated_at = Utc::now();
145 Ok(())
146 }
147
148 pub fn reactivate(&mut self) -> Result<(), String> {
150 if self.is_active {
151 return Err("Payment method is already active".to_string());
152 }
153
154 self.is_active = true;
155 self.updated_at = Utc::now();
156 Ok(())
157 }
158
159 pub fn set_metadata(&mut self, metadata: String) {
161 self.metadata = Some(metadata);
162 self.updated_at = Utc::now();
163 }
164
165 pub fn set_expiry(&mut self, expires_at: DateTime<Utc>) -> Result<(), String> {
167 if self.method_type != PaymentMethodType::Card {
168 return Err("Only cards have expiry dates".to_string());
169 }
170
171 self.expires_at = Some(expires_at);
172 self.updated_at = Utc::now();
173 Ok(())
174 }
175
176 pub fn is_expired(&self) -> bool {
178 if let Some(expires_at) = self.expires_at {
179 expires_at < Utc::now()
180 } else {
181 false
182 }
183 }
184
185 pub fn is_usable(&self) -> bool {
187 self.is_active && !self.is_expired()
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 fn create_test_card() -> PaymentMethod {
196 PaymentMethod::new(
197 Uuid::new_v4(),
198 Uuid::new_v4(),
199 PaymentMethodType::Card,
200 "pm_test_card_123456789".to_string(),
201 "cus_test_123456789".to_string(),
202 "Visa •••• 4242".to_string(),
203 true,
204 )
205 .unwrap()
206 }
207
208 fn create_test_sepa() -> PaymentMethod {
209 PaymentMethod::new(
210 Uuid::new_v4(),
211 Uuid::new_v4(),
212 PaymentMethodType::SepaDebit,
213 "sepa_debit_test_123456789".to_string(),
214 "cus_test_123456789".to_string(),
215 "SEPA BE68 5390 0754".to_string(),
216 false,
217 )
218 .unwrap()
219 }
220
221 #[test]
222 fn test_create_card_success() {
223 let card = create_test_card();
224 assert_eq!(card.method_type, PaymentMethodType::Card);
225 assert_eq!(card.display_label, "Visa •••• 4242");
226 assert!(card.is_default);
227 assert!(card.is_active);
228 assert!(card.is_usable());
229 }
230
231 #[test]
232 fn test_create_sepa_success() {
233 let sepa = create_test_sepa();
234 assert_eq!(sepa.method_type, PaymentMethodType::SepaDebit);
235 assert_eq!(sepa.display_label, "SEPA BE68 5390 0754");
236 assert!(!sepa.is_default);
237 assert!(sepa.is_active);
238 assert!(sepa.is_usable());
239 }
240
241 #[test]
242 fn test_create_invalid_stripe_id() {
243 let result = PaymentMethod::new(
244 Uuid::new_v4(),
245 Uuid::new_v4(),
246 PaymentMethodType::Card,
247 "".to_string(), "cus_123".to_string(),
249 "Visa 4242".to_string(),
250 false,
251 );
252 assert!(result.is_err());
253 assert!(result.unwrap_err().contains("payment method ID"));
254 }
255
256 #[test]
257 fn test_create_invalid_display_label() {
258 let result = PaymentMethod::new(
259 Uuid::new_v4(),
260 Uuid::new_v4(),
261 PaymentMethodType::Card,
262 "pm_123".to_string(),
263 "cus_123".to_string(),
264 "".to_string(), false,
266 );
267 assert!(result.is_err());
268 assert!(result.unwrap_err().contains("Display label"));
269 }
270
271 #[test]
272 fn test_set_unset_default() {
273 let mut card = create_test_card();
274 assert!(card.is_default);
275
276 card.unset_default();
277 assert!(!card.is_default);
278
279 card.set_default();
280 assert!(card.is_default);
281 }
282
283 #[test]
284 fn test_deactivate_reactivate() {
285 let mut card = create_test_card();
286 assert!(card.is_active);
287 assert!(card.is_usable());
288
289 assert!(card.deactivate().is_ok());
291 assert!(!card.is_active);
292 assert!(!card.is_usable());
293
294 assert!(card.deactivate().is_err());
296
297 assert!(card.reactivate().is_ok());
299 assert!(card.is_active);
300 assert!(card.is_usable());
301 }
302
303 #[test]
304 fn test_card_expiry() {
305 let mut card = create_test_card();
306 assert!(!card.is_expired());
307
308 let past = Utc::now() - chrono::Duration::days(30);
310 assert!(card.set_expiry(past).is_ok());
311 assert!(card.is_expired());
312 assert!(!card.is_usable()); let future = Utc::now() + chrono::Duration::days(365);
316 assert!(card.set_expiry(future).is_ok());
317 assert!(!card.is_expired());
318 assert!(card.is_usable());
319 }
320
321 #[test]
322 fn test_sepa_no_expiry() {
323 let mut sepa = create_test_sepa();
324
325 let future = Utc::now() + chrono::Duration::days(365);
327 let result = sepa.set_expiry(future);
328 assert!(result.is_err());
329 assert!(result.unwrap_err().contains("Only cards"));
330 }
331
332 #[test]
333 fn test_set_metadata() {
334 let mut card = create_test_card();
335 assert!(card.metadata.is_none());
336
337 let metadata =
338 r#"{"brand": "visa", "last4": "4242", "exp_month": 12, "exp_year": 2025}"#.to_string();
339 card.set_metadata(metadata.clone());
340 assert_eq!(card.metadata, Some(metadata));
341 }
342}