koprogo_api/infrastructure/database/repositories/
payment_method_repository_impl.rs

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
7/// PostgreSQL implementation of PaymentMethodRepository
8pub struct PostgresPaymentMethodRepository {
9    pool: PgPool,
10}
11
12impl PostgresPaymentMethodRepository {
13    pub fn new(pool: PgPool) -> Self {
14        Self { pool }
15    }
16
17    /// Convert PaymentMethodType enum to database string
18    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    /// Convert database string to PaymentMethodType enum
26    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        // Start a transaction to ensure atomicity
443        let mut tx = self
444            .pool
445            .begin()
446            .await
447            .map_err(|e| format!("Failed to begin transaction: {}", e))?;
448
449        // Unset all other default payment methods for this owner
450        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        // Set this payment method as default
459        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        // Commit transaction
478        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}