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

692
Cargo.lock generated

File diff suppressed because it is too large Load Diff

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 => {
let status = match err.status_code {
500..=511 => storage::enums::AttemptStatus::Pending,
_ => storage::enums::AttemptStatus::Failure,
};
(
None,
Some(storage::PaymentAttemptUpdate::ErrorUpdate {
connector: None,
status: match err.status_code {
500..=511 => storage::enums::AttemptStatus::Pending,
_ => storage::enums::AttemptStatus::Failure,
},
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,
},
}
}
}

View File

@ -0,0 +1,3 @@
-- This file should undo anything in `up.sql`
ALTER TABLE payment_attempt
DROP COLUMN amount_capturable;

View File

@ -0,0 +1,3 @@
-- Your SQL goes here
ALTER TABLE payment_attempt
ADD COLUMN IF NOT EXISTS amount_capturable BIGINT NOT NULL DEFAULT 0;