feat(charges): integrated PaymentSync for stripe connect (#4771)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Kashif
2024-09-26 12:45:33 +05:30
committed by GitHub
parent 962b9978d6
commit e0630a7447
14 changed files with 172 additions and 5 deletions

View File

@ -761,6 +761,13 @@ impl
)];
let mut api_key = self.get_auth_header(&req.connector_auth_type)?;
header.append(&mut api_key);
req.request.charges.as_ref().map(|charges| {
transformers::transform_headers_for_connect_platform(
charges.charge_type.clone(),
charges.transfer_account_id.clone(),
&mut header,
)
});
Ok(header)
}
@ -1618,6 +1625,13 @@ impl services::ConnectorIntegration<api::RSync, types::RefundsData, types::Refun
)];
let mut api_key = self.get_auth_header(&req.connector_auth_type)?;
header.append(&mut api_key);
req.request.charges.as_ref().map(|charges| {
transformers::transform_headers_for_connect_platform(
charges.charge_type.clone(),
charges.transfer_account_id.clone(),
&mut header,
)
});
Ok(header)
}

View File

@ -11,7 +11,7 @@ use common_utils::{
use diesel_models::enums as storage_enums;
use error_stack::ResultExt;
use hyperswitch_domain_models::mandates::AcceptanceType;
use masking::{ExposeInterface, ExposeOptionInterface, PeekInterface, Secret};
use masking::{ExposeInterface, ExposeOptionInterface, Mask, PeekInterface, Secret};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use time::PrimitiveDateTime;
@ -27,7 +27,7 @@ use crate::{
},
consts,
core::errors,
services,
headers, services,
types::{
self, api, domain,
storage::enums,
@ -4025,6 +4025,21 @@ impl ForeignTryFrom<(&Option<ErrorDetails>, u16, String)> for types::PaymentsRes
}
}
pub(super) fn transform_headers_for_connect_platform(
charge_type: api::enums::PaymentChargeType,
transfer_account_id: String,
header: &mut Vec<(String, services::request::Maskable<String>)>,
) {
if let api::enums::PaymentChargeType::Stripe(api::enums::StripeChargeType::Direct) = charge_type
{
let mut customer_account_header = vec![(
headers::STRIPE_COMPATIBLE_CONNECT_ACCOUNT.to_string(),
transfer_account_id.into_masked(),
)];
header.append(&mut customer_account_header);
}
}
#[cfg(test)]
mod test_validate_shipping_address_against_payment_method {
#![allow(clippy::unwrap_used)]

View File

@ -5,7 +5,7 @@ use api_models::customers::CustomerRequestWithEmail;
use api_models::{
mandates::RecurringDetails,
payments::{
additional_info as payment_additional_types, AddressDetailsWithPhone,
additional_info as payment_additional_types, AddressDetailsWithPhone, PaymentChargeRequest,
RequestSurchargeDetails,
},
};
@ -5741,3 +5741,29 @@ pub async fn validate_merchant_connector_ids_in_connector_mandate_details(
}
Ok(())
}
pub fn validate_platform_fees_for_marketplace(
amount: api::Amount,
charges: &PaymentChargeRequest,
) -> Result<(), errors::ApiErrorResponse> {
match amount {
api::Amount::Zero => {
if charges.fees.get_amount_as_i64() != 0 {
Err(errors::ApiErrorResponse::InvalidDataValue {
field_name: "charges.fees",
})
} else {
Ok(())
}
}
api::Amount::Value(amount) => {
if charges.fees.get_amount_as_i64() > amount.into() {
Err(errors::ApiErrorResponse::InvalidDataValue {
field_name: "charges.fees",
})
} else {
Ok(())
}
}
}
}

View File

@ -952,6 +952,11 @@ impl<F: Send + Clone> ValidateRequest<F, api::PaymentsRequest, PaymentData<F>> f
)?;
}
if let Some(charges) = &request.charges {
let amount = request.amount.get_required_value("amount")?;
helpers::validate_platform_fees_for_marketplace(amount, charges)?;
};
let _request_straight_through: Option<api::routing::StraightThroughAlgorithm> = request
.routing
.clone()

View File

