Skip to content

Commit

Permalink
fix(core): introduce new attempt and intent status to handle multiple…
Browse files Browse the repository at this point in the history
… partial captures (#2802)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Narayan Bhat <[email protected]>
  • Loading branch information
3 people authored Nov 17, 2023
1 parent f735fb0 commit cb88be0
Show file tree
Hide file tree
Showing 46 changed files with 600 additions and 59 deletions.
7 changes: 5 additions & 2 deletions crates/common_enums/src/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pub enum AttemptStatus {
VoidFailed,
AutoRefunded,
PartialCharged,
PartialChargedAndChargeable,
Unresolved,
#[default]
Pending,
Expand All @@ -68,7 +69,8 @@ impl AttemptStatus {
| Self::Voided
| Self::VoidFailed
| Self::CaptureFailed
| Self::Failure => true,
| Self::Failure
| Self::PartialCharged => true,
Self::Started
| Self::AuthenticationFailed
| Self::AuthenticationPending
Expand All @@ -79,7 +81,7 @@ impl AttemptStatus {
| Self::CodInitiated
| Self::VoidInitiated
| Self::CaptureInitiated
| Self::PartialCharged
| Self::PartialChargedAndChargeable
| Self::Unresolved
| Self::Pending
| Self::PaymentMethodAwaited
Expand Down Expand Up @@ -861,6 +863,7 @@ pub enum IntentStatus {
RequiresConfirmation,
RequiresCapture,
PartiallyCaptured,
PartiallyCapturedAndCapturable,
}

#[derive(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -405,15 +405,17 @@ 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
| api_enums::IntentStatus::RequiresMerchantAction => Self::RequiresAction,
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,
}
}
Expand Down
6 changes: 4 additions & 2 deletions crates/router/src/compatibility/stripe/setup_intents/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,15 +313,17 @@ 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,
api_enums::IntentStatus::RequiresMerchantAction => Self::RequiresAction,
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
}
Expand Down
48 changes: 47 additions & 1 deletion crates/router/src/connector/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 3 additions & 3 deletions crates/router/src/core/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion crates/router/src/core/payments/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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),

Expand Down
23 changes: 12 additions & 11 deletions crates/router/src/core/payments/operations/payment_response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use tracing_futures::Instrument;

use super::{Operation, PostUpdateTracker};
use crate::{
connector::utils::PaymentResponseRouterData,
core::{
errors::{self, RouterResult, StorageErrorExt},
mandate,
Expand All @@ -26,7 +27,7 @@ use crate::{
self, enums,
payment_attempt::{AttemptStatusExt, PaymentAttemptExt},
},
transformers::ForeignTryFrom,
transformers::{ForeignFrom, ForeignTryFrom},
CaptureSyncResponse,
},
utils,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(),
);
Expand All @@ -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,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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(),
Expand Down
1 change: 1 addition & 0 deletions crates/router/src/core/payments/retry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion crates/router/src/core/payments/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
10 changes: 0 additions & 10 deletions crates/router/src/types/storage/payment_attempt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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)
}
Expand Down
9 changes: 7 additions & 2 deletions crates/router/src/types/transformers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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,
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
SELECT 1;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- Your SQL goes here
ALTER TYPE "IntentStatus" ADD VALUE IF NOT EXISTS 'partially_captured_and_capturable';
ALTER TYPE "AttemptStatus" ADD VALUE IF NOT EXISTS 'partial_charged_and_chargeable';
4 changes: 3 additions & 1 deletion openapi/openapi_spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -2619,6 +2619,7 @@
"void_failed",
"auto_refunded",
"partial_charged",
"partial_charged_and_chargeable",
"unresolved",
"pending",
"failure",
Expand Down Expand Up @@ -6145,7 +6146,8 @@
"requires_payment_method",
"requires_confirmation",
"requires_capture",
"partially_captured"
"partially_captured",
"partially_captured_and_capturable"
]
},
"JCSVoucherData": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ if (jsonData?.client_secret) {
// Response body should have value "cancellation succeeded" for "payment status"
if (jsonData?.status) {
pm.test(
"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'succeeded'",
"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured'",
function () {
pm.expect(jsonData.status).to.eql("succeeded");
pm.expect(jsonData.status).to.eql("partially_captured");
},
);
}
Expand Down
Loading

0 comments on commit cb88be0

Please sign in to comment.