mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-02 04:04:43 +08:00
fix(core): introduce new attempt and intent status to handle multiple partial captures (#2802)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com>
This commit is contained in:
@ -405,7 +405,9 @@ pub enum StripePaymentStatus {
|
||||
impl From<api_enums::IntentStatus> for StripePaymentStatus {
|
||||
fn from(item: api_enums::IntentStatus) -> Self {
|
||||
match item {
|
||||
api_enums::IntentStatus::Succeeded => Self::Succeeded,
|
||||
api_enums::IntentStatus::Succeeded | api_enums::IntentStatus::PartiallyCaptured => {
|
||||
Self::Succeeded
|
||||
}
|
||||
api_enums::IntentStatus::Failed => Self::Canceled,
|
||||
api_enums::IntentStatus::Processing => Self::Processing,
|
||||
api_enums::IntentStatus::RequiresCustomerAction
|
||||
@ -413,7 +415,7 @@ impl From<api_enums::IntentStatus> for StripePaymentStatus {
|
||||
api_enums::IntentStatus::RequiresPaymentMethod => Self::RequiresPaymentMethod,
|
||||
api_enums::IntentStatus::RequiresConfirmation => Self::RequiresConfirmation,
|
||||
api_enums::IntentStatus::RequiresCapture
|
||||
| api_enums::IntentStatus::PartiallyCaptured => Self::RequiresCapture,
|
||||
| api_enums::IntentStatus::PartiallyCapturedAndCapturable => Self::RequiresCapture,
|
||||
api_enums::IntentStatus::Cancelled => Self::Canceled,
|
||||
}
|
||||
}
|
||||
|
||||
@ -313,7 +313,9 @@ pub enum StripeSetupStatus {
|
||||
impl From<api_enums::IntentStatus> for StripeSetupStatus {
|
||||
fn from(item: api_enums::IntentStatus) -> Self {
|
||||
match item {
|
||||
api_enums::IntentStatus::Succeeded => Self::Succeeded,
|
||||
api_enums::IntentStatus::Succeeded | api_enums::IntentStatus::PartiallyCaptured => {
|
||||
Self::Succeeded
|
||||
}
|
||||
api_enums::IntentStatus::Failed => Self::Canceled,
|
||||
api_enums::IntentStatus::Processing => Self::Processing,
|
||||
api_enums::IntentStatus::RequiresCustomerAction => Self::RequiresAction,
|
||||
@ -321,7 +323,7 @@ impl From<api_enums::IntentStatus> for StripeSetupStatus {
|
||||
api_enums::IntentStatus::RequiresPaymentMethod => Self::RequiresPaymentMethod,
|
||||
api_enums::IntentStatus::RequiresConfirmation => Self::RequiresConfirmation,
|
||||
api_enums::IntentStatus::RequiresCapture
|
||||
| api_enums::IntentStatus::PartiallyCaptured => {
|
||||
| api_enums::IntentStatus::PartiallyCapturedAndCapturable => {
|
||||
logger::error!("Invalid status change");
|
||||
Self::Canceled
|
||||
}
|
||||
|
||||
@ -19,7 +19,10 @@ use serde::Serializer;
|
||||
|
||||
use crate::{
|
||||
consts,
|
||||
core::errors::{self, CustomResult},
|
||||
core::{
|
||||
errors::{self, CustomResult},
|
||||
payments::PaymentData,
|
||||
},
|
||||
pii::PeekInterface,
|
||||
types::{self, api, transformers::ForeignTryFrom, PaymentsCancelData, ResponseId},
|
||||
utils::{OptionExt, ValueExt},
|
||||
@ -74,6 +77,49 @@ pub trait RouterData {
|
||||
#[cfg(feature = "payouts")]
|
||||
fn get_quote_id(&self) -> Result<String, Error>;
|
||||
}
|
||||
|
||||
pub trait PaymentResponseRouterData {
|
||||
fn get_attempt_status_for_db_update<F>(
|
||||
&self,
|
||||
payment_data: &PaymentData<F>,
|
||||
) -> enums::AttemptStatus
|
||||
where
|
||||
F: Clone;
|
||||
}
|
||||
|
||||
impl<Flow, Request, Response> PaymentResponseRouterData
|
||||
for types::RouterData<Flow, Request, Response>
|
||||
where
|
||||
Request: types::Capturable,
|
||||
{
|
||||
fn get_attempt_status_for_db_update<F>(
|
||||
&self,
|
||||
payment_data: &PaymentData<F>,
|
||||
) -> enums::AttemptStatus
|
||||
where
|
||||
F: Clone,
|
||||
{
|
||||
match self.status {
|
||||
enums::AttemptStatus::Voided => {
|
||||
if payment_data.payment_intent.amount_captured > Some(0) {
|
||||
enums::AttemptStatus::PartialCharged
|
||||
} else {
|
||||
self.status
|
||||
}
|
||||
}
|
||||
enums::AttemptStatus::Charged => {
|
||||
let captured_amount = types::Capturable::get_capture_amount(&self.request);
|
||||
if Some(payment_data.payment_intent.amount) == captured_amount {
|
||||
enums::AttemptStatus::Charged
|
||||
} else {
|
||||
enums::AttemptStatus::PartialCharged
|
||||
}
|
||||
}
|
||||
_ => self.status,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const SELECTED_PAYMENT_METHOD: &str = "Selected payment method";
|
||||
|
||||
pub fn get_unimplemented_payment_method_error_message(connector: &str) -> String {
|
||||
|
||||
@ -1736,19 +1736,19 @@ pub fn should_call_connector<Op: Debug, F: Clone>(
|
||||
| storage_enums::IntentStatus::RequiresCustomerAction
|
||||
| storage_enums::IntentStatus::RequiresMerchantAction
|
||||
| storage_enums::IntentStatus::RequiresCapture
|
||||
| storage_enums::IntentStatus::PartiallyCaptured
|
||||
| storage_enums::IntentStatus::PartiallyCapturedAndCapturable
|
||||
) && payment_data.force_sync.unwrap_or(false)
|
||||
}
|
||||
"PaymentCancel" => matches!(
|
||||
payment_data.payment_intent.status,
|
||||
storage_enums::IntentStatus::RequiresCapture
|
||||
| storage_enums::IntentStatus::PartiallyCaptured
|
||||
| storage_enums::IntentStatus::PartiallyCapturedAndCapturable
|
||||
),
|
||||
"PaymentCapture" => {
|
||||
matches!(
|
||||
payment_data.payment_intent.status,
|
||||
storage_enums::IntentStatus::RequiresCapture
|
||||
| storage_enums::IntentStatus::PartiallyCaptured
|
||||
| storage_enums::IntentStatus::PartiallyCapturedAndCapturable
|
||||
) || (matches!(
|
||||
payment_data.payment_intent.status,
|
||||
storage_enums::IntentStatus::Processing
|
||||
|
||||
@ -1579,7 +1579,7 @@ pub(crate) fn validate_status_with_capture_method(
|
||||
}
|
||||
utils::when(
|
||||
status != storage_enums::IntentStatus::RequiresCapture
|
||||
&& status != storage_enums::IntentStatus::PartiallyCaptured
|
||||
&& status != storage_enums::IntentStatus::PartiallyCapturedAndCapturable
|
||||
&& status != storage_enums::IntentStatus::Processing,
|
||||
|| {
|
||||
Err(report!(errors::ApiErrorResponse::PaymentUnexpectedState {
|
||||
@ -2784,6 +2784,7 @@ pub fn get_attempt_type(
|
||||
| enums::AttemptStatus::Pending
|
||||
| enums::AttemptStatus::ConfirmationAwaited
|
||||
| enums::AttemptStatus::PartialCharged
|
||||
| enums::AttemptStatus::PartialChargedAndChargeable
|
||||
| enums::AttemptStatus::Voided
|
||||
| enums::AttemptStatus::AutoRefunded
|
||||
| enums::AttemptStatus::PaymentMethodAwaited
|
||||
@ -2844,6 +2845,7 @@ pub fn get_attempt_type(
|
||||
enums::IntentStatus::Cancelled
|
||||
| enums::IntentStatus::RequiresCapture
|
||||
| enums::IntentStatus::PartiallyCaptured
|
||||
| enums::IntentStatus::PartiallyCapturedAndCapturable
|
||||
| enums::IntentStatus::Processing
|
||||
| enums::IntentStatus::Succeeded => {
|
||||
Err(report!(errors::ApiErrorResponse::PreconditionFailed {
|
||||
@ -3023,6 +3025,7 @@ pub fn is_manual_retry_allowed(
|
||||
| enums::AttemptStatus::Pending
|
||||
| enums::AttemptStatus::ConfirmationAwaited
|
||||
| enums::AttemptStatus::PartialCharged
|
||||
| enums::AttemptStatus::PartialChargedAndChargeable
|
||||
| enums::AttemptStatus::Voided
|
||||
| enums::AttemptStatus::AutoRefunded
|
||||
| enums::AttemptStatus::PaymentMethodAwaited
|
||||
@ -3042,6 +3045,7 @@ pub fn is_manual_retry_allowed(
|
||||
enums::IntentStatus::Cancelled
|
||||
| enums::IntentStatus::RequiresCapture
|
||||
| enums::IntentStatus::PartiallyCaptured
|
||||
| enums::IntentStatus::PartiallyCapturedAndCapturable
|
||||
| enums::IntentStatus::Processing
|
||||
| enums::IntentStatus::Succeeded => Some(false),
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ use tracing_futures::Instrument;
|
||||
|
||||
use super::{Operation, PostUpdateTracker};
|
||||
use crate::{
|
||||
connector::utils::PaymentResponseRouterData,
|
||||
core::{
|
||||
errors::{self, RouterResult, StorageErrorExt},
|
||||
mandate,
|
||||
@ -26,7 +27,7 @@ use crate::{
|
||||
self, enums,
|
||||
payment_attempt::{AttemptStatusExt, PaymentAttemptExt},
|
||||
},
|
||||
transformers::ForeignTryFrom,
|
||||
transformers::{ForeignFrom, ForeignTryFrom},
|
||||
CaptureSyncResponse,
|
||||
},
|
||||
utils,
|
||||
@ -389,7 +390,7 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
|
||||
types::PreprocessingResponseId::ConnectorTransactionId(_) => None,
|
||||
};
|
||||
let payment_attempt_update = storage::PaymentAttemptUpdate::PreprocessingUpdate {
|
||||
status: router_data.status,
|
||||
status: router_data.get_attempt_status_for_db_update(&payment_data),
|
||||
payment_method_id: Some(router_data.payment_method_id),
|
||||
connector_metadata,
|
||||
preprocessing_step_id,
|
||||
@ -434,7 +435,7 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
|
||||
|
||||
utils::add_apple_pay_payment_status_metrics(
|
||||
router_data.status,
|
||||
router_data.apple_pay_flow,
|
||||
router_data.apple_pay_flow.clone(),
|
||||
payment_data.payment_attempt.connector.clone(),
|
||||
payment_data.payment_attempt.merchant_id.clone(),
|
||||
);
|
||||
@ -456,7 +457,7 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
|
||||
None => (
|
||||
None,
|
||||
Some(storage::PaymentAttemptUpdate::ResponseUpdate {
|
||||
status: router_data.status,
|
||||
status: router_data.get_attempt_status_for_db_update(&payment_data),
|
||||
connector: None,
|
||||
connector_transaction_id: connector_transaction_id.clone(),
|
||||
authentication_type: None,
|
||||
@ -504,7 +505,7 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
|
||||
(
|
||||
None,
|
||||
Some(storage::PaymentAttemptUpdate::UnresolvedResponseUpdate {
|
||||
status: router_data.status,
|
||||
status: router_data.get_attempt_status_for_db_update(&payment_data),
|
||||
connector: None,
|
||||
connector_transaction_id,
|
||||
payment_method_id: Some(router_data.payment_method_id),
|
||||
@ -610,15 +611,15 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
|
||||
|
||||
let payment_intent_update = match &router_data.response {
|
||||
Err(_) => storage::PaymentIntentUpdate::PGStatusUpdate {
|
||||
status: payment_data
|
||||
.payment_attempt
|
||||
.get_intent_status(payment_data.payment_intent.amount_captured),
|
||||
status: api_models::enums::IntentStatus::foreign_from(
|
||||
payment_data.payment_attempt.status,
|
||||
),
|
||||
updated_by: storage_scheme.to_string(),
|
||||
},
|
||||
Ok(_) => storage::PaymentIntentUpdate::ResponseUpdate {
|
||||
status: payment_data
|
||||
.payment_attempt
|
||||
.get_intent_status(payment_data.payment_intent.amount_captured),
|
||||
status: api_models::enums::IntentStatus::foreign_from(
|
||||
payment_data.payment_attempt.status,
|
||||
),
|
||||
return_url: router_data.return_url.clone(),
|
||||
amount_captured,
|
||||
updated_by: storage_scheme.to_string(),
|
||||
|
||||
@ -566,6 +566,7 @@ impl<F: Send + Clone + Sync, FData: Send + Sync>
|
||||
| storage_enums::AttemptStatus::AutoRefunded
|
||||
| storage_enums::AttemptStatus::CaptureFailed
|
||||
| storage_enums::AttemptStatus::PartialCharged
|
||||
| storage_enums::AttemptStatus::PartialChargedAndChargeable
|
||||
| storage_enums::AttemptStatus::Pending
|
||||
| storage_enums::AttemptStatus::PaymentMethodAwaited
|
||||
| storage_enums::AttemptStatus::ConfirmationAwaited
|
||||
|
||||
@ -116,7 +116,7 @@ impl MultipleCaptureData {
|
||||
}
|
||||
let status_count_map = self.get_status_count();
|
||||
if status_count_map.get(&storage_enums::CaptureStatus::Charged) > Some(&0) {
|
||||
storage_enums::AttemptStatus::PartialCharged
|
||||
storage_enums::AttemptStatus::PartialChargedAndChargeable
|
||||
} else {
|
||||
storage_enums::AttemptStatus::CaptureInitiated
|
||||
}
|
||||
|
||||
@ -16,7 +16,6 @@ pub trait PaymentAttemptExt {
|
||||
) -> RouterResult<CaptureNew>;
|
||||
|
||||
fn get_next_capture_id(&self) -> String;
|
||||
fn get_intent_status(&self, amount_captured: Option<i64>) -> enums::IntentStatus;
|
||||
fn get_total_amount(&self) -> i64;
|
||||
}
|
||||
|
||||
@ -60,15 +59,6 @@ impl PaymentAttemptExt for PaymentAttempt {
|
||||
format!("{}_{}", self.attempt_id.clone(), next_sequence_number)
|
||||
}
|
||||
|
||||
fn get_intent_status(&self, amount_captured: Option<i64>) -> enums::IntentStatus {
|
||||
let intent_status = enums::IntentStatus::foreign_from(self.status);
|
||||
if intent_status == enums::IntentStatus::Cancelled && amount_captured > Some(0) {
|
||||
enums::IntentStatus::Succeeded
|
||||
} else {
|
||||
intent_status
|
||||
}
|
||||
}
|
||||
|
||||
fn get_total_amount(&self) -> i64 {
|
||||
self.amount + self.surcharge_amount.unwrap_or(0) + self.tax_amount.unwrap_or(0)
|
||||
}
|
||||
|
||||
@ -86,6 +86,9 @@ impl ForeignFrom<storage_enums::AttemptStatus> for storage_enums::IntentStatus {
|
||||
storage_enums::AttemptStatus::Unresolved => Self::RequiresMerchantAction,
|
||||
|
||||
storage_enums::AttemptStatus::PartialCharged => Self::PartiallyCaptured,
|
||||
storage_enums::AttemptStatus::PartialChargedAndChargeable => {
|
||||
Self::PartiallyCapturedAndCapturable
|
||||
}
|
||||
storage_enums::AttemptStatus::Started
|
||||
| storage_enums::AttemptStatus::AuthenticationSuccessful
|
||||
| storage_enums::AttemptStatus::Authorizing
|
||||
@ -135,7 +138,8 @@ impl ForeignTryFrom<storage_enums::AttemptStatus> for storage_enums::CaptureStat
|
||||
| storage_enums::AttemptStatus::Unresolved
|
||||
| storage_enums::AttemptStatus::PaymentMethodAwaited
|
||||
| storage_enums::AttemptStatus::ConfirmationAwaited
|
||||
| storage_enums::AttemptStatus::DeviceDataCollectionPending => {
|
||||
| storage_enums::AttemptStatus::DeviceDataCollectionPending
|
||||
| storage_enums::AttemptStatus::PartialChargedAndChargeable=> {
|
||||
Err(errors::ApiErrorResponse::PreconditionFailed {
|
||||
message: "AttemptStatus must be one of these for multiple partial captures [Charged, PartialCharged, Pending, CaptureInitiated, Failure, CaptureFailed]".into(),
|
||||
}.into())
|
||||
@ -414,7 +418,8 @@ impl ForeignFrom<api_enums::IntentStatus> for Option<storage_enums::EventType> {
|
||||
api_enums::IntentStatus::RequiresPaymentMethod
|
||||
| api_enums::IntentStatus::RequiresConfirmation
|
||||
| api_enums::IntentStatus::RequiresCapture
|
||||
| api_enums::IntentStatus::PartiallyCaptured => None,
|
||||
| api_enums::IntentStatus::PartiallyCaptured
|
||||
| api_enums::IntentStatus::PartiallyCapturedAndCapturable => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user