@ -1792,6 +1792,7 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsAuthoriz
}
}
#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "payment_v2")))]
impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsSyncData {
type Error = error_stack::Report<errors::ApiErrorResponse>;
@ -1823,6 +1824,57 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsSyncData
},
payment_method_type: payment_data.payment_attempt.payment_method_type,
currency: payment_data.currency,
charges: payment_data
.payment_intent
.charges
.as_ref()
.map(|charges| {
charges
.peek()
.clone()
.parse_value("PaymentCharges")
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to parse charges in to PaymentCharges")
})
.transpose()?,
payment_experience: payment_data.payment_attempt.payment_experience,
})
}
}
#[cfg(all(feature = "v2", feature = "payment_v2"))]
impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsSyncData {
type Error = error_stack::Report<errors::ApiErrorResponse>;
fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result<Self, Self::Error> {
let payment_data = additional_data.payment_data;
let amount = payment_data
.surcharge_details
.as_ref()
.map(|surcharge_details| surcharge_details.final_amount)
.unwrap_or(payment_data.amount.into());
Ok(Self {
amount,
integrity_object: None,
mandate_id: payment_data.mandate_id.clone(),
connector_transaction_id: match payment_data.payment_attempt.connector_transaction_id {
Some(connector_txn_id) => {
types::ResponseId::ConnectorTransactionId(connector_txn_id)
}
None => types::ResponseId::NoResponseId,
},
encoded_data: payment_data.payment_attempt.encoded_data,
capture_method: payment_data.payment_attempt.capture_method,
connector_meta: payment_data.payment_attempt.connector_metadata,
sync_type: match payment_data.multiple_capture_data {
Some(multiple_capture_data) => types::SyncRequestType::MultipleCaptureSync(
multiple_capture_data.get_pending_connector_capture_ids(),
),
None => types::SyncRequestType::SinglePaymentSync,
},
payment_method_type: payment_data.payment_attempt.payment_method_type,
currency: payment_data.currency,
charges: None,
payment_experience: payment_data.payment_attempt.payment_experience,
})
}

View File

@ -1,3 +1,4 @@
pub mod transformers;
pub mod validator;
#[cfg(feature = "olap")]
@ -34,7 +35,7 @@ use crate::{
api::{self, refunds},
domain,
storage::{self, enums},
transformers::{ForeignFrom, ForeignInto},
transformers::{ForeignFrom, ForeignInto, ForeignTryFrom},
ChargeRefunds,
},
utils::{self, OptionExt},
@ -444,6 +445,15 @@ pub async fn refund_retrieve_core(
.await
.transpose()?;
let charges_req = payment_intent
.charges
.clone()
.zip(refund.charges.clone())
.map(|(charges, refund_charges)| {
ForeignTryFrom::foreign_try_from((refund_charges, charges))
})
.transpose()?;
let response = if should_call_refund(&refund, request.force_sync.unwrap_or(false)) {
sync_refund_with_gateway(
&state,
@ -453,6 +463,7 @@ pub async fn refund_retrieve_core(
&payment_intent,
&refund,
creds_identifier,
charges_req,
)
.await
} else {
@ -479,6 +490,7 @@ fn should_call_refund(refund: &diesel_models::refund::Refund, force_sync: bool)
predicate1 && predicate2
}
#[allow(clippy::too_many_arguments)]
#[instrument(skip_all)]
pub async fn sync_refund_with_gateway(
state: &SessionState,
@ -488,6 +500,7 @@ pub async fn sync_refund_with_gateway(
payment_intent: &storage::PaymentIntent,
refund: &storage::Refund,
creds_identifier: Option<String>,
charges: Option<ChargeRefunds>,
) -> RouterResult<storage::Refund> {
let connector_id = refund.connector.to_string();
let connector: api::ConnectorData = api::ConnectorData::get_connector_by_name(
@ -513,7 +526,7 @@ pub async fn sync_refund_with_gateway(
payment_attempt,
refund,
creds_identifier.clone(),
None,
charges,
)
.await?;

View File

@ -0,0 +1,32 @@
use common_utils::{ext_traits::ValueExt, pii, types::ChargeRefunds};
use error_stack::{Report, ResultExt};
use hyperswitch_domain_models::router_request_types;
use masking::PeekInterface;
use super::validator;
use crate::{core::errors, types::transformers::ForeignTryFrom};
impl ForeignTryFrom<(ChargeRefunds, pii::SecretSerdeValue)>
for router_request_types::ChargeRefunds
{
type Error = Report<errors::ApiErrorResponse>;
fn foreign_try_from(item: (ChargeRefunds, pii::SecretSerdeValue)) -> Result<Self, Self::Error> {
let (refund_charges, charges) = item;
let payment_charges: router_request_types::PaymentCharges = charges
.peek()
.clone()
.parse_value("PaymentCharges")
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to parse charges into PaymentCharges")?;
Ok(Self {
charge_id: refund_charges.charge_id.clone(),
charge_type: payment_charges.charge_type.clone(),
transfer_account_id: payment_charges.transfer_account_id,
options: validator::validate_charge_refund(
&refund_charges,
&payment_charges.charge_type,
)?,
})
}
}