diff --git a/crates/router/src/connector/bluesnap.rs b/crates/router/src/connector/bluesnap.rs index 061619de99..b70f5610a4 100644 --- a/crates/router/src/connector/bluesnap.rs +++ b/crates/router/src/connector/bluesnap.rs @@ -494,7 +494,83 @@ impl api::PaymentSession for Bluesnap {} impl ConnectorIntegration for Bluesnap { - //TODO: implement sessions flow + fn get_headers( + &self, + req: &types::PaymentsSessionRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::PaymentsSessionRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}", + self.base_url(connectors), + "services/2/wallets" + )) + } + + fn get_request_body( + &self, + req: &types::PaymentsSessionRouterData, + ) -> CustomResult, errors::ConnectorError> { + let connector_req = bluesnap::BluesnapCreateWalletToken::try_from(req)?; + let bluesnap_req = + utils::Encode::::encode_to_string_of_json( + &connector_req, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(bluesnap_req)) + } + + fn build_request( + &self, + req: &types::PaymentsSessionRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsSessionType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PaymentsSessionType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsSessionType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsSessionRouterData, + res: Response, + ) -> CustomResult { + let response: bluesnap::BluesnapWalletTokenResponse = res + .response + .parse_struct("BluesnapWalletTokenResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } } impl api::PaymentAuthorize for Bluesnap {} diff --git a/crates/router/src/connector/bluesnap/transformers.rs b/crates/router/src/connector/bluesnap/transformers.rs index 6adbbb054d..7626d1f3ee 100644 --- a/crates/router/src/connector/bluesnap/transformers.rs +++ b/crates/router/src/connector/bluesnap/transformers.rs @@ -1,18 +1,20 @@ +use api_models::enums as api_enums; use base64::Engine; use common_utils::{ - ext_traits::{StringExt, ValueExt}, + ext_traits::{ByteSliceExt, StringExt, ValueExt}, pii::Email, }; -use error_stack::ResultExt; +use error_stack::{IntoReport, ResultExt}; +use masking::ExposeInterface; use serde::{Deserialize, Serialize}; use crate::{ - connector::utils, + connector::utils::{self, RouterData}, consts, core::errors, pii::Secret, types::{self, api, storage::enums, transformers::ForeignTryFrom}, - utils::Encode, + utils::{Encode, OptionExt}, }; #[derive(Debug, Serialize, PartialEq)] @@ -26,6 +28,15 @@ pub struct BluesnapPaymentsRequest { three_d_secure: Option, } +#[derive(Debug, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BluesnapCreateWalletToken { + wallet_type: String, + validation_url: Secret, + domain_name: String, + display_name: Option, +} + #[derive(Debug, Serialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct BluesnapThreeDSecureInfo { @@ -74,6 +85,56 @@ pub enum BluesnapWalletTypes { ApplePay, } +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct EncodedPaymentToken { + billing_contact: BillingDetails, + token: ApplepayPaymentData, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BillingDetails { + country_code: Option, + address_lines: Option>>, + family_name: Option>, + given_name: Option>, + postal_code: Option>, +} + +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ApplepayPaymentData { + payment_data: ApplePayEncodedPaymentData, + payment_method: ApplepayPaymentMethod, + transaction_identifier: String, +} + +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ApplepayPaymentMethod { + display_name: String, + network: String, + #[serde(rename = "type")] + pm_type: String, +} + +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct ApplePayEncodedPaymentData { + data: String, + header: Option, + signature: String, + version: String, +} + +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ApplepayHeader { + ephemeral_public_key: Secret, + public_key_hash: Secret, + transaction_id: Secret, +} + impl TryFrom<&types::PaymentsAuthorizeRouterData> for BluesnapPaymentsRequest { type Error = error_stack::Report; fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { @@ -104,13 +165,64 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for BluesnapPaymentsRequest { })) } api_models::payments::WalletData::ApplePay(payment_method_data) => { - let apple_pay_object = - Encode::::encode_to_string_of_json( - &BluesnapApplePayObject { - token: payment_method_data, + let apple_pay_payment_data = consts::BASE64_ENGINE + .decode(payment_method_data.payment_data) + .into_report() + .change_context(errors::ConnectorError::ParsingFailed)?; + + let apple_pay_payment_data: ApplePayEncodedPaymentData = apple_pay_payment_data + [..] + .parse_struct("ApplePayEncodedPaymentData") + .change_context(errors::ConnectorError::ParsingFailed)?; + + let billing = item + .address + .billing + .to_owned() + .get_required_value("billing") + .change_context(errors::ConnectorError::MissingRequiredField { + field_name: "billing", + })?; + + let billing_address = billing + .address + .get_required_value("billing_address") + .change_context(errors::ConnectorError::MissingRequiredField { + field_name: "billing", + })?; + + let mut address = Vec::new(); + if let Some(add) = billing_address.line1.to_owned() { + address.push(add) + } + if let Some(add) = billing_address.line2.to_owned() { + address.push(add) + } + if let Some(add) = billing_address.line3.to_owned() { + address.push(add) + } + + let apple_pay_object = Encode::::encode_to_string_of_json( + &EncodedPaymentToken { + token: ApplepayPaymentData { + payment_data: apple_pay_payment_data, + payment_method: payment_method_data + .payment_method + .to_owned() + .into(), + transaction_identifier: payment_method_data.transaction_identifier, }, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + billing_contact: BillingDetails { + country_code: billing_address.country, + address_lines: Some(address), + family_name: billing_address.last_name.to_owned(), + given_name: billing_address.first_name.to_owned(), + postal_code: billing_address.zip, + }, + }, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(PaymentMethodDetails::Wallet(BluesnapWallet { wallet_type: BluesnapWalletTypes::ApplePay, encoded_payment_token: consts::BASE64_ENGINE.encode(apple_pay_object), @@ -134,6 +246,94 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for BluesnapPaymentsRequest { } } +impl From for ApplepayPaymentMethod { + fn from(item: api_models::payments::ApplepayPaymentMethod) -> Self { + Self { + display_name: item.display_name, + network: item.network, + pm_type: item.pm_type, + } + } +} + +impl TryFrom<&types::PaymentsSessionRouterData> for BluesnapCreateWalletToken { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsSessionRouterData) -> Result { + let apple_pay_metadata = item.get_connector_meta()?.expose(); + let applepay_metadata = apple_pay_metadata + .parse_value::( + "ApplepaySessionTokenData", + ) + .change_context(errors::ConnectorError::ParsingFailed)?; + Ok(Self { + wallet_type: "APPLE_PAY".to_string(), + validation_url: consts::APPLEPAY_VALIDATION_URL.to_string().into(), + domain_name: applepay_metadata.data.session_token_data.initiative_context, + display_name: Some(applepay_metadata.data.session_token_data.display_name), + }) + } +} + +impl TryFrom> + for types::PaymentsSessionRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::PaymentsSessionResponseRouterData, + ) -> Result { + let response = &item.response; + + let wallet_token = consts::BASE64_ENGINE + .decode(response.wallet_token.clone().expose()) + .into_report() + .change_context(errors::ConnectorError::ParsingFailed)?; + + let session_response: api_models::payments::ApplePaySessionResponse = wallet_token[..] + .parse_struct("ApplePayResponse") + .change_context(errors::ConnectorError::ParsingFailed)?; + + let metadata = item.data.get_connector_meta()?.expose(); + let applepay_metadata = metadata + .parse_value::( + "ApplepaySessionTokenData", + ) + .change_context(errors::ConnectorError::ParsingFailed)?; + + Ok(Self { + response: Ok(types::PaymentsResponseData::SessionResponse { + session_token: types::api::SessionToken::ApplePay(Box::new( + api_models::payments::ApplepaySessionTokenResponse { + session_token_data: session_response, + payment_request_data: api_models::payments::ApplePayPaymentRequest { + country_code: item.data.get_billing_country()?, + currency_code: item.data.request.currency.to_string(), + total: api_models::payments::AmountInfo { + label: applepay_metadata.data.payment_request_data.label, + total_type: "final".to_string(), + amount: item.data.request.amount.to_string(), + }, + merchant_capabilities: applepay_metadata + .data + .payment_request_data + .merchant_capabilities, + supported_networks: applepay_metadata + .data + .payment_request_data + .supported_networks, + merchant_identifier: applepay_metadata + .data + .session_token_data + .merchant_identifier, + }, + connector: "bluesnap".to_string(), + }, + )), + }), + ..item.data + }) + } +} + impl TryFrom<&types::PaymentsCompleteAuthorizeRouterData> for BluesnapPaymentsRequest { type Error = error_stack::Report; fn try_from(item: &types::PaymentsCompleteAuthorizeRouterData) -> Result { @@ -374,6 +574,13 @@ pub struct BluesnapPaymentsResponse { card_transaction_type: BluesnapTxnType, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BluesnapWalletTokenResponse { + wallet_type: String, + wallet_token: Secret, +} + #[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Refund { diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index e9076b881c..08b6f891ec 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -27,3 +27,7 @@ pub(crate) const BASE64_ENGINE_URL_SAFE: base64::engine::GeneralPurpose = pub(crate) const API_KEY_LENGTH: usize = 64; pub(crate) const PUB_SUB_CHANNEL: &str = "hyperswitch_invalidate"; + +// Apple Pay validation url +pub(crate) const APPLEPAY_VALIDATION_URL: &str = + "https://apple-pay-gateway-cert.apple.com/paymentservices/startSession"; diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index 965f661102..6ac9616c4c 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -376,11 +376,13 @@ where for (connector, payment_method_type, business_sub_label) in connector_and_supporting_payment_method_type { - match api::ConnectorData::get_connector_by_name( - connectors, - &connector, - api::GetToken::from(payment_method_type), - ) { + let connector_type = get_connector_type_for_session_token( + payment_method_type, + request, + connector.to_owned(), + ); + match api::ConnectorData::get_connector_by_name(connectors, &connector, connector_type) + { Ok(connector_data) => session_connector_data.push(api::SessionConnectorData { payment_method_type, connector: connector_data, @@ -407,3 +409,19 @@ impl From for api::GetToken { } } } + +pub fn get_connector_type_for_session_token( + payment_method_type: api_models::enums::PaymentMethodType, + _request: &api::PaymentsSessionRequest, + connector: String, +) -> api::GetToken { + if payment_method_type == api_models::enums::PaymentMethodType::ApplePay { + if connector == *"bluesnap" { + api::GetToken::Connector + } else { + api::GetToken::ApplePayMetadata + } + } else { + api::GetToken::from(payment_method_type) + } +} diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index df981b9935..3b6e1f5019 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -15,6 +15,7 @@ use std::marker::PhantomData; pub use api_models::enums::Connector; use common_utils::{pii, pii::Email}; use error_stack::{IntoReport, ResultExt}; +use masking::Secret; use self::{api::payments, storage::enums as storage_enums}; pub use crate::core::payments::PaymentAddress; @@ -244,7 +245,7 @@ pub struct AuthorizeSessionTokenData { pub struct ConnectorCustomerData { pub description: Option, pub email: Option, - pub phone: Option>, + pub phone: Option>, pub name: Option, }