Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): enable payment refund when payment is partially captured #2991

Merged
merged 4 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions crates/data_models/src/payments/payment_attempt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ pub trait PaymentAttemptInterface {
storage_scheme: storage_enums::MerchantStorageScheme,
) -> 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(
&self,
merchant_id: &str,
Expand Down
36 changes: 36 additions & 0 deletions crates/diesel_models/src/query/payment_attempt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
pub async fn find_by_merchant_id_connector_txn_id(
conn: &PgPooledConn,
Expand Down
14 changes: 10 additions & 4 deletions crates/router/src/core/refunds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
},
)?;

Expand All @@ -75,7 +81,7 @@ pub async fn refund_create_core(
})?;

payment_attempt = db
.find_payment_attempt_last_successful_attempt_by_payment_id_merchant_id(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can refactor the existing method to accept a vector of payment statuses, you can create an issue for this and take up in a separate PR

.find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id(
&req.payment_id,
merchant_id,
merchant_account.storage_scheme,
Expand Down
20 changes: 20 additions & 0 deletions crates/storage_impl/src/mock_db/payment_attempt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,4 +205,24 @@ 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<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())
}
}
72 changes: 72 additions & 0 deletions crates/storage_impl/src/payments/payment_attempt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,27 @@ impl<T: DatabaseStore> PaymentAttemptInterface for RouterStore<T> {
.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(
&self,
merchant_id: &str,
Expand Down Expand Up @@ -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(
&self,
merchant_id: &str,
Expand Down
Loading