mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-28 20:23:43 +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,9 +50,15 @@ 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 {
|
||||||
|
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"))
|
.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