mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 00:49:42 +08:00
feat(payments): add support for manual retries in payments confirm call (#1170)
This commit is contained in:
committed by
GitHub
parent
5e51b6b16d
commit
1f52a66452
@ -219,6 +219,10 @@ pub struct PaymentsRequest {
|
||||
|
||||
/// Business sub label for the payment
|
||||
pub business_sub_label: Option<String>,
|
||||
|
||||
/// If enabled payment can be retried from the client side until the payment is successful or payment expires or the attempts(configured by the merchant) for payment are exhausted.
|
||||
#[serde(default)]
|
||||
pub manual_retry: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone, Copy, PartialEq, Eq)]
|
||||
|
||||
@ -89,6 +89,7 @@ where
|
||||
&merchant_account,
|
||||
)
|
||||
.await?;
|
||||
|
||||
authenticate_client_secret(
|
||||
req.get_client_secret(),
|
||||
&payment_data.payment_intent,
|
||||
|
||||
@ -25,6 +25,7 @@ use crate::{
|
||||
core::{
|
||||
errors::{self, CustomResult, RouterResult, StorageErrorExt},
|
||||
payment_methods::{cards, vault},
|
||||
payments,
|
||||
},
|
||||
db::StorageInterface,
|
||||
routes::{metrics, AppState},
|
||||
@ -1683,3 +1684,215 @@ pub fn router_data_type_conversion<F1, F2, Req1, Req2, Res1, Res2>(
|
||||
connector_customer: router_data.connector_customer,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_attempt_type(
|
||||
payment_intent: &storage::PaymentIntent,
|
||||
payment_attempt: &storage::PaymentAttempt,
|
||||
request: &api::PaymentsRequest,
|
||||
action: &str,
|
||||
) -> RouterResult<AttemptType> {
|
||||
match payment_intent.status {
|
||||
enums::IntentStatus::Failed => {
|
||||
if request.manual_retry {
|
||||
match payment_attempt.status {
|
||||
enums::AttemptStatus::Started
|
||||
| enums::AttemptStatus::AuthenticationPending
|
||||
| enums::AttemptStatus::AuthenticationSuccessful
|
||||
| enums::AttemptStatus::Authorized
|
||||
| enums::AttemptStatus::Charged
|
||||
| enums::AttemptStatus::Authorizing
|
||||
| enums::AttemptStatus::CodInitiated
|
||||
| enums::AttemptStatus::VoidInitiated
|
||||
| enums::AttemptStatus::CaptureInitiated
|
||||
| enums::AttemptStatus::Unresolved
|
||||
| enums::AttemptStatus::Pending
|
||||
| enums::AttemptStatus::ConfirmationAwaited
|
||||
| enums::AttemptStatus::PartialCharged
|
||||
| enums::AttemptStatus::Voided
|
||||
| enums::AttemptStatus::AutoRefunded
|
||||
| enums::AttemptStatus::PaymentMethodAwaited
|
||||
| enums::AttemptStatus::DeviceDataCollectionPending => {
|
||||
Err(errors::ApiErrorResponse::InternalServerError)
|
||||
.into_report()
|
||||
.attach_printable("Payment Attempt unexpected state")
|
||||
}
|
||||
|
||||
storage_enums::AttemptStatus::VoidFailed
|
||||
| storage_enums::AttemptStatus::RouterDeclined
|
||||
| storage_enums::AttemptStatus::CaptureFailed => Err(report!(errors::ApiErrorResponse::PreconditionFailed {
|
||||
message:
|
||||
format!("You cannot {action} this payment because it has status {}, and the previous attempt has the status {}", payment_intent.status, payment_attempt.status)
|
||||
}
|
||||
)),
|
||||
|
||||
storage_enums::AttemptStatus::AuthenticationFailed
|
||||
| storage_enums::AttemptStatus::AuthorizationFailed
|
||||
| storage_enums::AttemptStatus::Failure => Ok(AttemptType::New),
|
||||
}
|
||||
} else {
|
||||
Err(report!(errors::ApiErrorResponse::PreconditionFailed {
|
||||
message:
|
||||
format!("You cannot {action} this payment because it has status {}, you can pass manual_retry as true in request to try this payment again", payment_intent.status)
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
enums::IntentStatus::Cancelled
|
||||
| enums::IntentStatus::RequiresCapture
|
||||
| enums::IntentStatus::Processing
|
||||
| enums::IntentStatus::Succeeded => {
|
||||
Err(report!(errors::ApiErrorResponse::PreconditionFailed {
|
||||
message: format!(
|
||||
"You cannot {action} this payment because it has status {}",
|
||||
payment_intent.status,
|
||||
),
|
||||
}))
|
||||
}
|
||||
|
||||
enums::IntentStatus::RequiresCustomerAction
|
||||
| enums::IntentStatus::RequiresMerchantAction
|
||||
| enums::IntentStatus::RequiresPaymentMethod
|
||||
| enums::IntentStatus::RequiresConfirmation => Ok(AttemptType::SameOld),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub enum AttemptType {
|
||||
New,
|
||||
SameOld,
|
||||
}
|
||||
|
||||
impl AttemptType {
|
||||
// The function creates a new payment_attempt from the previous payment attempt but doesn't populate fields like payment_method, error_code etc.
|
||||
// Logic to override the fields with data provided in the request should be done after this if required.
|
||||
// In case if fields are not overridden by the request then they contain the same data that was in the previous attempt provided it is populated in this function.
|
||||
#[inline(always)]
|
||||
fn make_new_payment_attempt(
|
||||
payment_method_data: &Option<api_models::payments::PaymentMethodData>,
|
||||
old_payment_attempt: storage::PaymentAttempt,
|
||||
) -> storage::PaymentAttemptNew {
|
||||
let created_at @ modified_at @ last_synced = Some(common_utils::date_time::now());
|
||||
|
||||
storage::PaymentAttemptNew {
|
||||
payment_id: old_payment_attempt.payment_id,
|
||||
merchant_id: old_payment_attempt.merchant_id,
|
||||
attempt_id: uuid::Uuid::new_v4().simple().to_string(),
|
||||
|
||||
// A new payment attempt is getting created so, used the same function which is used to populate status in PaymentCreate Flow.
|
||||
status: payment_attempt_status_fsm(payment_method_data, Some(true)),
|
||||
|
||||
amount: old_payment_attempt.amount,
|
||||
currency: old_payment_attempt.currency,
|
||||
save_to_locker: old_payment_attempt.save_to_locker,
|
||||
|
||||
connector: None,
|
||||
|
||||
error_message: None,
|
||||
offer_amount: old_payment_attempt.offer_amount,
|
||||
surcharge_amount: old_payment_attempt.surcharge_amount,
|
||||
tax_amount: old_payment_attempt.tax_amount,
|
||||
payment_method_id: None,
|
||||
payment_method: None,
|
||||
capture_method: old_payment_attempt.capture_method,
|
||||
capture_on: old_payment_attempt.capture_on,
|
||||
confirm: old_payment_attempt.confirm,
|
||||
authentication_type: old_payment_attempt.authentication_type,
|
||||
created_at,
|
||||
modified_at,
|
||||
last_synced,
|
||||
cancellation_reason: None,
|
||||
amount_to_capture: old_payment_attempt.amount_to_capture,
|
||||
|
||||
// Once the payment_attempt is authorised then mandate_id is created. If this payment attempt is authorised then mandate_id will be overridden.
|
||||
// Since mandate_id is a contract between merchant and customer to debit customers amount adding it to newly created attempt
|
||||
mandate_id: old_payment_attempt.mandate_id,
|
||||
|
||||
// The payment could be done from a different browser or same browser, it would probably be overridden by request data.
|
||||
browser_info: None,
|
||||
|
||||
error_code: None,
|
||||
payment_token: None,
|
||||
connector_metadata: None,
|
||||
payment_experience: None,
|
||||
payment_method_type: None,
|
||||
payment_method_data: None,
|
||||
|
||||
// In case it is passed in create and not in confirm,
|
||||
business_sub_label: old_payment_attempt.business_sub_label,
|
||||
// If the algorithm is entered in Create call from server side, it needs to be populated here, however it could be overridden from the request.
|
||||
straight_through_algorithm: old_payment_attempt.straight_through_algorithm,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn modify_payment_intent_and_payment_attempt(
|
||||
&self,
|
||||
request: &api::PaymentsRequest,
|
||||
fetched_payment_intent: storage::PaymentIntent,
|
||||
fetched_payment_attempt: storage::PaymentAttempt,
|
||||
db: &dyn StorageInterface,
|
||||
storage_scheme: storage::enums::MerchantStorageScheme,
|
||||
) -> RouterResult<(storage::PaymentIntent, storage::PaymentAttempt)> {
|
||||
match self {
|
||||
Self::SameOld => Ok((fetched_payment_intent, fetched_payment_attempt)),
|
||||
Self::New => {
|
||||
let new_payment_attempt = db
|
||||
.insert_payment_attempt(
|
||||
Self::make_new_payment_attempt(
|
||||
&request.payment_method_data,
|
||||
fetched_payment_attempt,
|
||||
),
|
||||
storage_scheme,
|
||||
)
|
||||
.await
|
||||
.to_duplicate_response(errors::ApiErrorResponse::DuplicatePayment {
|
||||
payment_id: fetched_payment_intent.payment_id.to_owned(),
|
||||
})?;
|
||||
|
||||
let updated_payment_intent = db
|
||||
.update_payment_intent(
|
||||
fetched_payment_intent,
|
||||
storage::PaymentIntentUpdate::StatusAndAttemptUpdate {
|
||||
status: payment_intent_status_fsm(
|
||||
&request.payment_method_data,
|
||||
Some(true),
|
||||
),
|
||||
active_attempt_id: new_payment_attempt.attempt_id.to_owned(),
|
||||
},
|
||||
storage_scheme,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
|
||||
|
||||
Ok((updated_payment_intent, new_payment_attempt))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_connector_response(
|
||||
&self,
|
||||
payment_attempt: &storage::PaymentAttempt,
|
||||
db: &dyn StorageInterface,
|
||||
storage_scheme: storage::enums::MerchantStorageScheme,
|
||||
) -> RouterResult<storage::ConnectorResponse> {
|
||||
match self {
|
||||
Self::New => db
|
||||
.insert_connector_response(
|
||||
payments::PaymentCreate::make_connector_response(payment_attempt),
|
||||
storage_scheme,
|
||||
)
|
||||
.await
|
||||
.to_duplicate_response(errors::ApiErrorResponse::DuplicatePayment {
|
||||
payment_id: payment_attempt.payment_id.clone(),
|
||||
}),
|
||||
Self::SameOld => db
|
||||
.find_connector_response_by_payment_id_merchant_id_attempt_id(
|
||||
&payment_attempt.payment_id,
|
||||
&payment_attempt.merchant_id,
|
||||
&payment_attempt.attempt_id,
|
||||
storage_scheme,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,20 +57,46 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
|
||||
|
||||
helpers::validate_payment_status_against_not_allowed_statuses(
|
||||
&payment_intent.status,
|
||||
&[
|
||||
storage_enums::IntentStatus::Cancelled,
|
||||
storage_enums::IntentStatus::Succeeded,
|
||||
storage_enums::IntentStatus::Processing,
|
||||
storage_enums::IntentStatus::RequiresCapture,
|
||||
storage_enums::IntentStatus::RequiresMerchantAction,
|
||||
],
|
||||
"confirm",
|
||||
)?;
|
||||
|
||||
payment_attempt = db
|
||||
.find_payment_attempt_by_payment_id_merchant_id_attempt_id(
|
||||
payment_intent.payment_id.as_str(),
|
||||
merchant_id,
|
||||
payment_intent.active_attempt_id.as_str(),
|
||||
storage_scheme,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
|
||||
|
||||
let attempt_type =
|
||||
helpers::get_attempt_type(&payment_intent, &payment_attempt, request, "confirm")?;
|
||||
|
||||
(payment_intent, payment_attempt) = attempt_type
|
||||
.modify_payment_intent_and_payment_attempt(
|
||||
request,
|
||||
payment_intent,
|
||||
payment_attempt,
|
||||
db,
|
||||
storage_scheme,
|
||||
)
|
||||
.await?;
|
||||
|
||||
payment_intent.setup_future_usage = request
|
||||
.setup_future_usage
|
||||
.map(ForeignInto::foreign_into)
|
||||
.or(payment_intent.setup_future_usage);
|
||||
|
||||
helpers::validate_payment_status_against_not_allowed_statuses(
|
||||
&payment_intent.status,
|
||||
&[
|
||||
storage_enums::IntentStatus::Failed,
|
||||
storage_enums::IntentStatus::Succeeded,
|
||||
],
|
||||
"confirm",
|
||||
)?;
|
||||
|
||||
let (token, payment_method, setup_mandate) = helpers::get_token_pm_type_mandate_details(
|
||||
state,
|
||||
request,
|
||||
@ -88,16 +114,6 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
|
||||
field_name: "browser_info",
|
||||
})?;
|
||||
|
||||
payment_attempt = db
|
||||
.find_payment_attempt_by_payment_id_merchant_id_attempt_id(
|
||||
payment_intent.payment_id.as_str(),
|
||||
merchant_id,
|
||||
payment_intent.active_attempt_id.as_str(),
|
||||
storage_scheme,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
|
||||
|
||||
let token = token.or_else(|| payment_attempt.payment_token.clone());
|
||||
|
||||
helpers::validate_pm_or_token_given(
|
||||
@ -119,6 +135,11 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
|
||||
.payment_experience
|
||||
.map(|experience| experience.foreign_into());
|
||||
|
||||
payment_attempt.capture_method = request
|
||||
.capture_method
|
||||
.or(payment_attempt.capture_method.map(|cm| cm.foreign_into()))
|
||||
.map(|cm| cm.foreign_into());
|
||||
|
||||
currency = payment_attempt.currency.get_required_value("currency")?;
|
||||
amount = payment_attempt.amount.into();
|
||||
|
||||
@ -149,15 +170,9 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
|
||||
)
|
||||
.await?;
|
||||
|
||||
connector_response = db
|
||||
.find_connector_response_by_payment_id_merchant_id_attempt_id(
|
||||
&payment_attempt.payment_id,
|
||||
&payment_attempt.merchant_id,
|
||||
&payment_attempt.attempt_id,
|
||||
storage_scheme,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
|
||||
connector_response = attempt_type
|
||||
.get_connector_response(&payment_attempt, db, storage_scheme)
|
||||
.await?;
|
||||
|
||||
payment_intent.shipping_address_id = shipping_address.clone().map(|i| i.address_id);
|
||||
payment_intent.billing_address_id = billing_address.clone().map(|i| i.address_id);
|
||||
|
||||
@ -123,6 +123,10 @@ pub enum PaymentIntentUpdate {
|
||||
PaymentAttemptUpdate {
|
||||
active_attempt_id: String,
|
||||
},
|
||||
StatusAndAttemptUpdate {
|
||||
status: storage_enums::IntentStatus,
|
||||
active_attempt_id: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)]
|
||||
@ -273,6 +277,14 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
|
||||
active_attempt_id: Some(active_attempt_id),
|
||||
..Default::default()
|
||||
},
|
||||
PaymentIntentUpdate::StatusAndAttemptUpdate {
|
||||
status,
|
||||
active_attempt_id,
|
||||
} => Self {
|
||||
status: Some(status),
|
||||
active_attempt_id: Some(active_attempt_id),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user