mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-10-31 01:57:45 +08:00 
			
		
		
		
	feat(connector): [Bluesnap] Add support for ApplePay (#1178)
This commit is contained in:
		 Sangamesh Kulkarni
					Sangamesh Kulkarni
				
			
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			 GitHub
						GitHub
					
				
			
						parent
						
							ed22b2af76
						
					
				
				
					commit
					919c03e679
				
			| @ -494,7 +494,83 @@ impl api::PaymentSession for Bluesnap {} | ||||
| impl ConnectorIntegration<api::Session, types::PaymentsSessionData, types::PaymentsResponseData> | ||||
|     for Bluesnap | ||||
| { | ||||
|     //TODO: implement sessions flow | ||||
|     fn get_headers( | ||||
|         &self, | ||||
|         req: &types::PaymentsSessionRouterData, | ||||
|         connectors: &settings::Connectors, | ||||
|     ) -> CustomResult<Vec<(String, String)>, 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<String, errors::ConnectorError> { | ||||
|         Ok(format!( | ||||
|             "{}{}", | ||||
|             self.base_url(connectors), | ||||
|             "services/2/wallets" | ||||
|         )) | ||||
|     } | ||||
|  | ||||
|     fn get_request_body( | ||||
|         &self, | ||||
|         req: &types::PaymentsSessionRouterData, | ||||
|     ) -> CustomResult<Option<String>, errors::ConnectorError> { | ||||
|         let connector_req = bluesnap::BluesnapCreateWalletToken::try_from(req)?; | ||||
|         let bluesnap_req = | ||||
|             utils::Encode::<bluesnap::BluesnapCreateWalletToken>::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<Option<services::Request>, 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<types::PaymentsSessionRouterData, errors::ConnectorError> { | ||||
|         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<ErrorResponse, errors::ConnectorError> { | ||||
|         self.build_error_response(res) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl api::PaymentAuthorize for Bluesnap {} | ||||
|  | ||||
| @ -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<BluesnapThreeDSecureInfo>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize, PartialEq)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct BluesnapCreateWalletToken { | ||||
|     wallet_type: String, | ||||
|     validation_url: Secret<String>, | ||||
|     domain_name: String, | ||||
|     display_name: Option<String>, | ||||
| } | ||||
|  | ||||
| #[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<api_enums::CountryAlpha2>, | ||||
|     address_lines: Option<Vec<Secret<String>>>, | ||||
|     family_name: Option<Secret<String>>, | ||||
|     given_name: Option<Secret<String>>, | ||||
|     postal_code: Option<Secret<String>>, | ||||
| } | ||||
|  | ||||
| #[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<ApplepayHeader>, | ||||
|     signature: String, | ||||
|     version: String, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct ApplepayHeader { | ||||
|     ephemeral_public_key: Secret<String>, | ||||
|     public_key_hash: Secret<String>, | ||||
|     transaction_id: Secret<String>, | ||||
| } | ||||
|  | ||||
| impl TryFrom<&types::PaymentsAuthorizeRouterData> for BluesnapPaymentsRequest { | ||||
|     type Error = error_stack::Report<errors::ConnectorError>; | ||||
|     fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> { | ||||
| @ -104,13 +165,64 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for BluesnapPaymentsRequest { | ||||
|                     })) | ||||
|                 } | ||||
|                 api_models::payments::WalletData::ApplePay(payment_method_data) => { | ||||
|                     let apple_pay_object = | ||||
|                         Encode::<BluesnapApplePayObject>::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::<EncodedPaymentToken>::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<api_models::payments::ApplepayPaymentMethod> 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<errors::ConnectorError>; | ||||
|     fn try_from(item: &types::PaymentsSessionRouterData) -> Result<Self, Self::Error> { | ||||
|         let apple_pay_metadata = item.get_connector_meta()?.expose(); | ||||
|         let applepay_metadata = apple_pay_metadata | ||||
|             .parse_value::<api_models::payments::ApplepaySessionTokenData>( | ||||
|                 "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<types::PaymentsSessionResponseRouterData<BluesnapWalletTokenResponse>> | ||||
|     for types::PaymentsSessionRouterData | ||||
| { | ||||
|     type Error = error_stack::Report<errors::ConnectorError>; | ||||
|     fn try_from( | ||||
|         item: types::PaymentsSessionResponseRouterData<BluesnapWalletTokenResponse>, | ||||
|     ) -> Result<Self, Self::Error> { | ||||
|         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::<api_models::payments::ApplepaySessionTokenData>( | ||||
|                 "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<errors::ConnectorError>; | ||||
|     fn try_from(item: &types::PaymentsCompleteAuthorizeRouterData) -> Result<Self, Self::Error> { | ||||
| @ -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<String>, | ||||
| } | ||||
|  | ||||
| #[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct Refund { | ||||
|  | ||||
| @ -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"; | ||||
|  | ||||
| @ -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<api_models::enums::PaymentMethodType> 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) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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<String>, | ||||
|     pub email: Option<Email>, | ||||
|     pub phone: Option<masking::Secret<String>>, | ||||
|     pub phone: Option<Secret<String>>, | ||||
|     pub name: Option<String>, | ||||
| } | ||||
|  | ||||
|  | ||||
		Reference in New Issue
	
	Block a user