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:
Hrithikesh
2023-11-17 10:04:34 +05:30
committed by GitHub
parent f735fb0551
commit cb88be01f2
46 changed files with 600 additions and 59 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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