1use crate::application::ports::PaymentMethodRepository;
2use crate::domain::entities::payment_method::{PaymentMethod, PaymentMethodType};
3use async_trait::async_trait;
4use sqlx::PgPool;
5use uuid::Uuid;
6
7pub struct PostgresPaymentMethodRepository {
9 pool: PgPool,
10}
11
12impl PostgresPaymentMethodRepository {
13 pub fn new(pool: PgPool) -> Self {
14 Self { pool }
15 }
16
17 fn method_type_to_db(method_type: &PaymentMethodType) -> &'static str {
19 match method_type {
20 PaymentMethodType::Card => "card",
21 PaymentMethodType::SepaDebit => "sepa_debit",
22 }
23 }
24
25 fn method_type_from_db(s: &str) -> Result<PaymentMethodType, String> {
27 match s {
28 "card" => Ok(PaymentMethodType::Card),
29 "sepa_debit" => Ok(PaymentMethodType::SepaDebit),
30 _ => Err(format!("Invalid payment method type: {}", s)),
31 }
32 }
33}
34
35#[async_trait]
36impl PaymentMethodRepository for PostgresPaymentMethodRepository {
37 async fn create(&self, payment_method: &PaymentMethod) -> Result<PaymentMethod, String> {
38 let method_type_str = Self::method_type_to_db(&payment_method.method_type);
39
40 let row = sqlx::query!(
41 r#"
42 INSERT INTO payment_methods (
43 id, organization_id, owner_id, method_type,
44 stripe_payment_method_id, stripe_customer_id, display_label,
45 is_default, is_active, metadata, expires_at,
46 created_at, updated_at
47 )
48 VALUES ($1, $2, $3, $4::TEXT::payment_method_type, $5, $6, $7, $8, $9, $10, $11, $12, $13)
49 RETURNING id, organization_id, owner_id,
50 method_type AS "method_type: String",
51 stripe_payment_method_id, stripe_customer_id, display_label,
52 is_default, is_active, metadata, expires_at,
53 created_at, updated_at
54 "#,
55 payment_method.id,
56 payment_method.organization_id,
57 payment_method.owner_id,
58 method_type_str,
59 &payment_method.stripe_payment_method_id,
60 &payment_method.stripe_customer_id,
61 &payment_method.display_label,
62 payment_method.is_default,
63 payment_method.is_active,
64 payment_method.metadata.as_deref().and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok()),
65 payment_method.expires_at,
66 payment_method.created_at,
67 payment_method.updated_at,
68 )
69 .fetch_one(&self.pool)
70 .await
71 .map_err(|e| format!("Failed to create payment method: {}", e))?;
72
73 Ok(PaymentMethod {
74 id: row.id,
75 organization_id: row.organization_id,
76 owner_id: row.owner_id,
77 method_type: Self::method_type_from_db(&row.method_type)?,
78 stripe_payment_method_id: row.stripe_payment_method_id,
79 stripe_customer_id: row.stripe_customer_id,
80 display_label: row.display_label,
81 is_default: row.is_default,
82 is_active: row.is_active,
83 metadata: row.metadata.map(|v| v.to_string()),
84 expires_at: row.expires_at,
85 created_at: row.created_at,
86 updated_at: row.updated_at,
87 })
88 }
89
90 async fn find_by_id(&self, id: Uuid) -> Result<Option<PaymentMethod>, String> {
91 let row = sqlx::query!(
92 r#"
93 SELECT id, organization_id, owner_id,
94 method_type AS "method_type: String",
95 stripe_payment_method_id, stripe_customer_id, display_label,
96 is_default, is_active, metadata, expires_at,
97 created_at, updated_at
98 FROM payment_methods
99 WHERE id = $1
100 "#,
101 id
102 )
103 .fetch_optional(&self.pool)
104 .await
105 .map_err(|e| format!("Failed to find payment method: {}", e))?;
106
107 match row {
108 Some(row) => Ok(Some(PaymentMethod {
109 id: row.id,
110 organization_id: row.organization_id,
111 owner_id: row.owner_id,
112 method_type: Self::method_type_from_db(&row.method_type)?,
113 stripe_payment_method_id: row.stripe_payment_method_id,
114 stripe_customer_id: row.stripe_customer_id,
115 display_label: row.display_label,
116 is_default: row.is_default,
117 is_active: row.is_active,
118 metadata: row.metadata.map(|v| v.to_string()),
119 expires_at: row.expires_at,
120 created_at: row.created_at,
121 updated_at: row.updated_at,
122 })),
123 None => Ok(None),
124 }
125 }
126
127 async fn find_by_stripe_payment_method_id(
128 &self,
129 stripe_payment_method_id: &str,
130 ) -> Result<Option<PaymentMethod>, String> {
131 let row = sqlx::query!(
132 r#"
133 SELECT id, organization_id, owner_id,
134 method_type AS "method_type: String",
135 stripe_payment_method_id, stripe_customer_id, display_label,
136 is_default, is_active, metadata, expires_at,
137 created_at, updated_at
138 FROM payment_methods
139 WHERE stripe_payment_method_id = $1
140 "#,
141 stripe_payment_method_id
142 )
143 .fetch_optional(&self.pool)
144 .await
145 .map_err(|e| format!("Failed to find payment method by Stripe ID: {}", e))?;
146
147 match row {
148 Some(row) => Ok(Some(PaymentMethod {
149 id: row.id,
150 organization_id: row.organization_id,
151 owner_id: row.owner_id,
152 method_type: Self::method_type_from_db(&row.method_type)?,
153 stripe_payment_method_id: row.stripe_payment_method_id,
154 stripe_customer_id: row.stripe_customer_id,
155 display_label: row.display_label,
156 is_default: row.is_default,
157 is_active: row.is_active,
158 metadata: row.metadata.map(|v| v.to_string()),
159 expires_at: row.expires_at,
160 created_at: row.created_at,
161 updated_at: row.updated_at,
162 })),
163 None => Ok(None),
164 }
165 }
166
167 async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<PaymentMethod>, String> {
168 let rows = sqlx::query!(
169 r#"
170 SELECT id, organization_id, owner_id,
171 method_type AS "method_type: String",
172 stripe_payment_method_id, stripe_customer_id, display_label,
173 is_default, is_active, metadata, expires_at,
174 created_at, updated_at
175 FROM payment_methods
176 WHERE owner_id = $1
177 ORDER BY is_default DESC, created_at DESC
178 "#,
179 owner_id
180 )
181 .fetch_all(&self.pool)
182 .await
183 .map_err(|e| format!("Failed to find payment methods by owner: {}", e))?;
184
185 rows.into_iter()
186 .map(|row| {
187 Ok(PaymentMethod {
188 id: row.id,
189 organization_id: row.organization_id,
190 owner_id: row.owner_id,
191 method_type: Self::method_type_from_db(&row.method_type)?,
192 stripe_payment_method_id: row.stripe_payment_method_id,
193 stripe_customer_id: row.stripe_customer_id,
194 display_label: row.display_label,
195 is_default: row.is_default,
196 is_active: row.is_active,
197 metadata: row.metadata.map(|v| v.to_string()),
198 expires_at: row.expires_at,
199 created_at: row.created_at,
200 updated_at: row.updated_at,
201 })
202 })
203 .collect()
204 }
205
206 async fn find_active_by_owner(&self, owner_id: Uuid) -> Result<Vec<PaymentMethod>, String> {
207 let rows = sqlx::query!(
208 r#"
209 SELECT id, organization_id, owner_id,
210 method_type AS "method_type: String",
211 stripe_payment_method_id, stripe_customer_id, display_label,
212 is_default, is_active, metadata, expires_at,
213 created_at, updated_at
214 FROM payment_methods
215 WHERE owner_id = $1 AND is_active = TRUE
216 ORDER BY is_default DESC, created_at DESC
217 "#,
218 owner_id
219 )
220 .fetch_all(&self.pool)
221 .await
222 .map_err(|e| format!("Failed to find active payment methods by owner: {}", e))?;
223
224 rows.into_iter()
225 .map(|row| {
226 Ok(PaymentMethod {
227 id: row.id,
228 organization_id: row.organization_id,
229 owner_id: row.owner_id,
230 method_type: Self::method_type_from_db(&row.method_type)?,
231 stripe_payment_method_id: row.stripe_payment_method_id,
232 stripe_customer_id: row.stripe_customer_id,
233 display_label: row.display_label,
234 is_default: row.is_default,
235 is_active: row.is_active,
236 metadata: row.metadata.map(|v| v.to_string()),
237 expires_at: row.expires_at,
238 created_at: row.created_at,
239 updated_at: row.updated_at,
240 })
241 })
242 .collect()
243 }
244
245 async fn find_default_by_owner(&self, owner_id: Uuid) -> Result<Option<PaymentMethod>, String> {
246 let row = sqlx::query!(
247 r#"
248 SELECT id, organization_id, owner_id,
249 method_type AS "method_type: String",
250 stripe_payment_method_id, stripe_customer_id, display_label,
251 is_default, is_active, metadata, expires_at,
252 created_at, updated_at
253 FROM payment_methods
254 WHERE owner_id = $1 AND is_default = TRUE AND is_active = TRUE
255 LIMIT 1
256 "#,
257 owner_id
258 )
259 .fetch_optional(&self.pool)
260 .await
261 .map_err(|e| format!("Failed to find default payment method: {}", e))?;
262
263 match row {
264 Some(row) => Ok(Some(PaymentMethod {
265 id: row.id,
266 organization_id: row.organization_id,
267 owner_id: row.owner_id,
268 method_type: Self::method_type_from_db(&row.method_type)?,
269 stripe_payment_method_id: row.stripe_payment_method_id,
270 stripe_customer_id: row.stripe_customer_id,
271 display_label: row.display_label,
272 is_default: row.is_default,
273 is_active: row.is_active,
274 metadata: row.metadata.map(|v| v.to_string()),
275 expires_at: row.expires_at,
276 created_at: row.created_at,
277 updated_at: row.updated_at,
278 })),
279 None => Ok(None),
280 }
281 }
282
283 async fn find_by_organization(
284 &self,
285 organization_id: Uuid,
286 ) -> Result<Vec<PaymentMethod>, String> {
287 let rows = sqlx::query!(
288 r#"
289 SELECT id, organization_id, owner_id,
290 method_type AS "method_type: String",
291 stripe_payment_method_id, stripe_customer_id, display_label,
292 is_default, is_active, metadata, expires_at,
293 created_at, updated_at
294 FROM payment_methods
295 WHERE organization_id = $1
296 ORDER BY created_at DESC
297 "#,
298 organization_id
299 )
300 .fetch_all(&self.pool)
301 .await
302 .map_err(|e| format!("Failed to find payment methods by organization: {}", e))?;
303
304 rows.into_iter()
305 .map(|row| {
306 Ok(PaymentMethod {
307 id: row.id,
308 organization_id: row.organization_id,
309 owner_id: row.owner_id,
310 method_type: Self::method_type_from_db(&row.method_type)?,
311 stripe_payment_method_id: row.stripe_payment_method_id,
312 stripe_customer_id: row.stripe_customer_id,
313 display_label: row.display_label,
314 is_default: row.is_default,
315 is_active: row.is_active,
316 metadata: row.metadata.map(|v| v.to_string()),
317 expires_at: row.expires_at,
318 created_at: row.created_at,
319 updated_at: row.updated_at,
320 })
321 })
322 .collect()
323 }
324
325 async fn find_by_owner_and_type(
326 &self,
327 owner_id: Uuid,
328 method_type: PaymentMethodType,
329 ) -> Result<Vec<PaymentMethod>, String> {
330 let method_type_str = Self::method_type_to_db(&method_type);
331
332 let rows = sqlx::query!(
333 r#"
334 SELECT id, organization_id, owner_id,
335 method_type AS "method_type: String",
336 stripe_payment_method_id, stripe_customer_id, display_label,
337 is_default, is_active, metadata, expires_at,
338 created_at, updated_at
339 FROM payment_methods
340 WHERE owner_id = $1 AND method_type = $2::TEXT::payment_method_type
341 ORDER BY is_default DESC, created_at DESC
342 "#,
343 owner_id,
344 method_type_str
345 )
346 .fetch_all(&self.pool)
347 .await
348 .map_err(|e| format!("Failed to find payment methods by owner and type: {}", e))?;
349
350 rows.into_iter()
351 .map(|row| {
352 Ok(PaymentMethod {
353 id: row.id,
354 organization_id: row.organization_id,
355 owner_id: row.owner_id,
356 method_type: Self::method_type_from_db(&row.method_type)?,
357 stripe_payment_method_id: row.stripe_payment_method_id,
358 stripe_customer_id: row.stripe_customer_id,
359 display_label: row.display_label,
360 is_default: row.is_default,
361 is_active: row.is_active,
362 metadata: row.metadata.map(|v| v.to_string()),
363 expires_at: row.expires_at,
364 created_at: row.created_at,
365 updated_at: row.updated_at,
366 })
367 })
368 .collect()
369 }
370
371 async fn update(&self, payment_method: &PaymentMethod) -> Result<PaymentMethod, String> {
372 let method_type_str = Self::method_type_to_db(&payment_method.method_type);
373
374 let row = sqlx::query!(
375 r#"
376 UPDATE payment_methods
377 SET organization_id = $2,
378 owner_id = $3,
379 method_type = $4::TEXT::payment_method_type,
380 stripe_payment_method_id = $5,
381 stripe_customer_id = $6,
382 display_label = $7,
383 is_default = $8,
384 is_active = $9,
385 metadata = $10,
386 expires_at = $11,
387 updated_at = $12
388 WHERE id = $1
389 RETURNING id, organization_id, owner_id,
390 method_type AS "method_type: String",
391 stripe_payment_method_id, stripe_customer_id, display_label,
392 is_default, is_active, metadata, expires_at,
393 created_at, updated_at
394 "#,
395 payment_method.id,
396 payment_method.organization_id,
397 payment_method.owner_id,
398 method_type_str,
399 &payment_method.stripe_payment_method_id,
400 &payment_method.stripe_customer_id,
401 &payment_method.display_label,
402 payment_method.is_default,
403 payment_method.is_active,
404 payment_method
405 .metadata
406 .as_deref()
407 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok()),
408 payment_method.expires_at,
409 payment_method.updated_at,
410 )
411 .fetch_one(&self.pool)
412 .await
413 .map_err(|e| format!("Failed to update payment method: {}", e))?;
414
415 Ok(PaymentMethod {
416 id: row.id,
417 organization_id: row.organization_id,
418 owner_id: row.owner_id,
419 method_type: Self::method_type_from_db(&row.method_type)?,
420 stripe_payment_method_id: row.stripe_payment_method_id,
421 stripe_customer_id: row.stripe_customer_id,
422 display_label: row.display_label,
423 is_default: row.is_default,
424 is_active: row.is_active,
425 metadata: row.metadata.map(|v| v.to_string()),
426 expires_at: row.expires_at,
427 created_at: row.created_at,
428 updated_at: row.updated_at,
429 })
430 }
431
432 async fn delete(&self, id: Uuid) -> Result<bool, String> {
433 let result = sqlx::query!("DELETE FROM payment_methods WHERE id = $1", id)
434 .execute(&self.pool)
435 .await
436 .map_err(|e| format!("Failed to delete payment method: {}", e))?;
437
438 Ok(result.rows_affected() > 0)
439 }
440
441 async fn set_as_default(&self, id: Uuid, owner_id: Uuid) -> Result<PaymentMethod, String> {
442 let mut tx = self
444 .pool
445 .begin()
446 .await
447 .map_err(|e| format!("Failed to begin transaction: {}", e))?;
448
449 sqlx::query!(
451 "UPDATE payment_methods SET is_default = FALSE WHERE owner_id = $1",
452 owner_id
453 )
454 .execute(&mut *tx)
455 .await
456 .map_err(|e| format!("Failed to unset default payment methods: {}", e))?;
457
458 let row = sqlx::query!(
460 r#"
461 UPDATE payment_methods
462 SET is_default = TRUE, updated_at = NOW()
463 WHERE id = $1 AND owner_id = $2
464 RETURNING id, organization_id, owner_id,
465 method_type AS "method_type: String",
466 stripe_payment_method_id, stripe_customer_id, display_label,
467 is_default, is_active, metadata, expires_at,
468 created_at, updated_at
469 "#,
470 id,
471 owner_id
472 )
473 .fetch_one(&mut *tx)
474 .await
475 .map_err(|e| format!("Failed to set payment method as default: {}", e))?;
476
477 tx.commit()
479 .await
480 .map_err(|e| format!("Failed to commit transaction: {}", e))?;
481
482 Ok(PaymentMethod {
483 id: row.id,
484 organization_id: row.organization_id,
485 owner_id: row.owner_id,
486 method_type: Self::method_type_from_db(&row.method_type)?,
487 stripe_payment_method_id: row.stripe_payment_method_id,
488 stripe_customer_id: row.stripe_customer_id,
489 display_label: row.display_label,
490 is_default: row.is_default,
491 is_active: row.is_active,
492 metadata: row.metadata.map(|v| v.to_string()),
493 expires_at: row.expires_at,
494 created_at: row.created_at,
495 updated_at: row.updated_at,
496 })
497 }
498
499 async fn count_active_by_owner(&self, owner_id: Uuid) -> Result<i64, String> {
500 let row = sqlx::query!(
501 r#"
502 SELECT COUNT(*) AS "count!"
503 FROM payment_methods
504 WHERE owner_id = $1 AND is_active = TRUE
505 "#,
506 owner_id
507 )
508 .fetch_one(&self.pool)
509 .await
510 .map_err(|e| format!("Failed to count active payment methods: {}", e))?;
511
512 Ok(row.count)
513 }
514
515 async fn has_active_payment_methods(&self, owner_id: Uuid) -> Result<bool, String> {
516 let count = self.count_active_by_owner(owner_id).await?;
517 Ok(count > 0)
518 }
519}