feat(connector) : [Globalpay]add mandates and bank redirects support for globalpay (#830)

Signed-off-by: chikke srujan <121822803+srujanchikke@users.noreply.github.com>
This commit is contained in:
chikke srujan
2023-04-11 17:46:01 +05:30
committed by GitHub
parent 6188d51579
commit ce912dd852
6 changed files with 206 additions and 78 deletions

View File

@ -18,7 +18,10 @@ use self::{
use crate::{ use crate::{
configs::settings, configs::settings,
connector::utils as conn_utils, connector::utils as conn_utils,
core::errors::{self, CustomResult}, core::{
errors::{self, CustomResult},
payments,
},
db, headers, db, headers,
services::{self, ConnectorIntegration}, services::{self, ConnectorIntegration},
types::{ types::{
@ -882,3 +885,27 @@ impl api::IncomingWebhook for Globalpay {
Ok(res_json) Ok(res_json)
} }
} }
impl services::ConnectorRedirectResponse for Globalpay {
fn get_flow_type(
&self,
query_params: &str,
_json_payload: Option<Value>,
_action: services::PaymentAction,
) -> CustomResult<payments::CallConnectorAction, errors::ConnectorError> {
let query = serde_urlencoded::from_str::<response::GlobalpayRedirectResponse>(query_params)
.into_report()
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
Ok(query.status.map_or(
payments::CallConnectorAction::Trigger,
|status| match status {
response::GlobalpayPaymentStatus::Captured => {
payments::CallConnectorAction::StatusUpdate(
storage_models::enums::AttemptStatus::from(status),
)
}
_ => payments::CallConnectorAction::Trigger,
},
))
}
}

View File

@ -332,9 +332,6 @@ pub struct Card {
pub chip_condition: Option<ChipCondition>, pub chip_condition: Option<ChipCondition>,
/// The numeric value printed on the physical card. /// The numeric value printed on the physical card.
pub cvv: Secret<String>, pub cvv: Secret<String>,
/// Card Verification Value Indicator sent by the Merchant indicating the CVV
/// availability.
pub cvv_indicator: CvvIndicator,
/// The 2 digit expiry date month of the card. /// The 2 digit expiry date month of the card.
pub expiry_month: Secret<String>, pub expiry_month: Secret<String>,
/// The 2 digit expiry date year of the card. /// The 2 digit expiry date year of the card.
@ -375,8 +372,6 @@ pub struct StoredCredential {
/// Indicates the transaction processing model being executed when using stored /// Indicates the transaction processing model being executed when using stored
/// credentials. /// credentials.
pub model: Option<Model>, pub model: Option<Model>,
/// The reason stored credentials are being used to to create a transaction.
pub reason: Option<Reason>,
/// Indicates the order of this transaction in the sequence of a planned repeating /// Indicates the order of this transaction in the sequence of a planned repeating
/// transaction processing model. /// transaction processing model.
pub sequence: Option<Sequence>, pub sequence: Option<Sequence>,
@ -578,6 +573,7 @@ pub enum ApmProvider {
Ideal, Ideal,
Paypal, Paypal,
Sofort, Sofort,
Eps,
Testpay, Testpay,
} }
@ -642,20 +638,6 @@ pub enum ChipCondition {
PrevSuccess, PrevSuccess,
} }
/// Card Verification Value Indicator sent by the Merchant indicating the CVV
/// availability.
#[derive(Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum CvvIndicator {
/// indicates the cvv is present but cannot be read.
Illegible,
/// indicates the cvv is not present on the card.
NotPresent,
#[default]
/// indicates the cvv is present.
Present,
}
/// Indicates whether the card is a debit or credit card. /// Indicates whether the card is a debit or credit card.
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")] #[serde(rename_all = "SCREAMING_SNAKE_CASE")]

View File

@ -1,5 +1,4 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url;
use super::requests; use super::requests;
@ -133,8 +132,6 @@ pub struct PaymentMethod {
pub message: Option<String>, pub message: Option<String>,
/// Result code from the payment method provider. /// Result code from the payment method provider.
pub result: Option<String>, pub result: Option<String>,
/// Redirect url for payment method provider
pub redirect_url: Option<Url>,
} }
/// Data associated with the response of an APM transaction. /// Data associated with the response of an APM transaction.
@ -156,7 +153,12 @@ pub struct Apm {
/// The reference the payment method provider created for the transaction. /// The reference the payment method provider created for the transaction.
pub provider_transaction_reference: Option<String>, pub provider_transaction_reference: Option<String>,
/// URL to redirect the payer from the merchant's system to the payment method's system. /// URL to redirect the payer from the merchant's system to the payment method's system.
pub redirect_url: Option<Url>, //1)paypal sends redirect_url as provider_redirect_url for require_customer_action
//2)bankredirects sends redirect_url as redirect_url for require_customer_action
//3)after completeauthorize in paypal it doesn't send redirect_url
//4)after customer action in bankredirects it sends empty string in redirect_url
#[serde(alias = "provider_redirect_url")]
pub redirect_url: Option<String>,
/// A string generated by the payment method to represent the session created on the payment /// A string generated by the payment method to represent the session created on the payment
/// method's platform to facilitate the creation of a transaction. /// method's platform to facilitate the creation of a transaction.
pub session_token: Option<String>, pub session_token: Option<String>,
@ -283,6 +285,7 @@ pub enum ApmProvider {
Ideal, Ideal,
Paypal, Paypal,
Sofort, Sofort,
Eps,
Testpay, Testpay,
} }
@ -416,3 +419,8 @@ pub enum GlobalpayWebhookStatus {
Declined, Declined,
Captured, Captured,
} }
#[derive(Debug, Serialize, Deserialize)]
pub struct GlobalpayRedirectResponse {
pub status: Option<GlobalpayPaymentStatus>,
}

