mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 00:49:42 +08:00
feat(core): enable payments void for multiple partial capture (#2048)
This commit is contained in:
@ -59,6 +59,36 @@ pub enum AttemptStatus {
|
||||
DeviceDataCollectionPending,
|
||||
}
|
||||
|
||||
impl AttemptStatus {
|
||||
pub fn is_terminal_status(self) -> bool {
|
||||
match self {
|
||||
Self::RouterDeclined
|
||||
| Self::Charged
|
||||
| Self::AutoRefunded
|
||||
| Self::Voided
|
||||
| Self::VoidFailed
|
||||
| Self::CaptureFailed
|
||||
| Self::Failure => true,
|
||||
Self::Started
|
||||
| Self::AuthenticationFailed
|
||||
| Self::AuthenticationPending
|
||||
| Self::AuthenticationSuccessful
|
||||
| Self::Authorized
|
||||
| Self::AuthorizationFailed
|
||||
| Self::Authorizing
|
||||
| Self::CodInitiated
|
||||
| Self::VoidInitiated
|
||||
| Self::CaptureInitiated
|
||||
| Self::PartialCharged
|
||||
| Self::Unresolved
|
||||
| Self::Pending
|
||||
| Self::PaymentMethodAwaited
|
||||
| Self::ConfirmationAwaited
|
||||
| Self::DeviceDataCollectionPending => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
|
||||
@ -137,6 +137,7 @@ pub struct PaymentAttempt {
|
||||
pub multiple_capture_count: Option<i16>,
|
||||
// reference to the payment at connector side
|
||||
pub connector_response_reference_id: Option<String>,
|
||||
pub amount_capturable: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
@ -192,6 +193,7 @@ pub struct PaymentAttemptNew {
|
||||
pub error_reason: Option<String>,
|
||||
pub connector_response_reference_id: Option<String>,
|
||||
pub multiple_capture_count: Option<i16>,
|
||||
pub amount_capturable: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@ -257,6 +259,7 @@ pub enum PaymentAttemptUpdate {
|
||||
error_message: Option<Option<String>>,
|
||||
error_reason: Option<Option<String>>,
|
||||
connector_response_reference_id: Option<String>,
|
||||
amount_capturable: Option<i64>,
|
||||
},
|
||||
UnresolvedResponseUpdate {
|
||||
status: storage_enums::AttemptStatus,
|
||||
@ -277,10 +280,15 @@ pub enum PaymentAttemptUpdate {
|
||||
error_code: Option<Option<String>>,
|
||||
error_message: Option<Option<String>>,
|
||||
error_reason: Option<Option<String>>,
|
||||
amount_capturable: Option<i64>,
|
||||
},
|
||||
MultipleCaptureCountUpdate {
|
||||
multiple_capture_count: i16,
|
||||
},
|
||||
AmountToCaptureUpdate {
|
||||
status: storage_enums::AttemptStatus,
|
||||
amount_capturable: i64,
|
||||
},
|
||||
PreprocessingUpdate {
|
||||
status: storage_enums::AttemptStatus,
|
||||
payment_method_id: Option<Option<String>>,
|
||||
|
||||
@ -56,6 +56,7 @@ pub struct PaymentAttempt {
|
||||
pub multiple_capture_count: Option<i16>,
|
||||
// reference to the payment at connector side
|
||||
pub connector_response_reference_id: Option<String>,
|
||||
pub amount_capturable: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Queryable, Serialize, Deserialize)]
|
||||
@ -113,6 +114,7 @@ pub struct PaymentAttemptNew {
|
||||
pub error_reason: Option<String>,
|
||||
pub connector_response_reference_id: Option<String>,
|
||||
pub multiple_capture_count: Option<i16>,
|
||||
pub amount_capturable: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@ -178,6 +180,7 @@ pub enum PaymentAttemptUpdate {
|
||||
error_message: Option<Option<String>>,
|
||||
error_reason: Option<Option<String>>,
|
||||
connector_response_reference_id: Option<String>,
|
||||
amount_capturable: Option<i64>,
|
||||
},
|
||||
UnresolvedResponseUpdate {
|
||||
status: storage_enums::AttemptStatus,
|
||||
@ -198,10 +201,15 @@ pub enum PaymentAttemptUpdate {
|
||||
error_code: Option<Option<String>>,
|
||||
error_message: Option<Option<String>>,
|
||||
error_reason: Option<Option<String>>,
|
||||
amount_capturable: Option<i64>,
|
||||
},
|
||||
MultipleCaptureCountUpdate {
|
||||
multiple_capture_count: i16,
|
||||
},
|
||||
AmountToCaptureUpdate {
|
||||
status: storage_enums::AttemptStatus,
|
||||
amount_capturable: i64,
|
||||
},
|
||||
PreprocessingUpdate {
|
||||
status: storage_enums::AttemptStatus,
|
||||
payment_method_id: Option<Option<String>>,
|
||||
@ -242,6 +250,7 @@ pub struct PaymentAttemptUpdateInternal {
|
||||
capture_method: Option<storage_enums::CaptureMethod>,
|
||||
connector_response_reference_id: Option<String>,
|
||||
multiple_capture_count: Option<i16>,
|
||||
amount_capturable: Option<i64>,
|
||||
}
|
||||
|
||||
impl PaymentAttemptUpdate {
|
||||
@ -380,6 +389,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
|
||||
error_message,
|
||||
error_reason,
|
||||
connector_response_reference_id,
|
||||
amount_capturable,
|
||||
} => Self {
|
||||
status: Some(status),
|
||||
connector,
|
||||
@ -394,6 +404,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
|
||||
payment_token,
|
||||
error_reason,
|
||||
connector_response_reference_id,
|
||||
amount_capturable,
|
||||
..Default::default()
|
||||
},
|
||||
PaymentAttemptUpdate::ErrorUpdate {
|
||||
@ -402,6 +413,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
|
||||
error_code,
|
||||
error_message,
|
||||
error_reason,
|
||||
amount_capturable,
|
||||
} => Self {
|
||||
connector,
|
||||
status: Some(status),
|
||||
@ -409,6 +421,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
|
||||
error_code,
|
||||
modified_at: Some(common_utils::date_time::now()),
|
||||
error_reason,
|
||||
amount_capturable,
|
||||
..Default::default()
|
||||
},
|
||||
PaymentAttemptUpdate::StatusUpdate { status } => Self {
|
||||
@ -469,6 +482,14 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
|
||||
multiple_capture_count: Some(multiple_capture_count),
|
||||
..Default::default()
|
||||
},
|
||||
PaymentAttemptUpdate::AmountToCaptureUpdate {
|
||||
status,
|
||||
amount_capturable,
|
||||
} => Self {
|
||||
status: Some(status),
|
||||
amount_capturable: Some(amount_capturable),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -545,6 +545,7 @@ diesel::table! {
|
||||
multiple_capture_count -> Nullable<Int2>,
|
||||
#[max_length = 128]
|
||||
connector_response_reference_id -> Nullable<Varchar>,
|
||||
amount_capturable -> Int8,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1483,6 +1483,7 @@ pub fn should_call_connector<Op: Debug, F: Clone>(
|
||||
"PaymentCancel" => matches!(
|
||||
payment_data.payment_intent.status,
|
||||
storage_enums::IntentStatus::RequiresCapture
|
||||
| storage_enums::IntentStatus::PartiallyCaptured
|
||||
),
|
||||
"PaymentCapture" => {
|
||||
matches!(
|
||||
|
||||
@ -2696,6 +2696,7 @@ impl AttemptType {
|
||||
error_reason: None,
|
||||
multiple_capture_count: None,
|
||||
connector_response_reference_id: None,
|
||||
amount_capturable: old_payment_attempt.amount,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -91,6 +91,12 @@ impl<F: Send + Clone> GetTracker<F, payments::PaymentData<F>, api::PaymentsCaptu
|
||||
let amount_to_capture = request
|
||||
.amount_to_capture
|
||||
.get_required_value("amount_to_capture")?;
|
||||
|
||||
helpers::validate_amount_to_capture(
|
||||
payment_attempt.amount_capturable,
|
||||
Some(amount_to_capture),
|
||||
)?;
|
||||
|
||||
let previous_captures = db
|
||||
.find_all_captures_by_merchant_id_payment_id_authorized_attempt_id(
|
||||
&payment_attempt.merchant_id,
|
||||
@ -100,20 +106,6 @@ impl<F: Send + Clone> GetTracker<F, payments::PaymentData<F>, api::PaymentsCaptu
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
|
||||
let previously_blocked_amount =
|
||||
previous_captures.iter().fold(0, |accumulator, capture| {
|
||||
accumulator
|
||||
+ match capture.status {
|
||||
enums::CaptureStatus::Charged | enums::CaptureStatus::Pending => {
|
||||
capture.amount
|
||||
}
|
||||
enums::CaptureStatus::Started | enums::CaptureStatus::Failed => 0,
|
||||
}
|
||||
});
|
||||
helpers::validate_amount_to_capture(
|
||||
payment_attempt.amount - previously_blocked_amount,
|
||||
Some(amount_to_capture),
|
||||
)?;
|
||||
|
||||
let capture = db
|
||||
.insert_capture(
|
||||
|
||||
@ -17,8 +17,8 @@ use crate::{
|
||||
services::RedirectForm,
|
||||
types::{
|
||||
self, api,
|
||||
storage::{self, enums},
|
||||
transformers::{ForeignFrom, ForeignTryFrom},
|
||||
storage::{self, enums, payment_attempt::PaymentAttemptExt},
|
||||
transformers::ForeignTryFrom,
|
||||
CaptureSyncResponse,
|
||||
},
|
||||
utils,
|
||||
@ -305,19 +305,27 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
|
||||
)];
|
||||
(Some((multiple_capture_data, capture_update_list)), None)
|
||||
}
|
||||
None => (
|
||||
None,
|
||||
Some(storage::PaymentAttemptUpdate::ErrorUpdate {
|
||||
connector: None,
|
||||
status: match err.status_code {
|
||||
500..=511 => storage::enums::AttemptStatus::Pending,
|
||||
_ => storage::enums::AttemptStatus::Failure,
|
||||
},
|
||||
error_message: Some(Some(err.message)),
|
||||
error_code: Some(Some(err.code)),
|
||||
error_reason: Some(err.reason),
|
||||
}),
|
||||
),
|
||||
None => {
|
||||
let status = match err.status_code {
|
||||
500..=511 => storage::enums::AttemptStatus::Pending,
|
||||
_ => storage::enums::AttemptStatus::Failure,
|
||||
};
|
||||
(
|
||||
None,
|
||||
Some(storage::PaymentAttemptUpdate::ErrorUpdate {
|
||||
connector: None,
|
||||
status,
|
||||
error_message: Some(Some(err.message)),
|
||||
error_code: Some(Some(err.code)),
|
||||
error_reason: Some(err.reason),
|
||||
amount_capturable: if status.is_terminal_status() {
|
||||
Some(0)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
};
|
||||
(
|
||||
capture_update,
|
||||
@ -422,6 +430,11 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
|
||||
error_message: error_status.clone(),
|
||||
error_reason: error_status,
|
||||
connector_response_reference_id,
|
||||
amount_capturable: if router_data.status.is_terminal_status() {
|
||||
Some(0)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
}),
|
||||
),
|
||||
};
|
||||
@ -499,8 +512,10 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
|
||||
|
||||
let authorized_amount = payment_data.payment_attempt.amount;
|
||||
|
||||
payment_attempt_update = Some(storage::PaymentAttemptUpdate::StatusUpdate {
|
||||
payment_attempt_update = Some(storage::PaymentAttemptUpdate::AmountToCaptureUpdate {
|
||||
status: multiple_capture_data.get_attempt_status(authorized_amount),
|
||||
amount_capturable: payment_data.payment_attempt.amount
|
||||
- multiple_capture_data.get_total_blocked_amount(),
|
||||
});
|
||||
Some(multiple_capture_data)
|
||||
}
|
||||
@ -553,10 +568,14 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
|
||||
);
|
||||
let payment_intent_update = match &router_data.response {
|
||||
Err(_) => storage::PaymentIntentUpdate::PGStatusUpdate {
|
||||
status: enums::IntentStatus::foreign_from(payment_data.payment_attempt.status),
|
||||
status: payment_data
|
||||
.payment_attempt
|
||||
.get_intent_status(payment_data.payment_intent.amount_captured),
|
||||
},
|
||||
Ok(_) => storage::PaymentIntentUpdate::ResponseUpdate {
|
||||
status: enums::IntentStatus::foreign_from(payment_data.payment_attempt.status),
|
||||
status: payment_data
|
||||
.payment_attempt
|
||||
.get_intent_status(payment_data.payment_intent.amount_captured),
|
||||
return_url: router_data.return_url.clone(),
|
||||
amount_captured,
|
||||
},
|
||||
|
||||
@ -519,16 +519,13 @@ where
|
||||
connector_name,
|
||||
)
|
||||
});
|
||||
|
||||
let amount_captured = payment_intent.amount_captured.unwrap_or_default();
|
||||
let amount_capturable = Some(payment_attempt.amount - amount_captured);
|
||||
services::ApplicationResponse::JsonWithHeaders((
|
||||
response
|
||||
.set_payment_id(Some(payment_attempt.payment_id))
|
||||
.set_merchant_id(Some(payment_attempt.merchant_id))
|
||||
.set_status(payment_intent.status)
|
||||
.set_amount(payment_attempt.amount)
|
||||
.set_amount_capturable(amount_capturable)
|
||||
.set_amount_capturable(Some(payment_attempt.amount_capturable))
|
||||
.set_amount_received(payment_intent.amount_captured)
|
||||
.set_connector(routed_through)
|
||||
.set_client_secret(payment_intent.client_secret.map(masking::Secret::new))
|
||||
|
||||
@ -4,7 +4,9 @@ pub use data_models::payments::payment_attempt::{
|
||||
use diesel_models::{capture::CaptureNew, enums};
|
||||
use error_stack::ResultExt;
|
||||
|
||||
use crate::{core::errors, errors::RouterResult, utils::OptionExt};
|
||||
use crate::{
|
||||
core::errors, errors::RouterResult, types::transformers::ForeignFrom, utils::OptionExt,
|
||||
};
|
||||
|
||||
pub trait PaymentAttemptExt {
|
||||
fn make_new_capture(
|
||||
@ -14,6 +16,7 @@ pub trait PaymentAttemptExt {
|
||||
) -> RouterResult<CaptureNew>;
|
||||
|
||||
fn get_next_capture_id(&self) -> String;
|
||||
fn get_intent_status(&self, amount_captured: Option<i64>) -> enums::IntentStatus;
|
||||
}
|
||||
|
||||
impl PaymentAttemptExt for PaymentAttempt {
|
||||
@ -55,6 +58,15 @@ impl PaymentAttemptExt for PaymentAttempt {
|
||||
let next_sequence_number = self.multiple_capture_count.unwrap_or_default() + 1;
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@ -137,6 +137,7 @@ impl PaymentAttemptInterface for MockDb {
|
||||
error_reason: payment_attempt.error_reason,
|
||||
multiple_capture_count: payment_attempt.multiple_capture_count,
|
||||
connector_response_reference_id: None,
|
||||
amount_capturable: payment_attempt.amount_capturable,
|
||||
};
|
||||
payment_attempts.push(payment_attempt.clone());
|
||||
Ok(payment_attempt)
|
||||
|
||||
@ -341,6 +341,7 @@ impl<T: DatabaseStore> PaymentAttemptInterface for KVRouterStore<T> {
|
||||
error_reason: payment_attempt.error_reason.clone(),
|
||||
multiple_capture_count: payment_attempt.multiple_capture_count,
|
||||
connector_response_reference_id: None,
|
||||
amount_capturable: payment_attempt.amount_capturable,
|
||||
};
|
||||
|
||||
let field = format!("pa_{}", created_attempt.attempt_id);
|
||||
@ -905,6 +906,7 @@ impl DataModelExt for PaymentAttempt {
|
||||
error_reason: self.error_reason,
|
||||
multiple_capture_count: self.multiple_capture_count,
|
||||
connector_response_reference_id: self.connector_response_reference_id,
|
||||
amount_capturable: self.amount_capturable,
|
||||
}
|
||||
}
|
||||
|
||||
@ -952,6 +954,7 @@ impl DataModelExt for PaymentAttempt {
|
||||
error_reason: storage_model.error_reason,
|
||||
multiple_capture_count: storage_model.multiple_capture_count,
|
||||
connector_response_reference_id: storage_model.connector_response_reference_id,
|
||||
amount_capturable: storage_model.amount_capturable,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -999,6 +1002,7 @@ impl DataModelExt for PaymentAttemptNew {
|
||||
error_reason: self.error_reason,
|
||||
connector_response_reference_id: self.connector_response_reference_id,
|
||||
multiple_capture_count: self.multiple_capture_count,
|
||||
amount_capturable: self.amount_capturable,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1044,6 +1048,7 @@ impl DataModelExt for PaymentAttemptNew {
|
||||
error_reason: storage_model.error_reason,
|
||||
connector_response_reference_id: storage_model.connector_response_reference_id,
|
||||
multiple_capture_count: storage_model.multiple_capture_count,
|
||||
amount_capturable: storage_model.amount_capturable,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1147,6 +1152,7 @@ impl DataModelExt for PaymentAttemptUpdate {
|
||||
error_message,
|
||||
error_reason,
|
||||
connector_response_reference_id,
|
||||
amount_capturable,
|
||||
} => DieselPaymentAttemptUpdate::ResponseUpdate {
|
||||
status,
|
||||
connector,
|
||||
@ -1160,6 +1166,7 @@ impl DataModelExt for PaymentAttemptUpdate {
|
||||
error_message,
|
||||
error_reason,
|
||||
connector_response_reference_id,
|
||||
amount_capturable,
|
||||
},
|
||||
Self::UnresolvedResponseUpdate {
|
||||
status,
|
||||
@ -1187,12 +1194,14 @@ impl DataModelExt for PaymentAttemptUpdate {
|
||||
error_code,
|
||||
error_message,
|
||||
error_reason,
|
||||
amount_capturable,
|
||||
} => DieselPaymentAttemptUpdate::ErrorUpdate {
|
||||
connector,
|
||||
status,
|
||||
error_code,
|
||||
error_message,
|
||||
error_reason,
|
||||
amount_capturable,
|
||||
},
|
||||
Self::MultipleCaptureCountUpdate {
|
||||
multiple_capture_count,
|
||||
@ -1223,6 +1232,13 @@ impl DataModelExt for PaymentAttemptUpdate {
|
||||
error_code,
|
||||
error_message,
|
||||
},
|
||||
Self::AmountToCaptureUpdate {
|
||||
status,
|
||||
amount_capturable,
|
||||
} => DieselPaymentAttemptUpdate::AmountToCaptureUpdate {
|
||||
status,
|
||||
amount_capturable,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -1322,6 +1338,7 @@ impl DataModelExt for PaymentAttemptUpdate {
|
||||
error_message,
|
||||
error_reason,
|
||||
connector_response_reference_id,
|
||||
amount_capturable,
|
||||
} => Self::ResponseUpdate {
|
||||
status,
|
||||
connector,
|
||||
@ -1335,6 +1352,7 @@ impl DataModelExt for PaymentAttemptUpdate {
|
||||
error_message,
|
||||
error_reason,
|
||||
connector_response_reference_id,
|
||||
amount_capturable,
|
||||
},
|
||||
DieselPaymentAttemptUpdate::UnresolvedResponseUpdate {
|
||||
status,
|
||||
@ -1362,12 +1380,14 @@ impl DataModelExt for PaymentAttemptUpdate {
|
||||
error_code,
|
||||
error_message,
|
||||
error_reason,
|
||||
amount_capturable,
|
||||
} => Self::ErrorUpdate {
|
||||
connector,
|
||||
status,
|
||||
error_code,
|
||||
error_message,
|
||||
error_reason,
|
||||
amount_capturable,
|
||||
},
|
||||
DieselPaymentAttemptUpdate::MultipleCaptureCountUpdate {
|
||||
multiple_capture_count,
|
||||
@ -1398,6 +1418,13 @@ impl DataModelExt for PaymentAttemptUpdate {
|
||||
error_code,
|
||||
error_message,
|
||||
},
|
||||
DieselPaymentAttemptUpdate::AmountToCaptureUpdate {
|
||||
status,
|
||||
amount_capturable,
|
||||
} => Self::AmountToCaptureUpdate {
|
||||
status,
|
||||
amount_capturable,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user