feat(connector): [TrustPay] Add Google Pay support (#1515)

Co-authored-by: Sanchith Hegde <sanchith.hegde@juspay.in>
This commit is contained in:
Sangamesh Kulkarni
2023-07-01 17:15:06 +05:30
committed by GitHub
parent dd4ba63cc4
commit 47cd08a0b0
10 changed files with 717 additions and 378 deletions

View File

@ -17,7 +17,11 @@ use crate::{
consts,
core::errors,
pii::Secret,
types::{self, api, storage::enums, transformers::ForeignTryFrom},
types::{
self, api,
storage::enums,
transformers::{ForeignInto, ForeignTryFrom},
},
utils::{Encode, OptionExt},
};
@ -349,7 +353,7 @@ impl TryFrom<types::PaymentsSessionResponseRouterData<BluesnapWalletTokenRespons
),
payment_request_data: Some(api_models::payments::ApplePayPaymentRequest {
country_code: item.data.get_billing_country()?,
currency_code: item.data.request.currency.to_string(),
currency_code: item.data.request.currency.foreign_into(),
total: api_models::payments::AmountInfo {
label: applepay_metadata.data.payment_request_data.label,
total_type: Some("final".to_string()),

View File

@ -2,7 +2,7 @@ use std::collections::HashMap;
use api_models::payments::BankRedirectData;
use common_utils::{errors::CustomResult, pii};
use error_stack::{IntoReport, ResultExt};
use error_stack::{report, IntoReport, ResultExt};
use masking::Secret;
use reqwest::Url;
use serde::{Deserialize, Serialize};
@ -823,22 +823,25 @@ pub struct TrustpayCreateIntentRequest {
pub currency: String,
// If true, Apple Pay will be initialized
pub init_apple_pay: Option<bool>,
}
impl TryFrom<&types::PaymentsSessionRouterData> for TrustpayCreateIntentRequest {
type Error = Error;
fn try_from(item: &types::PaymentsSessionRouterData) -> Result<Self, Self::Error> {
Ok(Self {
amount: item.request.amount.to_string(),
currency: item.request.currency.to_string(),
init_apple_pay: Some(true),
})
}
// If true, Google pay will be initialized
pub init_google_pay: Option<bool>,
}
impl TryFrom<&types::PaymentsPreProcessingRouterData> for TrustpayCreateIntentRequest {
type Error = Error;
fn try_from(item: &types::PaymentsPreProcessingRouterData) -> Result<Self, Self::Error> {
let is_apple_pay = item
.request
.payment_method_type
.as_ref()
.map(|pmt| matches!(pmt, storage_models::enums::PaymentMethodType::ApplePay));
let is_google_pay = item
.request
.payment_method_type
.as_ref()
.map(|pmt| matches!(pmt, storage_models::enums::PaymentMethodType::GooglePay));
Ok(Self {
amount: item
.request
@ -856,90 +859,218 @@ impl TryFrom<&types::PaymentsPreProcessingRouterData> for TrustpayCreateIntentRe
field_name: "currency",
})?
.to_string(),
init_apple_pay: Some(true),
init_apple_pay: is_apple_pay,
init_google_pay: is_google_pay,
})
}
}
#[derive(Default, Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TrustpayCreateIntentResponse {
// TrustPay's authorization secrets used by client
pub secrets: SdkSecretInfo,
// Data object to be used for Apple Pay
pub apple_init_result_data: TrustpayApplePayResponse,
// Data object to be used for Apple Pay or Google Pay
#[serde(flatten)]
pub init_result_data: InitResultData,
// Unique operation/transaction identifier
pub instance_id: String,
}
#[derive(Default, Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum InitResultData {
AppleInitResultData(TrustpayApplePayResponse),
GoogleInitResultData(TrustpayGooglePayResponse),
}
#[derive(Clone, Default, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GooglePayTransactionInfo {
pub country_code: api_models::enums::CountryAlpha2,
pub currency_code: api_models::enums::Currency,
pub total_price_status: String,
pub total_price: String,
}
#[derive(Clone, Default, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GooglePayMerchantInfo {
pub merchant_name: String,
}
#[derive(Clone, Default, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GooglePayAllowedPaymentMethods {
#[serde(rename = "type")]
pub payment_method_type: String,
pub parameters: GpayAllowedMethodsParameters,
pub tokenization_specification: GpayTokenizationSpecification,
}
#[derive(Clone, Default, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GpayTokenParameters {
pub gateway: String,
pub gateway_merchant_id: String,
}
#[derive(Clone, Default, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GpayTokenizationSpecification {
#[serde(rename = "type")]
pub token_specification_type: String,
pub parameters: GpayTokenParameters,
}
#[derive(Clone, Default, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GpayAllowedMethodsParameters {
pub allowed_auth_methods: Vec<String>,
pub allowed_card_networks: Vec<String>,
}
#[derive(Clone, Default, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TrustpayGooglePayResponse {
pub merchant_info: GooglePayMerchantInfo,
pub allowed_payment_methods: Vec<GooglePayAllowedPaymentMethods>,
pub transaction_info: GooglePayTransactionInfo,
}
#[derive(Clone, Default, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SdkSecretInfo {
pub display: Secret<String>,
pub payment: Secret<String>,
}
#[derive(Default, Debug, Deserialize)]
#[derive(Clone, Default, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TrustpayApplePayResponse {
pub country_code: api_models::enums::CountryAlpha2,
pub currency_code: String,
pub currency_code: api_models::enums::Currency,
pub supported_networks: Vec<String>,
pub merchant_capabilities: Vec<String>,
pub total: ApplePayTotalInfo,
}
#[derive(Default, Debug, Deserialize)]
#[derive(Clone, Default, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApplePayTotalInfo {
pub label: String,
pub amount: String,
}
impl<F, T>
impl<F>
TryFrom<
types::ResponseRouterData<F, TrustpayCreateIntentResponse, T, types::PaymentsResponseData>,
> for types::RouterData<F, T, types::PaymentsResponseData>
types::ResponseRouterData<
F,
TrustpayCreateIntentResponse,
types::PaymentsPreProcessingData,
types::PaymentsResponseData,
>,
> for types::RouterData<F, types::PaymentsPreProcessingData, types::PaymentsResponseData>
{
type Error = Error;
fn try_from(
item: types::ResponseRouterData<
F,
TrustpayCreateIntentResponse,
T,
types::PaymentsPreProcessingData,
types::PaymentsResponseData,
>,
) -> Result<Self, Self::Error> {
let response = item.response;
let create_intent_response = item.response.init_result_data.to_owned();
let secrets = item.response.secrets.to_owned();
let instance_id = item.response.instance_id.to_owned();
let pmt = utils::PaymentsPreProcessingData::get_payment_method_type(&item.data.request)?;
Ok(Self {
response: Ok(types::PaymentsResponseData::PreProcessingResponse {
connector_metadata: None,
pre_processing_id: types::PreprocessingResponseId::ConnectorTransactionId(
response.instance_id,
),
session_token: Some(types::api::SessionToken::ApplePay(Box::new(
api_models::payments::ApplepaySessionTokenResponse {
session_token_data:
api_models::payments::ApplePaySessionResponse::ThirdPartySdk(
api_models::payments::ThirdPartySdkSessionResponse {
secrets: response.secrets.into(),
},
),
payment_request_data: Some(api_models::payments::ApplePayPaymentRequest {
country_code: response.apple_init_result_data.country_code,
currency_code: response.apple_init_result_data.currency_code.clone(),
supported_networks: response
.apple_init_result_data
.supported_networks
.clone(),
merchant_capabilities: response
.apple_init_result_data
.merchant_capabilities
.clone(),
total: response.apple_init_result_data.total.into(),
merchant_identifier: None,
}),
match (pmt, create_intent_response) {
(
storage_models::enums::PaymentMethodType::ApplePay,
InitResultData::AppleInitResultData(apple_pay_response),
) => get_apple_pay_session(instance_id, &secrets, apple_pay_response, item),
(
storage_models::enums::PaymentMethodType::GooglePay,
InitResultData::GoogleInitResultData(google_pay_response),
) => get_google_pay_session(instance_id, &secrets, google_pay_response, item),
_ => Err(report!(errors::ConnectorError::InvalidWallet)),
}
}
}
pub fn get_apple_pay_session<F, T>(
instance_id: String,
secrets: &SdkSecretInfo,
apple_pay_init_result: TrustpayApplePayResponse,
item: types::ResponseRouterData<
F,
TrustpayCreateIntentResponse,
T,
types::PaymentsResponseData,
>,
) -> Result<
types::RouterData<F, T, types::PaymentsResponseData>,
error_stack::Report<errors::ConnectorError>,
> {
Ok(types::RouterData {
response: Ok(types::PaymentsResponseData::PreProcessingResponse {
connector_metadata: None,
pre_processing_id: types::PreprocessingResponseId::ConnectorTransactionId(instance_id),
session_token: Some(types::api::SessionToken::ApplePay(Box::new(
api_models::payments::ApplepaySessionTokenResponse {
session_token_data:
api_models::payments::ApplePaySessionResponse::ThirdPartySdk(
api_models::payments::ThirdPartySdkSessionResponse {
secrets: secrets.to_owned().into(),
},
),
payment_request_data: Some(api_models::payments::ApplePayPaymentRequest {
country_code: apple_pay_init_result.country_code,
currency_code: apple_pay_init_result.currency_code,
supported_networks: apple_pay_init_result.supported_networks.clone(),
merchant_capabilities: apple_pay_init_result.merchant_capabilities.clone(),
total: apple_pay_init_result.total.into(),
merchant_identifier: None,
}),
connector: "trustpay".to_string(),
delayed_session_token: true,
sdk_next_action: {
api_models::payments::SdkNextAction {
next_action: api_models::payments::NextActionCall::Sync,
}
},
},
))),
}),
// We don't get status from TrustPay but status should be pending by default for session response
status: storage_models::enums::AttemptStatus::Pending,
..item.data
})
}
pub fn get_google_pay_session<F, T>(
instance_id: String,
secrets: &SdkSecretInfo,
google_pay_init_result: TrustpayGooglePayResponse,
item: types::ResponseRouterData<
F,
TrustpayCreateIntentResponse,
T,
types::PaymentsResponseData,
>,
) -> Result<
types::RouterData<F, T, types::PaymentsResponseData>,
error_stack::Report<errors::ConnectorError>,
> {
Ok(types::RouterData {
response: Ok(types::PaymentsResponseData::PreProcessingResponse {
connector_metadata: None,
pre_processing_id: types::PreprocessingResponseId::ConnectorTransactionId(instance_id),
session_token: Some(types::api::SessionToken::GooglePay(Box::new(
api_models::payments::GpaySessionTokenResponse::GooglePaySession(
api_models::payments::GooglePaySessionResponse {
connector: "trustpay".to_string(),
delayed_session_token: true,
sdk_next_action: {
@ -947,12 +1078,79 @@ impl<F, T>
next_action: api_models::payments::NextActionCall::Sync,
}
},
merchant_info: google_pay_init_result.merchant_info.into(),
allowed_payment_methods: google_pay_init_result
.allowed_payment_methods
.into_iter()
.map(Into::into)
.collect(),
transaction_info: google_pay_init_result.transaction_info.into(),
secrets: Some((*secrets).clone().into()),
},
))),
}),
status: storage_models::enums::AttemptStatus::Pending,
..item.data
})
),
))),
}),
// We don't get status from TrustPay but status should be pending by default for session response
status: storage_models::enums::AttemptStatus::Pending,
..item.data
})
}
impl From<GooglePayTransactionInfo> for api_models::payments::GpayTransactionInfo {
fn from(value: GooglePayTransactionInfo) -> Self {
Self {
country_code: value.country_code,
currency_code: value.currency_code,
total_price_status: value.total_price_status,
total_price: value.total_price,
}
}
}
impl From<GooglePayMerchantInfo> for api_models::payments::GpayMerchantInfo {
fn from(value: GooglePayMerchantInfo) -> Self {
Self {
merchant_name: value.merchant_name,
}
}
}
impl From<GooglePayAllowedPaymentMethods> for api_models::payments::GpayAllowedPaymentMethods {
fn from(value: GooglePayAllowedPaymentMethods) -> Self {
Self {
payment_method_type: value.payment_method_type,
parameters: value.parameters.into(),
tokenization_specification: value.tokenization_specification.into(),
}
}
}
impl From<GpayAllowedMethodsParameters> for api_models::payments::GpayAllowedMethodsParameters {
fn from(value: GpayAllowedMethodsParameters) -> Self {
Self {
allowed_auth_methods: value.allowed_auth_methods,
allowed_card_networks: value.allowed_card_networks,
}
}
}
impl From<GpayTokenizationSpecification> for api_models::payments::GpayTokenizationSpecification {
fn from(value: GpayTokenizationSpecification) -> Self {
Self {
token_specification_type: value.token_specification_type,
parameters: value.parameters.into(),
}
}
}
impl From<GpayTokenParameters> for api_models::payments::GpayTokenParameters {
fn from(value: GpayTokenParameters) -> Self {
Self {
gateway: value.gateway,
gateway_merchant_id: Some(value.gateway_merchant_id),
stripe_version: None,
stripe_publishable_key: None,
}
}
}

