diff --git a/crates/hyperswitch_connectors/src/connectors/stripe.rs b/crates/hyperswitch_connectors/src/connectors/stripe.rs index e50207c987..9817d4e056 100644 --- a/crates/hyperswitch_connectors/src/connectors/stripe.rs +++ b/crates/hyperswitch_connectors/src/connectors/stripe.rs @@ -263,6 +263,23 @@ impl ConnectorIntegration CustomResult { + if matches!( + req.request.split_payments, + Some(common_types::payments::SplitPaymentsRequest::StripeSplitPayment(_)) + ) { + return Ok(format!( + "{}{}", + self.base_url(connectors), + "v1/payment_methods" + )); + } Ok(format!("{}{}", self.base_url(connectors), "v1/tokens")) } @@ -526,7 +570,6 @@ impl ConnectorIntegration fo connectors: &Connectors, ) -> CustomResult { let id = req.request.connector_transaction_id.as_str(); - Ok(format!( "{}{}/{}/capture", self.base_url(connectors), diff --git a/crates/hyperswitch_connectors/src/connectors/stripe/transformers.rs b/crates/hyperswitch_connectors/src/connectors/stripe/transformers.rs index e774710379..f79d5fed5e 100644 --- a/crates/hyperswitch_connectors/src/connectors/stripe/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/stripe/transformers.rs @@ -37,7 +37,7 @@ use hyperswitch_domain_models::{ }, }; use hyperswitch_interfaces::{consts, errors::ConnectorError}; -use masking::{ExposeInterface, ExposeOptionInterface, Mask, Maskable, PeekInterface, Secret}; +use masking::{ExposeInterface, Mask, Maskable, PeekInterface, Secret}; use serde::{Deserialize, Serialize}; use serde_json::Value; use time::PrimitiveDateTime; @@ -170,7 +170,7 @@ pub struct PaymentIntentRequest { pub meta_data: HashMap, pub return_url: String, pub confirm: bool, - pub payment_method: Option, + pub payment_method: Option>, pub customer: Option>, #[serde(flatten)] pub setup_mandate_details: Option, @@ -486,9 +486,28 @@ pub enum StripePaymentMethodData { BankTransfer(StripeBankTransferData), } +#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize)] +pub struct StripeBillingAddressCardToken { + #[serde(rename = "billing_details[name]")] + pub name: Option>, + #[serde(rename = "billing_details[email]")] + pub email: Option, + #[serde(rename = "billing_details[phone]")] + pub phone: Option>, + #[serde(rename = "billing_details[address][line1]")] + pub address_line1: Option>, + #[serde(rename = "billing_details[address][line2]")] + pub address_line2: Option>, + #[serde(rename = "billing_details[address][state]")] + pub state: Option>, + #[serde(rename = "billing_details[address][city]")] + pub city: Option, +} // Struct to call the Stripe tokens API to create a PSP token for the card details provided #[derive(Debug, Eq, PartialEq, Serialize)] pub struct StripeCardToken { + #[serde(rename = "type")] + pub payment_method_type: Option, #[serde(rename = "card[number]")] pub token_card_number: cards::CardNumber, #[serde(rename = "card[exp_month]")] @@ -497,6 +516,8 @@ pub struct StripeCardToken { pub token_card_exp_year: Secret, #[serde(rename = "card[cvc]")] pub token_card_cvc: Secret, + #[serde(flatten)] + pub billing: StripeBillingAddressCardToken, } #[derive(Debug, Eq, PartialEq, Serialize)] @@ -1642,80 +1663,51 @@ impl TryFrom<(&PaymentsAuthorizeRouterData, MinorUnit)> for PaymentIntentRequest type Error = error_stack::Report; fn try_from(data: (&PaymentsAuthorizeRouterData, MinorUnit)) -> Result { let item = data.0; + + let payment_method_token = match &item.request.split_payments { + Some(common_types::payments::SplitPaymentsRequest::StripeSplitPayment(_)) => { + match item.payment_method_token.clone() { + Some(PaymentMethodToken::Token(secret)) => Some(secret), + _ => None, + } + } + _ => None, + }; + let amount = data.1; let order_id = item.connector_request_reference_id.clone(); - let shipping_address = match item.get_optional_shipping() { - Some(shipping_details) => { - let shipping_address = shipping_details.address.as_ref(); - shipping_address.and_then(|shipping_detail| { - shipping_detail - .first_name - .as_ref() - .map(|first_name| StripeShippingAddress { - city: shipping_address.and_then(|a| a.city.clone()), - country: shipping_address.and_then(|a| a.country), - line1: shipping_address.and_then(|a| a.line1.clone()), - line2: shipping_address.and_then(|a| a.line2.clone()), - zip: shipping_address.and_then(|a| a.zip.clone()), - state: shipping_address.and_then(|a| a.state.clone()), - name: format!( - "{} {}", - first_name.clone().expose(), - shipping_detail - .last_name - .clone() - .expose_option() - .unwrap_or_default() - ) - .into(), - phone: shipping_details.phone.as_ref().map(|p| { - format!( - "{}{}", - p.country_code.clone().unwrap_or_default(), - p.number.clone().expose_option().unwrap_or_default() - ) - .into() - }), - }) - }) - } - None => None, + let shipping_address = if payment_method_token.is_some() { + None + } else { + Some(StripeShippingAddress { + city: item.get_optional_shipping_city(), + country: item.get_optional_shipping_country(), + line1: item.get_optional_shipping_line1(), + line2: item.get_optional_shipping_line2(), + zip: item.get_optional_shipping_zip(), + state: item.get_optional_shipping_state(), + name: item.get_optional_shipping_full_name(), + phone: item.get_optional_shipping_phone_number(), + }) }; - let billing_address = match item.get_optional_billing() { - Some(billing_details) => { - let billing_address = billing_details.address.as_ref(); - StripeBillingAddress { - city: billing_address.and_then(|a| a.city.clone()), - country: billing_address.and_then(|a| a.country), - address_line1: billing_address.and_then(|a| a.line1.clone()), - address_line2: billing_address.and_then(|a| a.line2.clone()), - zip_code: billing_address.and_then(|a| a.zip.clone()), - state: billing_address.and_then(|a| a.state.clone()), - name: billing_address.and_then(|a| { - a.first_name.as_ref().map(|first_name| { - format!( - "{} {}", - first_name.clone().expose(), - a.last_name.clone().expose_option().unwrap_or_default() - ) - .into() - }) - }), - email: billing_details.email.clone(), - phone: billing_details.phone.as_ref().map(|p| { - format!( - "{}{}", - p.country_code.clone().unwrap_or_default(), - p.number.clone().expose_option().unwrap_or_default() - ) - .into() - }), - } - } - None => StripeBillingAddress::default(), + let billing_address = if payment_method_token.is_some() { + None + } else { + Some(StripeBillingAddress { + city: item.get_optional_billing_city(), + country: item.get_optional_billing_country(), + address_line1: item.get_optional_billing_line1(), + address_line2: item.get_optional_billing_line2(), + zip_code: item.get_optional_billing_zip(), + state: item.get_optional_billing_state(), + name: item.get_optional_billing_full_name(), + email: item.get_optional_billing_email(), + phone: item.get_optional_billing_phone_number(), + }) }; + let mut payment_method_options = None; let ( @@ -1724,7 +1716,9 @@ impl TryFrom<(&PaymentsAuthorizeRouterData, MinorUnit)> for PaymentIntentRequest billing_address, payment_method_types, setup_future_usage, - ) = { + ) = if payment_method_token.is_some() { + (None, None, StripeBillingAddress::default(), None, None) + } else { match item .request .mandate_id @@ -1812,7 +1806,11 @@ impl TryFrom<(&PaymentsAuthorizeRouterData, MinorUnit)> for PaymentIntentRequest &item.request, ), ), - billing_address, + billing_address.ok_or_else(|| { + ConnectorError::MissingRequiredField { + field_name: "billing_address", + } + })?, )?; validate_shipping_address_against_payment_method( @@ -1831,38 +1829,42 @@ impl TryFrom<(&PaymentsAuthorizeRouterData, MinorUnit)> for PaymentIntentRequest } }; - payment_data = match item.request.payment_method_data { - PaymentMethodData::Wallet(WalletData::ApplePay(_)) => { - let payment_method_token = item - .payment_method_token - .to_owned() - .get_required_value("payment_token") - .change_context(ConnectorError::InvalidWalletToken { - wallet_name: "Apple Pay".to_string(), - })?; - - let payment_method_token = match payment_method_token { - PaymentMethodToken::Token(payment_method_token) => payment_method_token, - PaymentMethodToken::ApplePayDecrypt(_) => { - Err(ConnectorError::InvalidWalletToken { + if payment_method_token.is_none() { + payment_data = match item.request.payment_method_data { + PaymentMethodData::Wallet(WalletData::ApplePay(_)) => { + let payment_method_token = item + .payment_method_token + .to_owned() + .get_required_value("payment_token") + .change_context(ConnectorError::InvalidWalletToken { wallet_name: "Apple Pay".to_string(), - })? - } - PaymentMethodToken::PazeDecrypt(_) => { - Err(crate::unimplemented_payment_method!("Paze", "Stripe"))? - } - PaymentMethodToken::GooglePayDecrypt(_) => { - Err(crate::unimplemented_payment_method!("Google Pay", "Stripe"))? - } - }; - Some(StripePaymentMethodData::Wallet( - StripeWallet::ApplepayPayment(ApplepayPayment { - token: payment_method_token, - payment_method_types: StripePaymentMethodType::Card, - }), - )) + })?; + + let payment_method_token = match payment_method_token { + PaymentMethodToken::Token(payment_method_token) => payment_method_token, + PaymentMethodToken::ApplePayDecrypt(_) => { + Err(ConnectorError::InvalidWalletToken { + wallet_name: "Apple Pay".to_string(), + })? + } + PaymentMethodToken::PazeDecrypt(_) => { + Err(crate::unimplemented_payment_method!("Paze", "Stripe"))? + } + PaymentMethodToken::GooglePayDecrypt(_) => { + Err(crate::unimplemented_payment_method!("Google Pay", "Stripe"))? + } + }; + Some(StripePaymentMethodData::Wallet( + StripeWallet::ApplepayPayment(ApplepayPayment { + token: payment_method_token, + payment_method_types: StripePaymentMethodType::Card, + }), + )) + } + _ => payment_data, } - _ => payment_data, + } else { + payment_data = None }; let setup_mandate_details = item @@ -1932,7 +1934,7 @@ impl TryFrom<(&PaymentsAuthorizeRouterData, MinorUnit)> for PaymentIntentRequest // We pass browser_info only when payment_data exists. // Hence, we're pass Null during recurring payments as payment_method_data[type] is not passed - let browser_info = if payment_data.is_some() { + let browser_info = if payment_data.is_some() && payment_method_token.is_none() { item.request .browser_info .clone() @@ -1941,31 +1943,40 @@ impl TryFrom<(&PaymentsAuthorizeRouterData, MinorUnit)> for PaymentIntentRequest None }; - let (charges, customer) = match &item.request.split_payments { + let charges = match &item.request.split_payments { Some(common_types::payments::SplitPaymentsRequest::StripeSplitPayment( stripe_split_payment, - )) => { - let charges = match &stripe_split_payment.charge_type { - PaymentChargeType::Stripe(charge_type) => match charge_type { - StripeChargeType::Direct => Some(IntentCharges { - application_fee_amount: stripe_split_payment.application_fees, - destination_account_id: None, - }), - StripeChargeType::Destination => Some(IntentCharges { - application_fee_amount: stripe_split_payment.application_fees, - destination_account_id: Some( - stripe_split_payment.transfer_account_id.clone(), - ), - }), - }, - }; - (charges, None) - } + )) => match &stripe_split_payment.charge_type { + PaymentChargeType::Stripe(charge_type) => match charge_type { + StripeChargeType::Direct => Some(IntentCharges { + application_fee_amount: stripe_split_payment.application_fees, + destination_account_id: None, + }), + StripeChargeType::Destination => Some(IntentCharges { + application_fee_amount: stripe_split_payment.application_fees, + destination_account_id: Some( + stripe_split_payment.transfer_account_id.clone(), + ), + }), + }, + }, Some(common_types::payments::SplitPaymentsRequest::AdyenSplitPayment(_)) | Some(common_types::payments::SplitPaymentsRequest::XenditSplitPayment(_)) - | None => (None, item.connector_customer.to_owned().map(Secret::new)), + | None => None, }; + let pm = match (payment_method, payment_method_token.clone()) { + (Some(method), _) => Some(Secret::new(method)), + (None, Some(token)) => Some(token), + (None, None) => None, + }; + + let customer_id = item.connector_customer.clone().ok_or_else(|| { + ConnectorError::MissingRequiredField { + field_name: "connector_customer", + } + })?; + Ok(Self { amount, //hopefully we don't loose some cents here currency: item.request.currency.to_string(), //we need to copy the value and not transfer ownership @@ -1984,8 +1995,8 @@ impl TryFrom<(&PaymentsAuthorizeRouterData, MinorUnit)> for PaymentIntentRequest capture_method: StripeCaptureMethod::from(item.request.capture_method), payment_data, payment_method_options, - payment_method, - customer, + payment_method: pm, + customer: Some(Secret::new(customer_id)), setup_mandate_details, off_session: item.request.off_session, setup_future_usage, @@ -2076,14 +2087,26 @@ impl TryFrom<&SetupMandateRouterData> for SetupIntentRequest { impl TryFrom<&TokenizationRouterData> for TokenRequest { type Error = error_stack::Report; fn try_from(item: &TokenizationRouterData) -> Result { + let billing_address = StripeBillingAddressCardToken { + name: item.get_optional_billing_full_name(), + email: item.get_optional_billing_email(), + phone: item.get_optional_billing_phone_number(), + address_line1: item.get_optional_billing_line1(), + address_line2: item.get_optional_billing_line2(), + city: item.get_optional_billing_city(), + state: item.get_optional_billing_state(), + }; + // Card flow for tokenization is handled separately because of API contact difference let request_payment_data = match &item.request.payment_method_data { PaymentMethodData::Card(card_details) => { StripePaymentMethodData::CardToken(StripeCardToken { + payment_method_type: Some(StripePaymentMethodType::Card), token_card_number: card_details.card_number.clone(), token_card_exp_month: card_details.card_exp_month.clone(), token_card_exp_year: card_details.card_exp_year.clone(), token_card_cvc: card_details.card_cvc.clone(), + billing: billing_address, }) } _ => { @@ -3327,7 +3350,7 @@ pub struct StripeShippingAddress { #[serde(rename = "shipping[address][state]")] pub state: Option>, #[serde(rename = "shipping[name]")] - pub name: Secret, + pub name: Option>, #[serde(rename = "shipping[phone]")] pub phone: Option>, } @@ -3593,10 +3616,9 @@ impl TryFrom, ) -> Result { + let token = item.response.id.clone().expose(); Ok(Self { - response: Ok(PaymentsResponseData::TokenizationResponse { - token: item.response.id.expose(), - }), + response: Ok(PaymentsResponseData::TokenizationResponse { token }), ..item.data }) } @@ -4392,7 +4414,7 @@ mod test_validate_shipping_address_against_payment_method { zip: Option, ) -> StripeShippingAddress { StripeShippingAddress { - name: Secret::new(name), + name: Some(Secret::new(name)), line1: line1.map(Secret::new), country, zip: zip.map(Secret::new), diff --git a/crates/hyperswitch_domain_models/src/router_request_types.rs b/crates/hyperswitch_domain_models/src/router_request_types.rs index 8250f919c5..da2ce5c933 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types.rs @@ -177,6 +177,7 @@ pub struct ConnectorCustomerData { pub name: Option>, pub preprocessing_id: Option, pub payment_method_data: Option, + pub split_payments: Option, } impl TryFrom for ConnectorCustomerData { @@ -189,6 +190,7 @@ impl TryFrom for ConnectorCustomerData { phone: None, name: None, preprocessing_id: None, + split_payments: None, }) } } @@ -213,6 +215,7 @@ impl phone: None, name: data.request.customer_name.clone(), preprocessing_id: data.preprocessing_id.clone(), + split_payments: data.request.split_payments.clone(), }) } } @@ -236,6 +239,7 @@ impl TryFrom<&RouterData, pub currency: storage_enums::Currency, pub amount: Option, + pub split_payments: Option, } impl TryFrom for PaymentMethodTokenizationData { @@ -257,6 +262,7 @@ impl TryFrom for PaymentMethodTokenizationData { browser_info: None, currency: data.currency, amount: data.amount, + split_payments: None, }) } } @@ -271,6 +277,7 @@ impl From<&RouterData for PaymentMethodTokenizationData { browser_info: data.browser_info, currency: data.currency, amount: Some(data.amount), + split_payments: data.split_payments.clone(), }) } } @@ -302,6 +310,7 @@ impl TryFrom for PaymentMethodTokenizationData { browser_info: data.browser_info, currency: data.currency, amount: Some(data.amount), + split_payments: None, }) } } diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index d17f0cd234..f41b83dad7 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -3227,6 +3227,7 @@ async fn create_single_use_tokenization_flow( browser_info: None, currency: api_models::enums::Currency::default(), amount: None, + split_payments: None, }; let payment_method_session_address = types::PaymentAddress::new( diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 2fd7e9fa4e..1b1f842239 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -5241,6 +5241,7 @@ async fn decide_payment_method_tokenize_action( state: &SessionState, connector_name: &str, payment_method: storage::enums::PaymentMethod, + payment_intent_data: payments::PaymentIntent, pm_parent_token: Option<&str>, is_connector_tokenization_enabled: bool, apple_pay_flow: Option, @@ -5276,6 +5277,11 @@ async fn decide_payment_method_tokenize_action( } } } + } else if matches!( + payment_intent_data.split_payments, + Some(common_types::payments::SplitPaymentsRequest::StripeSplitPayment(_)) + ) { + Ok(TokenizationAction::TokenizeInConnector) } else { match pm_parent_token { None => Ok(match (is_connector_tokenization_enabled, apple_pay_flow) { @@ -5445,6 +5451,7 @@ where state, &connector, payment_method, + payment_data.get_payment_intent().clone(), payment_data.get_token(), is_connector_tokenization_enabled, apple_pay_flow, diff --git a/crates/router/tests/connectors/square.rs b/crates/router/tests/connectors/square.rs index 8f5fa4a32c..81ea811227 100644 --- a/crates/router/tests/connectors/square.rs +++ b/crates/router/tests/connectors/square.rs @@ -67,6 +67,7 @@ fn token_details() -> Option { browser_info: None, amount: None, currency: enums::Currency::USD, + split_payments: None, }) } @@ -440,6 +441,7 @@ async fn should_fail_payment_for_incorrect_cvc() { browser_info: None, amount: None, currency: enums::Currency::USD, + split_payments: None, }), get_default_payment_info(None), ) @@ -471,6 +473,7 @@ async fn should_fail_payment_for_invalid_exp_month() { browser_info: None, amount: None, currency: enums::Currency::USD, + split_payments: None, }), get_default_payment_info(None), ) @@ -502,6 +505,7 @@ async fn should_fail_payment_for_incorrect_expiry_year() { browser_info: None, amount: None, currency: enums::Currency::USD, + split_payments: None, }), get_default_payment_info(None), ) diff --git a/crates/router/tests/connectors/stax.rs b/crates/router/tests/connectors/stax.rs index 4bc76d1290..93c2394354 100644 --- a/crates/router/tests/connectors/stax.rs +++ b/crates/router/tests/connectors/stax.rs @@ -72,6 +72,7 @@ fn token_details() -> Option { browser_info: None, amount: None, currency: enums::Currency::USD, + split_payments: None, }) } @@ -482,6 +483,7 @@ async fn should_fail_payment_for_incorrect_cvc() { browser_info: None, amount: None, currency: enums::Currency::USD, + split_payments: None, }), get_default_payment_info(connector_customer_id, None), ) @@ -520,6 +522,7 @@ async fn should_fail_payment_for_invalid_exp_month() { browser_info: None, amount: None, currency: enums::Currency::USD, + split_payments: None, }), get_default_payment_info(connector_customer_id, None), ) @@ -558,6 +561,7 @@ async fn should_fail_payment_for_incorrect_expiry_year() { browser_info: None, amount: None, currency: enums::Currency::USD, + split_payments: None, }), get_default_payment_info(connector_customer_id, None), ) diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 3cfd3b277e..49ead71cb2 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -1107,6 +1107,7 @@ impl Default for CustomerType { phone: None, name: None, preprocessing_id: None, + split_payments: None, }; Self(data) } @@ -1119,6 +1120,7 @@ impl Default for TokenType { browser_info: None, amount: Some(100), currency: enums::Currency::USD, + split_payments: None, }; Self(data) }