From 8a615f45f9fa07dc0fd288fc6f57e1cceba2398b Mon Sep 17 00:00:00 2001 From: hrithikeshvm Date: Tue, 28 Nov 2023 12:35:26 +0530 Subject: [PATCH 1/2] feat(core): enable payment refund when payment is partially captured --- .../src/payments/payment_attempt.rs | 7 ++ .../src/query/payment_attempt.rs | 36 ++++++++++ crates/router/src/core/refunds.rs | 14 ++-- .../src/mock_db/payment_attempt.rs | 19 +++++ .../src/payments/payment_attempt.rs | 69 +++++++++++++++++++ 5 files changed, 141 insertions(+), 4 deletions(-) diff --git a/crates/data_models/src/payments/payment_attempt.rs b/crates/data_models/src/payments/payment_attempt.rs index a937c785902f..44aa48b142ad 100644 --- a/crates/data_models/src/payments/payment_attempt.rs +++ b/crates/data_models/src/payments/payment_attempt.rs @@ -36,6 +36,13 @@ pub trait PaymentAttemptInterface { storage_scheme: storage_enums::MerchantStorageScheme, ) -> error_stack::Result; + async fn find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + &self, + payment_id: &str, + merchant_id: &str, + storage_scheme: storage_enums::MerchantStorageScheme, + ) -> error_stack::Result; + async fn find_payment_attempt_by_merchant_id_connector_txn_id( &self, merchant_id: &str, diff --git a/crates/diesel_models/src/query/payment_attempt.rs b/crates/diesel_models/src/query/payment_attempt.rs index 4737233e3048..9e9195f5e0bb 100644 --- a/crates/diesel_models/src/query/payment_attempt.rs +++ b/crates/diesel_models/src/query/payment_attempt.rs @@ -120,6 +120,42 @@ impl PaymentAttempt { ) } + pub async fn find_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + conn: &PgPooledConn, + payment_id: &str, + merchant_id: &str, + ) -> StorageResult { + // perform ordering on the application level instead of database level + generics::generic_filter::< + ::Table, + _, + <::Table as Table>::PrimaryKey, + Self, + >( + conn, + dsl::payment_id + .eq(payment_id.to_owned()) + .and(dsl::merchant_id.eq(merchant_id.to_owned())) + .and( + dsl::status + .eq(enums::AttemptStatus::Charged) + .or(dsl::status.eq(enums::AttemptStatus::PartialCharged)), + ), + None, + None, + None, + ) + .await? + .into_iter() + .fold( + Err(DatabaseError::NotFound).into_report(), + |acc, cur| match acc { + Ok(value) if value.modified_at > cur.modified_at => Ok(value), + _ => Ok(cur), + }, + ) + } + #[instrument(skip(conn))] pub async fn find_by_merchant_id_connector_txn_id( conn: &PgPooledConn, diff --git a/crates/router/src/core/refunds.rs b/crates/router/src/core/refunds.rs index aba6e9794e04..2d572cee9513 100644 --- a/crates/router/src/core/refunds.rs +++ b/crates/router/src/core/refunds.rs @@ -50,10 +50,16 @@ pub async fn refund_create_core( .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; utils::when( - payment_intent.status != enums::IntentStatus::Succeeded, + !(payment_intent.status == enums::IntentStatus::Succeeded + || payment_intent.status == enums::IntentStatus::PartiallyCaptured), || { - Err(report!(errors::ApiErrorResponse::PaymentNotSucceeded) - .attach_printable("unable to refund for a unsuccessful payment intent")) + Err(report!(errors::ApiErrorResponse::PaymentUnexpectedState { + current_flow: "refund".into(), + field_name: "status".into(), + current_value: payment_intent.status.to_string(), + states: "succeeded, partially_captured".to_string() + }) + .attach_printable("unable to refund for a unsuccessful payment intent")) }, )?; @@ -75,7 +81,7 @@ pub async fn refund_create_core( })?; payment_attempt = db - .find_payment_attempt_last_successful_attempt_by_payment_id_merchant_id( + .find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( &req.payment_id, merchant_id, merchant_account.storage_scheme, diff --git a/crates/storage_impl/src/mock_db/payment_attempt.rs b/crates/storage_impl/src/mock_db/payment_attempt.rs index fe244b10325f..bcbafc822dca 100644 --- a/crates/storage_impl/src/mock_db/payment_attempt.rs +++ b/crates/storage_impl/src/mock_db/payment_attempt.rs @@ -205,4 +205,23 @@ impl PaymentAttemptInterface for MockDb { .cloned() .unwrap()) } + #[allow(clippy::unwrap_used)] + async fn find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + &self, + payment_id: &str, + merchant_id: &str, + _storage_scheme: storage_enums::MerchantStorageScheme, + ) -> CustomResult { + let payment_attempts = self.payment_attempts.lock().await; + + Ok(payment_attempts + .iter() + .find(|payment_attempt| { + payment_attempt.payment_id == payment_id + && payment_attempt.merchant_id == merchant_id + && payment_attempt.status == storage_enums::AttemptStatus::PartialCharged + }) + .cloned() + .unwrap()) + } } diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index 06aacccc769d..e65b5b2e04b4 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -115,6 +115,27 @@ impl PaymentAttemptInterface for RouterStore { .map(PaymentAttempt::from_storage_model) } + #[instrument(skip_all)] + async fn find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + &self, + payment_id: &str, + merchant_id: &str, + _storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + let conn = pg_connection_read(self).await?; + DieselPaymentAttempt::find_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + &conn, + payment_id, + merchant_id, + ) + .await + .map_err(|er| { + let new_err = diesel_error_to_data_error(er.current_context()); + er.change_context(new_err) + }) + .map(PaymentAttempt::from_storage_model) + } + async fn find_payment_attempt_by_merchant_id_connector_txn_id( &self, merchant_id: &str, @@ -618,6 +639,54 @@ impl PaymentAttemptInterface for KVRouterStore { } } + async fn find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + &self, + payment_id: &str, + merchant_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result { + let database_call = || { + self.router_store + .find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + payment_id, + merchant_id, + storage_scheme, + ) + }; + match storage_scheme { + MerchantStorageScheme::PostgresOnly => database_call().await, + MerchantStorageScheme::RedisKv => { + let key = format!("mid_{merchant_id}_pid_{payment_id}"); + let pattern = "pa_*"; + + let redis_fut = async { + let kv_result = kv_wrapper::( + self, + KvOperation::::Scan(pattern), + key, + ) + .await? + .try_into_scan(); + kv_result.and_then(|mut payment_attempts| { + payment_attempts.sort_by(|a, b| b.modified_at.cmp(&a.modified_at)); + payment_attempts + .iter() + .find(|&pa| pa.status == api_models::enums::AttemptStatus::Charged) + .cloned() + .ok_or(error_stack::report!( + redis_interface::errors::RedisError::NotFound + )) + }) + }; + Box::pin(try_redis_get_else_try_database_get( + redis_fut, + database_call, + )) + .await + } + } + } + async fn find_payment_attempt_by_merchant_id_connector_txn_id( &self, merchant_id: &str, From 6ca52197ec68e4225be867bfe7facb426b7442b2 Mon Sep 17 00:00:00 2001 From: hrithikeshvm Date: Tue, 28 Nov 2023 16:05:03 +0530 Subject: [PATCH 2/2] add correct conditions in KV --- crates/storage_impl/src/mock_db/payment_attempt.rs | 3 ++- crates/storage_impl/src/payments/payment_attempt.rs | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/storage_impl/src/mock_db/payment_attempt.rs b/crates/storage_impl/src/mock_db/payment_attempt.rs index bcbafc822dca..6137b444f963 100644 --- a/crates/storage_impl/src/mock_db/payment_attempt.rs +++ b/crates/storage_impl/src/mock_db/payment_attempt.rs @@ -219,7 +219,8 @@ impl PaymentAttemptInterface for MockDb { .find(|payment_attempt| { payment_attempt.payment_id == payment_id && payment_attempt.merchant_id == merchant_id - && payment_attempt.status == storage_enums::AttemptStatus::PartialCharged + && (payment_attempt.status == storage_enums::AttemptStatus::PartialCharged + || payment_attempt.status == storage_enums::AttemptStatus::Charged) }) .cloned() .unwrap()) diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index e65b5b2e04b4..e86119e41af6 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -671,7 +671,10 @@ impl PaymentAttemptInterface for KVRouterStore { payment_attempts.sort_by(|a, b| b.modified_at.cmp(&a.modified_at)); payment_attempts .iter() - .find(|&pa| pa.status == api_models::enums::AttemptStatus::Charged) + .find(|&pa| { + pa.status == api_models::enums::AttemptStatus::Charged + || pa.status == api_models::enums::AttemptStatus::PartialCharged + }) .cloned() .ok_or(error_stack::report!( redis_interface::errors::RedisError::NotFound