mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-02 12:06:56 +08:00
feat(core): add psync for multiple partial captures (#1934)
This commit is contained in:
1067
Cargo.lock
generated
1067
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -308,7 +308,7 @@ pub struct PaymentAttemptResponse {
|
||||
/// The payment attempt amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.,
|
||||
pub amount: i64,
|
||||
/// The currency of the amount of the payment attempt
|
||||
#[schema(value_type = Option<Currency>, example = "usd")]
|
||||
#[schema(value_type = Option<Currency>, example = "USD")]
|
||||
pub currency: Option<enums::Currency>,
|
||||
/// The connector used for the payment
|
||||
pub connector: Option<String>,
|
||||
@ -346,6 +346,38 @@ pub struct PaymentAttemptResponse {
|
||||
pub reference_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Default, Debug, serde::Serialize, Clone, PartialEq, ToSchema, router_derive::PolymorphicSchema,
|
||||
)]
|
||||
pub struct CaptureResponse {
|
||||
/// unique identifier for the capture
|
||||
pub capture_id: String,
|
||||
/// The status of the capture
|
||||
#[schema(value_type = CaptureStatus, example = "charged")]
|
||||
pub status: enums::CaptureStatus,
|
||||
/// The capture amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.,
|
||||
pub amount: i64,
|
||||
/// The currency of the amount of the capture
|
||||
#[schema(value_type = Option<Currency>, example = "USD")]
|
||||
pub currency: Option<enums::Currency>,
|
||||
/// The connector used for the payment
|
||||
pub connector: String,
|
||||
/// unique identifier for the parent attempt on which this capture is made
|
||||
pub authorized_attempt_id: String,
|
||||
/// A unique identifier for a capture provided by the connector
|
||||
pub connector_capture_id: Option<String>,
|
||||
/// sequence number of this capture
|
||||
pub capture_sequence: i16,
|
||||
/// If there was an error while calling the connector the error message is received here
|
||||
pub error_message: Option<String>,
|
||||
/// If there was an error while calling the connectors the code is received here
|
||||
pub error_code: Option<String>,
|
||||
/// If there was an error while calling the connectors the reason is received here
|
||||
pub error_reason: Option<String>,
|
||||
/// reference to the capture at connector side
|
||||
pub reference_id: Option<String>,
|
||||
}
|
||||
|
||||
impl PaymentsRequest {
|
||||
pub fn get_feature_metadata_as_value(
|
||||
&self,
|
||||
@ -1711,6 +1743,11 @@ pub struct PaymentsResponse {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub attempts: Option<Vec<PaymentAttemptResponse>>,
|
||||
|
||||
/// List of captures done on latest attempt
|
||||
#[schema(value_type = Option<Vec<CaptureResponse>>)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub captures: Option<Vec<CaptureResponse>>,
|
||||
|
||||
/// A unique identifier to link the payment to a mandate, can be use instead of payment_method_data
|
||||
#[schema(max_length = 255, example = "mandate_iwer89rnjef349dni3")]
|
||||
pub mandate_id: Option<String>,
|
||||
|
||||
@ -7,11 +7,12 @@ use utoipa::ToSchema;
|
||||
pub mod diesel_exports {
|
||||
pub use super::{
|
||||
DbAttemptStatus as AttemptStatus, DbAuthenticationType as AuthenticationType,
|
||||
DbCaptureMethod as CaptureMethod, DbConnectorType as ConnectorType,
|
||||
DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency, DbDisputeStage as DisputeStage,
|
||||
DbDisputeStatus as DisputeStatus, DbEventType as EventType, DbFutureUsage as FutureUsage,
|
||||
DbIntentStatus as IntentStatus, DbMandateStatus as MandateStatus,
|
||||
DbPaymentMethodIssuerCode as PaymentMethodIssuerCode, DbRefundStatus as RefundStatus,
|
||||
DbCaptureMethod as CaptureMethod, DbCaptureStatus as CaptureStatus,
|
||||
DbConnectorType as ConnectorType, DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency,
|
||||
DbDisputeStage as DisputeStage, DbDisputeStatus as DisputeStatus, DbEventType as EventType,
|
||||
DbFutureUsage as FutureUsage, DbIntentStatus as IntentStatus,
|
||||
DbMandateStatus as MandateStatus, DbPaymentMethodIssuerCode as PaymentMethodIssuerCode,
|
||||
DbRefundStatus as RefundStatus,
|
||||
};
|
||||
}
|
||||
|
||||
@ -96,6 +97,7 @@ pub enum AuthenticationType {
|
||||
serde::Serialize,
|
||||
strum::Display,
|
||||
strum::EnumString,
|
||||
ToSchema,
|
||||
Hash,
|
||||
)]
|
||||
#[router_derive::diesel_enum(storage_type = "pg_enum")]
|
||||
|
||||
@ -4,7 +4,7 @@ use time::PrimitiveDateTime;
|
||||
|
||||
use crate::{enums as storage_enums, schema::captures};
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Identifiable, Queryable, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Identifiable, Queryable, Serialize, Deserialize, Hash)]
|
||||
#[diesel(table_name = captures)]
|
||||
#[diesel(primary_key(capture_id))]
|
||||
pub struct Capture {
|
||||
@ -14,7 +14,7 @@ pub struct Capture {
|
||||
pub status: storage_enums::CaptureStatus,
|
||||
pub amount: i64,
|
||||
pub currency: Option<storage_enums::Currency>,
|
||||
pub connector: Option<String>,
|
||||
pub connector: String,
|
||||
pub error_message: Option<String>,
|
||||
pub error_code: Option<String>,
|
||||
pub error_reason: Option<String>,
|
||||
@ -24,8 +24,10 @@ pub struct Capture {
|
||||
#[serde(with = "common_utils::custom_serde::iso8601")]
|
||||
pub modified_at: PrimitiveDateTime,
|
||||
pub authorized_attempt_id: String,
|
||||
pub connector_transaction_id: Option<String>,
|
||||
pub connector_capture_id: Option<String>,
|
||||
pub capture_sequence: i16,
|
||||
// reference to the capture at connector side
|
||||
pub connector_response_reference_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Insertable, router_derive::DebugAsDisplay, Serialize, Deserialize)]
|
||||
@ -37,7 +39,7 @@ pub struct CaptureNew {
|
||||
pub status: storage_enums::CaptureStatus,
|
||||
pub amount: i64,
|
||||
pub currency: Option<storage_enums::Currency>,
|
||||
pub connector: Option<String>,
|
||||
pub connector: String,
|
||||
pub error_message: Option<String>,
|
||||
pub error_code: Option<String>,
|
||||
pub error_reason: Option<String>,
|
||||
@ -47,15 +49,17 @@ pub struct CaptureNew {
|
||||
#[serde(with = "common_utils::custom_serde::iso8601")]
|
||||
pub modified_at: PrimitiveDateTime,
|
||||
pub authorized_attempt_id: String,
|
||||
pub connector_transaction_id: Option<String>,
|
||||
pub connector_capture_id: Option<String>,
|
||||
pub capture_sequence: i16,
|
||||
pub connector_response_reference_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum CaptureUpdate {
|
||||
ResponseUpdate {
|
||||
status: storage_enums::CaptureStatus,
|
||||
connector_transaction_id: Option<String>,
|
||||
connector_capture_id: Option<String>,
|
||||
connector_response_reference_id: Option<String>,
|
||||
},
|
||||
ErrorUpdate {
|
||||
status: storage_enums::CaptureStatus,
|
||||
@ -73,7 +77,8 @@ pub struct CaptureUpdateInternal {
|
||||
pub error_code: Option<String>,
|
||||
pub error_reason: Option<String>,
|
||||
pub modified_at: Option<PrimitiveDateTime>,
|
||||
pub connector_transaction_id: Option<String>,
|
||||
pub connector_capture_id: Option<String>,
|
||||
pub connector_response_reference_id: Option<String>,
|
||||
}
|
||||
|
||||
impl CaptureUpdate {
|
||||
@ -96,11 +101,13 @@ impl From<CaptureUpdate> for CaptureUpdateInternal {
|
||||
match payment_attempt_child_update {
|
||||
CaptureUpdate::ResponseUpdate {
|
||||
status,
|
||||
connector_transaction_id,
|
||||
connector_capture_id: connector_transaction_id,
|
||||
connector_response_reference_id,
|
||||
} => Self {
|
||||
status: Some(status),
|
||||
connector_transaction_id,
|
||||
connector_capture_id: connector_transaction_id,
|
||||
modified_at: now,
|
||||
connector_response_reference_id,
|
||||
..Self::default()
|
||||
},
|
||||
CaptureUpdate::ErrorUpdate {
|
||||
|
||||
@ -193,9 +193,8 @@ pub enum PaymentAttemptUpdate {
|
||||
error_message: Option<Option<String>>,
|
||||
error_reason: Option<Option<String>>,
|
||||
},
|
||||
MultipleCaptureUpdate {
|
||||
status: Option<storage_enums::AttemptStatus>,
|
||||
multiple_capture_count: Option<i16>,
|
||||
MultipleCaptureCountUpdate {
|
||||
multiple_capture_count: i16,
|
||||
},
|
||||
PreprocessingUpdate {
|
||||
status: storage_enums::AttemptStatus,
|
||||
@ -444,12 +443,10 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
|
||||
connector_response_reference_id,
|
||||
..Default::default()
|
||||
},
|
||||
PaymentAttemptUpdate::MultipleCaptureUpdate {
|
||||
status,
|
||||
PaymentAttemptUpdate::MultipleCaptureCountUpdate {
|
||||
multiple_capture_count,
|
||||
} => Self {
|
||||
status,
|
||||
multiple_capture_count,
|
||||
multiple_capture_count: Some(multiple_capture_count),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods, Table};
|
||||
use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods};
|
||||
use router_env::{instrument, tracing};
|
||||
|
||||
use super::generics;
|
||||
@ -58,12 +58,7 @@ impl Capture {
|
||||
authorized_attempt_id: &str,
|
||||
conn: &PgPooledConn,
|
||||
) -> StorageResult<Vec<Self>> {
|
||||
generics::generic_filter::<
|
||||
<Self as HasTable>::Table,
|
||||
_,
|
||||
<<Self as HasTable>::Table as Table>::PrimaryKey,
|
||||
_,
|
||||
>(
|
||||
generics::generic_filter::<<Self as HasTable>::Table, _, _, _>(
|
||||
conn,
|
||||
dsl::authorized_attempt_id
|
||||
.eq(authorized_attempt_id.to_owned())
|
||||
@ -71,7 +66,7 @@ impl Capture {
|
||||
.and(dsl::payment_id.eq(payment_id.to_owned())),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(dsl::created_at.asc()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@ -96,7 +96,7 @@ diesel::table! {
|
||||
amount -> Int8,
|
||||
currency -> Nullable<Currency>,
|
||||
#[max_length = 255]
|
||||
connector -> Nullable<Varchar>,
|
||||
connector -> Varchar,
|
||||
#[max_length = 255]
|
||||
error_message -> Nullable<Varchar>,
|
||||
#[max_length = 255]
|
||||
@ -109,8 +109,10 @@ diesel::table! {
|
||||
#[max_length = 64]
|
||||
authorized_attempt_id -> Varchar,
|
||||
#[max_length = 128]
|
||||
connector_transaction_id -> Nullable<Varchar>,
|
||||
connector_capture_id -> Nullable<Varchar>,
|
||||
capture_sequence -> Int2,
|
||||
#[max_length = 128]
|
||||
connector_response_reference_id -> Nullable<Varchar>,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,8 +5,9 @@ pub mod helpers;
|
||||
pub mod operations;
|
||||
pub mod tokenization;
|
||||
pub mod transformers;
|
||||
pub mod types;
|
||||
|
||||
use std::{collections::HashMap, fmt::Debug, marker::PhantomData, ops::Deref, time::Instant};
|
||||
use std::{fmt::Debug, marker::PhantomData, ops::Deref, time::Instant};
|
||||
|
||||
use api_models::payments::FrmMessage;
|
||||
use common_utils::{ext_traits::AsyncExt, pii};
|
||||
@ -36,7 +37,7 @@ use crate::{
|
||||
scheduler::{utils as pt_utils, workflows::payment_sync},
|
||||
services::{self, api::Authenticate},
|
||||
types::{
|
||||
self, api, domain,
|
||||
self as router_types, api, domain,
|
||||
storage::{self, enums as storage_enums, ProcessTrackerExt},
|
||||
},
|
||||
utils::{add_connector_http_status_code_metrics, Encode, OptionExt, ValueExt},
|
||||
@ -58,12 +59,12 @@ where
|
||||
Op: Operation<F, Req> + Send + Sync,
|
||||
|
||||
// To create connector flow specific interface data
|
||||
PaymentData<F>: ConstructFlowSpecificData<F, FData, types::PaymentsResponseData>,
|
||||
types::RouterData<F, FData, types::PaymentsResponseData>: Feature<F, FData>,
|
||||
PaymentData<F>: ConstructFlowSpecificData<F, FData, router_types::PaymentsResponseData>,
|
||||
router_types::RouterData<F, FData, router_types::PaymentsResponseData>: Feature<F, FData>,
|
||||
|
||||
// To construct connector flow specific api
|
||||
dyn types::api::Connector:
|
||||
services::api::ConnectorIntegration<F, FData, types::PaymentsResponseData>,
|
||||
dyn router_types::api::Connector:
|
||||
services::api::ConnectorIntegration<F, FData, router_types::PaymentsResponseData>,
|
||||
|
||||
// To perform router related operation for PaymentResponse
|
||||
PaymentResponse: Operation<F, FData>,
|
||||
@ -251,12 +252,12 @@ where
|
||||
Req: Debug + Authenticate,
|
||||
Res: transformers::ToResponse<Req, PaymentData<F>, Op>,
|
||||
// To create connector flow specific interface data
|
||||
PaymentData<F>: ConstructFlowSpecificData<F, FData, types::PaymentsResponseData>,
|
||||
types::RouterData<F, FData, types::PaymentsResponseData>: Feature<F, FData>,
|
||||
PaymentData<F>: ConstructFlowSpecificData<F, FData, router_types::PaymentsResponseData>,
|
||||
router_types::RouterData<F, FData, router_types::PaymentsResponseData>: Feature<F, FData>,
|
||||
|
||||
// To construct connector flow specific api
|
||||
dyn types::api::Connector:
|
||||
services::api::ConnectorIntegration<F, FData, types::PaymentsResponseData>,
|
||||
dyn router_types::api::Connector:
|
||||
services::api::ConnectorIntegration<F, FData, router_types::PaymentsResponseData>,
|
||||
|
||||
// To perform router related operation for PaymentResponse
|
||||
PaymentResponse: Operation<F, FData>,
|
||||
@ -315,7 +316,7 @@ pub trait PaymentRedirectFlow: Sync {
|
||||
fn generate_response(
|
||||
&self,
|
||||
payments_response: api_models::payments::PaymentsResponse,
|
||||
merchant_account: types::domain::MerchantAccount,
|
||||
merchant_account: router_types::domain::MerchantAccount,
|
||||
payment_id: String,
|
||||
connector: String,
|
||||
) -> RouterResult<api::RedirectionResponse>;
|
||||
@ -435,7 +436,7 @@ impl PaymentRedirectFlow for PaymentRedirectCompleteAuthorize {
|
||||
fn generate_response(
|
||||
&self,
|
||||
payments_response: api_models::payments::PaymentsResponse,
|
||||
merchant_account: types::domain::MerchantAccount,
|
||||
merchant_account: router_types::domain::MerchantAccount,
|
||||
payment_id: String,
|
||||
connector: String,
|
||||
) -> RouterResult<api::RedirectionResponse> {
|
||||
@ -524,7 +525,7 @@ impl PaymentRedirectFlow for PaymentRedirectSync {
|
||||
fn generate_response(
|
||||
&self,
|
||||
payments_response: api_models::payments::PaymentsResponse,
|
||||
merchant_account: types::domain::MerchantAccount,
|
||||
merchant_account: router_types::domain::MerchantAccount,
|
||||
payment_id: String,
|
||||
connector: String,
|
||||
) -> RouterResult<api::RedirectionResponse> {
|
||||
@ -555,18 +556,19 @@ pub async fn call_connector_service<F, RouterDReq, ApiRequest>(
|
||||
updated_customer: Option<storage::CustomerUpdate>,
|
||||
requeue: bool,
|
||||
schedule_time: Option<time::PrimitiveDateTime>,
|
||||
) -> RouterResult<types::RouterData<F, RouterDReq, types::PaymentsResponseData>>
|
||||
) -> RouterResult<router_types::RouterData<F, RouterDReq, router_types::PaymentsResponseData>>
|
||||
where
|
||||
F: Send + Clone + Sync,
|
||||
RouterDReq: Send + Sync,
|
||||
|
||||
// To create connector flow specific interface data
|
||||
PaymentData<F>: ConstructFlowSpecificData<F, RouterDReq, types::PaymentsResponseData>,
|
||||
types::RouterData<F, RouterDReq, types::PaymentsResponseData>: Feature<F, RouterDReq> + Send,
|
||||
PaymentData<F>: ConstructFlowSpecificData<F, RouterDReq, router_types::PaymentsResponseData>,
|
||||
router_types::RouterData<F, RouterDReq, router_types::PaymentsResponseData>:
|
||||
Feature<F, RouterDReq> + Send,
|
||||
|
||||
// To construct connector flow specific api
|
||||
dyn api::Connector:
|
||||
services::api::ConnectorIntegration<F, RouterDReq, types::PaymentsResponseData>,
|
||||
services::api::ConnectorIntegration<F, RouterDReq, router_types::PaymentsResponseData>,
|
||||
{
|
||||
let stime_connector = Instant::now();
|
||||
|
||||
@ -607,7 +609,7 @@ where
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Ok(types::PaymentsResponseData::PreProcessingResponse {
|
||||
if let Ok(router_types::PaymentsResponseData::PreProcessingResponse {
|
||||
session_token: Some(session_token),
|
||||
..
|
||||
}) = router_data.response.to_owned()
|
||||
@ -697,11 +699,12 @@ where
|
||||
F: Send + Clone,
|
||||
|
||||
// To create connector flow specific interface data
|
||||
PaymentData<F>: ConstructFlowSpecificData<F, Req, types::PaymentsResponseData>,
|
||||
types::RouterData<F, Req, types::PaymentsResponseData>: Feature<F, Req>,
|
||||
PaymentData<F>: ConstructFlowSpecificData<F, Req, router_types::PaymentsResponseData>,
|
||||
router_types::RouterData<F, Req, router_types::PaymentsResponseData>: Feature<F, Req>,
|
||||
|
||||
// To construct connector flow specific api
|
||||
dyn api::Connector: services::api::ConnectorIntegration<F, Req, types::PaymentsResponseData>,
|
||||
dyn api::Connector:
|
||||
services::api::ConnectorIntegration<F, Req, router_types::PaymentsResponseData>,
|
||||
|
||||
// To perform router related operation for PaymentResponse
|
||||
PaymentResponse: Operation<F, Req>,
|
||||
@ -733,8 +736,10 @@ where
|
||||
let connector_name = session_connector.connector.connector_name.to_string();
|
||||
match connector_res {
|
||||
Ok(connector_response) => {
|
||||
if let Ok(types::PaymentsResponseData::SessionResponse { session_token, .. }) =
|
||||
connector_response.response
|
||||
if let Ok(router_types::PaymentsResponseData::SessionResponse {
|
||||
session_token,
|
||||
..
|
||||
}) = connector_response.response
|
||||
{
|
||||
// If session token is NoSessionTokenReceived, it is not pushed into the sessions_token as there is no response or there can be some error
|
||||
// In case of error, that error is already logged
|
||||
@ -776,11 +781,12 @@ where
|
||||
Req: Send + Sync,
|
||||
|
||||
// To create connector flow specific interface data
|
||||
PaymentData<F>: ConstructFlowSpecificData<F, Req, types::PaymentsResponseData>,
|
||||
types::RouterData<F, Req, types::PaymentsResponseData>: Feature<F, Req> + Send,
|
||||
PaymentData<F>: ConstructFlowSpecificData<F, Req, router_types::PaymentsResponseData>,
|
||||
router_types::RouterData<F, Req, router_types::PaymentsResponseData>: Feature<F, Req> + Send,
|
||||
|
||||
// To construct connector flow specific api
|
||||
dyn api::Connector: services::api::ConnectorIntegration<F, Req, types::PaymentsResponseData>,
|
||||
dyn api::Connector:
|
||||
services::api::ConnectorIntegration<F, Req, router_types::PaymentsResponseData>,
|
||||
|
||||
// To perform router related operation for PaymentResponse
|
||||
PaymentResponse: Operation<F, Req>,
|
||||
@ -850,21 +856,25 @@ async fn complete_preprocessing_steps_if_required<F, Req>(
|
||||
state: &AppState,
|
||||
connector: &api::ConnectorData,
|
||||
payment_data: &PaymentData<F>,
|
||||
mut router_data: types::RouterData<F, Req, types::PaymentsResponseData>,
|
||||
mut router_data: router_types::RouterData<F, Req, router_types::PaymentsResponseData>,
|
||||
should_continue_payment: bool,
|
||||
) -> RouterResult<(types::RouterData<F, Req, types::PaymentsResponseData>, bool)>
|
||||
) -> RouterResult<(
|
||||
router_types::RouterData<F, Req, router_types::PaymentsResponseData>,
|
||||
bool,
|
||||
)>
|
||||
where
|
||||
F: Send + Clone + Sync,
|
||||
Req: Send + Sync,
|
||||
types::RouterData<F, Req, types::PaymentsResponseData>: Feature<F, Req> + Send,
|
||||
dyn api::Connector: services::api::ConnectorIntegration<F, Req, types::PaymentsResponseData>,
|
||||
router_types::RouterData<F, Req, router_types::PaymentsResponseData>: Feature<F, Req> + Send,
|
||||
dyn api::Connector:
|
||||
services::api::ConnectorIntegration<F, Req, router_types::PaymentsResponseData>,
|
||||
{
|
||||
//TODO: For ACH transfers, if preprocessing_step is not required for connectors encountered in future, add the check
|
||||
let router_data_and_should_continue_payment = match payment_data.payment_method_data.clone() {
|
||||
Some(api_models::payments::PaymentMethodData::BankTransfer(data)) => match data.deref() {
|
||||
api_models::payments::BankTransferData::AchBankTransfer { .. }
|
||||
| api_models::payments::BankTransferData::MultibancoBankTransfer { .. }
|
||||
if connector.connector_name == types::Connector::Stripe =>
|
||||
if connector.connector_name == router_types::Connector::Stripe =>
|
||||
{
|
||||
if payment_data.payment_attempt.preprocessing_step_id.is_none() {
|
||||
(
|
||||
@ -888,7 +898,7 @@ where
|
||||
}
|
||||
}
|
||||
Some(api_models::payments::PaymentMethodData::Card(_)) => {
|
||||
if connector.connector_name == types::Connector::Payme {
|
||||
if connector.connector_name == router_types::Connector::Payme {
|
||||
router_data = router_data.preprocessing_steps(state, connector).await?;
|
||||
|
||||
let is_error_in_response = router_data.response.is_err();
|
||||
@ -1118,7 +1128,7 @@ where
|
||||
pub flow: PhantomData<F>,
|
||||
pub payment_intent: storage::PaymentIntent,
|
||||
pub payment_attempt: storage::PaymentAttempt,
|
||||
pub multiple_capture_data: Option<MultipleCaptureData>,
|
||||
pub multiple_capture_data: Option<types::MultipleCaptureData>,
|
||||
pub connector_response: storage::ConnectorResponse,
|
||||
pub amount: api::Amount,
|
||||
pub mandate_id: Option<api_models::payments::MandateIds>,
|
||||
@ -1145,96 +1155,6 @@ where
|
||||
pub frm_message: Option<FrmMessage>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MultipleCaptureData {
|
||||
previous_captures: Vec<storage::Capture>,
|
||||
current_capture: storage::Capture,
|
||||
}
|
||||
|
||||
impl MultipleCaptureData {
|
||||
fn get_previously_blocked_amount(&self) -> i64 {
|
||||
self.previous_captures
|
||||
.iter()
|
||||
.fold(0, |accumulator, capture| {
|
||||
accumulator
|
||||
+ match capture.status {
|
||||
storage_enums::CaptureStatus::Charged
|
||||
| storage_enums::CaptureStatus::Pending => capture.amount,
|
||||
storage_enums::CaptureStatus::Started
|
||||
| storage_enums::CaptureStatus::Failed => 0,
|
||||
}
|
||||
})
|
||||
}
|
||||
fn get_total_blocked_amount(&self) -> i64 {
|
||||
self.get_previously_blocked_amount()
|
||||
+ match self.current_capture.status {
|
||||
api_models::enums::CaptureStatus::Charged
|
||||
| api_models::enums::CaptureStatus::Pending => self.current_capture.amount,
|
||||
api_models::enums::CaptureStatus::Failed
|
||||
| api_models::enums::CaptureStatus::Started => 0,
|
||||
}
|
||||
}
|
||||
fn get_previously_charged_amount(&self) -> i64 {
|
||||
self.previous_captures
|
||||
.iter()
|
||||
.fold(0, |accumulator, capture| {
|
||||
accumulator
|
||||
+ match capture.status {
|
||||
storage_enums::CaptureStatus::Charged => capture.amount,
|
||||
storage_enums::CaptureStatus::Pending
|
||||
| storage_enums::CaptureStatus::Started
|
||||
| storage_enums::CaptureStatus::Failed => 0,
|
||||
}
|
||||
})
|
||||
}
|
||||
fn get_total_charged_amount(&self) -> i64 {
|
||||
self.get_previously_charged_amount()
|
||||
+ match self.current_capture.status {
|
||||
storage_enums::CaptureStatus::Charged => self.current_capture.amount,
|
||||
storage_enums::CaptureStatus::Pending
|
||||
| storage_enums::CaptureStatus::Started
|
||||
| storage_enums::CaptureStatus::Failed => 0,
|
||||
}
|
||||
}
|
||||
fn get_captures_count(&self) -> RouterResult<i16> {
|
||||
i16::try_from(1 + self.previous_captures.len())
|
||||
.into_report()
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Error while converting from usize to i16")
|
||||
}
|
||||
fn get_status_count(&self) -> HashMap<storage_enums::CaptureStatus, i16> {
|
||||
let mut hash_map: HashMap<storage_enums::CaptureStatus, i16> = HashMap::new();
|
||||
hash_map.insert(storage_enums::CaptureStatus::Charged, 0);
|
||||
hash_map.insert(storage_enums::CaptureStatus::Pending, 0);
|
||||
hash_map.insert(storage_enums::CaptureStatus::Started, 0);
|
||||
hash_map.insert(storage_enums::CaptureStatus::Failed, 0);
|
||||
hash_map
|
||||
.entry(self.current_capture.status)
|
||||
.and_modify(|count| *count += 1);
|
||||
self.previous_captures
|
||||
.iter()
|
||||
.fold(hash_map, |mut accumulator, capture| {
|
||||
let current_capture_status = capture.status;
|
||||
accumulator
|
||||
.entry(current_capture_status)
|
||||
.and_modify(|count| *count += 1);
|
||||
accumulator
|
||||
})
|
||||
}
|
||||
fn get_attempt_status(&self, authorized_amount: i64) -> storage_enums::AttemptStatus {
|
||||
let total_captured_amount = self.get_total_charged_amount();
|
||||
if authorized_amount == total_captured_amount {
|
||||
return storage_enums::AttemptStatus::Charged;
|
||||
}
|
||||
let status_count_map = self.get_status_count();
|
||||
if status_count_map.get(&storage_enums::CaptureStatus::Charged) > Some(&0) {
|
||||
storage_enums::AttemptStatus::PartialCharged
|
||||
} else {
|
||||
storage_enums::AttemptStatus::CaptureInitiated
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct RecurringMandatePaymentData {
|
||||
pub payment_method_type: Option<storage_enums::PaymentMethodType>, //required for making recurring payment using saved payment method through stripe
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use super::{ConstructFlowSpecificData, Feature};
|
||||
use crate::{
|
||||
core::{
|
||||
errors::{ConnectorErrorExt, RouterResult},
|
||||
errors::{ApiErrorResponse, ConnectorErrorExt, RouterResult},
|
||||
payments::{self, access_token, transformers, PaymentData},
|
||||
},
|
||||
routes::AppState,
|
||||
@ -42,7 +44,7 @@ impl Feature<api::PSync, types::PaymentsSyncData>
|
||||
for types::RouterData<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
|
||||
{
|
||||
async fn decide_flows<'a>(
|
||||
self,
|
||||
mut self,
|
||||
state: &AppState,
|
||||
connector: &api::ConnectorData,
|
||||
_customer: &Option<domain::Customer>,
|
||||
@ -56,17 +58,44 @@ impl Feature<api::PSync, types::PaymentsSyncData>
|
||||
types::PaymentsSyncData,
|
||||
types::PaymentsResponseData,
|
||||
> = connector.connector.get_connector_integration();
|
||||
let resp = services::execute_connector_processing_step(
|
||||
state,
|
||||
connector_integration,
|
||||
&self,
|
||||
call_connector_action,
|
||||
connector_request,
|
||||
)
|
||||
.await
|
||||
.to_payment_failed_response()?;
|
||||
|
||||
Ok(resp)
|
||||
let capture_sync_method_result = connector_integration
|
||||
.get_multiple_capture_sync_method()
|
||||
.to_payment_failed_response();
|
||||
|
||||
match (
|
||||
self.request.capture_sync_type.clone(),
|
||||
capture_sync_method_result,
|
||||
) {
|
||||
(
|
||||
types::CaptureSyncType::MultipleCaptureSync(pending_connector_capture_id_list),
|
||||
Ok(services::CaptureSyncMethod::Individual),
|
||||
) => {
|
||||
let resp = self
|
||||
.execute_connector_processing_step_for_each_capture(
|
||||
state,
|
||||
pending_connector_capture_id_list,
|
||||
call_connector_action,
|
||||
connector_integration,
|
||||
)
|
||||
.await?;
|
||||
Ok(resp)
|
||||
}
|
||||
(types::CaptureSyncType::MultipleCaptureSync(_), Err(err)) => Err(err),
|
||||
_ => {
|
||||
// for bulk sync of captures, above logic needs to be handled at connector end
|
||||
let resp = services::execute_connector_processing_step(
|
||||
state,
|
||||
connector_integration,
|
||||
&self,
|
||||
call_connector_action,
|
||||
connector_request,
|
||||
)
|
||||
.await
|
||||
.to_payment_failed_response()?;
|
||||
Ok(resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn add_access_token<'a>(
|
||||
@ -103,3 +132,57 @@ impl Feature<api::PSync, types::PaymentsSyncData>
|
||||
Ok((request, true))
|
||||
}
|
||||
}
|
||||
|
||||
impl types::RouterData<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData> {
|
||||
async fn execute_connector_processing_step_for_each_capture(
|
||||
mut self,
|
||||
state: &AppState,
|
||||
pending_connector_capture_id_list: Vec<String>,
|
||||
call_connector_action: payments::CallConnectorAction,
|
||||
connector_integration: services::BoxedConnectorIntegration<
|
||||
'_,
|
||||
api::PSync,
|
||||
types::PaymentsSyncData,
|
||||
types::PaymentsResponseData,
|
||||
>,
|
||||
) -> RouterResult<Self> {
|
||||
let mut capture_sync_response_list = HashMap::new();
|
||||
for connector_capture_id in pending_connector_capture_id_list {
|
||||
self.request.connector_transaction_id =
|
||||
types::ResponseId::ConnectorTransactionId(connector_capture_id.clone());
|
||||
let resp = services::execute_connector_processing_step(
|
||||
state,
|
||||
connector_integration.clone(),
|
||||
&self,
|
||||
call_connector_action.clone(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.to_payment_failed_response()?;
|
||||
let capture_sync_response = match resp.response {
|
||||
Err(err) => types::CaptureSyncResponse::Error {
|
||||
code: err.code,
|
||||
message: err.message,
|
||||
reason: err.reason,
|
||||
status_code: err.status_code,
|
||||
},
|
||||
Ok(types::PaymentsResponseData::TransactionResponse {
|
||||
resource_id,
|
||||
connector_response_reference_id,
|
||||
..
|
||||
}) => types::CaptureSyncResponse::Success {
|
||||
resource_id,
|
||||
status: resp.status,
|
||||
connector_response_reference_id,
|
||||
},
|
||||
// this error is never meant to occur. response type will always be PaymentsResponseData::TransactionResponse
|
||||
_ => Err(ApiErrorResponse::PreconditionFailed { message: "Response type must be PaymentsResponseData::TransactionResponse for payment sync".into() })?,
|
||||
};
|
||||
capture_sync_response_list.insert(connector_capture_id.clone(), capture_sync_response);
|
||||
}
|
||||
self.response = Ok(types::PaymentsResponseData::MultipleCaptureResponse {
|
||||
capture_sync_response_list,
|
||||
});
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ use super::{BoxedOperation, Domain, GetTracker, Operation, UpdateTracker, Valida
|
||||
use crate::{
|
||||
core::{
|
||||
errors::{self, RouterResult, StorageErrorExt},
|
||||
payments::{self, helpers, operations},
|
||||
payments::{self, helpers, operations, types::MultipleCaptureData},
|
||||
},
|
||||
db::StorageInterface,
|
||||
routes::AppState,
|
||||
@ -85,78 +85,77 @@ impl<F: Send + Clone> GetTracker<F, payments::PaymentData<F>, api::PaymentsCaptu
|
||||
|
||||
helpers::validate_capture_method(capture_method)?;
|
||||
|
||||
let (multiple_capture_data, connector_response) =
|
||||
if capture_method == enums::CaptureMethod::ManualMultiple {
|
||||
let amount_to_capture = request
|
||||
.amount_to_capture
|
||||
.get_required_value("amount_to_capture")?;
|
||||
let previous_captures = db
|
||||
.find_all_captures_by_merchant_id_payment_id_authorized_attempt_id(
|
||||
&payment_attempt.merchant_id,
|
||||
&payment_attempt.payment_id,
|
||||
&payment_attempt.attempt_id,
|
||||
storage_scheme,
|
||||
)
|
||||
.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(
|
||||
payment_attempt
|
||||
.make_new_capture(amount_to_capture, enums::CaptureStatus::Started),
|
||||
storage_scheme,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::DuplicatePayment {
|
||||
payment_id: payment_id.clone(),
|
||||
})?;
|
||||
let new_connector_response = db
|
||||
.insert_connector_response(
|
||||
ConnectorResponse::make_new_connector_response(
|
||||
capture.payment_id.clone(),
|
||||
capture.merchant_id.clone(),
|
||||
capture.capture_id.clone(),
|
||||
capture.connector.clone(),
|
||||
),
|
||||
storage_scheme,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::DuplicatePayment {
|
||||
payment_id: payment_id.clone(),
|
||||
})?;
|
||||
(
|
||||
Some(payments::MultipleCaptureData {
|
||||
previous_captures,
|
||||
current_capture: capture,
|
||||
}),
|
||||
new_connector_response,
|
||||
let (multiple_capture_data, connector_response) = if capture_method
|
||||
== enums::CaptureMethod::ManualMultiple
|
||||
{
|
||||
let amount_to_capture = request
|
||||
.amount_to_capture
|
||||
.get_required_value("amount_to_capture")?;
|
||||
let previous_captures = db
|
||||
.find_all_captures_by_merchant_id_payment_id_authorized_attempt_id(
|
||||
&payment_attempt.merchant_id,
|
||||
&payment_attempt.payment_id,
|
||||
&payment_attempt.attempt_id,
|
||||
storage_scheme,
|
||||
)
|
||||
} else {
|
||||
let connector_response = db
|
||||
.find_connector_response_by_payment_id_merchant_id_attempt_id(
|
||||
&payment_attempt.payment_id,
|
||||
&payment_attempt.merchant_id,
|
||||
&payment_attempt.attempt_id,
|
||||
storage_scheme,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
|
||||
(None, connector_response)
|
||||
};
|
||||
.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(
|
||||
payment_attempt
|
||||
.make_new_capture(amount_to_capture, enums::CaptureStatus::Started)?,
|
||||
storage_scheme,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::DuplicatePayment {
|
||||
payment_id: payment_id.clone(),
|
||||
})?;
|
||||
let new_connector_response = db
|
||||
.insert_connector_response(
|
||||
ConnectorResponse::make_new_connector_response(
|
||||
capture.payment_id.clone(),
|
||||
capture.merchant_id.clone(),
|
||||
capture.capture_id.clone(),
|
||||
Some(capture.connector.clone()),
|
||||
),
|
||||
storage_scheme,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::DuplicatePayment { payment_id })?;
|
||||
(
|
||||
Some(MultipleCaptureData::new_for_create(
|
||||
previous_captures,
|
||||
capture,
|
||||
)),
|
||||
new_connector_response,
|
||||
)
|
||||
} else {
|
||||
let connector_response = db
|
||||
.find_connector_response_by_payment_id_merchant_id_attempt_id(
|
||||
&payment_attempt.payment_id,
|
||||
&payment_attempt.merchant_id,
|
||||
&payment_attempt.attempt_id,
|
||||
storage_scheme,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
|
||||
(None, connector_response)
|
||||
};
|
||||
|
||||
currency = payment_attempt.currency.get_required_value("currency")?;
|
||||
|
||||
@ -262,24 +261,16 @@ impl<F: Clone> UpdateTracker<F, payments::PaymentData<F>, api::PaymentsCaptureRe
|
||||
F: 'b + Send,
|
||||
{
|
||||
payment_data.payment_attempt = match &payment_data.multiple_capture_data {
|
||||
Some(multiple_capture_data) => {
|
||||
let mut updated_payment_attempt = db
|
||||
.update_payment_attempt_with_attempt_id(
|
||||
payment_data.payment_attempt,
|
||||
storage::PaymentAttemptUpdate::MultipleCaptureUpdate {
|
||||
status: None,
|
||||
multiple_capture_count: Some(
|
||||
multiple_capture_data.current_capture.capture_sequence,
|
||||
),
|
||||
},
|
||||
storage_scheme,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
|
||||
updated_payment_attempt.amount_to_capture =
|
||||
Some(multiple_capture_data.current_capture.amount);
|
||||
updated_payment_attempt
|
||||
}
|
||||
Some(multiple_capture_data) => db
|
||||
.update_payment_attempt_with_attempt_id(
|
||||
payment_data.payment_attempt,
|
||||
storage::PaymentAttemptUpdate::MultipleCaptureCountUpdate {
|
||||
multiple_capture_count: multiple_capture_data.get_captures_count()?,
|
||||
},
|
||||
storage_scheme,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?,
|
||||
None => payment_data.payment_attempt,
|
||||
};
|
||||
Ok((Box::new(self), payment_data))
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use error_stack::ResultExt;
|
||||
use router_derive;
|
||||
@ -7,7 +9,7 @@ use crate::{
|
||||
core::{
|
||||
errors::{self, RouterResult, StorageErrorExt},
|
||||
mandate,
|
||||
payments::PaymentData,
|
||||
payments::{types::MultipleCaptureData, PaymentData},
|
||||
},
|
||||
db::StorageInterface,
|
||||
routes::metrics,
|
||||
@ -16,6 +18,7 @@ use crate::{
|
||||
self, api,
|
||||
storage::{self, enums},
|
||||
transformers::{ForeignFrom, ForeignTryFrom},
|
||||
CaptureSyncResponse,
|
||||
},
|
||||
utils,
|
||||
};
|
||||
@ -229,8 +232,8 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
|
||||
{
|
||||
Err(err) => {
|
||||
let (capture_update, attempt_update) = match payment_data.multiple_capture_data {
|
||||
Some(_) => (
|
||||
Some(storage::CaptureUpdate::ErrorUpdate {
|
||||
Some(multiple_capture_data) => {
|
||||
let capture_update = storage::CaptureUpdate::ErrorUpdate {
|
||||
status: match err.status_code {
|
||||
500..=511 => storage::enums::CaptureStatus::Pending,
|
||||
_ => storage::enums::CaptureStatus::Failed,
|
||||
@ -238,10 +241,13 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
|
||||
error_code: Some(err.code),
|
||||
error_message: Some(err.message),
|
||||
error_reason: err.reason,
|
||||
}),
|
||||
// attempt status will depend on collective capture status
|
||||
None,
|
||||
),
|
||||
};
|
||||
let capture_update_list = vec![(
|
||||
multiple_capture_data.get_latest_capture().clone(),
|
||||
capture_update,
|
||||
)];
|
||||
(Some((multiple_capture_data, capture_update_list)), None)
|
||||
}
|
||||
None => (
|
||||
None,
|
||||
Some(storage::PaymentAttemptUpdate::ErrorUpdate {
|
||||
@ -327,17 +333,20 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
|
||||
metrics::SUCCESSFUL_PAYMENT.add(&metrics::CONTEXT, 1, &[]);
|
||||
}
|
||||
|
||||
let (capture_update, payment_attempt_update) =
|
||||
let (capture_updates, payment_attempt_update) =
|
||||
match payment_data.multiple_capture_data {
|
||||
Some(_) => (
|
||||
//if payment_data.multiple_capture_data will be Some only for multiple partial capture.
|
||||
Some(storage::CaptureUpdate::ResponseUpdate {
|
||||
Some(multiple_capture_data) => {
|
||||
let capture_update = storage::CaptureUpdate::ResponseUpdate {
|
||||
status: enums::CaptureStatus::foreign_try_from(router_data.status)?,
|
||||
connector_transaction_id: connector_transaction_id.clone(),
|
||||
}),
|
||||
// attempt status will depend on collective capture status
|
||||
None,
|
||||
),
|
||||
connector_capture_id: connector_transaction_id.clone(),
|
||||
connector_response_reference_id,
|
||||
};
|
||||
let capture_update_list = vec![(
|
||||
multiple_capture_data.get_latest_capture().clone(),
|
||||
capture_update,
|
||||
)];
|
||||
(Some((multiple_capture_data, capture_update_list)), None)
|
||||
}
|
||||
None => (
|
||||
None,
|
||||
Some(storage::PaymentAttemptUpdate::ResponseUpdate {
|
||||
@ -368,7 +377,7 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
|
||||
};
|
||||
|
||||
(
|
||||
capture_update,
|
||||
capture_updates,
|
||||
payment_attempt_update,
|
||||
Some(connector_response_update),
|
||||
)
|
||||
@ -403,28 +412,38 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
|
||||
types::PaymentsResponseData::TokenizationResponse { .. } => (None, None, None),
|
||||
types::PaymentsResponseData::ConnectorCustomerResponse { .. } => (None, None, None),
|
||||
types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. } => (None, None, None),
|
||||
types::PaymentsResponseData::MultipleCaptureResponse {
|
||||
capture_sync_response_list,
|
||||
} => match payment_data.multiple_capture_data {
|
||||
Some(multiple_capture_data) => {
|
||||
let capture_update_list = response_to_capture_update(
|
||||
&multiple_capture_data,
|
||||
capture_sync_response_list,
|
||||
)?;
|
||||
(
|
||||
Some((multiple_capture_data, capture_update_list)),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
}
|
||||
None => (None, None, None),
|
||||
},
|
||||
},
|
||||
};
|
||||
payment_data.multiple_capture_data = match capture_update
|
||||
.zip(payment_data.multiple_capture_data)
|
||||
{
|
||||
Some((capture_update, mut multiple_capture_data)) => {
|
||||
let updated_capture = db
|
||||
.update_capture_with_capture_id(
|
||||
multiple_capture_data.current_capture,
|
||||
capture_update,
|
||||
storage_scheme,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
|
||||
|
||||
multiple_capture_data.current_capture = updated_capture;
|
||||
payment_data.multiple_capture_data = match capture_update {
|
||||
Some((mut multiple_capture_data, capture_updates)) => {
|
||||
for (capture, capture_update) in capture_updates {
|
||||
let updated_capture = db
|
||||
.update_capture_with_capture_id(capture, capture_update, storage_scheme)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
|
||||
multiple_capture_data.update_capture(updated_capture);
|
||||
}
|
||||
|
||||
let authorized_amount = payment_data.payment_attempt.amount;
|
||||
|
||||
payment_attempt_update = Some(storage::PaymentAttemptUpdate::MultipleCaptureUpdate {
|
||||
status: Some(multiple_capture_data.get_attempt_status(authorized_amount)),
|
||||
multiple_capture_count: Some(multiple_capture_data.get_captures_count()?),
|
||||
payment_attempt_update = Some(storage::PaymentAttemptUpdate::StatusUpdate {
|
||||
status: multiple_capture_data.get_attempt_status(authorized_amount),
|
||||
});
|
||||
Some(multiple_capture_data)
|
||||
}
|
||||
@ -492,6 +511,21 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
|
||||
Ok(payment_data)
|
||||
}
|
||||
|
||||
fn response_to_capture_update(
|
||||
multiple_capture_data: &MultipleCaptureData,
|
||||
response_list: HashMap<String, CaptureSyncResponse>,
|
||||
) -> RouterResult<Vec<(storage::Capture, storage::CaptureUpdate)>> {
|
||||
let mut capture_update_list = vec![];
|
||||
for (connector_capture_id, capture_sync_response) in response_list {
|
||||
let capture =
|
||||
multiple_capture_data.get_capture_by_connector_capture_id(connector_capture_id);
|
||||
if let Some(capture) = capture {
|
||||
capture_update_list.push((capture.clone(), capture_sync_response.try_into()?))
|
||||
}
|
||||
}
|
||||
Ok(capture_update_list)
|
||||
}
|
||||
|
||||
fn get_total_amount_captured<F: Clone, T: types::Capturable>(
|
||||
request: T,
|
||||
amount_captured: Option<i64>,
|
||||
|
||||
@ -11,7 +11,7 @@ use super::{BoxedOperation, Domain, GetTracker, Operation, UpdateTracker, Valida
|
||||
use crate::{
|
||||
core::{
|
||||
errors::{self, CustomResult, RouterResult, StorageErrorExt},
|
||||
payments::{helpers, operations, CustomerDetails, PaymentAddress, PaymentData},
|
||||
payments::{helpers, operations, types, CustomerDetails, PaymentAddress, PaymentData},
|
||||
},
|
||||
db::StorageInterface,
|
||||
routes::AppState,
|
||||
@ -218,7 +218,7 @@ async fn get_tracker_for_sync<
|
||||
storage_scheme,
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.change_context(errors::ApiErrorResponse::PaymentNotFound)
|
||||
.attach_printable("Database error when finding connector response")?;
|
||||
|
||||
connector_response.encoded_data = request.param.clone();
|
||||
@ -243,7 +243,7 @@ async fn get_tracker_for_sync<
|
||||
Some(db
|
||||
.find_attempts_by_merchant_id_payment_id(merchant_id, &payment_id_str, storage_scheme)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.change_context(errors::ApiErrorResponse::PaymentNotFound)
|
||||
.attach_printable_lazy(|| {
|
||||
format!("Error while retrieving attempt list for, merchant_id: {merchant_id}, payment_id: {payment_id_str}")
|
||||
})?)
|
||||
@ -251,10 +251,28 @@ async fn get_tracker_for_sync<
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let multiple_capture_data = if payment_attempt.multiple_capture_count > Some(0) {
|
||||
let captures = db
|
||||
.find_all_captures_by_merchant_id_payment_id_authorized_attempt_id(
|
||||
&payment_attempt.merchant_id,
|
||||
&payment_attempt.payment_id,
|
||||
&payment_attempt.attempt_id,
|
||||
storage_scheme,
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::PaymentNotFound)
|
||||
.attach_printable_lazy(|| {
|
||||
format!("Error while retrieving capture list for, merchant_id: {merchant_id}, payment_id: {payment_id_str}")
|
||||
})?;
|
||||
Some(types::MultipleCaptureData::new_for_sync(captures)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let refunds = db
|
||||
.find_refund_by_payment_id_merchant_id(&payment_id_str, merchant_id, storage_scheme)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.change_context(errors::ApiErrorResponse::PaymentNotFound)
|
||||
.attach_printable_lazy(|| {
|
||||
format!(
|
||||
"Failed while getting refund list for, payment_id: {}, merchant_id: {}",
|
||||
@ -265,7 +283,7 @@ async fn get_tracker_for_sync<
|
||||
let disputes = db
|
||||
.find_disputes_by_merchant_id_payment_id(merchant_id, &payment_id_str)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.change_context(errors::ApiErrorResponse::PaymentNotFound)
|
||||
.attach_printable_lazy(|| {
|
||||
format!("Error while retrieving dispute list for, merchant_id: {merchant_id}, payment_id: {payment_id_str}")
|
||||
})?;
|
||||
@ -273,7 +291,7 @@ async fn get_tracker_for_sync<
|
||||
let frm_response = db
|
||||
.find_fraud_check_by_payment_id(payment_id_str.to_string(), merchant_id.to_string())
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.change_context(errors::ApiErrorResponse::PaymentNotFound)
|
||||
.attach_printable_lazy(|| {
|
||||
format!("Error while retrieving frm_response, merchant_id: {merchant_id}, payment_id: {payment_id_str}")
|
||||
});
|
||||
@ -353,7 +371,7 @@ async fn get_tracker_for_sync<
|
||||
connector_customer_id: None,
|
||||
recurring_mandate_payment_data: None,
|
||||
ephemeral_key: None,
|
||||
multiple_capture_data: None,
|
||||
multiple_capture_data,
|
||||
redirect_response: None,
|
||||
frm_message,
|
||||
},
|
||||
|
||||
@ -19,7 +19,8 @@ use crate::{
|
||||
types::{
|
||||
self, api, domain,
|
||||
storage::{self, enums},
|
||||
transformers::{ForeignFrom, ForeignInto},
|
||||
transformers::{ForeignFrom, ForeignInto, ForeignTryFrom},
|
||||
MultipleCaptureRequestData,
|
||||
},
|
||||
utils::{OptionExt, ValueExt},
|
||||
};
|
||||
@ -185,6 +186,15 @@ where
|
||||
payment_data.refunds,
|
||||
payment_data.disputes,
|
||||
payment_data.attempts,
|
||||
payment_data
|
||||
.multiple_capture_data
|
||||
.map(|multiple_capture_data| {
|
||||
multiple_capture_data
|
||||
.get_all_captures()
|
||||
.into_iter()
|
||||
.cloned()
|
||||
.collect()
|
||||
}),
|
||||
payment_data.payment_method_data,
|
||||
customer,
|
||||
auth_flow,
|
||||
@ -300,6 +310,7 @@ pub fn payments_to_payments_response<R, Op>(
|
||||
refunds: Vec<storage::Refund>,
|
||||
disputes: Vec<storage::Dispute>,
|
||||
option_attempts: Option<Vec<storage::PaymentAttempt>>,
|
||||
captures: Option<Vec<storage::Capture>>,
|
||||
payment_method_data: Option<api::PaymentMethodData>,
|
||||
customer: Option<domain::Customer>,
|
||||
auth_flow: services::AuthFlow,
|
||||
@ -349,6 +360,14 @@ where
|
||||
.map(ForeignInto::foreign_into)
|
||||
.collect()
|
||||
});
|
||||
|
||||
let captures_response = captures.map(|captures| {
|
||||
captures
|
||||
.into_iter()
|
||||
.map(ForeignInto::foreign_into)
|
||||
.collect()
|
||||
});
|
||||
|
||||
let merchant_id = payment_attempt.merchant_id.to_owned();
|
||||
let payment_method_type = payment_attempt
|
||||
.payment_method_type
|
||||
@ -508,6 +527,7 @@ where
|
||||
.set_refunds(refunds_response) // refunds.iter().map(refund_to_refund_response),
|
||||
.set_disputes(disputes_response)
|
||||
.set_attempts(attempts_response)
|
||||
.set_captures(captures_response)
|
||||
.set_payment_method(
|
||||
payment_attempt.payment_method,
|
||||
auth_flow == services::AuthFlow::Merchant,
|
||||
@ -577,6 +597,7 @@ where
|
||||
refunds: refunds_response,
|
||||
disputes: disputes_response,
|
||||
attempts: attempts_response,
|
||||
captures: captures_response,
|
||||
payment_method: payment_attempt.payment_method,
|
||||
capture_method: payment_attempt.capture_method,
|
||||
error_message: payment_attempt
|
||||
@ -920,6 +941,12 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsSyncData
|
||||
encoded_data: payment_data.connector_response.encoded_data,
|
||||
capture_method: payment_data.payment_attempt.capture_method,
|
||||
connector_meta: payment_data.payment_attempt.connector_metadata,
|
||||
capture_sync_type: match payment_data.multiple_capture_data {
|
||||
Some(multiple_capture_data) => types::CaptureSyncType::MultipleCaptureSync(
|
||||
multiple_capture_data.get_pending_connector_capture_ids(),
|
||||
),
|
||||
None => types::CaptureSyncType::SingleCaptureSync,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -975,10 +1002,16 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsCaptureD
|
||||
.ok_or(errors::ApiErrorResponse::ResourceIdNotFound)?,
|
||||
payment_amount: payment_data.amount.into(),
|
||||
connector_meta: payment_data.payment_attempt.connector_metadata,
|
||||
capture_method: payment_data
|
||||
.payment_attempt
|
||||
.capture_method
|
||||
.unwrap_or_default(),
|
||||
multiple_capture_data: match payment_data.multiple_capture_data {
|
||||
Some(multiple_capture_data) => Some(MultipleCaptureRequestData {
|
||||
capture_sequence: multiple_capture_data.get_captures_count()?,
|
||||
capture_reference: multiple_capture_data
|
||||
.get_latest_capture()
|
||||
.capture_id
|
||||
.clone(),
|
||||
}),
|
||||
None => None,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1084,6 +1117,44 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::VerifyRequestDat
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<types::CaptureSyncResponse> for storage::CaptureUpdate {
|
||||
type Error = error_stack::Report<errors::ApiErrorResponse>;
|
||||
|
||||
fn try_from(capture_sync_response: types::CaptureSyncResponse) -> Result<Self, Self::Error> {
|
||||
match capture_sync_response {
|
||||
types::CaptureSyncResponse::Success {
|
||||
resource_id,
|
||||
status,
|
||||
connector_response_reference_id,
|
||||
} => {
|
||||
let connector_capture_id = match resource_id {
|
||||
types::ResponseId::ConnectorTransactionId(id) => Some(id),
|
||||
types::ResponseId::EncodedData(_) | types::ResponseId::NoResponseId => None,
|
||||
};
|
||||
Ok(Self::ResponseUpdate {
|
||||
status: enums::CaptureStatus::foreign_try_from(status)?,
|
||||
connector_capture_id,
|
||||
connector_response_reference_id,
|
||||
})
|
||||
}
|
||||
types::CaptureSyncResponse::Error {
|
||||
code,
|
||||
message,
|
||||
reason,
|
||||
status_code,
|
||||
} => Ok(Self::ErrorUpdate {
|
||||
status: match status_code {
|
||||
500..=511 => storage::enums::CaptureStatus::Pending,
|
||||
_ => storage::enums::CaptureStatus::Failed,
|
||||
},
|
||||
error_code: Some(code),
|
||||
error_message: Some(message),
|
||||
error_reason: reason,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::CompleteAuthorizeData {
|
||||
type Error = error_stack::Report<errors::ApiErrorResponse>;
|
||||
|
||||
|
||||
154
crates/router/src/core/payments/types.rs
Normal file
154
crates/router/src/core/payments/types.rs
Normal file
@ -0,0 +1,154 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use error_stack::{IntoReport, ResultExt};
|
||||
|
||||
use crate::{
|
||||
core::errors::{self, RouterResult},
|
||||
types::storage::{self, enums as storage_enums},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MultipleCaptureData {
|
||||
// key -> capture_id, value -> Capture
|
||||
all_captures: HashMap<String, storage::Capture>,
|
||||
latest_capture: storage::Capture,
|
||||
_private: Private, // to restrict direct construction of MultipleCaptureData
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
struct Private {}
|
||||
|
||||
impl MultipleCaptureData {
|
||||
pub fn new_for_sync(captures: Vec<storage::Capture>) -> RouterResult<Self> {
|
||||
let latest_capture = captures
|
||||
.last()
|
||||
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
||||
.into_report()
|
||||
.attach_printable("Cannot create MultipleCaptureData with empty captures list")?
|
||||
.clone();
|
||||
let multiple_capture_data = Self {
|
||||
all_captures: captures
|
||||
.into_iter()
|
||||
.map(|capture| (capture.capture_id.clone(), capture))
|
||||
.collect(),
|
||||
latest_capture,
|
||||
_private: Private {},
|
||||
};
|
||||
Ok(multiple_capture_data)
|
||||
}
|
||||
|
||||
pub fn new_for_create(
|
||||
mut previous_captures: Vec<storage::Capture>,
|
||||
new_capture: storage::Capture,
|
||||
) -> Self {
|
||||
previous_captures.push(new_capture.clone());
|
||||
Self {
|
||||
all_captures: previous_captures
|
||||
.into_iter()
|
||||
.map(|capture| (capture.capture_id.clone(), capture))
|
||||
.collect(),
|
||||
latest_capture: new_capture,
|
||||
_private: Private {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_capture(&mut self, updated_capture: storage::Capture) {
|
||||
let capture_id = &updated_capture.capture_id;
|
||||
if self.all_captures.contains_key(capture_id) {
|
||||
self.all_captures
|
||||
.entry(capture_id.into())
|
||||
.and_modify(|capture| *capture = updated_capture.clone());
|
||||
}
|
||||
}
|
||||
pub fn get_total_blocked_amount(&self) -> i64 {
|
||||
self.all_captures.iter().fold(0, |accumulator, capture| {
|
||||
accumulator
|
||||
+ match capture.1.status {
|
||||
storage_enums::CaptureStatus::Charged
|
||||
| storage_enums::CaptureStatus::Pending => capture.1.amount,
|
||||
storage_enums::CaptureStatus::Started
|
||||
| storage_enums::CaptureStatus::Failed => 0,
|
||||
}
|
||||
})
|
||||
}
|
||||
pub fn get_total_charged_amount(&self) -> i64 {
|
||||
self.all_captures.iter().fold(0, |accumulator, capture| {
|
||||
accumulator
|
||||
+ match capture.1.status {
|
||||
storage_enums::CaptureStatus::Charged => capture.1.amount,
|
||||
storage_enums::CaptureStatus::Pending
|
||||
| storage_enums::CaptureStatus::Started
|
||||
| storage_enums::CaptureStatus::Failed => 0,
|
||||
}
|
||||
})
|
||||
}
|
||||
pub fn get_captures_count(&self) -> RouterResult<i16> {
|
||||
i16::try_from(self.all_captures.len())
|
||||
.into_report()
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Error while converting from usize to i16")
|
||||
}
|
||||
pub fn get_status_count(&self) -> HashMap<storage_enums::CaptureStatus, i16> {
|
||||
let mut hash_map: HashMap<storage_enums::CaptureStatus, i16> = HashMap::new();
|
||||
hash_map.insert(storage_enums::CaptureStatus::Charged, 0);
|
||||
hash_map.insert(storage_enums::CaptureStatus::Pending, 0);
|
||||
hash_map.insert(storage_enums::CaptureStatus::Started, 0);
|
||||
hash_map.insert(storage_enums::CaptureStatus::Failed, 0);
|
||||
self.all_captures
|
||||
.iter()
|
||||
.fold(hash_map, |mut accumulator, capture| {
|
||||
let current_capture_status = capture.1.status;
|
||||
accumulator
|
||||
.entry(current_capture_status)
|
||||
.and_modify(|count| *count += 1);
|
||||
accumulator
|
||||
})
|
||||
}
|
||||
pub fn get_attempt_status(&self, authorized_amount: i64) -> storage_enums::AttemptStatus {
|
||||
let total_captured_amount = self.get_total_charged_amount();
|
||||
if authorized_amount == total_captured_amount {
|
||||
return storage_enums::AttemptStatus::Charged;
|
||||
}
|
||||
let status_count_map = self.get_status_count();
|
||||
if status_count_map.get(&storage_enums::CaptureStatus::Charged) > Some(&0) {
|
||||
storage_enums::AttemptStatus::PartialCharged
|
||||
} else {
|
||||
storage_enums::AttemptStatus::CaptureInitiated
|
||||
}
|
||||
}
|
||||
pub fn get_pending_captures(&self) -> Vec<&storage::Capture> {
|
||||
self.all_captures
|
||||
.iter()
|
||||
.filter(|capture| capture.1.status == storage_enums::CaptureStatus::Pending)
|
||||
.map(|key_value| key_value.1)
|
||||
.collect()
|
||||
}
|
||||
pub fn get_all_captures(&self) -> Vec<&storage::Capture> {
|
||||
self.all_captures
|
||||
.iter()
|
||||
.map(|key_value| key_value.1)
|
||||
.collect()
|
||||
}
|
||||
pub fn get_capture_by_capture_id(&self, capture_id: String) -> Option<&storage::Capture> {
|
||||
self.all_captures.get(&capture_id)
|
||||
}
|
||||
pub fn get_capture_by_connector_capture_id(
|
||||
&self,
|
||||
connector_capture_id: String,
|
||||
) -> Option<&storage::Capture> {
|
||||
self.all_captures
|
||||
.iter()
|
||||
.find(|(_, capture)| capture.connector_capture_id == Some(connector_capture_id.clone()))
|
||||
.map(|(_, capture)| capture)
|
||||
}
|
||||
pub fn get_latest_capture(&self) -> &storage::Capture {
|
||||
&self.latest_capture
|
||||
}
|
||||
pub fn get_pending_connector_capture_ids(&self) -> Vec<String> {
|
||||
let pending_connector_capture_ids = self
|
||||
.get_pending_captures()
|
||||
.into_iter()
|
||||
.filter_map(|capture| capture.connector_capture_id.clone())
|
||||
.collect();
|
||||
pending_connector_capture_ids
|
||||
}
|
||||
}
|
||||
@ -192,7 +192,8 @@ impl CaptureInterface for MockDb {
|
||||
modified_at: capture.modified_at,
|
||||
authorized_attempt_id: capture.authorized_attempt_id,
|
||||
capture_sequence: capture.capture_sequence,
|
||||
connector_transaction_id: capture.connector_transaction_id,
|
||||
connector_capture_id: capture.connector_capture_id,
|
||||
connector_response_reference_id: capture.connector_response_reference_id,
|
||||
};
|
||||
captures.push(capture.clone());
|
||||
Ok(capture)
|
||||
|
||||
@ -164,6 +164,7 @@ Never share your secret api keys. Keep them guarded and secure.
|
||||
api_models::enums::FrmPreferredFlowTypes,
|
||||
api_models::enums::RetryAction,
|
||||
api_models::enums::AttemptStatus,
|
||||
api_models::enums::CaptureStatus,
|
||||
api_models::admin::MerchantConnectorCreate,
|
||||
api_models::admin::MerchantConnectorUpdate,
|
||||
api_models::admin::PrimaryBusinessDetails,
|
||||
@ -292,6 +293,7 @@ Never share your secret api keys. Keep them guarded and secure.
|
||||
api_models::payments::BacsBankTransferInstructions,
|
||||
api_models::payments::RedirectResponse,
|
||||
api_models::payments::PaymentAttemptResponse,
|
||||
api_models::payments::CaptureResponse,
|
||||
api_models::payment_methods::RequiredFieldInfo,
|
||||
api_models::refunds::RefundListRequest,
|
||||
api_models::refunds::RefundListResponse,
|
||||
|
||||
@ -171,6 +171,16 @@ pub trait ConnectorIntegration<T, Req, Resp>: ConnectorIntegrationAny<T, Req, Re
|
||||
})
|
||||
}
|
||||
|
||||
// whenever capture sync is implemented at the connector side, this method should be overridden
|
||||
fn get_multiple_capture_sync_method(
|
||||
&self,
|
||||
) -> CustomResult<CaptureSyncMethod, errors::ConnectorError> {
|
||||
Err(
|
||||
errors::ConnectorError::NotImplemented("multiple capture sync not implemented".into())
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
|
||||
fn get_certificate(
|
||||
&self,
|
||||
_req: &types::RouterData<T, Req, Resp>,
|
||||
@ -186,6 +196,11 @@ pub trait ConnectorIntegration<T, Req, Resp>: ConnectorIntegrationAny<T, Req, Re
|
||||
}
|
||||
}
|
||||
|
||||
pub enum CaptureSyncMethod {
|
||||
Individual,
|
||||
Bulk,
|
||||
}
|
||||
|
||||
/// Handle the flow by interacting with connector module
|
||||
/// `connector_request` is applicable only in case if the `CallConnectorAction` is `Trigger`
|
||||
/// In other cases, It will be created if required, even if it is not passed
|
||||
|
||||
@ -11,7 +11,7 @@ pub mod domain;
|
||||
pub mod storage;
|
||||
pub mod transformers;
|
||||
|
||||
use std::marker::PhantomData;
|
||||
use std::{collections::HashMap, marker::PhantomData};
|
||||
|
||||
pub use api_models::{
|
||||
enums::{Connector, PayoutConnectors},
|
||||
@ -331,10 +331,17 @@ pub struct PaymentsCaptureData {
|
||||
pub currency: storage_enums::Currency,
|
||||
pub connector_transaction_id: String,
|
||||
pub payment_amount: i64,
|
||||
pub capture_method: storage_enums::CaptureMethod,
|
||||
pub multiple_capture_data: Option<MultipleCaptureRequestData>,
|
||||
pub connector_meta: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct MultipleCaptureRequestData {
|
||||
pub capture_sequence: i16,
|
||||
pub capture_reference: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AuthorizeSessionTokenData {
|
||||
pub amount_to_capture: Option<i64>,
|
||||
@ -405,9 +412,17 @@ pub struct PaymentsSyncData {
|
||||
pub encoded_data: Option<String>,
|
||||
pub capture_method: Option<storage_enums::CaptureMethod>,
|
||||
pub connector_meta: Option<serde_json::Value>,
|
||||
pub capture_sync_type: CaptureSyncType,
|
||||
pub mandate_id: Option<api_models::payments::MandateIds>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub enum CaptureSyncType {
|
||||
MultipleCaptureSync(Vec<String>),
|
||||
#[default]
|
||||
SingleCaptureSync,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct PaymentsCancelData {
|
||||
pub amount: Option<i64>,
|
||||
@ -494,6 +509,21 @@ pub struct MandateReference {
|
||||
pub payment_method_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CaptureSyncResponse {
|
||||
Success {
|
||||
resource_id: ResponseId,
|
||||
status: storage_enums::AttemptStatus,
|
||||
connector_response_reference_id: Option<String>,
|
||||
},
|
||||
Error {
|
||||
code: String,
|
||||
message: String,
|
||||
reason: Option<String>,
|
||||
status_code: u16,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PaymentsResponseData {
|
||||
TransactionResponse {
|
||||
@ -504,6 +534,10 @@ pub enum PaymentsResponseData {
|
||||
network_txn_id: Option<String>,
|
||||
connector_response_reference_id: Option<String>,
|
||||
},
|
||||
MultipleCaptureResponse {
|
||||
// pending_capture_id_list: Vec<String>,
|
||||
capture_sync_response_list: HashMap<String, CaptureSyncResponse>,
|
||||
},
|
||||
SessionResponse {
|
||||
session_token: api::SessionToken,
|
||||
},
|
||||
|
||||
@ -2,6 +2,9 @@ pub use diesel_models::payment_attempt::{
|
||||
PaymentAttempt, PaymentAttemptNew, PaymentAttemptUpdate, PaymentAttemptUpdateInternal,
|
||||
};
|
||||
use diesel_models::{capture::CaptureNew, enums};
|
||||
use error_stack::ResultExt;
|
||||
|
||||
use crate::{core::errors, errors::RouterResult, utils::OptionExt};
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct RoutingData {
|
||||
@ -14,7 +17,7 @@ pub trait PaymentAttemptExt {
|
||||
&self,
|
||||
capture_amount: i64,
|
||||
capture_status: enums::CaptureStatus,
|
||||
) -> CaptureNew;
|
||||
) -> RouterResult<CaptureNew>;
|
||||
|
||||
fn get_next_capture_id(&self) -> String;
|
||||
}
|
||||
@ -24,17 +27,24 @@ impl PaymentAttemptExt for PaymentAttempt {
|
||||
&self,
|
||||
capture_amount: i64,
|
||||
capture_status: enums::CaptureStatus,
|
||||
) -> CaptureNew {
|
||||
) -> RouterResult<CaptureNew> {
|
||||
let capture_sequence = self.multiple_capture_count.unwrap_or_default() + 1;
|
||||
let now = common_utils::date_time::now();
|
||||
CaptureNew {
|
||||
Ok(CaptureNew {
|
||||
payment_id: self.payment_id.clone(),
|
||||
merchant_id: self.merchant_id.clone(),
|
||||
capture_id: self.get_next_capture_id(),
|
||||
status: capture_status,
|
||||
amount: capture_amount,
|
||||
currency: self.currency,
|
||||
connector: self.connector.clone(),
|
||||
connector: self
|
||||
.connector
|
||||
.clone()
|
||||
.get_required_value("connector")
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable(
|
||||
"connector field is required in payment_attempt to create a capture",
|
||||
)?,
|
||||
error_message: None,
|
||||
tax_amount: None,
|
||||
created_at: now,
|
||||
@ -43,8 +53,9 @@ impl PaymentAttemptExt for PaymentAttempt {
|
||||
error_reason: None,
|
||||
authorized_attempt_id: self.attempt_id.clone(),
|
||||
capture_sequence,
|
||||
connector_transaction_id: None,
|
||||
}
|
||||
connector_capture_id: None,
|
||||
connector_response_reference_id: None,
|
||||
})
|
||||
}
|
||||
fn get_next_capture_id(&self) -> String {
|
||||
let next_sequence_number = self.multiple_capture_count.unwrap_or_default() + 1;
|
||||
|
||||
@ -636,6 +636,25 @@ impl ForeignFrom<storage::PaymentAttempt> for api_models::payments::PaymentAttem
|
||||
}
|
||||
}
|
||||
|
||||
impl ForeignFrom<storage::Capture> for api_models::payments::CaptureResponse {
|
||||
fn foreign_from(capture: storage::Capture) -> Self {
|
||||
Self {
|
||||
capture_id: capture.capture_id,
|
||||
status: capture.status,
|
||||
amount: capture.amount,
|
||||
currency: capture.currency,
|
||||
connector: capture.connector,
|
||||
authorized_attempt_id: capture.authorized_attempt_id,
|
||||
connector_capture_id: capture.connector_capture_id,
|
||||
capture_sequence: capture.capture_sequence,
|
||||
error_message: capture.error_message,
|
||||
error_code: capture.error_code,
|
||||
error_reason: capture.error_reason,
|
||||
reference_id: capture.connector_response_reference_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ForeignFrom<api_models::payouts::Bank> for api_enums::PaymentMethodType {
|
||||
fn foreign_from(value: api_models::payouts::Bank) -> Self {
|
||||
match value {
|
||||
|
||||
@ -80,6 +80,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData {
|
||||
payment_method_token: None,
|
||||
connector_customer: None,
|
||||
recurring_mandate_payment_data: None,
|
||||
|
||||
preprocessing_id: None,
|
||||
connector_request_reference_id: uuid::Uuid::new_v4().to_string(),
|
||||
#[cfg(feature = "payouts")]
|
||||
@ -133,6 +134,7 @@ fn construct_refund_router_data<F>() -> types::RefundsRouterData<F> {
|
||||
payment_method_token: None,
|
||||
connector_customer: None,
|
||||
recurring_mandate_payment_data: None,
|
||||
|
||||
preprocessing_id: None,
|
||||
connector_request_reference_id: uuid::Uuid::new_v4().to_string(),
|
||||
#[cfg(feature = "payouts")]
|
||||
|
||||
@ -106,6 +106,7 @@ async fn should_sync_authorized_payment() {
|
||||
),
|
||||
encoded_data: None,
|
||||
capture_method: Some(diesel_models::enums::CaptureMethod::Manual),
|
||||
capture_sync_type: types::CaptureSyncType::SingleCaptureSync,
|
||||
connector_meta: None,
|
||||
}),
|
||||
None,
|
||||
@ -219,6 +220,7 @@ async fn should_sync_auto_captured_payment() {
|
||||
),
|
||||
encoded_data: None,
|
||||
capture_method: Some(enums::CaptureMethod::Automatic),
|
||||
capture_sync_type: types::CaptureSyncType::SingleCaptureSync,
|
||||
connector_meta: None,
|
||||
}),
|
||||
None,
|
||||
|
||||
@ -150,6 +150,7 @@ async fn should_sync_authorized_payment() {
|
||||
),
|
||||
encoded_data: None,
|
||||
capture_method: None,
|
||||
capture_sync_type: types::CaptureSyncType::SingleCaptureSync,
|
||||
connector_meta: None,
|
||||
mandate_id: None,
|
||||
}),
|
||||
|
||||
@ -120,6 +120,7 @@ async fn should_sync_authorized_payment() {
|
||||
connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(txn_id),
|
||||
encoded_data: None,
|
||||
capture_method: None,
|
||||
capture_sync_type: types::CaptureSyncType::SingleCaptureSync,
|
||||
connector_meta,
|
||||
mandate_id: None,
|
||||
}),
|
||||
|
||||
@ -138,6 +138,7 @@ async fn should_sync_authorized_payment() {
|
||||
connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(txn_id),
|
||||
encoded_data: None,
|
||||
capture_method: None,
|
||||
capture_sync_type: types::CaptureSyncType::SingleCaptureSync,
|
||||
connector_meta,
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
@ -330,6 +331,7 @@ async fn should_sync_auto_captured_payment() {
|
||||
),
|
||||
encoded_data: None,
|
||||
capture_method: Some(enums::CaptureMethod::Automatic),
|
||||
capture_sync_type: types::CaptureSyncType::SingleCaptureSync,
|
||||
connector_meta,
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
|
||||
@ -496,6 +496,7 @@ pub trait ConnectorActions: Connector {
|
||||
payment_method_token: info.clone().and_then(|a| a.payment_method_token),
|
||||
connector_customer: info.clone().and_then(|a| a.connector_customer),
|
||||
recurring_mandate_payment_data: None,
|
||||
|
||||
preprocessing_id: None,
|
||||
connector_request_reference_id: uuid::Uuid::new_v4().to_string(),
|
||||
#[cfg(feature = "payouts")]
|
||||
@ -523,6 +524,7 @@ pub trait ConnectorActions: Connector {
|
||||
Ok(types::PaymentsResponseData::ConnectorCustomerResponse { .. }) => None,
|
||||
Ok(types::PaymentsResponseData::PreProcessingResponse { .. }) => None,
|
||||
Ok(types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. }) => None,
|
||||
Ok(types::PaymentsResponseData::MultipleCaptureResponse { .. }) => None,
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
@ -918,6 +920,7 @@ impl Default for PaymentSyncType {
|
||||
),
|
||||
encoded_data: None,
|
||||
capture_method: None,
|
||||
capture_sync_type: types::CaptureSyncType::SingleCaptureSync,
|
||||
connector_meta: None,
|
||||
};
|
||||
Self(data)
|
||||
@ -978,6 +981,7 @@ pub fn get_connector_transaction_id(
|
||||
Ok(types::PaymentsResponseData::PreProcessingResponse { .. }) => None,
|
||||
Ok(types::PaymentsResponseData::ConnectorCustomerResponse { .. }) => None,
|
||||
Ok(types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. }) => None,
|
||||
Ok(types::PaymentsResponseData::MultipleCaptureResponse { .. }) => None,
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,6 +99,7 @@ async fn should_sync_authorized_payment() {
|
||||
),
|
||||
encoded_data: None,
|
||||
capture_method: None,
|
||||
capture_sync_type: types::CaptureSyncType::SingleCaptureSync,
|
||||
connector_meta: None,
|
||||
mandate_id: None,
|
||||
}),
|
||||
@ -212,6 +213,7 @@ async fn should_sync_auto_captured_payment() {
|
||||
),
|
||||
encoded_data: None,
|
||||
capture_method: Some(enums::CaptureMethod::Automatic),
|
||||
capture_sync_type: types::CaptureSyncType::SingleCaptureSync,
|
||||
connector_meta: None,
|
||||
mandate_id: None,
|
||||
}),
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
ALTER TABLE captures ALTER COLUMN connector DROP NOT NULL;
|
||||
ALTER TABLE captures RENAME COLUMN connector_capture_id TO connector_transaction_id;
|
||||
ALTER TABLE captures DROP COLUMN IF EXISTS connector_response_reference_id;
|
||||
@ -0,0 +1,4 @@
|
||||
-- Your SQL goes here
|
||||
ALTER TABLE captures ALTER COLUMN connector SET NOT NULL;
|
||||
ALTER TABLE captures RENAME COLUMN connector_transaction_id TO connector_capture_id;
|
||||
ALTER TABLE captures add COLUMN IF NOT EXISTS connector_response_reference_id VARCHAR(128);
|
||||
@ -3571,6 +3571,86 @@
|
||||
"scheduled"
|
||||
]
|
||||
},
|
||||
"CaptureResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"capture_id",
|
||||
"status",
|
||||
"amount",
|
||||
"connector",
|
||||
"authorized_attempt_id",
|
||||
"capture_sequence"
|
||||
],
|
||||
"properties": {
|
||||
"capture_id": {
|
||||
"type": "string",
|
||||
"description": "unique identifier for the capture"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/components/schemas/CaptureStatus"
|
||||
},
|
||||
"amount": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "The capture amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.,"
|
||||
},
|
||||
"currency": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Currency"
|
||||
}
|
||||
],
|
||||
"nullable": true
|
||||
},
|
||||
"connector": {
|
||||
"type": "string",
|
||||
"description": "The connector used for the payment"
|
||||
},
|
||||
"authorized_attempt_id": {
|
||||
"type": "string",
|
||||
"description": "unique identifier for the parent attempt on which this capture is made"
|
||||
},
|
||||
"connector_capture_id": {
|
||||
"type": "string",
|
||||
"description": "A unique identifier for a capture provided by the connector",
|
||||
"nullable": true
|
||||
},
|
||||
"capture_sequence": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"description": "sequence number of this capture"
|
||||
},
|
||||
"error_message": {
|
||||
"type": "string",
|
||||
"description": "If there was an error while calling the connector the error message is received here",
|
||||
"nullable": true
|
||||
},
|
||||
"error_code": {
|
||||
"type": "string",
|
||||
"description": "If there was an error while calling the connectors the code is received here",
|
||||
"nullable": true
|
||||
},
|
||||
"error_reason": {
|
||||
"type": "string",
|
||||
"description": "If there was an error while calling the connectors the reason is received here",
|
||||
"nullable": true
|
||||
},
|
||||
"reference_id": {
|
||||
"type": "string",
|
||||
"description": "reference to the capture at connector side",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"CaptureStatus": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"started",
|
||||
"charged",
|
||||
"pending",
|
||||
"failed"
|
||||
]
|
||||
},
|
||||
"Card": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@ -5931,7 +6011,7 @@
|
||||
"description": "Will be used to expire client secret after certain amount of time to be supplied in seconds\n(900) for 15 mins",
|
||||
"example": 900,
|
||||
"nullable": true,
|
||||
"minimum": 0
|
||||
"minimum": 0.0
|
||||
},
|
||||
"organization_id": {
|
||||
"type": "string",
|
||||
@ -6225,7 +6305,7 @@
|
||||
"format": "int32",
|
||||
"description": "Will be used to expire client secret after certain amount of time to be supplied in seconds\n(900) for 15 mins",
|
||||
"nullable": true,
|
||||
"minimum": 0
|
||||
"minimum": 0.0
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -6946,13 +7026,13 @@
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "Timestamp at which session is requested",
|
||||
"minimum": 0
|
||||
"minimum": 0.0
|
||||
},
|
||||
"expires_at": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "Timestamp at which session expires",
|
||||
"minimum": 0
|
||||
"minimum": 0.0
|
||||
},
|
||||
"merchant_session_identifier": {
|
||||
"type": "string",
|
||||
@ -6986,7 +7066,7 @@
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"description": "The number of retries to get the session response",
|
||||
"minimum": 0
|
||||
"minimum": 0.0
|
||||
},
|
||||
"psp_id": {
|
||||
"type": "string",
|
||||
@ -7040,7 +7120,7 @@
|
||||
"format": "int32",
|
||||
"description": "The quantity of the product to be purchased",
|
||||
"example": 1,
|
||||
"minimum": 0
|
||||
"minimum": 0.0
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -7063,7 +7143,7 @@
|
||||
"format": "int32",
|
||||
"description": "The quantity of the product to be purchased",
|
||||
"example": 1,
|
||||
"minimum": 0
|
||||
"minimum": 0.0
|
||||
},
|
||||
"amount": {
|
||||
"type": "integer",
|
||||
@ -7375,7 +7455,6 @@
|
||||
"$ref": "#/components/schemas/AuthenticationType"
|
||||
}
|
||||
],
|
||||
"default": "three_ds",
|
||||
"nullable": true
|
||||
},
|
||||
"cancellation_reason": {
|
||||
@ -7564,7 +7643,7 @@
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"description": "The number of payments included in the list",
|
||||
"minimum": 0
|
||||
"minimum": 0.0
|
||||
},
|
||||
"data": {
|
||||
"type": "array",
|
||||
@ -8215,7 +8294,7 @@
|
||||
"description": "The payment amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.,",
|
||||
"example": 6540,
|
||||
"nullable": true,
|
||||
"minimum": 0
|
||||
"minimum": 0.0
|
||||
},
|
||||
"routing": {
|
||||
"allOf": [
|
||||
@ -8349,7 +8428,6 @@
|
||||
"$ref": "#/components/schemas/AuthenticationType"
|
||||
}
|
||||
],
|
||||
"default": "three_ds",
|
||||
"nullable": true
|
||||
},
|
||||
"payment_method_data": {
|
||||
@ -8550,7 +8628,7 @@
|
||||
"description": "The payment amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.,",
|
||||
"example": 6540,
|
||||
"nullable": true,
|
||||
"minimum": 0
|
||||
"minimum": 0.0
|
||||
},
|
||||
"routing": {
|
||||
"allOf": [
|
||||
@ -8684,7 +8762,6 @@
|
||||
"$ref": "#/components/schemas/AuthenticationType"
|
||||
}
|
||||
],
|
||||
"default": "three_ds",
|
||||
"nullable": true
|
||||
},
|
||||
"payment_method_data": {
|
||||
@ -8888,12 +8965,7 @@
|
||||
"maxLength": 255
|
||||
},
|
||||
"status": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/IntentStatus"
|
||||
}
|
||||
],
|
||||
"default": "requires_confirmation"
|
||||
"$ref": "#/components/schemas/IntentStatus"
|
||||
},
|
||||
"amount": {
|
||||
"type": "integer",
|
||||
@ -8907,7 +8979,7 @@
|
||||
"description": "The maximum amount that could be captured from the payment",
|
||||
"example": 6540,
|
||||
"nullable": true,
|
||||
"minimum": 100
|
||||
"minimum": 100.0
|
||||
},
|
||||
"amount_received": {
|
||||
"type": "integer",
|
||||
@ -8915,7 +8987,7 @@
|
||||
"description": "The amount which is already captured from the payment",
|
||||
"example": 6540,
|
||||
"nullable": true,
|
||||
"minimum": 100
|
||||
"minimum": 100.0
|
||||
},
|
||||
"connector": {
|
||||
"type": "string",
|
||||
@ -8976,6 +9048,14 @@
|
||||
"description": "List of attempts that happened on this intent",
|
||||
"nullable": true
|
||||
},
|
||||
"captures": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/CaptureResponse"
|
||||
},
|
||||
"description": "List of captures done on latest attempt",
|
||||
"nullable": true
|
||||
},
|
||||
"mandate_id": {
|
||||
"type": "string",
|
||||
"description": "A unique identifier to link the payment to a mandate, can be use instead of payment_method_data",
|
||||
@ -9095,7 +9175,6 @@
|
||||
"$ref": "#/components/schemas/AuthenticationType"
|
||||
}
|
||||
],
|
||||
"default": "three_ds",
|
||||
"nullable": true
|
||||
},
|
||||
"statement_descriptor_name": {
|
||||
@ -9987,7 +10066,12 @@
|
||||
"count": {
|
||||
"type": "integer",
|
||||
"description": "The number of refunds included in the list",
|
||||
"minimum": 0
|
||||
"minimum": 0.0
|
||||
},
|
||||
"total_count": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "The total number of refunds in the list"
|
||||
},
|
||||
"total_count": {
|
||||
"type": "integer",
|
||||
@ -10037,7 +10121,7 @@
|
||||
"description": "Total amount for which the refund is to be initiated. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc., If not provided, this will default to the full payment amount",
|
||||
"example": 6540,
|
||||
"nullable": true,
|
||||
"minimum": 100
|
||||
"minimum": 100.0
|
||||
},
|
||||
"reason": {
|
||||
"type": "string",
|
||||
@ -10052,7 +10136,6 @@
|
||||
"$ref": "#/components/schemas/RefundType"
|
||||
}
|
||||
],
|
||||
"default": "Instant",
|
||||
"nullable": true
|
||||
},
|
||||
"metadata": {
|
||||
|
||||
Reference in New Issue
Block a user