diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index f0ce3839fc..5f9618738b 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -2939,13 +2939,17 @@ pub enum NextActionType { #[serde(tag = "type", rename_all = "snake_case")] pub enum NextActionData { /// Contains the url for redirection flow - RedirectToUrl { redirect_to_url: String }, + RedirectToUrl { + redirect_to_url: String, + }, /// Informs the next steps for bank transfer and also contains the charges details (ex: amount received, amount charged etc) DisplayBankTransferInformation { bank_transfer_steps_and_charges_details: BankTransferNextStepsData, }, /// Contains third party sdk session token response - ThirdPartySdkSessionToken { session_token: Option }, + ThirdPartySdkSessionToken { + session_token: Option, + }, /// Contains url for Qr code image, this qr code has to be shown in sdk QrCodeInformation { #[schema(value_type = String)] @@ -2967,7 +2971,12 @@ pub enum NextActionData { display_to_timestamp: Option, }, /// Contains the information regarding three_ds_method_data submission, three_ds authentication, and authorization flows - ThreeDsInvoke { three_ds_data: ThreeDsData }, + ThreeDsInvoke { + three_ds_data: ThreeDsData, + }, + InvokeSdkClient { + next_action_data: SdkNextActionData, + }, } #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, ToSchema)] @@ -3030,6 +3039,12 @@ pub enum QrCodeInformation { }, } +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq, ToSchema)] +#[serde(rename_all = "snake_case")] +pub struct SdkNextActionData { + pub next_action: NextActionCall, +} + #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] pub struct BankTransferNextStepsData { /// The instructions for performing a bank transfer @@ -4030,6 +4045,17 @@ pub struct GpaySessionTokenData { pub data: GpayMetaData, } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct PaypalSdkMetaData { + pub client_id: String, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct PaypalSdkSessionTokenData { + #[serde(rename = "paypal_sdk")] + pub data: PaypalSdkMetaData, +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "camelCase")] pub struct ApplepaySessionRequest { @@ -4207,8 +4233,12 @@ pub struct KlarnaSessionTokenResponse { #[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, ToSchema)] #[serde(rename_all = "lowercase")] pub struct PaypalSessionTokenResponse { + /// Name of the connector + pub connector: String, /// The session token for PayPal pub session_token: String, + /// The next action for the sdk (ex: calling confirm or sync call) + pub sdk_next_action: SdkNextAction, } #[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, ToSchema)] @@ -4238,13 +4268,15 @@ pub struct SdkNextAction { pub next_action: NextActionCall, } -#[derive(Debug, Eq, PartialEq, serde::Serialize, Clone, ToSchema)] +#[derive(Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, Clone, ToSchema)] #[serde(rename_all = "snake_case")] pub enum NextActionCall { /// The next action call is confirm Confirm, /// The next action call is sync Sync, + /// The next action call is Complete Authorize + CompleteAuthorize, } #[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, ToSchema)] diff --git a/crates/connector_configs/src/common_config.rs b/crates/connector_configs/src/common_config.rs index b11a74a3ca..fff210b6e4 100644 --- a/crates/connector_configs/src/common_config.rs +++ b/crates/connector_configs/src/common_config.rs @@ -54,6 +54,12 @@ pub enum GooglePayData { Zen(ZenGooglePay), } +#[serde_with::skip_serializing_none] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct PaypalSdkData { + pub client_id: Option, +} + #[serde_with::skip_serializing_none] #[derive(Debug, Deserialize, serde::Serialize, Clone)] #[serde(untagged)] @@ -79,6 +85,7 @@ pub struct ApiModelMetaData { pub terminal_id: Option, pub merchant_id: Option, pub google_pay: Option, + pub paypal_sdk: Option, pub apple_pay: Option, pub apple_pay_combined: Option, pub endpoint_prefix: Option, @@ -180,6 +187,7 @@ pub struct DashboardMetaData { pub terminal_id: Option, pub merchant_id: Option, pub google_pay: Option, + pub paypal_sdk: Option, pub apple_pay: Option, pub apple_pay_combined: Option, pub endpoint_prefix: Option, diff --git a/crates/connector_configs/src/connector.rs b/crates/connector_configs/src/connector.rs index e1d55e3938..7118c87b09 100644 --- a/crates/connector_configs/src/connector.rs +++ b/crates/connector_configs/src/connector.rs @@ -10,7 +10,7 @@ use serde::Deserialize; #[cfg(any(feature = "sandbox", feature = "development", feature = "production"))] use toml; -use crate::common_config::{CardProvider, GooglePayData, Provider, ZenApplePay}; +use crate::common_config::{CardProvider, GooglePayData, PaypalSdkData, Provider, ZenApplePay}; #[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Classic { @@ -79,6 +79,7 @@ pub struct ConfigMetadata { pub account_name: Option, pub terminal_id: Option, pub google_pay: Option, + pub paypal_sdk: Option, pub apple_pay: Option, pub merchant_id: Option, pub endpoint_prefix: Option, diff --git a/crates/connector_configs/src/response_modifier.rs b/crates/connector_configs/src/response_modifier.rs index 8f7da58c4d..ebf97d8316 100644 --- a/crates/connector_configs/src/response_modifier.rs +++ b/crates/connector_configs/src/response_modifier.rs @@ -309,6 +309,7 @@ impl From for DashboardMetaData { terminal_id: api_model.terminal_id, merchant_id: api_model.merchant_id, google_pay: get_google_pay_metadata_response(api_model.google_pay), + paypal_sdk: api_model.paypal_sdk, apple_pay: api_model.apple_pay, apple_pay_combined: api_model.apple_pay_combined, endpoint_prefix: api_model.endpoint_prefix, diff --git a/crates/connector_configs/src/transformer.rs b/crates/connector_configs/src/transformer.rs index 01332c9d29..ae4b020676 100644 --- a/crates/connector_configs/src/transformer.rs +++ b/crates/connector_configs/src/transformer.rs @@ -46,7 +46,9 @@ impl DashboardRequestPayload { (Connector::Zen, GooglePay) | (Connector::Zen, ApplePay) => { Some(api_models::enums::PaymentExperience::RedirectToUrl) } - (Connector::Braintree, Paypal) | (Connector::Klarna, Klarna) => { + (Connector::Paypal, Paypal) + | (Connector::Braintree, Paypal) + | (Connector::Klarna, Klarna) => { Some(api_models::enums::PaymentExperience::InvokeSdkClient) } (Connector::Globepay, AliPay) @@ -194,6 +196,7 @@ impl DashboardRequestPayload { three_ds_requestor_name: None, three_ds_requestor_id: None, pull_mechanism_for_external_3ds_enabled: None, + paypal_sdk: None, }; let meta_data = match request.metadata { Some(data) => data, @@ -205,6 +208,7 @@ impl DashboardRequestPayload { let merchant_id = meta_data.merchant_id.clone(); let terminal_id = meta_data.terminal_id.clone(); let endpoint_prefix = meta_data.endpoint_prefix.clone(); + let paypal_sdk = meta_data.paypal_sdk; let apple_pay = meta_data.apple_pay; let apple_pay_combined = meta_data.apple_pay_combined; let merchant_config_currency = meta_data.merchant_config_currency; @@ -228,6 +232,7 @@ impl DashboardRequestPayload { merchant_config_currency, apple_pay_combined, endpoint_prefix, + paypal_sdk, mcc, merchant_country_code, merchant_name, diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index d9e7ec58f7..fcccf9c7e2 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -1645,6 +1645,8 @@ api_key="Client Secret" key1="Client ID" [paypal.connector_webhook_details] merchant_secret="Source verification key" +[paypal.metadata.paypal_sdk] +client_id="Client ID" [paypal_payout] [[paypal.wallet]] diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml index 202bce64c1..96fba850e7 100644 --- a/crates/connector_configs/toml/production.toml +++ b/crates/connector_configs/toml/production.toml @@ -1265,6 +1265,8 @@ api_key="Client Secret" key1="Client ID" [paypal.connector_webhook_details] merchant_secret="Source verification key" +[paypal.metadata.paypal_sdk] +client_id="Client ID" [payu] [[payu.credit]] diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index 450e14d108..932352f20e 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -1645,6 +1645,8 @@ api_key="Client Secret" key1="Client ID" [paypal.connector_webhook_details] merchant_secret="Source verification key" +[paypal.metadata.paypal_sdk] +client_id="Client ID" [paypal_payout] [[paypal.wallet]] diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index 8830c68993..c67ea2d274 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -367,6 +367,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::ApplepaySessionTokenResponse, api_models::payments::SdkNextAction, api_models::payments::NextActionCall, + api_models::payments::SdkNextActionData, api_models::payments::SamsungPayWalletData, api_models::payments::WeChatPay, api_models::payments::GpayTokenizationData, diff --git a/crates/router/src/compatibility/stripe/payment_intents/types.rs b/crates/router/src/compatibility/stripe/payment_intents/types.rs index d615acff1c..5e8c9ea08c 100644 --- a/crates/router/src/compatibility/stripe/payment_intents/types.rs +++ b/crates/router/src/compatibility/stripe/payment_intents/types.rs @@ -823,6 +823,9 @@ pub enum StripeNextAction { display_from_timestamp: i128, display_to_timestamp: Option, }, + InvokeSdkClient { + next_action_data: payments::SdkNextActionData, + }, } pub(crate) fn into_stripe_next_action( @@ -871,6 +874,9 @@ pub(crate) fn into_stripe_next_action( url: None, }, }, + payments::NextActionData::InvokeSdkClient { next_action_data } => { + StripeNextAction::InvokeSdkClient { next_action_data } + } }) } diff --git a/crates/router/src/compatibility/stripe/setup_intents/types.rs b/crates/router/src/compatibility/stripe/setup_intents/types.rs index 03335d4272..bbcafb65e9 100644 --- a/crates/router/src/compatibility/stripe/setup_intents/types.rs +++ b/crates/router/src/compatibility/stripe/setup_intents/types.rs @@ -389,6 +389,9 @@ pub enum StripeNextAction { display_from_timestamp: i128, display_to_timestamp: Option, }, + InvokeSdkClient { + next_action_data: payments::SdkNextActionData, + }, } pub(crate) fn into_stripe_next_action( @@ -437,6 +440,9 @@ pub(crate) fn into_stripe_next_action( url: None, }, }, + payments::NextActionData::InvokeSdkClient { next_action_data } => { + StripeNextAction::InvokeSdkClient { next_action_data } + } }) } diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index 1fe86a209e..0405f31807 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -7914,6 +7914,170 @@ impl Default for super::settings::RequiredFields { ]), }, ), + ( + // Added shipping fields for the SDK flow to accept it from wallet directly, + // this won't show up in SDK in payment's sheet but will be used in the background + enums::PaymentMethodType::Paypal, + ConnectorFields { + fields: HashMap::from([ + ( + enums::Connector::Braintree, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::from([ + ( + "shipping.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.first_name".to_string(), + display_name: "shipping_first_name".to_string(), + field_type: enums::FieldType::UserShippingName, + value: None, + } + ), + ( + "shipping.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.last_name".to_string(), + display_name: "shipping_last_name".to_string(), + field_type: enums::FieldType::UserShippingName, + value: None, + } + ), + ( + "shipping.address.city".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserShippingAddressCity, + value: None, + } + ), + ( + "shipping.address.state".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.state".to_string(), + display_name: "state".to_string(), + field_type: enums::FieldType::UserShippingAddressState, + value: None, + } + ), + ( + "shipping.address.zip".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserShippingAddressPincode, + value: None, + } + ), + ( + "shipping.address.country".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserShippingAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), + ( + "shipping.address.line1".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserShippingAddressLine1, + value: None, + } + ), + ]), + } + ), + ( + enums::Connector::Paypal, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new( + ), + common: HashMap::from( + [ + ( + "shipping.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.first_name".to_string(), + display_name: "shipping_first_name".to_string(), + field_type: enums::FieldType::UserShippingName, + value: None, + } + ), + ( + "shipping.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.last_name".to_string(), + display_name: "shipping_last_name".to_string(), + field_type: enums::FieldType::UserShippingName, + value: None, + } + ), + ( + "shipping.address.city".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserShippingAddressCity, + value: None, + } + ), + ( + "shipping.address.state".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.state".to_string(), + display_name: "state".to_string(), + field_type: enums::FieldType::UserShippingAddressState, + value: None, + } + ), + ( + "shipping.address.zip".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserShippingAddressPincode, + value: None, + } + ), + ( + "shipping.address.country".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserShippingAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), + ( + "shipping.address.line1".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserShippingAddressLine1, + value: None, + } + ), + ] + ), + } + ), + ]), + }, + ), ])), ), ( diff --git a/crates/router/src/connector/braintree/transformers.rs b/crates/router/src/connector/braintree/transformers.rs index a939f0a83f..136782cab6 100644 --- a/crates/router/src/connector/braintree/transformers.rs +++ b/crates/router/src/connector/braintree/transformers.rs @@ -308,6 +308,10 @@ impl session_token: api::SessionToken::Paypal(Box::new( payments::PaypalSessionTokenResponse { session_token: item.response.client_token.value.expose(), + connector: "braintree".to_string(), + sdk_next_action: payments::SdkNextAction { + next_action: payments::NextActionCall::Confirm, + }, }, )), }), diff --git a/crates/router/src/connector/paypal.rs b/crates/router/src/connector/paypal.rs index 9ff6b84a06..9e7b208ef7 100644 --- a/crates/router/src/connector/paypal.rs +++ b/crates/router/src/connector/paypal.rs @@ -34,7 +34,7 @@ use crate::{ self, api::{self, CompleteAuthorize, ConnectorCommon, ConnectorCommonExt, VerifyWebhookSource}, storage::enums as storage_enums, - transformers::ForeignFrom, + transformers::{ForeignFrom, ForeignTryFrom}, ConnectorAuthType, ErrorResponse, Response, }, utils::BytesExt, @@ -629,7 +629,7 @@ impl ConnectorIntegration { event_builder.map(|i| i.set_response_body(&response)); @@ -644,11 +644,14 @@ impl ConnectorIntegration { event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); - types::RouterData::try_from(types::ResponseRouterData { - response, - data: data.clone(), - http_code: res.status_code, - }) + types::RouterData::foreign_try_from(( + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }, + payment_method_data, + )) } PaypalAuthResponse::PaypalThreeDsResponse(response) => { event_builder.map(|i| i.set_response_body(&response)); @@ -1033,11 +1036,14 @@ impl ConnectorIntegration> for PaypalP let paypal_auth: PaypalAuthType = PaypalAuthType::try_from(&item.router_data.connector_auth_type)?; let payee = get_payee(&paypal_auth); + + let amount = OrderRequestAmount::from(item); + + let intent = if item.router_data.request.is_auto_capture()? { + PaypalPaymentIntent::Capture + } else { + PaypalPaymentIntent::Authorize + }; + + let connector_request_reference_id = + item.router_data.connector_request_reference_id.clone(); + + let shipping_address = ShippingAddress::try_from(item)?; + let item_details = vec![ItemDetails::from(item)]; + + let purchase_units = vec![PurchaseUnitRequest { + reference_id: Some(connector_request_reference_id.clone()), + custom_id: Some(connector_request_reference_id.clone()), + invoice_id: Some(connector_request_reference_id), + amount, + payee, + shipping: Some(shipping_address), + items: item_details, + }]; + match item.router_data.request.payment_method_data { domain::PaymentMethodData::Card(ref ccard) => { - let intent = if item.router_data.request.is_auto_capture()? { - PaypalPaymentIntent::Capture - } else { - PaypalPaymentIntent::Authorize - }; - let amount = OrderRequestAmount::from(item); - let connector_request_reference_id = - item.router_data.connector_request_reference_id.clone(); - let shipping_address = ShippingAddress::try_from(item)?; - let item_details = vec![ItemDetails::from(item)]; - - let purchase_units = vec![PurchaseUnitRequest { - reference_id: Some(connector_request_reference_id.clone()), - custom_id: Some(connector_request_reference_id.clone()), - invoice_id: Some(connector_request_reference_id), - amount, - payee, - shipping: Some(shipping_address), - items: item_details, - }]; let card = item.router_data.request.get_card()?; let expiry = Some(card.get_expiry_date_as_yyyymm("-")); @@ -441,27 +448,6 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP } domain::PaymentMethodData::Wallet(ref wallet_data) => match wallet_data { domain::WalletData::PaypalRedirect(_) => { - let intent = if item.router_data.request.is_auto_capture()? { - PaypalPaymentIntent::Capture - } else { - PaypalPaymentIntent::Authorize - }; - let amount = OrderRequestAmount::from(item); - - let connector_req_reference_id = - item.router_data.connector_request_reference_id.clone(); - let shipping_address = ShippingAddress::try_from(item)?; - let item_details = vec![ItemDetails::from(item)]; - - let purchase_units = vec![PurchaseUnitRequest { - reference_id: Some(connector_req_reference_id.clone()), - custom_id: Some(connector_req_reference_id.clone()), - invoice_id: Some(connector_req_reference_id), - amount, - payee, - shipping: Some(shipping_address), - items: item_details, - }]; let payment_source = Some(PaymentSourceItem::Paypal(PaypalRedirectionRequest { experience_context: ContextStruct { @@ -486,6 +472,23 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP payment_source, }) } + domain::WalletData::PaypalSdk(_) => { + let payment_source = + Some(PaymentSourceItem::Paypal(PaypalRedirectionRequest { + experience_context: ContextStruct { + return_url: None, + cancel_url: None, + shipping_preference: ShippingPreference::GetFromFile, + user_action: Some(UserAction::PayNow), + }, + })); + + Ok(Self { + intent, + purchase_units, + payment_source, + }) + } domain::WalletData::AliPayQr(_) | domain::WalletData::AliPayRedirect(_) | domain::WalletData::AliPayHkRedirect(_) @@ -502,7 +505,6 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP | domain::WalletData::GooglePayThirdPartySdk(_) | domain::WalletData::MbWayRedirect(_) | domain::WalletData::MobilePayRedirect(_) - | domain::WalletData::PaypalSdk(_) | domain::WalletData::SamsungPay(_) | domain::WalletData::TwintRedirect {} | domain::WalletData::VippsRedirect {} @@ -515,7 +517,7 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP ))?, }, domain::PaymentMethodData::BankRedirect(ref bank_redirection_data) => { - let intent = if item.router_data.request.is_auto_capture()? { + let bank_redirect_intent = if item.router_data.request.is_auto_capture()? { PaypalPaymentIntent::Capture } else { Err(errors::ConnectorError::FlowNotSupported { @@ -523,26 +525,12 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP connector: "Paypal".to_string(), })? }; - let amount = OrderRequestAmount::from(item); - let connector_req_reference_id = - item.router_data.connector_request_reference_id.clone(); - let shipping_address = ShippingAddress::try_from(item)?; - let item_details = vec![ItemDetails::from(item)]; - let purchase_units = vec![PurchaseUnitRequest { - reference_id: Some(connector_req_reference_id.clone()), - custom_id: Some(connector_req_reference_id.clone()), - invoice_id: Some(connector_req_reference_id), - amount, - payee, - shipping: Some(shipping_address), - items: item_details, - }]; let payment_source = Some(get_payment_source(item.router_data, bank_redirection_data)?); Ok(Self { - intent, + intent: bank_redirect_intent, purchase_units, payment_source, }) @@ -1060,6 +1048,7 @@ pub struct PaypalMeta { pub authorize_id: Option, pub capture_id: Option, pub psync_flow: PaypalPaymentIntent, + pub next_action: Option, } fn get_id_based_on_intent( @@ -1112,7 +1101,8 @@ impl serde_json::json!(PaypalMeta { authorize_id: None, capture_id: Some(id), - psync_flow: item.response.intent.clone() + psync_flow: item.response.intent.clone(), + next_action: None, }), types::ResponseId::ConnectorTransactionId(item.response.id.clone()), ), @@ -1121,7 +1111,8 @@ impl serde_json::json!(PaypalMeta { authorize_id: Some(id), capture_id: None, - psync_flow: item.response.intent.clone() + psync_flow: item.response.intent.clone(), + next_action: None, }), types::ResponseId::ConnectorTransactionId(item.response.id.clone()), ), @@ -1183,23 +1174,27 @@ fn get_redirect_url( } impl - TryFrom< + ForeignTryFrom<( types::ResponseRouterData< F, PaypalSyncResponse, types::PaymentsSyncData, types::PaymentsResponseData, >, - > for types::RouterData + Option, + )> for types::RouterData { type Error = error_stack::Report; - fn try_from( - item: types::ResponseRouterData< - F, - PaypalSyncResponse, - types::PaymentsSyncData, - types::PaymentsResponseData, - >, + fn foreign_try_from( + (item, payment_experience): ( + types::ResponseRouterData< + F, + PaypalSyncResponse, + types::PaymentsSyncData, + types::PaymentsResponseData, + >, + Option, + ), ) -> Result { match item.response { PaypalSyncResponse::PaypalOrdersSyncResponse(response) => { @@ -1209,13 +1204,14 @@ impl http_code: item.http_code, }) } - PaypalSyncResponse::PaypalRedirectSyncResponse(response) => { - Self::try_from(types::ResponseRouterData { + PaypalSyncResponse::PaypalRedirectSyncResponse(response) => Self::foreign_try_from(( + types::ResponseRouterData { response, data: item.data, http_code: item.http_code, - }) - } + }, + payment_experience, + )), PaypalSyncResponse::PaypalPaymentsSyncResponse(response) => { Self::try_from(types::ResponseRouterData { response, @@ -1235,25 +1231,96 @@ impl } impl - TryFrom> - for types::RouterData + ForeignTryFrom<( + types::ResponseRouterData, + Option, + )> for types::RouterData { type Error = error_stack::Report; - fn try_from( - item: types::ResponseRouterData, + fn foreign_try_from( + (item, payment_experience): ( + types::ResponseRouterData, + Option, + ), ) -> Result { let status = storage_enums::AttemptStatus::foreign_from(( item.response.clone().status, item.response.intent.clone(), )); let link = get_redirect_url(item.response.links.clone())?; + + // For Paypal SDK flow, we need to trigger SDK client and then complete authorize + let next_action = + if let Some(common_enums::PaymentExperience::InvokeSdkClient) = payment_experience { + Some(api_models::payments::NextActionCall::CompleteAuthorize) + } else { + None + }; let connector_meta = serde_json::json!(PaypalMeta { authorize_id: None, capture_id: None, - psync_flow: item.response.intent + psync_flow: item.response.intent, + next_action, }); let purchase_units = item.response.purchase_units.first(); + Ok(Self { + status, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(item.response.id.clone()), + redirection_data: Some(services::RedirectForm::from(( + link.ok_or(errors::ConnectorError::ResponseDeserializationFailed)?, + services::Method::Get, + ))), + mandate_reference: None, + connector_metadata: Some(connector_meta), + network_txn_id: None, + connector_response_reference_id: Some( + purchase_units.map_or(item.response.id, |item| item.invoice_id.clone()), + ), + incremental_authorization_allowed: None, + charge_id: None, + }), + ..item.data + }) + } +} +impl + ForeignTryFrom<( + types::ResponseRouterData, + domain::PaymentMethodData, + )> for types::RouterData +{ + type Error = error_stack::Report; + fn foreign_try_from( + (item, payment_method_data): ( + types::ResponseRouterData, + domain::PaymentMethodData, + ), + ) -> Result { + let status = storage_enums::AttemptStatus::foreign_from(( + item.response.clone().status, + item.response.intent.clone(), + )); + let link = get_redirect_url(item.response.links.clone())?; + + // For Paypal SDK flow, we need to trigger SDK client and then complete authorize + let next_action = + if let domain::PaymentMethodData::Wallet(domain::WalletData::PaypalSdk(_)) = + payment_method_data + { + Some(api_models::payments::NextActionCall::CompleteAuthorize) + } else { + None + }; + + let connector_meta = serde_json::json!(PaypalMeta { + authorize_id: None, + capture_id: None, + psync_flow: item.response.intent, + next_action, + }); + let purchase_units = item.response.purchase_units.first(); Ok(Self { status, response: Ok(types::PaymentsResponseData::TransactionResponse { @@ -1336,7 +1403,8 @@ impl let connector_meta = serde_json::json!(PaypalMeta { authorize_id: None, capture_id: None, - psync_flow: PaypalPaymentIntent::Authenticate // when there is no capture or auth id present + psync_flow: PaypalPaymentIntent::Authenticate, // when there is no capture or auth id present + next_action: None, }); let status = storage_enums::AttemptStatus::foreign_from(( @@ -1755,7 +1823,8 @@ impl TryFrom> connector_metadata: Some(serde_json::json!(PaypalMeta { authorize_id: connector_payment_id.authorize_id, capture_id: Some(item.response.id.clone()), - psync_flow: PaypalPaymentIntent::Capture + psync_flow: PaypalPaymentIntent::Capture, + next_action: None, })), network_txn_id: None, connector_response_reference_id: item diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index f760e90728..a39f0deb3c 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1012,6 +1012,7 @@ impl PaymentRedirectFlow for PaymentRedirectCompleteAuthorize { api_models::payments::NextActionData::DisplayVoucherInformation{ .. } => None, api_models::payments::NextActionData::WaitScreenInformation{..} => None, api_models::payments::NextActionData::ThreeDsInvoke{..} => None, + api_models::payments::NextActionData::InvokeSdkClient{..} => None, }) .ok_or(errors::ApiErrorResponse::InternalServerError) diff --git a/crates/router/src/core/payments/flows/session_flow.rs b/crates/router/src/core/payments/flows/session_flow.rs index 76ff96c702..c74ed4bae1 100644 --- a/crates/router/src/core/payments/flows/session_flow.rs +++ b/crates/router/src/core/payments/flows/session_flow.rs @@ -634,6 +634,42 @@ where ) -> RouterResult; } +fn create_paypal_sdk_session_token( + _state: &routes::AppState, + router_data: &types::PaymentsSessionRouterData, + connector: &api::ConnectorData, + _business_profile: &storage::business_profile::BusinessProfile, +) -> RouterResult { + let connector_metadata = router_data.connector_meta_data.clone(); + + let paypal_sdk_data = connector_metadata + .clone() + .parse_value::("PaypalSdkSessionTokenData") + .change_context(errors::ConnectorError::NoConnectorMetaData) + .attach_printable(format!( + "cannot parse paypal_sdk metadata from the given value {connector_metadata:?}" + )) + .change_context(errors::ApiErrorResponse::InvalidDataFormat { + field_name: "connector_metadata".to_string(), + expected_format: "paypal_sdk_metadata_format".to_string(), + })?; + + Ok(types::PaymentsSessionRouterData { + response: Ok(types::PaymentsResponseData::SessionResponse { + session_token: payment_types::SessionToken::Paypal(Box::new( + payment_types::PaypalSessionTokenResponse { + connector: connector.connector_name.to_string(), + session_token: paypal_sdk_data.data.client_id, + sdk_next_action: payment_types::SdkNextAction { + next_action: payment_types::NextActionCall::Confirm, + }, + }, + )), + }), + ..router_data.clone() + }) +} + #[async_trait] impl RouterDataSession for types::PaymentsSessionRouterData { async fn decide_flow<'a, 'b>( @@ -651,6 +687,9 @@ impl RouterDataSession for types::PaymentsSessionRouterData { api::GetToken::ApplePayMetadata => { create_applepay_session_token(state, self, connector, business_profile).await } + api::GetToken::PaypalSdkMetadata => { + create_paypal_sdk_session_token(state, self, connector, business_profile) + } api::GetToken::Connector => { let connector_integration: services::BoxedConnectorIntegration< '_, diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index e4d91d4a06..2aa01cd587 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -471,6 +471,7 @@ impl From for api::GetToken { match value { api_models::enums::PaymentMethodType::GooglePay => Self::GpayMetadata, api_models::enums::PaymentMethodType::ApplePay => Self::ApplePayMetadata, + api_models::enums::PaymentMethodType::Paypal => Self::PaypalSdkMetadata, _ => Self::Connector, } } diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 11ce9cd7ed..7f6b0cc1f6 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -539,6 +539,8 @@ where let next_action_containing_qr_code_url = qr_code_next_steps_check(payment_attempt.clone())?; + let papal_sdk_next_action = paypal_sdk_next_steps_check(payment_attempt.clone())?; + let next_action_containing_wait_screen = wait_screen_next_steps_check(payment_attempt.clone())?; @@ -547,6 +549,7 @@ where || next_action_voucher.is_some() || next_action_containing_qr_code_url.is_some() || next_action_containing_wait_screen.is_some() + || papal_sdk_next_action.is_some() || payment_data.authentication.is_some() { next_action_response = bank_transfer_next_steps @@ -563,6 +566,11 @@ where .or(next_action_containing_qr_code_url.map(|qr_code_data| { api_models::payments::NextActionData::foreign_from(qr_code_data) })) + .or(papal_sdk_next_action.map(|paypal_next_action_data| { + api_models::payments::NextActionData::InvokeSdkClient{ + next_action_data: paypal_next_action_data + } + })) .or(next_action_containing_wait_screen.map(|wait_screen_data| { api_models::payments::NextActionData::WaitScreenInformation { display_from_timestamp: wait_screen_data.display_from_timestamp, @@ -875,6 +883,21 @@ pub fn qr_code_next_steps_check( let qr_code_instructions = qr_code_steps.transpose().ok().flatten(); Ok(qr_code_instructions) } +pub fn paypal_sdk_next_steps_check( + payment_attempt: storage::PaymentAttempt, +) -> RouterResult> { + let paypal_connector_metadata: Option> = + payment_attempt.connector_metadata.map(|metadata| { + metadata.parse_value("SdkNextActionData").map_err(|_| { + crate::logger::warn!( + "SdkNextActionData parsing failed for paypal_connector_metadata" + ) + }) + }); + + let paypal_next_steps = paypal_connector_metadata.transpose().ok().flatten(); + Ok(paypal_next_steps) +} pub fn wait_screen_next_steps_check( payment_attempt: storage::PaymentAttempt, @@ -1275,6 +1298,7 @@ impl TryFrom> for types::PaymentsSyncData }, payment_method_type: payment_data.payment_attempt.payment_method_type, currency: payment_data.currency, + payment_experience: payment_data.payment_attempt.payment_experience, }) } } diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index e2f2f6c0b7..89c2629c10 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -466,6 +466,7 @@ pub struct PaymentsSyncData { pub mandate_id: Option, pub payment_method_type: Option, pub currency: storage_enums::Currency, + pub payment_experience: Option, } #[derive(Debug, Default, Clone)] diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 2013f48275..aea5e326cf 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -217,6 +217,7 @@ type BoxedConnector = Box<&'static (dyn Connector + Sync)>; pub enum GetToken { GpayMetadata, ApplePayMetadata, + PaypalSdkMetadata, Connector, } diff --git a/crates/router/tests/connectors/bambora.rs b/crates/router/tests/connectors/bambora.rs index 3115c1143d..d0bc77e750 100644 --- a/crates/router/tests/connectors/bambora.rs +++ b/crates/router/tests/connectors/bambora.rs @@ -110,6 +110,7 @@ async fn should_sync_authorized_payment() { connector_meta: None, payment_method_type: None, currency: enums::Currency::USD, + payment_experience: None, }), None, ) @@ -226,6 +227,7 @@ async fn should_sync_auto_captured_payment() { connector_meta: None, payment_method_type: None, currency: enums::Currency::USD, + payment_experience: None, }), None, ) diff --git a/crates/router/tests/connectors/forte.rs b/crates/router/tests/connectors/forte.rs index d690524bd1..11153aa708 100644 --- a/crates/router/tests/connectors/forte.rs +++ b/crates/router/tests/connectors/forte.rs @@ -159,6 +159,7 @@ async fn should_sync_authorized_payment() { mandate_id: None, payment_method_type: None, currency: enums::Currency::USD, + payment_experience: None, }), get_default_payment_info(), ) diff --git a/crates/router/tests/connectors/nexinets.rs b/crates/router/tests/connectors/nexinets.rs index cdf94113c5..c6a14ac202 100644 --- a/crates/router/tests/connectors/nexinets.rs +++ b/crates/router/tests/connectors/nexinets.rs @@ -126,6 +126,7 @@ async fn should_sync_authorized_payment() { mandate_id: None, payment_method_type: None, currency: enums::Currency::EUR, + payment_experience: None, }), None, ) diff --git a/crates/router/tests/connectors/paypal.rs b/crates/router/tests/connectors/paypal.rs index 704aeca593..b53cf71acf 100644 --- a/crates/router/tests/connectors/paypal.rs +++ b/crates/router/tests/connectors/paypal.rs @@ -143,6 +143,7 @@ async fn should_sync_authorized_payment() { connector_meta, payment_method_type: None, currency: enums::Currency::USD, + payment_experience: None, }), get_default_payment_info(), ) @@ -341,6 +342,7 @@ async fn should_sync_auto_captured_payment() { connector_meta, payment_method_type: None, currency: enums::Currency::USD, + payment_experience: None, }), get_default_payment_info(), ) diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 6718611b0c..9955cb77df 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -1002,6 +1002,7 @@ impl Default for PaymentSyncType { connector_meta: None, payment_method_type: None, currency: enums::Currency::USD, + payment_experience: None, }; Self(data) } diff --git a/crates/router/tests/connectors/zen.rs b/crates/router/tests/connectors/zen.rs index b11b78025d..552edcaaf6 100644 --- a/crates/router/tests/connectors/zen.rs +++ b/crates/router/tests/connectors/zen.rs @@ -105,6 +105,7 @@ async fn should_sync_authorized_payment() { mandate_id: None, payment_method_type: None, currency: enums::Currency::USD, + payment_experience: None, }), None, ) @@ -221,6 +222,7 @@ async fn should_sync_auto_captured_payment() { mandate_id: None, payment_method_type: None, currency: enums::Currency::USD, + payment_experience: None, }), None, ) diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 47da1ae107..b87b516de2 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -11660,7 +11660,8 @@ "type": "string", "enum": [ "confirm", - "sync" + "sync", + "complete_authorize" ] }, "NextActionData": { @@ -11816,6 +11817,24 @@ ] } } + }, + { + "type": "object", + "required": [ + "next_action_data", + "type" + ], + "properties": { + "next_action_data": { + "$ref": "#/components/schemas/SdkNextActionData" + }, + "type": { + "type": "string", + "enum": [ + "invoke_sdk_client" + ] + } + } } ], "discriminator": { @@ -16780,12 +16799,21 @@ "PaypalSessionTokenResponse": { "type": "object", "required": [ - "session_token" + "connector", + "session_token", + "sdk_next_action" ], "properties": { + "connector": { + "type": "string", + "description": "Name of the connector" + }, "session_token": { "type": "string", "description": "The session token for PayPal" + }, + "sdk_next_action": { + "$ref": "#/components/schemas/SdkNextAction" } } }, @@ -18030,6 +18058,17 @@ } } }, + "SdkNextActionData": { + "type": "object", + "required": [ + "next_action" + ], + "properties": { + "next_action": { + "$ref": "#/components/schemas/NextActionCall" + } + } + }, "SecretInfoToInitiateSdk": { "type": "object", "required": [