feat(connector): [Bluesnap] Add support for ApplePay (#1178)

This commit is contained in:
Sangamesh Kulkarni
2023-05-17 18:40:20 +05:30
committed by GitHub
parent ed22b2af76
commit 919c03e679
5 changed files with 323 additions and 17 deletions

View File

@ -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 {}

View File

@ -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,
},
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 {

View File

@ -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";

View File

@ -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)
}
}

View File

@ -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>,
}