View File

@ -1,17 +1,21 @@
use common_utils::crypto::{self, GenerateDigest}; use common_utils::crypto::{self, GenerateDigest};
use error_stack::ResultExt; use error_stack::{IntoReport, ResultExt};
use rand::distributions::DistString; use rand::distributions::DistString;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url;
use super::{ use super::{
requests::{self, GlobalpayPaymentsRequest, GlobalpayRefreshTokenRequest}, requests::{
self, ApmProvider, GlobalpayPaymentsRequest, GlobalpayRefreshTokenRequest, Initiator,
PaymentMethodData, StoredCredential,
},
response::{GlobalpayPaymentStatus, GlobalpayPaymentsResponse, GlobalpayRefreshTokenResponse}, response::{GlobalpayPaymentStatus, GlobalpayPaymentsResponse, GlobalpayRefreshTokenResponse},
}; };
use crate::{ use crate::{
connector::utils::{self, RouterData, WalletData}, connector::utils::{self, PaymentsAuthorizeRequestData, RouterData, WalletData},
consts, consts,
core::errors, core::errors,
services::{self}, services::{self, RedirectForm},
types::{self, api, storage::enums, ErrorResponse}, types::{self, api, storage::enums, ErrorResponse},
}; };
@ -26,46 +30,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for GlobalpayPaymentsRequest {
let metadata: GlobalPayMeta = let metadata: GlobalPayMeta =
utils::to_connector_meta_from_secret(item.connector_meta_data.clone())?; utils::to_connector_meta_from_secret(item.connector_meta_data.clone())?;
let account_name = metadata.account_name; let account_name = metadata.account_name;
let payment_method_data = match item.request.payment_method_data.clone() { let (initiator, stored_credential, brand_reference) = get_mandate_details(item)?;
api::PaymentMethodData::Card(ccard) => { let payment_method_data = get_payment_method_data(item, brand_reference)?;
requests::PaymentMethodData::Card(requests::Card {
number: ccard.card_number,
expiry_month: ccard.card_exp_month,
expiry_year: ccard.card_exp_year,
cvv: ccard.card_cvc,
account_type: None,
authcode: None,
avs_address: None,
avs_postal_code: None,
brand_reference: None,
chip_condition: None,
cvv_indicator: Default::default(),
funding: None,
pin_block: None,
tag: None,
track: None,
})
}
api::PaymentMethodData::Wallet(wallet_data) => match wallet_data {
api_models::payments::WalletData::PaypalRedirect(_) => {
requests::PaymentMethodData::Apm(requests::Apm {
provider: Some(requests::ApmProvider::Paypal),
})
}
api_models::payments::WalletData::GooglePay(_) => {
requests::PaymentMethodData::DigitalWallet(requests::DigitalWallet {
provider: Some(requests::DigitalWalletProvider::PayByGoogle),
payment_token: wallet_data.get_wallet_token_as_json()?,
})
}
_ => Err(errors::ConnectorError::NotImplemented(
"Payment methods".to_string(),
))?,
},
_ => Err(errors::ConnectorError::NotImplemented(
"Payment methods".to_string(),
))?,
};
Ok(Self { Ok(Self {
account_name, account_name,
amount: Some(item.request.amount.to_string()), amount: Some(item.request.amount.to_string()),
@ -87,7 +53,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for GlobalpayPaymentsRequest {
storage_mode: None, storage_mode: None,
}, },
notifications: Some(requests::Notifications { notifications: Some(requests::Notifications {
return_url: item.request.complete_authorize_url.clone(), return_url: get_return_url(item),
challenge_return_url: None, challenge_return_url: None,
decoupled_challenge_return_url: None, decoupled_challenge_return_url: None,
status_url: item.request.webhook_url.clone(), status_url: item.request.webhook_url.clone(),
@ -101,14 +67,14 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for GlobalpayPaymentsRequest {
description: None, description: None,
device: None, device: None,
gratuity_amount: None, gratuity_amount: None,
initiator: None, initiator,
ip_address: None, ip_address: None,
language: None, language: None,
lodging: None, lodging: None,
order: None, order: None,
payer_reference: None, payer_reference: None,
site_reference: None, site_reference: None,
stored_credential: None, stored_credential,
surcharge_amount: None, surcharge_amount: None,
total_capture_count: None, total_capture_count: None,
globalpay_payments_request_type: None, globalpay_payments_request_type: None,
@ -226,11 +192,12 @@ impl From<Option<enums::CaptureMethod>> for requests::CaptureMode {
fn get_payment_response( fn get_payment_response(
status: enums::AttemptStatus, status: enums::AttemptStatus,
response: GlobalpayPaymentsResponse, response: GlobalpayPaymentsResponse,
redirection_data: Option<RedirectForm>,
) -> Result<types::PaymentsResponseData, ErrorResponse> { ) -> Result<types::PaymentsResponseData, ErrorResponse> {
let redirection_data = response.payment_method.as_ref().and_then(|payment_method| { let mandate_reference = response.payment_method.as_ref().and_then(|pm| {
payment_method.redirect_url.as_ref().map(|redirect_url| { pm.card
services::RedirectForm::from((redirect_url.to_owned(), services::Method::Get)) .as_ref()
}) .and_then(|card| card.brand_reference.to_owned())
}); });
match status { match status {
enums::AttemptStatus::Failure => Err(ErrorResponse { enums::AttemptStatus::Failure => Err(ErrorResponse {
@ -243,7 +210,7 @@ fn get_payment_response(
_ => Ok(types::PaymentsResponseData::TransactionResponse { _ => Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(response.id), resource_id: types::ResponseId::ConnectorTransactionId(response.id),
redirection_data, redirection_data,
mandate_reference: None, mandate_reference,
connector_metadata: None, connector_metadata: None,
}), }),
} }
@ -263,9 +230,28 @@ impl<F, T>
>, >,
) -> Result<Self, Self::Error> { ) -> Result<Self, Self::Error> {
let status = enums::AttemptStatus::from(item.response.status); let status = enums::AttemptStatus::from(item.response.status);
let redirect_url = item
.response
.payment_method
.as_ref()
.and_then(|payment_method| {
payment_method
.apm
.as_ref()
.and_then(|apm| apm.redirect_url.as_ref())
})
.filter(|redirect_str| !redirect_str.is_empty())
.map(|url| {
Url::parse(url)
.into_report()
.change_context(errors::ConnectorError::FailedToObtainIntegrationUrl)
})
.transpose()?;
let redirection_data =
redirect_url.map(|url| services::RedirectForm::from((url, services::Method::Get)));
Ok(Self { Ok(Self {
status, status,
response: get_payment_response(status, item.response), response: get_payment_response(status, item.response, redirection_data),
..item.data ..item.data
}) })
} }
@ -338,3 +324,120 @@ pub struct GlobalpayErrorResponse {
pub detailed_error_code: String, pub detailed_error_code: String,
pub detailed_error_description: String, pub detailed_error_description: String,
} }
fn get_payment_method_data(
item: &types::PaymentsAuthorizeRouterData,
brand_reference: Option<String>,
) -> Result<PaymentMethodData, error_stack::Report<errors::ConnectorError>> {
match &item.request.payment_method_data {
api::PaymentMethodData::Card(ccard) => Ok(PaymentMethodData::Card(requests::Card {
number: ccard.card_number.clone(),
expiry_month: ccard.card_exp_month.clone(),
expiry_year: ccard.card_exp_year.clone(),
cvv: ccard.card_cvc.clone(),
account_type: None,
authcode: None,
avs_address: None,
avs_postal_code: None,
brand_reference,
chip_condition: None,
funding: None,
pin_block: None,
tag: None,
track: None,
})),
api::PaymentMethodData::Wallet(wallet_data) => get_wallet_data(wallet_data),
api::PaymentMethodData::BankRedirect(bank_redirect) => {
get_bank_redirect_data(bank_redirect)
}
_ => Err(errors::ConnectorError::NotImplemented(
"Payment methods".to_string(),
))?,
}
}
fn get_return_url(item: &types::PaymentsAuthorizeRouterData) -> Option<String> {
match item.request.payment_method_data.clone() {
api::PaymentMethodData::Wallet(api_models::payments::WalletData::PaypalRedirect(_)) => {
item.request.complete_authorize_url.clone()
}
_ => item.request.router_return_url.clone(),
}
}
type MandateDetails = (Option<Initiator>, Option<StoredCredential>, Option<String>);
fn get_mandate_details(
item: &types::PaymentsAuthorizeRouterData,
) -> Result<MandateDetails, error_stack::Report<errors::ConnectorError>> {
Ok(if item.request.is_mandate_payment() {
let connector_mandate_id = item
.request
.mandate_id
.as_ref()
.and_then(|mandate_ids| mandate_ids.connector_mandate_id.clone());
(
Some(match item.request.off_session {
Some(true) => Initiator::Merchant,
_ => Initiator::Payer,
}),
Some(StoredCredential {
model: Some(requests::Model::Recurring),
sequence: Some(match connector_mandate_id.is_some() {
true => requests::Sequence::Subsequent,
false => requests::Sequence::First,
}),
}),
connector_mandate_id,
)
} else {
(None, None, None)
})
}
fn get_wallet_data(
wallet_data: &api_models::payments::WalletData,
) -> Result<PaymentMethodData, error_stack::Report<errors::ConnectorError>> {
match wallet_data {
api_models::payments::WalletData::PaypalRedirect(_) => {
Ok(PaymentMethodData::Apm(requests::Apm {
provider: Some(ApmProvider::Paypal),
}))
}
api_models::payments::WalletData::GooglePay(_) => {
Ok(PaymentMethodData::DigitalWallet(requests::DigitalWallet {
provider: Some(requests::DigitalWalletProvider::PayByGoogle),
payment_token: wallet_data.get_wallet_token_as_json()?,
}))
}
_ => Err(errors::ConnectorError::NotImplemented(
"Payment methods".to_string(),
))?,
}
}
fn get_bank_redirect_data(
bank_redirect: &api_models::payments::BankRedirectData,
) -> Result<PaymentMethodData, error_stack::Report<errors::ConnectorError>> {
match bank_redirect {
api_models::payments::BankRedirectData::Eps { .. } => {
Ok(PaymentMethodData::Apm(requests::Apm {
provider: Some(ApmProvider::Eps),
}))
}
api_models::payments::BankRedirectData::Giropay { .. } => {
Ok(PaymentMethodData::Apm(requests::Apm {
provider: Some(ApmProvider::Giropay),
}))
}
api_models::payments::BankRedirectData::Ideal { .. } => {
Ok(PaymentMethodData::Apm(requests::Apm {
provider: Some(ApmProvider::Ideal),
}))
}
api_models::payments::BankRedirectData::Sofort { .. } => {
Ok(PaymentMethodData::Apm(requests::Apm {
provider: Some(ApmProvider::Sofort),
}))
}
}
}

View File

@ -145,6 +145,7 @@ pub trait PaymentsAuthorizeRequestData {
fn get_browser_info(&self) -> Result<types::BrowserInformation, Error>; fn get_browser_info(&self) -> Result<types::BrowserInformation, Error>;
fn get_card(&self) -> Result<api::Card, Error>; fn get_card(&self) -> Result<api::Card, Error>;
fn get_return_url(&self) -> Result<String, Error>; fn get_return_url(&self) -> Result<String, Error>;
fn is_mandate_payment(&self) -> bool;
fn get_webhook_url(&self) -> Result<String, Error>; fn get_webhook_url(&self) -> Result<String, Error>;
} }
@ -171,6 +172,14 @@ impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData {
.clone() .clone()
.ok_or_else(missing_field_err("return_url")) .ok_or_else(missing_field_err("return_url"))
} }
fn is_mandate_payment(&self) -> bool {
self.setup_mandate_details.is_some()
|| self
.mandate_id
.as_ref()
.and_then(|mandate_ids| mandate_ids.connector_mandate_id.as_ref())
.is_some()
}
fn get_webhook_url(&self) -> Result<String, Error> { fn get_webhook_url(&self) -> Result<String, Error> {
self.router_return_url self.router_return_url
.clone() .clone()

View File

@ -141,7 +141,6 @@ default_imp_for_connector_redirect_response!(
connector::Cybersource, connector::Cybersource,
connector::Dlocal, connector::Dlocal,
connector::Fiserv, connector::Fiserv,
connector::Globalpay,
connector::Klarna, connector::Klarna,
connector::Multisafepay, connector::Multisafepay,
connector::Opennode, connector::Opennode,