1use crate::application::ports::{PaymentRepository, PaymentStats};
2use crate::domain::entities::{Payment, PaymentMethodType, TransactionStatus};
3use async_trait::async_trait;
4use sqlx::PgPool;
5use uuid::Uuid;
6
7pub struct PostgresPaymentRepository {
9 pool: PgPool,
10}
11
12impl PostgresPaymentRepository {
13 pub fn new(pool: PgPool) -> Self {
14 Self { pool }
15 }
16
17 fn status_to_db(status: &TransactionStatus) -> &'static str {
19 match status {
20 TransactionStatus::Pending => "pending",
21 TransactionStatus::Processing => "processing",
22 TransactionStatus::RequiresAction => "requires_action",
23 TransactionStatus::Succeeded => "succeeded",
24 TransactionStatus::Failed => "failed",
25 TransactionStatus::Cancelled => "cancelled",
26 TransactionStatus::Refunded => "refunded",
27 }
28 }
29
30 fn status_from_db(s: &str) -> Result<TransactionStatus, String> {
32 match s {
33 "pending" => Ok(TransactionStatus::Pending),
34 "processing" => Ok(TransactionStatus::Processing),
35 "requires_action" => Ok(TransactionStatus::RequiresAction),
36 "succeeded" => Ok(TransactionStatus::Succeeded),
37 "failed" => Ok(TransactionStatus::Failed),
38 "cancelled" => Ok(TransactionStatus::Cancelled),
39 "refunded" => Ok(TransactionStatus::Refunded),
40 _ => Err(format!("Invalid transaction status: {}", s)),
41 }
42 }
43
44 fn method_type_to_db(method_type: &PaymentMethodType) -> &'static str {
46 match method_type {
47 PaymentMethodType::Card => "card",
48 PaymentMethodType::SepaDebit => "sepa_debit",
49 PaymentMethodType::BankTransfer => "bank_transfer",
50 PaymentMethodType::Cash => "cash",
51 }
52 }
53
54 fn method_type_from_db(s: &str) -> Result<PaymentMethodType, String> {
56 match s {
57 "card" => Ok(PaymentMethodType::Card),
58 "sepa_debit" => Ok(PaymentMethodType::SepaDebit),
59 "bank_transfer" => Ok(PaymentMethodType::BankTransfer),
60 "cash" => Ok(PaymentMethodType::Cash),
61 _ => Err(format!("Invalid payment method type: {}", s)),
62 }
63 }
64}
65
66#[async_trait]
67impl PaymentRepository for PostgresPaymentRepository {
68 async fn create(&self, payment: &Payment) -> Result<Payment, String> {
69 let status_str = Self::status_to_db(&payment.status);
70 let method_type_str = Self::method_type_to_db(&payment.payment_method_type);
71
72 let row = sqlx::query!(
73 r#"
74 INSERT INTO payments (
75 id, organization_id, building_id, owner_id, expense_id,
76 amount_cents, currency, status, payment_method_type,
77 stripe_payment_intent_id, stripe_customer_id, payment_method_id,
78 idempotency_key, description, metadata, failure_reason,
79 refunded_amount_cents, succeeded_at, failed_at, cancelled_at,
80 created_at, updated_at
81 )
82 VALUES ($1, $2, $3, $4, $5, $6, $7, $8::TEXT::transaction_status, $9::TEXT::payment_method_type,
83 $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22)
84 RETURNING id, organization_id, building_id, owner_id, expense_id,
85 amount_cents, currency, status AS "status: String",
86 payment_method_type AS "payment_method_type: String",
87 stripe_payment_intent_id, stripe_customer_id, payment_method_id,
88 idempotency_key, description, metadata, failure_reason,
89 refunded_amount_cents, succeeded_at, failed_at, cancelled_at,
90 created_at, updated_at
91 "#,
92 payment.id,
93 payment.organization_id,
94 payment.building_id,
95 payment.owner_id,
96 payment.expense_id,
97 payment.amount_cents,
98 &payment.currency,
99 status_str,
100 method_type_str,
101 payment.stripe_payment_intent_id.as_deref(),
102 payment.stripe_customer_id.as_deref(),
103 payment.payment_method_id,
104 &payment.idempotency_key,
105 payment.description.as_deref(),
106 payment.metadata.as_deref().and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok()),
107 payment.failure_reason.as_deref(),
108 payment.refunded_amount_cents,
109 payment.succeeded_at,
110 payment.failed_at,
111 payment.cancelled_at,
112 payment.created_at,
113 payment.updated_at,
114 )
115 .fetch_one(&self.pool)
116 .await
117 .map_err(|e| format!("Failed to create payment: {}", e))?;
118
119 Ok(Payment {
120 id: row.id,
121 organization_id: row.organization_id,
122 building_id: row.building_id,
123 owner_id: row.owner_id,
124 expense_id: row.expense_id,
125 amount_cents: row.amount_cents,
126 currency: row.currency,
127 status: Self::status_from_db(&row.status)?,
128 payment_method_type: Self::method_type_from_db(&row.payment_method_type)?,
129 stripe_payment_intent_id: row.stripe_payment_intent_id,
130 stripe_customer_id: row.stripe_customer_id,
131 payment_method_id: row.payment_method_id,
132 idempotency_key: row.idempotency_key,
133 description: row.description,
134 metadata: row.metadata.map(|v: serde_json::Value| v.to_string()),
135 failure_reason: row.failure_reason,
136 refunded_amount_cents: row.refunded_amount_cents,
137 succeeded_at: row.succeeded_at,
138 failed_at: row.failed_at,
139 cancelled_at: row.cancelled_at,
140 created_at: row.created_at,
141 updated_at: row.updated_at,
142 })
143 }
144
145 async fn find_by_id(&self, id: Uuid) -> Result<Option<Payment>, String> {
146 let row = sqlx::query!(
147 r#"
148 SELECT id, organization_id, building_id, owner_id, expense_id,
149 amount_cents, currency, status AS "status: String",
150 payment_method_type AS "payment_method_type: String",
151 stripe_payment_intent_id, stripe_customer_id, payment_method_id,
152 idempotency_key, description, metadata, failure_reason,
153 refunded_amount_cents, succeeded_at, failed_at, cancelled_at,
154 created_at, updated_at
155 FROM payments
156 WHERE id = $1
157 "#,
158 id
159 )
160 .fetch_optional(&self.pool)
161 .await
162 .map_err(|e| format!("Failed to find payment: {}", e))?;
163
164 match row {
165 Some(row) => Ok(Some(Payment {
166 id: row.id,
167 organization_id: row.organization_id,
168 building_id: row.building_id,
169 owner_id: row.owner_id,
170 expense_id: row.expense_id,
171 amount_cents: row.amount_cents,
172 currency: row.currency,
173 status: Self::status_from_db(&row.status)?,
174 payment_method_type: Self::method_type_from_db(&row.payment_method_type)?,
175 stripe_payment_intent_id: row.stripe_payment_intent_id,
176 stripe_customer_id: row.stripe_customer_id,
177 payment_method_id: row.payment_method_id,
178 idempotency_key: row.idempotency_key,
179 description: row.description,
180 metadata: row.metadata.map(|v: serde_json::Value| v.to_string()),
181 failure_reason: row.failure_reason,
182 refunded_amount_cents: row.refunded_amount_cents,
183 succeeded_at: row.succeeded_at,
184 failed_at: row.failed_at,
185 cancelled_at: row.cancelled_at,
186 created_at: row.created_at,
187 updated_at: row.updated_at,
188 })),
189 None => Ok(None),
190 }
191 }
192
193 async fn find_by_stripe_payment_intent_id(
194 &self,
195 stripe_payment_intent_id: &str,
196 ) -> Result<Option<Payment>, String> {
197 let row = sqlx::query!(
198 r#"
199 SELECT id, organization_id, building_id, owner_id, expense_id,
200 amount_cents, currency, status AS "status: String",
201 payment_method_type AS "payment_method_type: String",
202 stripe_payment_intent_id, stripe_customer_id, payment_method_id,
203 idempotency_key, description, metadata, failure_reason,
204 refunded_amount_cents, succeeded_at, failed_at, cancelled_at,
205 created_at, updated_at
206 FROM payments
207 WHERE stripe_payment_intent_id = $1
208 "#,
209 stripe_payment_intent_id
210 )
211 .fetch_optional(&self.pool)
212 .await
213 .map_err(|e| format!("Failed to find payment by Stripe payment intent: {}", e))?;
214
215 match row {
216 Some(row) => Ok(Some(Payment {
217 id: row.id,
218 organization_id: row.organization_id,
219 building_id: row.building_id,
220 owner_id: row.owner_id,
221 expense_id: row.expense_id,
222 amount_cents: row.amount_cents,
223 currency: row.currency,
224 status: Self::status_from_db(&row.status)?,
225 payment_method_type: Self::method_type_from_db(&row.payment_method_type)?,
226 stripe_payment_intent_id: row.stripe_payment_intent_id,
227 stripe_customer_id: row.stripe_customer_id,
228 payment_method_id: row.payment_method_id,
229 idempotency_key: row.idempotency_key,
230 description: row.description,
231 metadata: row.metadata.map(|v: serde_json::Value| v.to_string()),
232 failure_reason: row.failure_reason,
233 refunded_amount_cents: row.refunded_amount_cents,
234 succeeded_at: row.succeeded_at,
235 failed_at: row.failed_at,
236 cancelled_at: row.cancelled_at,
237 created_at: row.created_at,
238 updated_at: row.updated_at,
239 })),
240 None => Ok(None),
241 }
242 }
243
244 async fn find_by_idempotency_key(
245 &self,
246 organization_id: Uuid,
247 idempotency_key: &str,
248 ) -> Result<Option<Payment>, String> {
249 let row = sqlx::query!(
250 r#"
251 SELECT id, organization_id, building_id, owner_id, expense_id,
252 amount_cents, currency, status AS "status: String",
253 payment_method_type AS "payment_method_type: String",
254 stripe_payment_intent_id, stripe_customer_id, payment_method_id,
255 idempotency_key, description, metadata, failure_reason,
256 refunded_amount_cents, succeeded_at, failed_at, cancelled_at,
257 created_at, updated_at
258 FROM payments
259 WHERE organization_id = $1 AND idempotency_key = $2
260 "#,
261 organization_id,
262 idempotency_key
263 )
264 .fetch_optional(&self.pool)
265 .await
266 .map_err(|e| format!("Failed to find payment by idempotency key: {}", e))?;
267
268 match row {
269 Some(row) => Ok(Some(Payment {
270 id: row.id,
271 organization_id: row.organization_id,
272 building_id: row.building_id,
273 owner_id: row.owner_id,
274 expense_id: row.expense_id,
275 amount_cents: row.amount_cents,
276 currency: row.currency,
277 status: Self::status_from_db(&row.status)?,
278 payment_method_type: Self::method_type_from_db(&row.payment_method_type)?,
279 stripe_payment_intent_id: row.stripe_payment_intent_id,
280 stripe_customer_id: row.stripe_customer_id,
281 payment_method_id: row.payment_method_id,
282 idempotency_key: row.idempotency_key,
283 description: row.description,
284 metadata: row.metadata.map(|v: serde_json::Value| v.to_string()),
285 failure_reason: row.failure_reason,
286 refunded_amount_cents: row.refunded_amount_cents,
287 succeeded_at: row.succeeded_at,
288 failed_at: row.failed_at,
289 cancelled_at: row.cancelled_at,
290 created_at: row.created_at,
291 updated_at: row.updated_at,
292 })),
293 None => Ok(None),
294 }
295 }
296
297 async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<Payment>, String> {
298 let rows = sqlx::query!(
299 r#"
300 SELECT id, organization_id, building_id, owner_id, expense_id,
301 amount_cents, currency, status AS "status: String",
302 payment_method_type AS "payment_method_type: String",
303 stripe_payment_intent_id, stripe_customer_id, payment_method_id,
304 idempotency_key, description, metadata, failure_reason,
305 refunded_amount_cents, succeeded_at, failed_at, cancelled_at,
306 created_at, updated_at
307 FROM payments
308 WHERE owner_id = $1
309 ORDER BY created_at DESC
310 "#,
311 owner_id
312 )
313 .fetch_all(&self.pool)
314 .await
315 .map_err(|e| format!("Failed to find payments by owner: {}", e))?;
316
317 rows.into_iter()
318 .map(|row| {
319 Ok(Payment {
320 id: row.id,
321 organization_id: row.organization_id,
322 building_id: row.building_id,
323 owner_id: row.owner_id,
324 expense_id: row.expense_id,
325 amount_cents: row.amount_cents,
326 currency: row.currency,
327 status: Self::status_from_db(&row.status)?,
328 payment_method_type: Self::method_type_from_db(&row.payment_method_type)?,
329 stripe_payment_intent_id: row.stripe_payment_intent_id,
330 stripe_customer_id: row.stripe_customer_id,
331 payment_method_id: row.payment_method_id,
332 idempotency_key: row.idempotency_key,
333 description: row.description,
334 metadata: row.metadata.map(|v: serde_json::Value| v.to_string()),
335 failure_reason: row.failure_reason,
336 refunded_amount_cents: row.refunded_amount_cents,
337 succeeded_at: row.succeeded_at,
338 failed_at: row.failed_at,
339 cancelled_at: row.cancelled_at,
340 created_at: row.created_at,
341 updated_at: row.updated_at,
342 })
343 })
344 .collect()
345 }
346
347 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Payment>, String> {
348 let rows = sqlx::query!(
349 r#"
350 SELECT id, organization_id, building_id, owner_id, expense_id,
351 amount_cents, currency, status AS "status: String",
352 payment_method_type AS "payment_method_type: String",
353 stripe_payment_intent_id, stripe_customer_id, payment_method_id,
354 idempotency_key, description, metadata, failure_reason,
355 refunded_amount_cents, succeeded_at, failed_at, cancelled_at,
356 created_at, updated_at
357 FROM payments
358 WHERE building_id = $1
359 ORDER BY created_at DESC
360 "#,
361 building_id
362 )
363 .fetch_all(&self.pool)
364 .await
365 .map_err(|e| format!("Failed to find payments by building: {}", e))?;
366
367 rows.into_iter()
368 .map(|row| {
369 Ok(Payment {
370 id: row.id,
371 organization_id: row.organization_id,
372 building_id: row.building_id,
373 owner_id: row.owner_id,
374 expense_id: row.expense_id,
375 amount_cents: row.amount_cents,
376 currency: row.currency,
377 status: Self::status_from_db(&row.status)?,
378 payment_method_type: Self::method_type_from_db(&row.payment_method_type)?,
379 stripe_payment_intent_id: row.stripe_payment_intent_id,
380 stripe_customer_id: row.stripe_customer_id,
381 payment_method_id: row.payment_method_id,
382 idempotency_key: row.idempotency_key,
383 description: row.description,
384 metadata: row.metadata.map(|v: serde_json::Value| v.to_string()),
385 failure_reason: row.failure_reason,
386 refunded_amount_cents: row.refunded_amount_cents,
387 succeeded_at: row.succeeded_at,
388 failed_at: row.failed_at,
389 cancelled_at: row.cancelled_at,
390 created_at: row.created_at,
391 updated_at: row.updated_at,
392 })
393 })
394 .collect()
395 }
396
397 async fn find_by_expense(&self, expense_id: Uuid) -> Result<Vec<Payment>, String> {
398 let rows = sqlx::query!(
399 r#"
400 SELECT id, organization_id, building_id, owner_id, expense_id,
401 amount_cents, currency, status AS "status: String",
402 payment_method_type AS "payment_method_type: String",
403 stripe_payment_intent_id, stripe_customer_id, payment_method_id,
404 idempotency_key, description, metadata, failure_reason,
405 refunded_amount_cents, succeeded_at, failed_at, cancelled_at,
406 created_at, updated_at
407 FROM payments
408 WHERE expense_id = $1
409 ORDER BY created_at DESC
410 "#,
411 expense_id
412 )
413 .fetch_all(&self.pool)
414 .await
415 .map_err(|e| format!("Failed to find payments by expense: {}", e))?;
416
417 rows.into_iter()
418 .map(|row| {
419 Ok(Payment {
420 id: row.id,
421 organization_id: row.organization_id,
422 building_id: row.building_id,
423 owner_id: row.owner_id,
424 expense_id: row.expense_id,
425 amount_cents: row.amount_cents,
426 currency: row.currency,
427 status: Self::status_from_db(&row.status)?,
428 payment_method_type: Self::method_type_from_db(&row.payment_method_type)?,
429 stripe_payment_intent_id: row.stripe_payment_intent_id,
430 stripe_customer_id: row.stripe_customer_id,
431 payment_method_id: row.payment_method_id,
432 idempotency_key: row.idempotency_key,
433 description: row.description,
434 metadata: row.metadata.map(|v: serde_json::Value| v.to_string()),
435 failure_reason: row.failure_reason,
436 refunded_amount_cents: row.refunded_amount_cents,
437 succeeded_at: row.succeeded_at,
438 failed_at: row.failed_at,
439 cancelled_at: row.cancelled_at,
440 created_at: row.created_at,
441 updated_at: row.updated_at,
442 })
443 })
444 .collect()
445 }
446
447 async fn find_by_organization(&self, organization_id: Uuid) -> Result<Vec<Payment>, String> {
448 let rows = sqlx::query!(
449 r#"
450 SELECT id, organization_id, building_id, owner_id, expense_id,
451 amount_cents, currency, status AS "status: String",
452 payment_method_type AS "payment_method_type: String",
453 stripe_payment_intent_id, stripe_customer_id, payment_method_id,
454 idempotency_key, description, metadata, failure_reason,
455 refunded_amount_cents, succeeded_at, failed_at, cancelled_at,
456 created_at, updated_at
457 FROM payments
458 WHERE organization_id = $1
459 ORDER BY created_at DESC
460 "#,
461 organization_id
462 )
463 .fetch_all(&self.pool)
464 .await
465 .map_err(|e| format!("Failed to find payments by organization: {}", e))?;
466
467 rows.into_iter()
468 .map(|row| {
469 Ok(Payment {
470 id: row.id,
471 organization_id: row.organization_id,
472 building_id: row.building_id,
473 owner_id: row.owner_id,
474 expense_id: row.expense_id,
475 amount_cents: row.amount_cents,
476 currency: row.currency,
477 status: Self::status_from_db(&row.status)?,
478 payment_method_type: Self::method_type_from_db(&row.payment_method_type)?,
479 stripe_payment_intent_id: row.stripe_payment_intent_id,
480 stripe_customer_id: row.stripe_customer_id,
481 payment_method_id: row.payment_method_id,
482 idempotency_key: row.idempotency_key,
483 description: row.description,
484 metadata: row.metadata.map(|v: serde_json::Value| v.to_string()),
485 failure_reason: row.failure_reason,
486 refunded_amount_cents: row.refunded_amount_cents,
487 succeeded_at: row.succeeded_at,
488 failed_at: row.failed_at,
489 cancelled_at: row.cancelled_at,
490 created_at: row.created_at,
491 updated_at: row.updated_at,
492 })
493 })
494 .collect()
495 }
496
497 async fn find_by_status(
498 &self,
499 organization_id: Uuid,
500 status: TransactionStatus,
501 ) -> Result<Vec<Payment>, String> {
502 let status_str = Self::status_to_db(&status);
503
504 let rows = sqlx::query!(
505 r#"
506 SELECT id, organization_id, building_id, owner_id, expense_id,
507 amount_cents, currency, status AS "status: String",
508 payment_method_type AS "payment_method_type: String",
509 stripe_payment_intent_id, stripe_customer_id, payment_method_id,
510 idempotency_key, description, metadata, failure_reason,
511 refunded_amount_cents, succeeded_at, failed_at, cancelled_at,
512 created_at, updated_at
513 FROM payments
514 WHERE organization_id = $1 AND status = $2::TEXT::transaction_status
515 ORDER BY created_at DESC
516 "#,
517 organization_id,
518 status_str
519 )
520 .fetch_all(&self.pool)
521 .await
522 .map_err(|e| format!("Failed to find payments by status: {}", e))?;
523
524 rows.into_iter()
525 .map(|row| {
526 Ok(Payment {
527 id: row.id,
528 organization_id: row.organization_id,
529 building_id: row.building_id,
530 owner_id: row.owner_id,
531 expense_id: row.expense_id,
532 amount_cents: row.amount_cents,
533 currency: row.currency,
534 status: Self::status_from_db(&row.status)?,
535 payment_method_type: Self::method_type_from_db(&row.payment_method_type)?,
536 stripe_payment_intent_id: row.stripe_payment_intent_id,
537 stripe_customer_id: row.stripe_customer_id,
538 payment_method_id: row.payment_method_id,
539 idempotency_key: row.idempotency_key,
540 description: row.description,
541 metadata: row.metadata.map(|v: serde_json::Value| v.to_string()),
542 failure_reason: row.failure_reason,
543 refunded_amount_cents: row.refunded_amount_cents,
544 succeeded_at: row.succeeded_at,
545 failed_at: row.failed_at,
546 cancelled_at: row.cancelled_at,
547 created_at: row.created_at,
548 updated_at: row.updated_at,
549 })
550 })
551 .collect()
552 }
553
554 async fn find_by_building_and_status(
555 &self,
556 building_id: Uuid,
557 status: TransactionStatus,
558 ) -> Result<Vec<Payment>, String> {
559 let status_str = Self::status_to_db(&status);
560
561 let rows = sqlx::query!(
562 r#"
563 SELECT id, organization_id, building_id, owner_id, expense_id,
564 amount_cents, currency, status AS "status: String",
565 payment_method_type AS "payment_method_type: String",
566 stripe_payment_intent_id, stripe_customer_id, payment_method_id,
567 idempotency_key, description, metadata, failure_reason,
568 refunded_amount_cents, succeeded_at, failed_at, cancelled_at,
569 created_at, updated_at
570 FROM payments
571 WHERE building_id = $1 AND status = $2::TEXT::transaction_status
572 ORDER BY created_at DESC
573 "#,
574 building_id,
575 status_str
576 )
577 .fetch_all(&self.pool)
578 .await
579 .map_err(|e| format!("Failed to find payments by building and status: {}", e))?;
580
581 rows.into_iter()
582 .map(|row| {
583 Ok(Payment {
584 id: row.id,
585 organization_id: row.organization_id,
586 building_id: row.building_id,
587 owner_id: row.owner_id,
588 expense_id: row.expense_id,
589 amount_cents: row.amount_cents,
590 currency: row.currency,
591 status: Self::status_from_db(&row.status)?,
592 payment_method_type: Self::method_type_from_db(&row.payment_method_type)?,
593 stripe_payment_intent_id: row.stripe_payment_intent_id,
594 stripe_customer_id: row.stripe_customer_id,
595 payment_method_id: row.payment_method_id,
596 idempotency_key: row.idempotency_key,
597 description: row.description,
598 metadata: row.metadata.map(|v: serde_json::Value| v.to_string()),
599 failure_reason: row.failure_reason,
600 refunded_amount_cents: row.refunded_amount_cents,
601 succeeded_at: row.succeeded_at,
602 failed_at: row.failed_at,
603 cancelled_at: row.cancelled_at,
604 created_at: row.created_at,
605 updated_at: row.updated_at,
606 })
607 })
608 .collect()
609 }
610
611 async fn find_pending(&self, organization_id: Uuid) -> Result<Vec<Payment>, String> {
612 self.find_by_status(organization_id, TransactionStatus::Pending)
613 .await
614 }
615
616 async fn find_failed(&self, organization_id: Uuid) -> Result<Vec<Payment>, String> {
617 self.find_by_status(organization_id, TransactionStatus::Failed)
618 .await
619 }
620
621 async fn update(&self, payment: &Payment) -> Result<Payment, String> {
622 let status_str = Self::status_to_db(&payment.status);
623 let method_type_str = Self::method_type_to_db(&payment.payment_method_type);
624
625 let row = sqlx::query!(
626 r#"
627 UPDATE payments
628 SET organization_id = $2,
629 building_id = $3,
630 owner_id = $4,
631 expense_id = $5,
632 amount_cents = $6,
633 currency = $7,
634 status = $8::TEXT::transaction_status,
635 payment_method_type = $9::TEXT::payment_method_type,
636 stripe_payment_intent_id = $10,
637 stripe_customer_id = $11,
638 payment_method_id = $12,
639 idempotency_key = $13,
640 description = $14,
641 metadata = $15,
642 failure_reason = $16,
643 refunded_amount_cents = $17,
644 succeeded_at = $18,
645 failed_at = $19,
646 cancelled_at = $20,
647 updated_at = $21
648 WHERE id = $1
649 RETURNING id, organization_id, building_id, owner_id, expense_id,
650 amount_cents, currency, status AS "status: String",
651 payment_method_type AS "payment_method_type: String",
652 stripe_payment_intent_id, stripe_customer_id, payment_method_id,
653 idempotency_key, description, metadata, failure_reason,
654 refunded_amount_cents, succeeded_at, failed_at, cancelled_at,
655 created_at, updated_at
656 "#,
657 payment.id,
658 payment.organization_id,
659 payment.building_id,
660 payment.owner_id,
661 payment.expense_id,
662 payment.amount_cents,
663 &payment.currency,
664 status_str,
665 method_type_str,
666 payment.stripe_payment_intent_id.as_deref(),
667 payment.stripe_customer_id.as_deref(),
668 payment.payment_method_id,
669 &payment.idempotency_key,
670 payment.description.as_deref(),
671 payment
672 .metadata
673 .as_deref()
674 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok()),
675 payment.failure_reason.as_deref(),
676 payment.refunded_amount_cents,
677 payment.succeeded_at,
678 payment.failed_at,
679 payment.cancelled_at,
680 payment.updated_at,
681 )
682 .fetch_one(&self.pool)
683 .await
684 .map_err(|e| format!("Failed to update payment: {}", e))?;
685
686 Ok(Payment {
687 id: row.id,
688 organization_id: row.organization_id,
689 building_id: row.building_id,
690 owner_id: row.owner_id,
691 expense_id: row.expense_id,
692 amount_cents: row.amount_cents,
693 currency: row.currency,
694 status: Self::status_from_db(&row.status)?,
695 payment_method_type: Self::method_type_from_db(&row.payment_method_type)?,
696 stripe_payment_intent_id: row.stripe_payment_intent_id,
697 stripe_customer_id: row.stripe_customer_id,
698 payment_method_id: row.payment_method_id,
699 idempotency_key: row.idempotency_key,
700 description: row.description,
701 metadata: row.metadata.map(|v: serde_json::Value| v.to_string()),
702 failure_reason: row.failure_reason,
703 refunded_amount_cents: row.refunded_amount_cents,
704 succeeded_at: row.succeeded_at,
705 failed_at: row.failed_at,
706 cancelled_at: row.cancelled_at,
707 created_at: row.created_at,
708 updated_at: row.updated_at,
709 })
710 }
711
712 async fn delete(&self, id: Uuid) -> Result<bool, String> {
713 let result = sqlx::query!("DELETE FROM payments WHERE id = $1", id)
714 .execute(&self.pool)
715 .await
716 .map_err(|e| format!("Failed to delete payment: {}", e))?;
717
718 Ok(result.rows_affected() > 0)
719 }
720
721 async fn get_total_paid_for_expense(&self, expense_id: Uuid) -> Result<i64, String> {
722 let row = sqlx::query!(
723 r#"
724 SELECT COALESCE(SUM(amount_cents - refunded_amount_cents), 0)::BIGINT AS "total!"
725 FROM payments
726 WHERE expense_id = $1 AND status = 'succeeded'
727 "#,
728 expense_id
729 )
730 .fetch_one(&self.pool)
731 .await
732 .map_err(|e| format!("Failed to get total paid for expense: {}", e))?;
733
734 Ok(row.total)
735 }
736
737 async fn get_total_paid_by_owner(&self, owner_id: Uuid) -> Result<i64, String> {
738 let row = sqlx::query!(
739 r#"
740 SELECT COALESCE(SUM(amount_cents - refunded_amount_cents), 0)::BIGINT AS "total!"
741 FROM payments
742 WHERE owner_id = $1 AND status = 'succeeded'
743 "#,
744 owner_id
745 )
746 .fetch_one(&self.pool)
747 .await
748 .map_err(|e| format!("Failed to get total paid by owner: {}", e))?;
749
750 Ok(row.total)
751 }
752
753 async fn get_total_paid_for_building(&self, building_id: Uuid) -> Result<i64, String> {
754 let row = sqlx::query!(
755 r#"
756 SELECT COALESCE(SUM(amount_cents - refunded_amount_cents), 0)::BIGINT AS "total!"
757 FROM payments
758 WHERE building_id = $1 AND status = 'succeeded'
759 "#,
760 building_id
761 )
762 .fetch_one(&self.pool)
763 .await
764 .map_err(|e| format!("Failed to get total paid for building: {}", e))?;
765
766 Ok(row.total)
767 }
768
769 async fn get_owner_payment_stats(&self, owner_id: Uuid) -> Result<PaymentStats, String> {
770 let row = sqlx::query!(
771 r#"
772 SELECT
773 COUNT(*) AS "total_count!",
774 COUNT(*) FILTER (WHERE status = 'succeeded') AS "succeeded_count!",
775 COUNT(*) FILTER (WHERE status = 'failed') AS "failed_count!",
776 COUNT(*) FILTER (WHERE status = 'pending') AS "pending_count!",
777 COALESCE(SUM(amount_cents)::BIGINT, 0) AS "total_amount_cents!",
778 COALESCE((SUM(amount_cents) FILTER (WHERE status = 'succeeded'))::BIGINT, 0) AS "total_succeeded_cents!",
779 COALESCE(SUM(refunded_amount_cents)::BIGINT, 0) AS "total_refunded_cents!"
780 FROM payments
781 WHERE owner_id = $1
782 "#,
783 owner_id
784 )
785 .fetch_one(&self.pool)
786 .await
787 .map_err(|e| format!("Failed to get owner payment stats: {}", e))?;
788
789 Ok(PaymentStats {
790 total_count: row.total_count,
791 succeeded_count: row.succeeded_count,
792 failed_count: row.failed_count,
793 pending_count: row.pending_count,
794 total_amount_cents: row.total_amount_cents,
795 total_succeeded_cents: row.total_succeeded_cents,
796 total_refunded_cents: row.total_refunded_cents,
797 net_amount_cents: row.total_succeeded_cents - row.total_refunded_cents,
798 })
799 }
800
801 async fn get_building_payment_stats(&self, building_id: Uuid) -> Result<PaymentStats, String> {
802 let row = sqlx::query!(
803 r#"
804 SELECT
805 COUNT(*) AS "total_count!",
806 COUNT(*) FILTER (WHERE status = 'succeeded') AS "succeeded_count!",
807 COUNT(*) FILTER (WHERE status = 'failed') AS "failed_count!",
808 COUNT(*) FILTER (WHERE status = 'pending') AS "pending_count!",
809 COALESCE(SUM(amount_cents)::BIGINT, 0) AS "total_amount_cents!",
810 COALESCE((SUM(amount_cents) FILTER (WHERE status = 'succeeded'))::BIGINT, 0) AS "total_succeeded_cents!",
811 COALESCE(SUM(refunded_amount_cents)::BIGINT, 0) AS "total_refunded_cents!"
812 FROM payments
813 WHERE building_id = $1
814 "#,
815 building_id
816 )
817 .fetch_one(&self.pool)
818 .await
819 .map_err(|e| format!("Failed to get building payment stats: {}", e))?;
820
821 Ok(PaymentStats {
822 total_count: row.total_count,
823 succeeded_count: row.succeeded_count,
824 failed_count: row.failed_count,
825 pending_count: row.pending_count,
826 total_amount_cents: row.total_amount_cents,
827 total_succeeded_cents: row.total_succeeded_cents,
828 total_refunded_cents: row.total_refunded_cents,
829 net_amount_cents: row.total_succeeded_cents - row.total_refunded_cents,
830 })
831 }
832}