From e0630a74473b4b14613a5f11369e2ac2ef8aca12 Mon Sep 17 00:00:00 2001 From: Kashif Date: Thu, 26 Sep 2024 12:45:33 +0530 Subject: [PATCH] feat(charges): integrated PaymentSync for stripe connect (#4771) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .../src/router_request_types.rs | 1 + crates/router/src/connector/stripe.rs | 14 +++++ .../src/connector/stripe/transformers.rs | 19 ++++++- crates/router/src/core/payments/helpers.rs | 28 +++++++++- .../payments/operations/payment_create.rs | 5 ++ .../router/src/core/payments/transformers.rs | 52 +++++++++++++++++++ crates/router/src/core/refunds.rs | 17 +++++- .../router/src/core/refunds/transformers.rs | 32 ++++++++++++ crates/router/tests/connectors/bambora.rs | 2 + crates/router/tests/connectors/forte.rs | 1 + crates/router/tests/connectors/nexinets.rs | 1 + crates/router/tests/connectors/paypal.rs | 2 + crates/router/tests/connectors/utils.rs | 1 + crates/router/tests/connectors/zen.rs | 2 + 14 files changed, 172 insertions(+), 5 deletions(-) create mode 100644 crates/router/src/core/refunds/transformers.rs diff --git a/crates/hyperswitch_domain_models/src/router_request_types.rs b/crates/hyperswitch_domain_models/src/router_request_types.rs index 93ace230f1..2146b97d6e 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types.rs @@ -421,6 +421,7 @@ pub struct PaymentsSyncData { pub payment_method_type: Option, pub currency: storage_enums::Currency, pub payment_experience: Option, + pub charges: Option, pub amount: MinorUnit, pub integrity_object: Option, diff --git a/crates/router/src/connector/stripe.rs b/crates/router/src/connector/stripe.rs index 2af38249e9..1125c0af6d 100644 --- a/crates/router/src/connector/stripe.rs +++ b/crates/router/src/connector/stripe.rs @@ -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, 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)>, +) { + 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)] diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 23ac67c910..483c8c56ac 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -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(()) + } + } + } +} diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 4b931d68f1..5b171aa440 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -952,6 +952,11 @@ impl ValidateRequest> 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 = request .routing .clone() diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 6b84ceb975..e5cce9ad3a 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -1792,6 +1792,7 @@ impl TryFrom> for types::PaymentsAuthoriz } } +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "payment_v2")))] impl TryFrom> for types::PaymentsSyncData { type Error = error_stack::Report; @@ -1823,6 +1824,57 @@ impl TryFrom> 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 TryFrom> for types::PaymentsSyncData { + type Error = error_stack::Report; + + fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result { + 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, }) } diff --git a/crates/router/src/core/refunds.rs b/crates/router/src/core/refunds.rs index 7ee74ec5dd..7a26048a4d 100644 --- a/crates/router/src/core/refunds.rs +++ b/crates/router/src/core/refunds.rs @@ -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, + charges: Option, ) -> RouterResult { 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?; diff --git a/crates/router/src/core/refunds/transformers.rs b/crates/router/src/core/refunds/transformers.rs new file mode 100644 index 0000000000..e7cc0aa2ec --- /dev/null +++ b/crates/router/src/core/refunds/transformers.rs @@ -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; + fn foreign_try_from(item: (ChargeRefunds, pii::SecretSerdeValue)) -> Result { + 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, + )?, + }) + } +} diff --git a/crates/router/tests/connectors/bambora.rs b/crates/router/tests/connectors/bambora.rs index a4cb9b4c5e..f0478b561a 100644 --- a/crates/router/tests/connectors/bambora.rs +++ b/crates/router/tests/connectors/bambora.rs @@ -114,6 +114,7 @@ async fn should_sync_authorized_payment() { payment_experience: None, integrity_object: None, amount: MinorUnit::new(100), + ..Default::default() }), None, ) @@ -233,6 +234,7 @@ async fn should_sync_auto_captured_payment() { payment_experience: None, integrity_object: None, amount: MinorUnit::new(100), + ..Default::default() }), None, ) diff --git a/crates/router/tests/connectors/forte.rs b/crates/router/tests/connectors/forte.rs index 8447ab358e..fa084fc4b2 100644 --- a/crates/router/tests/connectors/forte.rs +++ b/crates/router/tests/connectors/forte.rs @@ -163,6 +163,7 @@ async fn should_sync_authorized_payment() { payment_experience: None, integrity_object: None, amount: MinorUnit::new(100), + ..Default::default() }), get_default_payment_info(), ) diff --git a/crates/router/tests/connectors/nexinets.rs b/crates/router/tests/connectors/nexinets.rs index 7909aa9c7f..4d30d0e08b 100644 --- a/crates/router/tests/connectors/nexinets.rs +++ b/crates/router/tests/connectors/nexinets.rs @@ -130,6 +130,7 @@ async fn should_sync_authorized_payment() { payment_experience: None, integrity_object: None, amount: MinorUnit::new(100), + ..Default::default() }), None, ) diff --git a/crates/router/tests/connectors/paypal.rs b/crates/router/tests/connectors/paypal.rs index 8de891ef09..5dfe186e11 100644 --- a/crates/router/tests/connectors/paypal.rs +++ b/crates/router/tests/connectors/paypal.rs @@ -147,6 +147,7 @@ async fn should_sync_authorized_payment() { payment_experience: None, integrity_object: None, amount: MinorUnit::new(100), + ..Default::default() }), get_default_payment_info(), ) @@ -348,6 +349,7 @@ async fn should_sync_auto_captured_payment() { payment_experience: None, amount: MinorUnit::new(100), integrity_object: None, + ..Default::default() }), get_default_payment_info(), ) diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 582c212c24..c68032369a 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -1008,6 +1008,7 @@ impl Default for PaymentSyncType { payment_experience: None, amount: MinorUnit::new(100), integrity_object: None, + ..Default::default() }; Self(data) } diff --git a/crates/router/tests/connectors/zen.rs b/crates/router/tests/connectors/zen.rs index 5c9cc2694c..20948a90c6 100644 --- a/crates/router/tests/connectors/zen.rs +++ b/crates/router/tests/connectors/zen.rs @@ -108,6 +108,7 @@ async fn should_sync_authorized_payment() { payment_experience: None, amount: MinorUnit::new(100), integrity_object: None, + ..Default::default() }), None, ) @@ -227,6 +228,7 @@ async fn should_sync_auto_captured_payment() { payment_experience: None, amount: MinorUnit::new(100), integrity_object: None, + ..Default::default() }), None, )