View File

@ -170,12 +170,18 @@ impl<Flow, Request, Response> RouterData for types::RouterData<Flow, Request, Re
pub trait PaymentsPreProcessingData {
fn get_email(&self) -> Result<Email, Error>;
fn get_payment_method_type(&self) -> Result<storage_models::enums::PaymentMethodType, Error>;
}
impl PaymentsPreProcessingData for types::PaymentsPreProcessingData {
fn get_email(&self) -> Result<Email, Error> {
self.email.clone().ok_or_else(missing_field_err("email"))
}
fn get_payment_method_type(&self) -> Result<storage_models::enums::PaymentMethodType, Error> {
self.payment_method_type
.to_owned()
.ok_or_else(missing_field_err("payment_method_type"))
}
}
pub trait PaymentsAuthorizeRequestData {

View File

@ -345,6 +345,7 @@ impl TryFrom<types::PaymentsAuthorizeData> for types::PaymentsPreProcessingData
email: data.email,
currency: Some(data.currency),
amount: Some(data.amount),
payment_method_type: data.payment_method_type,
})
}
}

View File

@ -13,7 +13,7 @@ use crate::{
headers, logger,
routes::{self, metrics},
services,
types::{self, api, domain},
types::{self, api, domain, transformers::ForeignInto},
utils::{self, OptionExt},
};
@ -154,14 +154,7 @@ async fn create_applepay_session_token(
router_data: &types::PaymentsSessionRouterData,
connector: &api::ConnectorData,
) -> RouterResult<types::PaymentsSessionRouterData> {
let connectors_with_delayed_response = &state
.conf
.delayed_session_response
.connectors_with_delayed_session_response;
let connector_name = connector.connector_name;
let delayed_response = connectors_with_delayed_response.contains(&connector_name);
let delayed_response = is_session_response_delayed(state, connector);
if delayed_response {
let delayed_response_apple_pay_session =
Some(payment_types::ApplePaySessionResponse::NoSessionResponse);
@ -169,7 +162,7 @@ async fn create_applepay_session_token(
router_data,
delayed_response_apple_pay_session,
None, // Apple pay payment request will be none for delayed session response
connector_name.to_string(),
connector.connector_name.to_string(),
delayed_response,
payment_types::NextActionCall::Confirm,
)
@ -197,7 +190,7 @@ async fn create_applepay_session_token(
.change_context(errors::ApiErrorResponse::MissingRequiredField {
field_name: "country_code",
})?,
currency_code: router_data.request.currency.to_string(),
currency_code: router_data.request.currency.foreign_into(),
total: amount_info,
merchant_capabilities: applepay_metadata
.data
@ -249,7 +242,7 @@ async fn create_applepay_session_token(
router_data,
session_response,
Some(applepay_payment_request),
connector_name.to_string(),
connector.connector_name.to_string(),
delayed_response,
payment_types::NextActionCall::Confirm,
)
@ -289,53 +282,90 @@ fn create_apple_pay_session_response(
}
fn create_gpay_session_token(
state: &routes::AppState,
router_data: &types::PaymentsSessionRouterData,
connector: &api::ConnectorData,
) -> RouterResult<types::PaymentsSessionRouterData> {
let connector_metadata = router_data.connector_meta_data.clone();
let delayed_response = is_session_response_delayed(state, connector);
let gpay_data = connector_metadata
.clone()
.parse_value::<payment_types::GpaySessionTokenData>("GpaySessionTokenData")
.change_context(errors::ConnectorError::NoConnectorMetaData)
.attach_printable(format!(
"cannot parse gpay metadata from the given value {connector_metadata:?}"
))
.change_context(errors::ApiErrorResponse::InvalidDataFormat {
field_name: "connector_metadata".to_string(),
expected_format: "gpay_metadata_format".to_string(),
})?;
if delayed_response {
Ok(types::PaymentsSessionRouterData {
response: Ok(types::PaymentsResponseData::SessionResponse {
session_token: payment_types::SessionToken::GooglePay(Box::new(
payment_types::GpaySessionTokenResponse::ThirdPartyResponse(
payment_types::GooglePayThirdPartySdk {
delayed_session_token: true,
connector: connector.connector_name.to_string(),
sdk_next_action: payment_types::SdkNextAction {
next_action: payment_types::NextActionCall::Confirm,
},
},
),
)),
}),
..router_data.clone()
})
} else {
let gpay_data = connector_metadata
.clone()
.parse_value::<payment_types::GpaySessionTokenData>("GpaySessionTokenData")
.change_context(errors::ConnectorError::NoConnectorMetaData)
.attach_printable(format!(
"cannot parse gpay metadata from the given value {connector_metadata:?}"
))
.change_context(errors::ApiErrorResponse::InvalidDataFormat {
field_name: "connector_metadata".to_string(),
expected_format: "gpay_metadata_format".to_string(),
})?;
let session_data = router_data.request.clone();
let transaction_info = payment_types::GpayTransactionInfo {
country_code: session_data.country.unwrap_or_default(),
currency_code: router_data.request.currency.to_string(),
total_price_status: "Final".to_string(),
total_price: utils::to_currency_base_unit(
router_data.request.amount,
router_data.request.currency,
)
.attach_printable("Cannot convert given amount to base currency denomination".to_string())
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "amount",
})?,
};
let session_data = router_data.request.clone();
let transaction_info = payment_types::GpayTransactionInfo {
country_code: session_data.country.unwrap_or_default(),
currency_code: router_data.request.currency.foreign_into(),
total_price_status: "Final".to_string(),
total_price: utils::to_currency_base_unit(
router_data.request.amount,
router_data.request.currency,
)
.attach_printable(
"Cannot convert given amount to base currency denomination".to_string(),
)
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "amount",
})?,
};
let response_router_data = types::PaymentsSessionRouterData {
response: Ok(types::PaymentsResponseData::SessionResponse {
session_token: payment_types::SessionToken::GooglePay(Box::new(
payment_types::GpaySessionTokenResponse {
merchant_info: gpay_data.data.merchant_info,
allowed_payment_methods: gpay_data.data.allowed_payment_methods,
transaction_info,
connector: connector.connector_name.to_string(),
},
)),
}),
..router_data.clone()
};
Ok(types::PaymentsSessionRouterData {
response: Ok(types::PaymentsResponseData::SessionResponse {
session_token: payment_types::SessionToken::GooglePay(Box::new(
payment_types::GpaySessionTokenResponse::GooglePaySession(
payment_types::GooglePaySessionResponse {
merchant_info: gpay_data.data.merchant_info,
allowed_payment_methods: gpay_data.data.allowed_payment_methods,
transaction_info,
connector: connector.connector_name.to_string(),
sdk_next_action: payment_types::SdkNextAction {
next_action: payment_types::NextActionCall::Confirm,
},
delayed_session_token: false,
secrets: None,
},
),
)),
}),
..router_data.clone()
})
}
}
Ok(response_router_data)
fn is_session_response_delayed(state: &routes::AppState, connector: &api::ConnectorData) -> bool {
let connectors_with_delayed_response = &state
.conf
.delayed_session_response
.connectors_with_delayed_session_response;
connectors_with_delayed_response.contains(&connector.connector_name)
}
fn log_session_response_if_error(
@ -360,7 +390,7 @@ impl types::PaymentsSessionRouterData {
call_connector_action: payments::CallConnectorAction,
) -> RouterResult<Self> {
match connector.get_token {
api::GetToken::GpayMetadata => create_gpay_session_token(self, connector),
api::GetToken::GpayMetadata => create_gpay_session_token(state, self, connector),
api::GetToken::ApplePayMetadata => {
create_applepay_session_token(state, self, connector).await
}

View File

@ -945,6 +945,7 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsPreProce
email: payment_data.email,
currency: Some(payment_data.currency),
amount: Some(payment_data.amount.into()),
payment_method_type: payment_data.payment_attempt.payment_method_type,
})
}
}

