feat(core): enable payments void for multiple partial capture (#2048)

This commit is contained in:
Hrithikesh
2023-09-11 23:09:36 +05:30
committed by GitHub
parent ffe9009d65
commit a81bfe28ed
15 changed files with 230 additions and 652 deletions

View File

@ -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,

View File

@ -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>>,

View File

@ -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()
},
}
}
}

View File

@ -545,6 +545,7 @@ diesel::table! {
multiple_capture_count -> Nullable<Int2>,
#[max_length = 128]
connector_response_reference_id -> Nullable<Varchar>,
amount_capturable -> Int8,
}
}

View File

@ -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!(

View File

@ -2696,6 +2696,7 @@ impl AttemptType {
error_reason: None,
multiple_capture_count: None,
connector_response_reference_id: None,
amount_capturable: old_payment_attempt.amount,
}
}

View File

@ -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(

View File

@ -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,
},

View File

@ -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))

View File

@ -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)]

View File

@ -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)

View File

@ -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,
},
}
}
}