feat(core): add psync for multiple partial captures (#1934)

This commit is contained in:
Hrithikesh
2023-08-23 10:13:54 +05:30
committed by GitHub
parent 1b346fcf56
commit 5657ad6933
30 changed files with 1219 additions and 1020 deletions

1067
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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