feat(connector): [Trustpay] add authorize (cards 3ds, no3ds and bank redirects), refund, psync, rsync (#717)

Co-authored-by: Sangamesh <sangamesh.kulkarni@juspay.in>
Co-authored-by: sai harsha <sai.harsha@sai.harsha-MacBookPro>
Co-authored-by: Arun Raj M <jarnura47@gmail.com>
This commit is contained in:
saiharsha-juspay
2023-03-13 17:23:39 +05:30
committed by GitHub
parent 230fcdd4e1
commit e102cae76c
18 changed files with 2002 additions and 11 deletions

View File

@ -65,6 +65,7 @@ cards = [
"stripe", "stripe",
"worldline", "worldline",
"worldpay", "worldpay",
"trustpay",
] ]
[refund] [refund]
@ -103,6 +104,8 @@ shift4.base_url = "https://api.shift4.com/"
stripe.base_url = "https://api.stripe.com/" stripe.base_url = "https://api.stripe.com/"
worldline.base_url = "https://eu.sandbox.api-ingenico.com/" worldline.base_url = "https://eu.sandbox.api-ingenico.com/"
worldpay.base_url = "https://try.access.worldpay.com/" worldpay.base_url = "https://try.access.worldpay.com/"
trustpay.base_url = "https://test-tpgw.trustpay.eu/"
trustpay.base_url_bank_redirects = "https://aapi.trustpay.eu/"
[scheduler] [scheduler]
stream = "SCHEDULER_STREAM" stream = "SCHEDULER_STREAM"

View File

@ -144,6 +144,8 @@ shift4.base_url = "https://api.shift4.com/"
stripe.base_url = "https://api.stripe.com/" stripe.base_url = "https://api.stripe.com/"
worldline.base_url = "https://eu.sandbox.api-ingenico.com/" worldline.base_url = "https://eu.sandbox.api-ingenico.com/"
worldpay.base_url = "https://try.access.worldpay.com/" worldpay.base_url = "https://try.access.worldpay.com/"
trustpay.base_url = "https://test-tpgw.trustpay.eu/"
trustpay.base_url_bank_redirects = "https://aapi.trustpay.eu/"
# This data is used to call respective connectors for wallets and cards # This data is used to call respective connectors for wallets and cards
[connectors.supported] [connectors.supported]

View File

@ -89,6 +89,8 @@ shift4.base_url = "https://api.shift4.com/"
stripe.base_url = "https://api.stripe.com/" stripe.base_url = "https://api.stripe.com/"
worldline.base_url = "https://eu.sandbox.api-ingenico.com/" worldline.base_url = "https://eu.sandbox.api-ingenico.com/"
worldpay.base_url = "https://try.access.worldpay.com/" worldpay.base_url = "https://try.access.worldpay.com/"
trustpay.base_url = "https://test-tpgw.trustpay.eu/"
trustpay.base_url_bank_redirects = "https://aapi.trustpay.eu/"
[connectors.supported] [connectors.supported]
wallets = ["klarna", "braintree", "applepay"] wallets = ["klarna", "braintree", "applepay"]
@ -112,6 +114,7 @@ cards = [
"stripe", "stripe",
"worldline", "worldline",
"worldpay", "worldpay",
"trustpay",
] ]

View File

@ -539,6 +539,7 @@ pub enum MandateStatus {
strum::Display, strum::Display,
strum::EnumString, strum::EnumString,
frunk::LabelledGeneric, frunk::LabelledGeneric,
Hash,
)] )]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")] #[strum(serialize_all = "snake_case")]
@ -567,11 +568,18 @@ pub enum Connector {
Stripe, Stripe,
Worldline, Worldline,
Worldpay, Worldpay,
Trustpay,
} }
impl Connector { impl Connector {
pub fn supports_access_token(&self) -> bool { pub fn supports_access_token(&self, payment_method: PaymentMethod) -> bool {
matches!(self, Self::Airwallex | Self::Globalpay | Self::Payu) matches!(
(self, payment_method),
(Self::Airwallex, _)
| (Self::Globalpay, _)
| (Self::Payu, _)
| (Self::Trustpay, PaymentMethod::BankRedirect)
)
} }
} }
@ -611,6 +619,7 @@ pub enum RoutableConnectors {
Worldline, Worldline,
Worldpay, Worldpay,
Multisafepay, Multisafepay,
Trustpay,
} }
/// Wallets which support obtaining session object /// Wallets which support obtaining session object

