From 00d69bd924c1c8368ea6ab1af32a2f258c1a94c1 Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Wed, 5 Mar 2025 16:30:28 +0530 Subject: [PATCH] fix(connector): [Braintree] Consume merchant_account_id and merchant_config_currency in payment requests (#7408) Co-authored-by: Debarshi Gupta --- api-reference-v2/openapi_spec.json | 25 ++++++ api-reference/openapi_spec.json | 25 ++++++ crates/api_models/src/payments.rs | 11 +++ crates/diesel_models/src/payment_attempt.rs | 1 + .../src/connectors/braintree/transformers.rs | 88 ++++++++++++++++--- .../src/router_request_types.rs | 6 ++ .../src/router_response_types.rs | 5 ++ crates/openapi/src/openapi.rs | 1 + crates/openapi/src/openapi_v2.rs | 1 + .../router/src/core/payments/transformers.rs | 43 +++++++++ crates/router/src/core/relay/utils.rs | 2 + crates/router/src/core/utils.rs | 19 ++++ crates/router/src/services/api.rs | 3 +- crates/router/src/types.rs | 2 + .../router/src/types/api/verify_connector.rs | 2 + crates/router/tests/connectors/utils.rs | 6 ++ 16 files changed, 229 insertions(+), 11 deletions(-) diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index bef1bcdeb5..1301d130cb 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -6372,6 +6372,23 @@ } } }, + "BraintreeData": { + "type": "object", + "required": [ + "merchant_account_id", + "merchant_config_currency" + ], + "properties": { + "merchant_account_id": { + "type": "string", + "description": "Information about the merchant_account_id that merchant wants to specify at connector level." + }, + "merchant_config_currency": { + "type": "string", + "description": "Information about the merchant_config_currency that merchant wants to specify at connector level." + } + } + }, "BrowserInformation": { "type": "object", "description": "Browser information to be used for 3DS 2.0", @@ -7652,6 +7669,14 @@ } ], "nullable": true + }, + "braintree": { + "allOf": [ + { + "$ref": "#/components/schemas/BraintreeData" + } + ], + "nullable": true } } }, diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 59791cc750..116e79383a 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -8510,6 +8510,23 @@ } } }, + "BraintreeData": { + "type": "object", + "required": [ + "merchant_account_id", + "merchant_config_currency" + ], + "properties": { + "merchant_account_id": { + "type": "string", + "description": "Information about the merchant_account_id that merchant wants to specify at connector level." + }, + "merchant_config_currency": { + "type": "string", + "description": "Information about the merchant_config_currency that merchant wants to specify at connector level." + } + } + }, "BrowserInformation": { "type": "object", "description": "Browser information to be used for 3DS 2.0", @@ -9759,6 +9776,14 @@ } ], "nullable": true + }, + "braintree": { + "allOf": [ + { + "$ref": "#/components/schemas/BraintreeData" + } + ], + "nullable": true } } }, diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 15d6e68f14..b8b6048136 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -6566,6 +6566,7 @@ pub struct ConnectorMetadata { pub apple_pay: Option, pub airwallex: Option, pub noon: Option, + pub braintree: Option, } impl ConnectorMetadata { @@ -6606,6 +6607,16 @@ pub struct NoonData { pub order_category: Option, } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +pub struct BraintreeData { + /// Information about the merchant_account_id that merchant wants to specify at connector level. + #[schema(value_type = String)] + pub merchant_account_id: Option>, + /// Information about the merchant_config_currency that merchant wants to specify at connector level. + #[schema(value_type = String)] + pub merchant_config_currency: Option, +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] pub struct ApplepayConnectorMetadataRequest { pub session_token_data: Option, diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index 1630ae2d3e..09121f7f00 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -3508,6 +3508,7 @@ pub enum RedirectForm { client_token: String, card_token: String, bin: String, + acs_url: String, }, Nmi { amount: String, diff --git a/crates/hyperswitch_connectors/src/connectors/braintree/transformers.rs b/crates/hyperswitch_connectors/src/connectors/braintree/transformers.rs index e2571e8b58..0ac30a204f 100644 --- a/crates/hyperswitch_connectors/src/connectors/braintree/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/braintree/transformers.rs @@ -274,11 +274,27 @@ impl TryFrom<&BraintreeRouterData<&types::PaymentsAuthorizeRouterData>> fn try_from( item: &BraintreeRouterData<&types::PaymentsAuthorizeRouterData>, ) -> Result { - let metadata: BraintreeMeta = + let metadata: BraintreeMeta = if let ( + Some(merchant_account_id), + Some(merchant_config_currency), + ) = ( + item.router_data.request.merchant_account_id.clone(), + item.router_data.request.merchant_config_currency, + ) { + router_env::logger::info!( + "BRAINTREE: Picking merchant_account_id and merchant_config_currency from payments request" + ); + + BraintreeMeta { + merchant_account_id, + merchant_config_currency, + } + } else { utils::to_connector_meta_from_secret(item.router_data.connector_meta_data.clone()) .change_context(errors::ConnectorError::InvalidConnectorConfig { config: "metadata", - })?; + })? + }; utils::validate_currency( item.router_data.request.currency, Some(metadata.merchant_config_currency), @@ -475,6 +491,7 @@ impl *client_token_data, item.data.get_payment_method_token()?, item.data.request.payment_method_data.clone(), + item.data.request.get_complete_authorize_url()?, )?)), mandate_reference: Box::new(None), connector_metadata: None, @@ -651,6 +668,7 @@ impl *client_token_data, item.data.get_payment_method_token()?, item.data.request.payment_method_data.clone(), + item.data.request.get_complete_authorize_url()?, )?)), mandate_reference: Box::new(None), connector_metadata: None, @@ -846,11 +864,27 @@ pub struct BraintreeRefundInput { impl TryFrom>> for BraintreeRefundRequest { type Error = error_stack::Report; fn try_from(item: BraintreeRouterData<&RefundsRouterData>) -> Result { - let metadata: BraintreeMeta = + let metadata: BraintreeMeta = if let ( + Some(merchant_account_id), + Some(merchant_config_currency), + ) = ( + item.router_data.request.merchant_account_id.clone(), + item.router_data.request.merchant_config_currency, + ) { + router_env::logger::info!( + "BRAINTREE: Picking merchant_account_id and merchant_config_currency from payments request" + ); + + BraintreeMeta { + merchant_account_id, + merchant_config_currency, + } + } else { utils::to_connector_meta_from_secret(item.router_data.connector_meta_data.clone()) .change_context(errors::ConnectorError::InvalidConnectorConfig { config: "metadata", - })?; + })? + }; utils::validate_currency( item.router_data.request.currency, @@ -957,10 +991,26 @@ pub struct RefundSearchInput { impl TryFrom<&types::RefundSyncRouterData> for BraintreeRSyncRequest { type Error = error_stack::Report; fn try_from(item: &types::RefundSyncRouterData) -> Result { - let metadata: BraintreeMeta = utils::to_connector_meta_from_secret( - item.connector_meta_data.clone(), - ) - .change_context(errors::ConnectorError::InvalidConnectorConfig { config: "metadata" })?; + let metadata: BraintreeMeta = if let ( + Some(merchant_account_id), + Some(merchant_config_currency), + ) = ( + item.request.merchant_account_id.clone(), + item.request.merchant_config_currency, + ) { + router_env::logger::info!( + "BRAINTREE: Picking merchant_account_id and merchant_config_currency from payments request" + ); + + BraintreeMeta { + merchant_account_id, + merchant_config_currency, + } + } else { + utils::to_connector_meta_from_secret(item.connector_meta_data.clone()).change_context( + errors::ConnectorError::InvalidConnectorConfig { config: "metadata" }, + )? + }; utils::validate_currency( item.request.currency, Some(metadata.merchant_config_currency), @@ -1617,11 +1667,27 @@ impl TryFrom<&BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>> fn try_from( item: &BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>, ) -> Result { - let metadata: BraintreeMeta = + let metadata: BraintreeMeta = if let ( + Some(merchant_account_id), + Some(merchant_config_currency), + ) = ( + item.router_data.request.merchant_account_id.clone(), + item.router_data.request.merchant_config_currency, + ) { + router_env::logger::info!( + "BRAINTREE: Picking merchant_account_id and merchant_config_currency from payments request" + ); + + BraintreeMeta { + merchant_account_id, + merchant_config_currency, + } + } else { utils::to_connector_meta_from_secret(item.router_data.connector_meta_data.clone()) .change_context(errors::ConnectorError::InvalidConnectorConfig { config: "metadata", - })?; + })? + }; utils::validate_currency( item.router_data.request.currency, Some(metadata.merchant_config_currency), @@ -1686,6 +1752,7 @@ fn get_braintree_redirect_form( client_token_data: ClientTokenResponse, payment_method_token: PaymentMethodToken, card_details: PaymentMethodData, + complete_authorize_url: String, ) -> Result> { Ok(RedirectForm::Braintree { client_token: client_token_data @@ -1730,6 +1797,7 @@ fn get_braintree_redirect_form( errors::ConnectorError::NotImplemented("given payment method".to_owned()), )?, }, + acs_url: complete_authorize_url, }) } diff --git a/crates/hyperswitch_domain_models/src/router_request_types.rs b/crates/hyperswitch_domain_models/src/router_request_types.rs index d4dcd337f8..5eaa899714 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types.rs @@ -71,6 +71,8 @@ pub struct PaymentsAuthorizeData { pub integrity_object: Option, pub shipping_cost: Option, pub additional_payment_method_data: Option, + pub merchant_account_id: Option>, + pub merchant_config_currency: Option, } #[derive(Debug, Clone)] pub struct PaymentsPostSessionTokensData { @@ -417,6 +419,8 @@ pub struct CompleteAuthorizeData { pub customer_acceptance: Option, // New amount for amount frame work pub minor_amount: MinorUnit, + pub merchant_account_id: Option>, + pub merchant_config_currency: Option, } #[derive(Debug, Clone)] @@ -636,6 +640,8 @@ pub struct RefundsData { pub minor_refund_amount: MinorUnit, pub integrity_object: Option, pub refund_status: storage_enums::RefundStatus, + pub merchant_account_id: Option>, + pub merchant_config_currency: Option, } #[derive(Debug, Clone, PartialEq)] diff --git a/crates/hyperswitch_domain_models/src/router_response_types.rs b/crates/hyperswitch_domain_models/src/router_response_types.rs index 3009d5521d..f9ffc3d417 100644 --- a/crates/hyperswitch_domain_models/src/router_response_types.rs +++ b/crates/hyperswitch_domain_models/src/router_response_types.rs @@ -271,6 +271,7 @@ pub enum RedirectForm { client_token: String, card_token: String, bin: String, + acs_url: String, }, Nmi { amount: String, @@ -351,10 +352,12 @@ impl From for diesel_models::payment_attempt::RedirectForm { client_token, card_token, bin, + acs_url, } => Self::Braintree { client_token, card_token, bin, + acs_url, }, RedirectForm::Nmi { amount, @@ -433,10 +436,12 @@ impl From for RedirectForm { client_token, card_token, bin, + acs_url, } => Self::Braintree { client_token, card_token, bin, + acs_url, }, diesel_models::payment_attempt::RedirectForm::Nmi { amount, diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index a98080aa6f..e9a5c2fd94 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -401,6 +401,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::KlarnaSdkPaymentMethodResponse, api_models::payments::SwishQrData, api_models::payments::AirwallexData, + api_models::payments::BraintreeData, api_models::payments::NoonData, api_models::payments::OrderDetailsWithAmount, api_models::payments::NextActionType, diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index 9ad15cfe0d..0241b03eac 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -367,6 +367,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::KlarnaSdkPaymentMethodResponse, api_models::payments::SwishQrData, api_models::payments::AirwallexData, + api_models::payments::BraintreeData, api_models::payments::NoonData, api_models::payments::OrderDetailsWithAmount, api_models::payments::NextActionType, diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index c4aa8d2d49..9fe4e1914b 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -304,6 +304,8 @@ pub async fn construct_payment_router_data_for_authorize<'a>( integrity_object: None, shipping_cost: payment_data.payment_intent.amount_details.shipping_cost, additional_payment_method_data: None, + merchant_account_id: None, + merchant_config_currency: None, }; let connector_mandate_request_reference_id = payment_data .payment_attempt @@ -3038,6 +3040,7 @@ impl TryFrom> for types::PaymentsAuthoriz .payment_data .payment_intent .connector_metadata + .clone() .map(|cm| { cm.parse_value::("ConnectorMetadata") .change_context(errors::ApiErrorResponse::InternalServerError) @@ -3046,6 +3049,25 @@ impl TryFrom> for types::PaymentsAuthoriz .transpose()? .and_then(|cm| cm.noon.and_then(|noon| noon.order_category)); + let braintree_metadata = additional_data + .payment_data + .payment_intent + .connector_metadata + .clone() + .map(|cm| { + cm.parse_value::("ConnectorMetadata") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed parsing ConnectorMetadata") + }) + .transpose()? + .and_then(|cm| cm.braintree); + + let merchant_account_id = braintree_metadata + .as_ref() + .and_then(|braintree| braintree.merchant_account_id.clone()); + let merchant_config_currency = + braintree_metadata.and_then(|braintree| braintree.merchant_config_currency); + let order_details = additional_data .payment_data .payment_intent @@ -3185,6 +3207,8 @@ impl TryFrom> for types::PaymentsAuthoriz integrity_object: None, additional_payment_method_data, shipping_cost, + merchant_account_id, + merchant_config_currency, }) } } @@ -4037,6 +4061,23 @@ impl TryFrom> for types::CompleteAuthoriz attempt, connector_name, )); + let braintree_metadata = payment_data + .payment_intent + .connector_metadata + .clone() + .map(|cm| { + cm.parse_value::("ConnectorMetadata") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed parsing ConnectorMetadata") + }) + .transpose()? + .and_then(|cm| cm.braintree); + + let merchant_account_id = braintree_metadata + .as_ref() + .and_then(|braintree| braintree.merchant_account_id.clone()); + let merchant_config_currency = + braintree_metadata.and_then(|braintree| braintree.merchant_config_currency); Ok(Self { setup_future_usage: payment_data.payment_intent.setup_future_usage, mandate_id: payment_data.mandate_id.clone(), @@ -4060,6 +4101,8 @@ impl TryFrom> for types::CompleteAuthoriz complete_authorize_url, metadata: payment_data.payment_intent.metadata, customer_acceptance: payment_data.customer_acceptance, + merchant_account_id, + merchant_config_currency, }) } } diff --git a/crates/router/src/core/relay/utils.rs b/crates/router/src/core/relay/utils.rs index 0f28db0f0e..969566c098 100644 --- a/crates/router/src/core/relay/utils.rs +++ b/crates/router/src/core/relay/utils.rs @@ -107,6 +107,8 @@ pub async fn construct_relay_refund_router_data( split_refunds: None, integrity_object: None, refund_status: common_enums::RefundStatus::from(relay_record.status), + merchant_account_id: None, + merchant_config_currency: None, }, response: Err(ErrorResponse::default()), diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 8f5a80b24d..537f67871e 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -338,6 +338,23 @@ pub async fn construct_refund_router_data<'a, F>( let connector_refund_id = refund.get_optional_connector_refund_id().cloned(); + let braintree_metadata = payment_intent + .connector_metadata + .clone() + .map(|cm| { + cm.parse_value::("ConnectorMetadata") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed parsing ConnectorMetadata") + }) + .transpose()? + .and_then(|cm| cm.braintree); + + let merchant_account_id = braintree_metadata + .as_ref() + .and_then(|braintree| braintree.merchant_account_id.clone()); + let merchant_config_currency = + braintree_metadata.and_then(|braintree| braintree.merchant_config_currency); + let router_data = types::RouterData { flow: PhantomData, merchant_id: merchant_account.get_id().clone(), @@ -376,6 +393,8 @@ pub async fn construct_refund_router_data<'a, F>( split_refunds, integrity_object: None, refund_status: refund.refund_status, + merchant_account_id, + merchant_config_currency, }, response: Ok(types::RefundsResponseData { diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 8095907eca..a7687111d2 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -1588,6 +1588,7 @@ pub fn build_redirection_form( client_token, card_token, bin, + acs_url, } => { maud::html! { (maud::DOCTYPE) @@ -1664,7 +1665,7 @@ pub fn build_redirection_form( }} else {{ // console.log(payload); var f = document.createElement('form'); - f.action=window.location.pathname.replace(/payments\\/redirect\\/(\\w+)\\/(\\w+)\\/\\w+/, \"payments/$1/$2/redirect/complete/braintree\"); + f.action=\"{acs_url}\"; var i = document.createElement('input'); i.type = 'hidden'; f.method='POST'; diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 7bfbc22d05..048343022c 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -905,6 +905,8 @@ impl ForeignFrom<&SetupMandateRouterData> for PaymentsAuthorizeData { integrity_object: None, additional_payment_method_data: None, shipping_cost: data.request.shipping_cost, + merchant_account_id: None, + merchant_config_currency: None, } } } diff --git a/crates/router/src/types/api/verify_connector.rs b/crates/router/src/types/api/verify_connector.rs index 4d248bbf0b..ed3c56787d 100644 --- a/crates/router/src/types/api/verify_connector.rs +++ b/crates/router/src/types/api/verify_connector.rs @@ -60,6 +60,8 @@ impl VerifyConnectorData { integrity_object: None, additional_payment_method_data: None, shipping_cost: None, + merchant_account_id: None, + merchant_config_currency: None, } } diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 427cba5eb9..ead0173ed5 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -406,6 +406,8 @@ pub trait ConnectorActions: Connector { split_refunds: None, integrity_object: None, refund_status: enums::RefundStatus::Pending, + merchant_account_id: None, + merchant_config_currency: None, }), payment_info, ); @@ -980,6 +982,8 @@ impl Default for PaymentAuthorizeType { merchant_order_reference_id: None, additional_payment_method_data: None, shipping_cost: None, + merchant_account_id: None, + merchant_config_currency: None, }; Self(data) } @@ -1069,6 +1073,8 @@ impl Default for PaymentRefundType { split_refunds: None, integrity_object: None, refund_status: enums::RefundStatus::Pending, + merchant_account_id: None, + merchant_config_currency: None, }; Self(data) }