Skip to content

Commit

Permalink
feat(connector): [Bluesnap] Add support for ApplePay (#1178)
Browse files Browse the repository at this point in the history
  • Loading branch information
Sangamesh26 authored May 17, 2023
1 parent ed22b2a commit 919c03e
Show file tree
Hide file tree
Showing 5 changed files with 323 additions and 17 deletions.
78 changes: 77 additions & 1 deletion crates/router/src/connector/bluesnap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Expand Down
227 changes: 217 additions & 10 deletions crates/router/src/connector/bluesnap/transformers.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -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 {
Expand Down Expand Up @@ -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> {
Expand Down Expand Up @@ -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),
Expand All @@ -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> {
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions crates/router/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Loading

0 comments on commit 919c03e

Please sign in to comment.