mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-10-31 10:06:32 +08:00 
			
		
		
		
	feat(core): enable payment refund when payment is partially captured (#2991)
Co-authored-by: Gnanasundari24 <118818938+Gnanasundari24@users.noreply.github.com>
This commit is contained in:
		| @ -36,6 +36,13 @@ pub trait PaymentAttemptInterface { | |||||||
|         storage_scheme: storage_enums::MerchantStorageScheme, |         storage_scheme: storage_enums::MerchantStorageScheme, | ||||||
|     ) -> error_stack::Result<PaymentAttempt, errors::StorageError>; |     ) -> error_stack::Result<PaymentAttempt, errors::StorageError>; | ||||||
|  |  | ||||||
|  |     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<PaymentAttempt, errors::StorageError>; | ||||||
|  |  | ||||||
|     async fn find_payment_attempt_by_merchant_id_connector_txn_id( |     async fn find_payment_attempt_by_merchant_id_connector_txn_id( | ||||||
|         &self, |         &self, | ||||||
|         merchant_id: &str, |         merchant_id: &str, | ||||||
|  | |||||||
| @ -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<Self> { | ||||||
|  |         // perform ordering on the application level instead of database level | ||||||
|  |         generics::generic_filter::< | ||||||
|  |             <Self as HasTable>::Table, | ||||||
|  |             _, | ||||||
|  |             <<Self as HasTable>::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))] |     #[instrument(skip(conn))] | ||||||
|     pub async fn find_by_merchant_id_connector_txn_id( |     pub async fn find_by_merchant_id_connector_txn_id( | ||||||
|         conn: &PgPooledConn, |         conn: &PgPooledConn, | ||||||
|  | |||||||
| @ -50,10 +50,16 @@ pub async fn refund_create_core( | |||||||
|         .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; |         .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; | ||||||
|  |  | ||||||
|     utils::when( |     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) |             Err(report!(errors::ApiErrorResponse::PaymentUnexpectedState { | ||||||
|                 .attach_printable("unable to refund for a unsuccessful payment intent")) |                 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 |     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, |             &req.payment_id, | ||||||
|             merchant_id, |             merchant_id, | ||||||
|             merchant_account.storage_scheme, |             merchant_account.storage_scheme, | ||||||
|  | |||||||
| @ -205,4 +205,24 @@ impl PaymentAttemptInterface for MockDb { | |||||||
|             .cloned() |             .cloned() | ||||||
|             .unwrap()) |             .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<PaymentAttempt, StorageError> { | ||||||
|  |         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 | ||||||
|  |                         || payment_attempt.status == storage_enums::AttemptStatus::Charged) | ||||||
|  |             }) | ||||||
|  |             .cloned() | ||||||
|  |             .unwrap()) | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -115,6 +115,27 @@ impl<T: DatabaseStore> PaymentAttemptInterface for RouterStore<T> { | |||||||
|         .map(PaymentAttempt::from_storage_model) |         .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<PaymentAttempt, errors::StorageError> { | ||||||
|  |         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( |     async fn find_payment_attempt_by_merchant_id_connector_txn_id( | ||||||
|         &self, |         &self, | ||||||
|         merchant_id: &str, |         merchant_id: &str, | ||||||
| @ -618,6 +639,57 @@ impl<T: DatabaseStore> PaymentAttemptInterface for KVRouterStore<T> { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     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<PaymentAttempt, errors::StorageError> { | ||||||
|  |         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::<PaymentAttempt, _, _>( | ||||||
|  |                         self, | ||||||
|  |                         KvOperation::<DieselPaymentAttempt>::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 | ||||||
|  |                                     || pa.status == api_models::enums::AttemptStatus::PartialCharged | ||||||
|  |                             }) | ||||||
|  |                             .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( |     async fn find_payment_attempt_by_merchant_id_connector_txn_id( | ||||||
|         &self, |         &self, | ||||||
|         merchant_id: &str, |         merchant_id: &str, | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Hrithikesh
					Hrithikesh