View File

@ -44,7 +44,7 @@ where
/// Functionality, for specifically encoding `Self` into `String` /// Functionality, for specifically encoding `Self` into `String`
/// after serialization by using `serde::Serialize` /// after serialization by using `serde::Serialize`
/// ///
fn encode(&'e self) -> CustomResult<String, errors::ParsingError> fn url_encode(&'e self) -> CustomResult<String, errors::ParsingError>
where where
Self: Serialize; Self: Serialize;
@ -103,7 +103,7 @@ where
} }
// Check without two functions can we combine this // Check without two functions can we combine this
fn encode(&'e self) -> CustomResult<String, errors::ParsingError> fn url_encode(&'e self) -> CustomResult<String, errors::ParsingError>
where where
Self: Serialize, Self: Serialize,
{ {

View File

@ -251,6 +251,7 @@ pub struct Connectors {
pub stripe: ConnectorParams, pub stripe: ConnectorParams,
pub worldline: ConnectorParams, pub worldline: ConnectorParams,
pub worldpay: ConnectorParams, pub worldpay: ConnectorParams,
pub trustpay: ConnectorParamsWithMoreUrls,
// Keep this field separate from the remaining fields // Keep this field separate from the remaining fields
pub supported: SupportedConnectors, pub supported: SupportedConnectors,
@ -262,6 +263,13 @@ pub struct ConnectorParams {
pub base_url: String, pub base_url: String,
} }
#[derive(Debug, Deserialize, Clone, Default)]
#[serde(default)]
pub struct ConnectorParamsWithMoreUrls {
pub base_url: String,
pub base_url_bank_redirects: String,
}
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
#[serde(default)] #[serde(default)]
pub struct SchedulerSettings { pub struct SchedulerSettings {

View File

@ -18,6 +18,7 @@ pub mod payu;
pub mod rapyd; pub mod rapyd;
pub mod shift4; pub mod shift4;
pub mod stripe; pub mod stripe;
pub mod trustpay;
pub mod utils; pub mod utils;
pub mod worldline; pub mod worldline;
pub mod worldpay; pub mod worldpay;
@ -27,5 +28,6 @@ pub use self::{
authorizedotnet::Authorizedotnet, bambora::Bambora, bluesnap::Bluesnap, braintree::Braintree, authorizedotnet::Authorizedotnet, bambora::Bambora, bluesnap::Bluesnap, braintree::Braintree,
checkout::Checkout, cybersource::Cybersource, dlocal::Dlocal, fiserv::Fiserv, checkout::Checkout, cybersource::Cybersource, dlocal::Dlocal, fiserv::Fiserv,
globalpay::Globalpay, klarna::Klarna, multisafepay::Multisafepay, nuvei::Nuvei, payu::Payu, globalpay::Globalpay, klarna::Klarna, multisafepay::Multisafepay, nuvei::Nuvei, payu::Payu,
rapyd::Rapyd, shift4::Shift4, stripe::Stripe, worldline::Worldline, worldpay::Worldpay, rapyd::Rapyd, shift4::Shift4, stripe::Stripe, trustpay::Trustpay, worldline::Worldline,
worldpay::Worldpay,
}; };

View File

@ -346,7 +346,7 @@ impl
req: &types::PaymentsAuthorizeRouterData, req: &types::PaymentsAuthorizeRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> { ) -> CustomResult<Option<String>, errors::ConnectorError> {
let req = stripe::PaymentIntentRequest::try_from(req)?; let req = stripe::PaymentIntentRequest::try_from(req)?;
let stripe_req = utils::Encode::<stripe::PaymentIntentRequest>::encode(&req) let stripe_req = utils::Encode::<stripe::PaymentIntentRequest>::url_encode(&req)
.change_context(errors::ConnectorError::RequestEncodingFailed)?; .change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(stripe_req)) Ok(Some(stripe_req))
} }

View File

@ -0,0 +1,630 @@
mod transformers;
use std::fmt::Debug;
use base64::Engine;
use error_stack::{IntoReport, ResultExt};
use transformers as trustpay;
use crate::{
configs::settings,
consts,
core::{
errors::{self, CustomResult},
payments,
},
headers,
services::{self, ConnectorIntegration},
types::{
self,
api::{self, ConnectorCommon, ConnectorCommonExt},
ErrorResponse, Response,
},
utils::{self, BytesExt},
};
#[derive(Debug, Clone)]
pub struct Trustpay;
impl<Flow, Request, Response> ConnectorCommonExt<Flow, Request, Response> for Trustpay
where
Self: ConnectorIntegration<Flow, Request, Response>,
{
fn build_headers(
&self,
req: &types::RouterData<Flow, Request, Response>,
_connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
match req.payment_method {
storage_models::enums::PaymentMethod::BankRedirect => {
let token = req
.access_token
.clone()
.ok_or(errors::ConnectorError::FailedToObtainAuthType)?;
Ok(vec![
(
headers::CONTENT_TYPE.to_string(),
"application/json".to_owned(),
),
(
headers::AUTHORIZATION.to_string(),
format!("Bearer {}", token.token),
),
])
}
_ => {
let mut header = vec![(
headers::CONTENT_TYPE.to_string(),
self.get_content_type().to_string(),
)];
let mut api_key = self.get_auth_header(&req.connector_auth_type)?;
header.append(&mut api_key);
Ok(header)
}
}
}
}
impl ConnectorCommon for Trustpay {
fn id(&self) -> &'static str {
"trustpay"
}
fn common_get_content_type(&self) -> &'static str {
"application/x-www-form-urlencoded"
}
fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str {
connectors.trustpay.base_url.as_ref()
}
fn get_auth_header(
&self,
auth_type: &types::ConnectorAuthType,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
let auth = trustpay::TrustpayAuthType::try_from(auth_type)
.change_context(errors::ConnectorError::FailedToObtainAuthType)?;
Ok(vec![(headers::X_API_KEY.to_string(), auth.api_key)])
}
fn build_error_response(
&self,
res: Response,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
let response: trustpay::TrustpayErrorResponse = res
.response
.parse_struct("trustpay ErrorResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
let default_error = trustpay::Errors {
code: 0,
description: consts::NO_ERROR_CODE.to_string(),
};
Ok(ErrorResponse {
status_code: res.status_code,
code: response.status.to_string(),
message: format!("{:?}", response.errors.first().unwrap_or(&default_error)),
reason: None,
})
}
}
impl api::Payment for Trustpay {}
impl api::PreVerify for Trustpay {}
impl ConnectorIntegration<api::Verify, types::VerifyRequestData, types::PaymentsResponseData>
for Trustpay
{
}
impl api::PaymentVoid for Trustpay {}
impl ConnectorIntegration<api::Void, types::PaymentsCancelData, types::PaymentsResponseData>
for Trustpay
{
}
impl api::ConnectorAccessToken for Trustpay {}
impl ConnectorIntegration<api::AccessTokenAuth, types::AccessTokenRequestData, types::AccessToken>
for Trustpay
{
fn get_url(
&self,
_req: &types::RefreshTokenRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!(
"{}{}",
connectors.trustpay.base_url_bank_redirects, "api/oauth2/token"
))
}
fn get_content_type(&self) -> &'static str {
self.common_get_content_type()
}
fn get_headers(
&self,
req: &types::RefreshTokenRouterData,
_connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
let auth = trustpay::TrustpayAuthType::try_from(&req.connector_auth_type)
.change_context(errors::ConnectorError::FailedToObtainAuthType)?;
let auth_value = format!(
"Basic {}",
consts::BASE64_ENGINE.encode(format!("{}:{}", auth.project_id, auth.secret_key))
);
Ok(vec![
(
headers::CONTENT_TYPE.to_string(),
types::RefreshTokenType::get_content_type(self).to_string(),
),
(headers::AUTHORIZATION.to_string(), auth_value),
])
}
fn get_request_body(
&self,
req: &types::RefreshTokenRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let trustpay_req =
utils::Encode::<trustpay::TrustpayAuthUpdateRequest>::convert_and_url_encode(req)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(trustpay_req))
}
fn build_request(
&self,
req: &types::RefreshTokenRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
let req = Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.headers(types::RefreshTokenType::get_headers(self, req, connectors)?)
.url(&types::RefreshTokenType::get_url(self, req, connectors)?)
.body(types::RefreshTokenType::get_request_body(self, req)?)
.build(),
);
Ok(req)
}
fn handle_response(
&self,
data: &types::RefreshTokenRouterData,
res: Response,
) -> CustomResult<types::RefreshTokenRouterData, errors::ConnectorError> {
let response: trustpay::TrustpayAuthUpdateResponse = res
.response
.parse_struct("trustpay TrustpayAuthUpdateResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: Response,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
let response: trustpay::TrustpayAccessTokenErrorResponse = res
.response
.parse_struct("Trustpay AccessTokenErrorResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
Ok(ErrorResponse {
status_code: res.status_code,
code: response.result_info.result_code.to_string(),
message: response.result_info.additional_info.unwrap_or_default(),
reason: None,
})
}
}
impl api::PaymentSync for Trustpay {}
impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
for Trustpay
{
fn get_headers(
&self,
req: &types::PaymentsSyncRouterData,
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::PaymentsSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
let id = req.request.connector_transaction_id.clone();
match req.payment_method {
storage_models::enums::PaymentMethod::BankRedirect => Ok(format!(
"{}{}/{}",
connectors.trustpay.base_url_bank_redirects,
"api/Payments/Payment",
id.get_connector_transaction_id()
.change_context(errors::ConnectorError::MissingConnectorTransactionID)?
)),
_ => Ok(format!(
"{}{}/{}",
self.base_url(connectors),
"api/v1/instance",
id.get_connector_transaction_id()
.change_context(errors::ConnectorError::MissingConnectorTransactionID)?
)),
}
}
fn build_request(
&self,
req: &types::PaymentsSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Get)
.url(&types::PaymentsSyncType::get_url(self, req, connectors)?)
.headers(types::PaymentsSyncType::get_headers(self, req, connectors)?)
.build(),
))
}
fn get_error_response(
&self,
res: Response,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
fn handle_response(
&self,
data: &types::PaymentsSyncRouterData,
res: Response,
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
let response: trustpay::TrustpayPaymentsResponse = res
.response
.parse_struct("trustpay PaymentsResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
}
impl api::PaymentCapture for Trustpay {}
impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::PaymentsResponseData>
for Trustpay
{
}
impl api::PaymentSession for Trustpay {}
impl ConnectorIntegration<api::Session, types::PaymentsSessionData, types::PaymentsResponseData>
for Trustpay
{
}
impl api::PaymentAuthorize for Trustpay {}
impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::PaymentsResponseData>
for Trustpay
{
fn get_headers(
&self,
req: &types::PaymentsAuthorizeRouterData,
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::PaymentsAuthorizeRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
match req.payment_method {
storage_models::enums::PaymentMethod::BankRedirect => Ok(format!(
"{}{}",
connectors.trustpay.base_url_bank_redirects, "api/Payments/Payment"
)),
_ => Ok(format!(
"{}{}",
self.base_url(connectors),
"api/v1/purchase"
)),
}
}
fn get_request_body(
&self,
req: &types::PaymentsAuthorizeRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let trustpay_req = trustpay::TrustpayPaymentsRequest::try_from(req)?;
let trustpay_req_string = match req.payment_method {
storage_models::enums::PaymentMethod::BankRedirect => {
utils::Encode::<trustpay::PaymentRequestBankRedirect>::encode_to_string_of_json(
&trustpay_req,
)
.change_context(errors::ConnectorError::RequestEncodingFailed)?
}
_ => utils::Encode::<trustpay::PaymentRequestCards>::url_encode(&trustpay_req)
.change_context(errors::ConnectorError::RequestEncodingFailed)?,
};
Ok(Some(trustpay_req_string))
}
fn build_request(
&self,
req: &types::PaymentsAuthorizeRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsAuthorizeType::get_url(
self, req, connectors,
)?)
.headers(types::PaymentsAuthorizeType::get_headers(
self, req, connectors,
)?)
.body(types::PaymentsAuthorizeType::get_request_body(self, req)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::PaymentsAuthorizeRouterData,
res: Response,
) -> CustomResult<types::PaymentsAuthorizeRouterData, errors::ConnectorError> {
let response: trustpay::TrustpayPaymentsResponse = res
.response
.parse_struct("trustpay PaymentsResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: Response,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl api::Refund for Trustpay {}
impl api::RefundExecute for Trustpay {}
impl api::RefundSync for Trustpay {}
impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsResponseData>
for Trustpay
{
fn get_headers(
&self,
req: &types::RefundsRouterData<api::Execute>,
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::RefundsRouterData<api::Execute>,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
match req.payment_method {
storage_models::enums::PaymentMethod::BankRedirect => Ok(format!(
"{}{}{}{}",
connectors.trustpay.base_url_bank_redirects,
"api/Payments/Payment/",
req.request.connector_transaction_id,
"/Refund"
)),
_ => Ok(format!("{}{}", self.base_url(connectors), "api/v1/Reverse")),
}
}
fn get_request_body(
&self,
req: &types::RefundsRouterData<api::Execute>,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let trustpay_req = trustpay::TrustpayRefundRequest::try_from(req)?;
let trustpay_req_string = match req.payment_method {
storage_models::enums::PaymentMethod::BankRedirect => utils::Encode::<
trustpay::TrustpayRefundRequestBankRedirect,
>::encode_to_string_of_json(
&trustpay_req
)
.change_context(errors::ConnectorError::RequestEncodingFailed)?,
_ => utils::Encode::<trustpay::TrustpayRefundRequestCards>::url_encode(&trustpay_req)
.change_context(errors::ConnectorError::RequestEncodingFailed)?,
};
Ok(Some(trustpay_req_string))
}
fn build_request(
&self,
req: &types::RefundsRouterData<api::Execute>,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
let request = services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::RefundExecuteType::get_url(self, req, connectors)?)
.headers(types::RefundExecuteType::get_headers(
self, req, connectors,
)?)
.body(types::RefundExecuteType::get_request_body(self, req)?)
.build();
Ok(Some(request))
}
fn handle_response(
&self,
data: &types::RefundsRouterData<api::Execute>,
res: Response,
) -> CustomResult<types::RefundsRouterData<api::Execute>, errors::ConnectorError> {
let response: trustpay::RefundResponse = res
.response
.parse_struct("trustpay RefundResponse")
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: Response,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponseData> for Trustpay {
fn get_headers(
&self,
req: &types::RefundSyncRouterData,
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::RefundSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
let id = req
.request
.connector_refund_id
.to_owned()
.ok_or_else(|| errors::ConnectorError::MissingConnectorRefundID)?;
match req.payment_method {
storage_models::enums::PaymentMethod::BankRedirect => Ok(format!(
"{}{}/{}",
connectors.trustpay.base_url_bank_redirects, "api/Payments/Payment", id
)),
_ => Ok(format!(
"{}{}/{}",
self.base_url(connectors),
"api/v1/instance",
id
)),
}
}
fn build_request(
&self,
req: &types::RefundSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Get)
.url(&types::RefundSyncType::get_url(self, req, connectors)?)
.headers(types::RefundSyncType::get_headers(self, req, connectors)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::RefundSyncRouterData,
res: Response,
) -> CustomResult<types::RefundSyncRouterData, errors::ConnectorError> {
let response: trustpay::RefundResponse = res
.response
.parse_struct("trustpay RefundResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: Response,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
#[async_trait::async_trait]
impl api::IncomingWebhook for Trustpay {
fn get_webhook_object_reference_id(
&self,
_request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<String, errors::ConnectorError> {
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
}
fn get_webhook_event_type(
&self,
_request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> {
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
}
fn get_webhook_resource_object(
&self,
_request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<serde_json::Value, errors::ConnectorError> {
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
}
}
impl services::ConnectorRedirectResponse for Trustpay {
fn get_flow_type(
&self,
query_params: &str,
) -> CustomResult<payments::CallConnectorAction, errors::ConnectorError> {
let query =
serde_urlencoded::from_str::<transformers::TrustpayRedirectResponse>(query_params)
.into_report()
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
crate::logger::debug!(trustpay_redirect_response=?query);
Ok(query.status.map_or(
payments::CallConnectorAction::Trigger,
|status| match status.as_str() {
"SuccessOk" => payments::CallConnectorAction::StatusUpdate(
storage_models::enums::AttemptStatus::Charged,
),
_ => payments::CallConnectorAction::Trigger,
},
))
}
}

File diff suppressed because it is too large Load Diff

View File

@ -222,6 +222,7 @@ pub enum CardIssuer {
pub trait CardData { pub trait CardData {
fn get_card_expiry_year_2_digit(&self) -> Secret<String>; fn get_card_expiry_year_2_digit(&self) -> Secret<String>;
fn get_card_issuer(&self) -> Result<CardIssuer, Error>; fn get_card_issuer(&self) -> Result<CardIssuer, Error>;
fn get_card_expiry_month_year_2_digit_with_delimiter(&self, delimiter: String) -> String;
} }
impl CardData for api::Card { impl CardData for api::Card {
@ -237,6 +238,15 @@ impl CardData for api::Card {
.map(|card| card.split_whitespace().collect()); .map(|card| card.split_whitespace().collect());
get_card_issuer(card.peek().clone().as_str()) get_card_issuer(card.peek().clone().as_str())
} }
fn get_card_expiry_month_year_2_digit_with_delimiter(&self, delimiter: String) -> String {
let year = self.get_card_expiry_year_2_digit();
format!(
"{}{}{}",
self.card_exp_month.peek().clone(),
delimiter,
year.peek()
)
}
} }
fn get_card_issuer(card_number: &str) -> Result<CardIssuer, Error> { fn get_card_issuer(card_number: &str) -> Result<CardIssuer, Error> {

View File

@ -86,7 +86,7 @@ pub fn mk_add_card_request(
name_on_card: Some("John Doe".to_string().into()), // [#256] name_on_card: Some("John Doe".to_string().into()), // [#256]
nickname: Some("router".to_string()), // nickname: Some("router".to_string()), //
}; };
let body = utils::Encode::<AddCardRequest<'_>>::encode(&add_card_req) let body = utils::Encode::<AddCardRequest<'_>>::url_encode(&add_card_req)
.change_context(errors::VaultError::RequestEncodingFailed)?; .change_context(errors::VaultError::RequestEncodingFailed)?;
let mut url = locker.host.to_owned(); let mut url = locker.host.to_owned();
url.push_str("/card/addCard"); url.push_str("/card/addCard");
@ -139,7 +139,7 @@ pub fn mk_get_card_request<'a>(
card_id, card_id,
}; };
let body = utils::Encode::<GetCard<'_>>::encode(&get_card_req) let body = utils::Encode::<GetCard<'_>>::url_encode(&get_card_req)
.change_context(errors::VaultError::RequestEncodingFailed)?; .change_context(errors::VaultError::RequestEncodingFailed)?;
let mut url = locker.host.to_owned(); let mut url = locker.host.to_owned();
url.push_str("/card/getCard"); url.push_str("/card/getCard");
@ -158,7 +158,7 @@ pub fn mk_delete_card_request<'a>(
merchant_id, merchant_id,
card_id, card_id,
}; };
let body = utils::Encode::<GetCard<'_>>::encode(&delete_card_req) let body = utils::Encode::<GetCard<'_>>::url_encode(&delete_card_req)
.change_context(errors::VaultError::RequestEncodingFailed)?; .change_context(errors::VaultError::RequestEncodingFailed)?;
let mut url = locker.host.to_owned(); let mut url = locker.host.to_owned();
url.push_str("/card/deleteCard"); url.push_str("/card/deleteCard");

View File

@ -10,7 +10,7 @@ use crate::{
}, },
routes::AppState, routes::AppState,
services, services,
types::{self, api as api_types, storage}, types::{self, api as api_types, storage, transformers::ForeignInto},
}; };
/// This function replaces the request and response type of routerdata with the /// This function replaces the request and response type of routerdata with the
@ -82,7 +82,10 @@ pub async fn add_access_token<
merchant_account: &storage::MerchantAccount, merchant_account: &storage::MerchantAccount,
router_data: &types::RouterData<F, Req, Res>, router_data: &types::RouterData<F, Req, Res>,
) -> RouterResult<types::AddAccessTokenResult> { ) -> RouterResult<types::AddAccessTokenResult> {
if connector.connector_name.supports_access_token() { if connector
.connector_name
.supports_access_token(router_data.payment_method.foreign_into())
{
let merchant_id = &merchant_account.merchant_id; let merchant_id = &merchant_account.merchant_id;
let store = &*state.store; let store = &*state.store;
let old_access_token = store let old_access_token = store

View File

@ -182,6 +182,7 @@ impl ConnectorData {
"worldline" => Ok(Box::new(&connector::Worldline)), "worldline" => Ok(Box::new(&connector::Worldline)),
"worldpay" => Ok(Box::new(&connector::Worldpay)), "worldpay" => Ok(Box::new(&connector::Worldpay)),
"multisafepay" => Ok(Box::new(&connector::Multisafepay)), "multisafepay" => Ok(Box::new(&connector::Multisafepay)),
"trustpay" => Ok(Box::new(&connector::Trustpay)),
_ => Err(report!(errors::ConnectorError::InvalidConnectorName) _ => Err(report!(errors::ConnectorError::InvalidConnectorName)
.attach_printable(format!("invalid connector name: {connector_name}"))) .attach_printable(format!("invalid connector name: {connector_name}")))
.change_context(errors::ApiErrorResponse::InternalServerError), .change_context(errors::ApiErrorResponse::InternalServerError),

View File

@ -22,6 +22,7 @@ pub(crate) struct ConnectorAuthentication {
pub stripe: Option<HeaderKey>, pub stripe: Option<HeaderKey>,
pub worldpay: Option<BodyKey>, pub worldpay: Option<BodyKey>,
pub worldline: Option<SignatureKey>, pub worldline: Option<SignatureKey>,
pub trustpay: Option<SignatureKey>,
} }
impl ConnectorAuthentication { impl ConnectorAuthentication {

View File

@ -18,6 +18,7 @@ mod payu;
mod rapyd; mod rapyd;
mod shift4; mod shift4;
mod stripe; mod stripe;
mod trustpay;
mod utils; mod utils;
mod worldline; mod worldline;
mod worldpay; mod worldpay;

View File

@ -0,0 +1,245 @@
use masking::Secret;
use router::types::{self, api, storage::enums, BrowserInformation};
use crate::{
connector_auth,
utils::{self, ConnectorActions},
};
#[derive(Clone, Copy)]
struct TrustpayTest;
impl ConnectorActions for TrustpayTest {}
impl utils::Connector for TrustpayTest {
fn get_data(&self) -> types::api::ConnectorData {
use router::connector::Trustpay;
types::api::ConnectorData {
connector: Box::new(&Trustpay),
connector_name: types::Connector::Trustpay,
get_token: types::api::GetToken::Connector,
}
}
fn get_auth_token(&self) -> types::ConnectorAuthType {
types::ConnectorAuthType::from(
connector_auth::ConnectorAuthentication::new()
.trustpay
.expect("Missing connector authentication configuration"),
)
}
fn get_name(&self) -> String {
"trustpay".to_string()
}
}
fn get_default_browser_info() -> BrowserInformation {
BrowserInformation {
color_depth: 24,
java_enabled: false,
java_script_enabled: true,
language: "en-US".to_string(),
screen_height: 1080,
screen_width: 1920,
time_zone: 3600,
accept_header: "*".to_string(),
user_agent: "none".to_string(),
ip_address: None,
}
}
fn get_default_payment_authorize_data() -> Option<types::PaymentsAuthorizeData> {
Some(types::PaymentsAuthorizeData {
payment_method_data: types::api::PaymentMethodData::Card(api::Card {
card_number: Secret::new("4200000000000000".to_string()),
card_exp_year: Secret::new("25".to_string()),
card_cvc: Secret::new("123".to_string()),
..utils::CCardType::default().0
}),
browser_info: Some(get_default_browser_info()),
..utils::PaymentAuthorizeType::default().0
})
}
fn get_default_payment_info() -> Option<utils::PaymentInfo> {
Some(utils::PaymentInfo {
address: Some(types::PaymentAddress {
billing: Some(api::Address {
address: Some(api::AddressDetails {
first_name: Some(Secret::new("first".to_string())),
last_name: Some(Secret::new("last".to_string())),
line1: Some(Secret::new("line1".to_string())),
line2: Some(Secret::new("line2".to_string())),
city: Some("city".to_string()),
zip: Some(Secret::new("zip".to_string())),
country: Some("IN".to_string()),
..Default::default()
}),
phone: None,
}),
..Default::default()
}),
router_return_url: Some(String::from("http://localhost:8080")),
..Default::default()
})
}
static CONNECTOR: TrustpayTest = TrustpayTest {};
// Cards Positive Tests
// Creates a payment using the automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_make_payment() {
let authorize_response = CONNECTOR
.make_payment(
get_default_payment_authorize_data(),
get_default_payment_info(),
)
.await
.unwrap();
assert_eq!(authorize_response.status, enums::AttemptStatus::Charged);
}
// Synchronizes a payment using the automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_sync_auto_captured_payment() {
let authorize_response = CONNECTOR
.make_payment(
get_default_payment_authorize_data(),
get_default_payment_info(),
)
.await
.unwrap();
assert_eq!(authorize_response.status, enums::AttemptStatus::Charged);
let txn_id = utils::get_connector_transaction_id(authorize_response.response);
assert_ne!(txn_id, None, "Empty connector transaction id");
let response = CONNECTOR
.psync_retry_till_status_matches(
enums::AttemptStatus::Charged,
Some(types::PaymentsSyncData {
connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(
txn_id.unwrap(),
),
encoded_data: None,
capture_method: None,
}),
None,
)
.await
.unwrap();
assert_eq!(response.status, enums::AttemptStatus::Charged,);
}
// Refunds a payment using the automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_refund_auto_captured_payment() {
let response = CONNECTOR
.make_payment_and_refund(
get_default_payment_authorize_data(),
None,
get_default_payment_info(),
)
.await
.unwrap();
assert_eq!(
response.response.unwrap().refund_status,
enums::RefundStatus::Success,
);
}
// Synchronizes a refund using the automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_sync_refund() {
let refund_response = CONNECTOR
.make_payment_and_refund(
get_default_payment_authorize_data(),
None,
get_default_payment_info(),
)
.await
.unwrap();
let response = CONNECTOR
.rsync_retry_till_status_matches(
enums::RefundStatus::Success,
refund_response.response.unwrap().connector_refund_id,
None,
None,
)
.await
.unwrap();
assert_eq!(
response.response.unwrap().refund_status,
enums::RefundStatus::Success,
);
}
// Cards Negative scenerios
// Creates a payment with incorrect card number.
#[actix_web::test]
async fn should_fail_payment_for_incorrect_card_number() {
let payment_authorize_data = types::PaymentsAuthorizeData {
payment_method_data: types::api::PaymentMethodData::Card(api::Card {
card_number: Secret::new("1234567891011".to_string()),
card_exp_year: Secret::new("25".to_string()),
card_cvc: Secret::new("123".to_string()),
..utils::CCardType::default().0
}),
browser_info: Some(get_default_browser_info()),
..utils::PaymentAuthorizeType::default().0
};
let response = CONNECTOR
.make_payment(Some(payment_authorize_data), get_default_payment_info())
.await
.unwrap();
assert_eq!(
response.response.unwrap_err().message,
"Errors { code: 61, description: \"invalid payment data (country or brand)\" }".to_string(),
);
}
// Creates a payment with empty card number.
#[actix_web::test]
async fn should_fail_payment_for_empty_card_number() {
let payment_authorize_data = types::PaymentsAuthorizeData {
payment_method_data: types::api::PaymentMethodData::Card(api::Card {
card_number: Secret::new("".to_string()),
card_exp_year: Secret::new("25".to_string()),
card_cvc: Secret::new("123".to_string()),
..utils::CCardType::default().0
}),
browser_info: Some(get_default_browser_info()),
..utils::PaymentAuthorizeType::default().0
};
let response = CONNECTOR
.make_payment(Some(payment_authorize_data), get_default_payment_info())
.await
.unwrap();
let x = response.response.unwrap_err();
assert_eq!(
x.message,
"Errors { code: 61, description: \"invalid payment data (country or brand)\" }",
);
}
// Creates a payment with incorrect expiry year.
#[actix_web::test]
async fn should_fail_payment_for_incorrect_expiry_year() {
let payment_authorize_data = Some(types::PaymentsAuthorizeData {
payment_method_data: types::api::PaymentMethodData::Card(api::Card {
card_number: Secret::new("4200000000000000".to_string()),
card_exp_year: Secret::new("22".to_string()),
card_cvc: Secret::new("123".to_string()),
..utils::CCardType::default().0
}),
browser_info: Some(get_default_browser_info()),
..utils::PaymentAuthorizeType::default().0
});
let response = CONNECTOR
.make_payment(payment_authorize_data, get_default_payment_info())
.await
.unwrap();
assert_eq!(
response.response.unwrap_err().message,
"Errors { code: 15, description: \"the provided expiration year is not valid\" }"
.to_string(),
);
}

View File

@ -75,6 +75,8 @@ shift4.base_url = "https://api.shift4.com/"
stripe.base_url = "https://api.stripe.com/" stripe.base_url = "https://api.stripe.com/"
worldline.base_url = "https://eu.sandbox.api-ingenico.com/" worldline.base_url = "https://eu.sandbox.api-ingenico.com/"
worldpay.base_url = "https://try.access.worldpay.com/" worldpay.base_url = "https://try.access.worldpay.com/"
trustpay.base_url = "https://test-tpgw.trustpay.eu/"
trustpay.base_url_bank_redirects = "https://aapi.trustpay.eu/"
[connectors.supported] [connectors.supported]
wallets = ["klarna", "braintree", "applepay"] wallets = ["klarna", "braintree", "applepay"]
@ -98,4 +100,5 @@ cards = [
"stripe", "stripe",
"worldline", "worldline",
"worldpay", "worldpay",
"trustpay",
] ]