View File

@ -219,6 +219,7 @@ Never share your secret api keys. Keep them guarded and secure.
api_models::payments::GpayTokenParameters,
api_models::payments::GpayTransactionInfo,
api_models::payments::GpaySessionTokenResponse,
api_models::payments::GooglePayThirdPartySdkData,
api_models::payments::KlarnaSessionTokenResponse,
api_models::payments::PaypalSessionTokenResponse,
api_models::payments::ApplepaySessionTokenResponse,
@ -242,6 +243,8 @@ Never share your secret api keys. Keep them guarded and secure.
api_models::payments::ApplePayRedirectData,
api_models::payments::ApplePayThirdPartySdkData,
api_models::payments::GooglePayRedirectData,
api_models::payments::GooglePayThirdPartySdk,
api_models::payments::GooglePaySessionResponse,
api_models::payments::SepaBankTransferInstructions,
api_models::payments::BacsBankTransferInstructions,
api_models::payments::RedirectResponse,

View File

@ -271,6 +271,7 @@ pub struct PaymentsPreProcessingData {
pub email: Option<Email>,
pub currency: Option<storage_enums::Currency>,
pub amount: Option<i64>,
pub payment_method_type: Option<storage_enums::PaymentMethodType>,
}
#[derive(Debug, Clone)]