feat(connector): [Bambora] Add support for cards Authorize, psync, capture, void, refund, Rsync (#677)

This commit is contained in:
Jagan
2023-02-28 03:31:50 +05:30
committed by GitHub
parent f931c4270f
commit 0de5d44195
22 changed files with 1714 additions and 51 deletions

View File

@ -49,6 +49,7 @@ cards = [
"adyen", "adyen",
"airwallex", "airwallex",
"authorizedotnet", "authorizedotnet",
"bambora",
"bluesnap", "bluesnap",
"braintree", "braintree",
"checkout", "checkout",
@ -111,7 +112,7 @@ base_url = "https://sandboxapi.rapyd.net"
base_url = "https://cert.api.fiservapps.com/" base_url = "https://cert.api.fiservapps.com/"
[connectors.worldpay] [connectors.worldpay]
base_url = "http://localhost:9090/" base_url = "https://try.access.worldpay.com/"
[connectors.payu] [connectors.payu]
base_url = "https://secure.snd.payu.com/" base_url = "https://secure.snd.payu.com/"
@ -134,6 +135,9 @@ base_url = "https://api-demo.airwallex.com/"
[connectors.dlocal] [connectors.dlocal]
base_url = "https://sandbox.dlocal.com/" base_url = "https://sandbox.dlocal.com/"
[connectors.bambora]
base_url = "https://api.na.bambora.com"
[scheduler] [scheduler]
stream = "SCHEDULER_STREAM" stream = "SCHEDULER_STREAM"

View File

@ -166,6 +166,9 @@ base_url = "https://api-demo.airwallex.com/"
base_url = "https://sandbox.dlocal.com/" base_url = "https://sandbox.dlocal.com/"
# 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.bambora]
base_url = "https://api.na.bambora.com"
[connectors.supported] [connectors.supported]
wallets = ["klarna", "braintree", "applepay"] wallets = ["klarna", "braintree", "applepay"]
cards = [ cards = [

View File

@ -117,12 +117,16 @@ base_url = "https://api-demo.airwallex.com/"
[connectors.dlocal] [connectors.dlocal]
base_url = "https://sandbox.dlocal.com/" base_url = "https://sandbox.dlocal.com/"
[connectors.bambora]
base_url = "https://api.na.bambora.com"
[connectors.supported] [connectors.supported]
wallets = ["klarna", "braintree", "applepay"] wallets = ["klarna", "braintree", "applepay"]
cards = [ cards = [
"adyen", "adyen",
"airwallex", "airwallex",
"authorizedotnet", "authorizedotnet",
"bambora",
"bluesnap", "bluesnap",
"braintree", "braintree",
"checkout", "checkout",

View File

@ -193,7 +193,7 @@ async fn should_sync_auto_captured_payment() {
txn_id.unwrap(), txn_id.unwrap(),
), ),
encoded_data: None, encoded_data: None,
capture_method: None, capture_method: Some(enums::CaptureMethod::Automatic),
}), }),
None, None,
) )

View File

@ -550,6 +550,7 @@ pub enum Connector {
Cybersource, Cybersource,
#[default] #[default]
Dummy, Dummy,
Bambora,
Dlocal, Dlocal,
Fiserv, Fiserv,
Globalpay, Globalpay,
@ -588,6 +589,7 @@ pub enum RoutableConnectors {
Adyen, Adyen,
Airwallex, Airwallex,
Authorizedotnet, Authorizedotnet,
Bambora,
Bluesnap, Bluesnap,
Braintree, Braintree,
Checkout, Checkout,

View File

@ -228,6 +228,7 @@ pub struct Connectors {
pub airwallex: ConnectorParams, pub airwallex: ConnectorParams,
pub applepay: ConnectorParams, pub applepay: ConnectorParams,
pub authorizedotnet: ConnectorParams, pub authorizedotnet: ConnectorParams,
pub bambora: ConnectorParams,
pub bluesnap: ConnectorParams, pub bluesnap: ConnectorParams,
pub braintree: ConnectorParams, pub braintree: ConnectorParams,
pub checkout: ConnectorParams, pub checkout: ConnectorParams,

View File

@ -3,10 +3,12 @@ pub mod adyen;
pub mod airwallex; pub mod airwallex;
pub mod applepay; pub mod applepay;
pub mod authorizedotnet; pub mod authorizedotnet;
pub mod bambora;
pub mod bluesnap; pub mod bluesnap;
pub mod braintree; pub mod braintree;
pub mod checkout; pub mod checkout;
pub mod cybersource; pub mod cybersource;
pub mod dlocal;
pub mod fiserv; pub mod fiserv;
pub mod globalpay; pub mod globalpay;
pub mod klarna; pub mod klarna;
@ -19,12 +21,10 @@ pub mod utils;
pub mod worldline; pub mod worldline;
pub mod worldpay; pub mod worldpay;
pub mod dlocal;
pub use self::{ pub use self::{
aci::Aci, adyen::Adyen, airwallex::Airwallex, applepay::Applepay, aci::Aci, adyen::Adyen, airwallex::Airwallex, applepay::Applepay,
authorizedotnet::Authorizedotnet, bluesnap::Bluesnap, braintree::Braintree, checkout::Checkout, authorizedotnet::Authorizedotnet, bambora::Bambora, bluesnap::Bluesnap, braintree::Braintree,
cybersource::Cybersource, dlocal::Dlocal, fiserv::Fiserv, globalpay::Globalpay, klarna::Klarna, checkout::Checkout, cybersource::Cybersource, dlocal::Dlocal, fiserv::Fiserv,
nuvei::Nuvei, payu::Payu, rapyd::Rapyd, shift4::Shift4, stripe::Stripe, worldline::Worldline, globalpay::Globalpay, klarna::Klarna, nuvei::Nuvei, payu::Payu, rapyd::Rapyd, shift4::Shift4,
worldpay::Worldpay, stripe::Stripe, worldline::Worldline, worldpay::Worldpay,
}; };

View File

@ -0,0 +1,634 @@
mod transformers;
use std::fmt::Debug;
use error_stack::{IntoReport, ResultExt};
use transformers as bambora;
use super::utils::RefundsRequestData;
use crate::{
configs::settings,
connector::utils::{PaymentsAuthorizeRequestData, PaymentsSyncRequestData},
core::{
errors::{self, CustomResult},
payments,
},
headers, logger,
services::{self, ConnectorIntegration},
types::{
self,
api::{self, ConnectorCommon, ConnectorCommonExt},
ErrorResponse, Response,
},
utils::{self, BytesExt},
};
#[derive(Debug, Clone)]
pub struct Bambora;
impl<Flow, Request, Response> ConnectorCommonExt<Flow, Request, Response> for Bambora
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> {
let mut header = vec![(
headers::CONTENT_TYPE.to_string(),
types::PaymentsAuthorizeType::get_content_type(self).to_string(),
)];
let mut api_key = self.get_auth_header(&req.connector_auth_type)?;
header.append(&mut api_key);
Ok(header)
}
}
impl ConnectorCommon for Bambora {
fn id(&self) -> &'static str {
"bambora"
}
fn common_get_content_type(&self) -> &'static str {
"application/json"
}
fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str {
connectors.bambora.base_url.as_ref()
}
fn get_auth_header(
&self,
auth_type: &types::ConnectorAuthType,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
let auth: bambora::BamboraAuthType = auth_type
.try_into()
.change_context(errors::ConnectorError::FailedToObtainAuthType)?;
Ok(vec![(headers::AUTHORIZATION.to_string(), auth.api_key)])
}
fn build_error_response(
&self,
res: Response,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
let response: bambora::BamboraErrorResponse = res
.response
.parse_struct("BamboraErrorResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
Ok(ErrorResponse {
status_code: res.status_code,
code: response.code.to_string(),
message: response.message,
reason: Some(serde_json::to_string(&response.details).unwrap_or_default()),
})
}
}
impl api::Payment for Bambora {}
impl api::PreVerify for Bambora {}
impl ConnectorIntegration<api::Verify, types::VerifyRequestData, types::PaymentsResponseData>
for Bambora
{
}
impl api::PaymentVoid for Bambora {}
impl ConnectorIntegration<api::Void, types::PaymentsCancelData, types::PaymentsResponseData>
for Bambora
{
fn get_headers(
&self,
req: &types::PaymentsCancelRouterData,
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::PaymentsCancelRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
let connector_payment_id = req.request.connector_transaction_id.clone();
Ok(format!(
"{}/v1/payments/{}{}",
self.base_url(connectors),
connector_payment_id,
"/completions"
))
}
fn get_request_body(
&self,
req: &types::PaymentsCancelRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let request = bambora::BamboraPaymentsRequest::try_from(req)?;
let bambora_req =
utils::Encode::<bambora::BamboraPaymentsRequest>::encode_to_string_of_json(&request)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(bambora_req))
}
fn build_request(
&self,
req: &types::PaymentsCancelRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsVoidType::get_url(self, req, connectors)?)
.headers(types::PaymentsVoidType::get_headers(self, req, connectors)?)
.body(self.get_request_body(req)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::PaymentsCancelRouterData,
res: Response,
) -> CustomResult<types::PaymentsCancelRouterData, errors::ConnectorError> {
let response: bambora::BamboraPaymentsResponse = res
.response
.parse_struct("bambora PaymentsResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from((
types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
},
bambora::PaymentFlow::Void,
))
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: Response,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl api::ConnectorAccessToken for Bambora {}
impl ConnectorIntegration<api::AccessTokenAuth, types::AccessTokenRequestData, types::AccessToken>
for Bambora
{
}
impl api::PaymentSync for Bambora {}
impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
for Bambora
{
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 connector_payment_id = req
.request
.connector_transaction_id
.get_connector_transaction_id()
.change_context(errors::ConnectorError::MissingConnectorTransactionID)?;
Ok(format!(
"{}{}{}",
self.base_url(connectors),
"/v1/payments/",
connector_payment_id
))
}
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: bambora::BamboraPaymentsResponse = res
.response
.parse_struct("bambora PaymentsResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from((
types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
},
get_payment_flow(data.request.is_auto_capture()),
))
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
}
impl api::PaymentCapture for Bambora {}
impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::PaymentsResponseData>
for Bambora
{
fn get_headers(
&self,
req: &types::PaymentsCaptureRouterData,
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::PaymentsCaptureRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!(
"{}{}{}{}",
self.base_url(connectors),
"/v1/payments/",
req.request.connector_transaction_id,
"/completions"
))
}
fn get_request_body(
&self,
req: &types::PaymentsCaptureRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let bambora_req =
utils::Encode::<bambora::BamboraPaymentsCaptureRequest>::convert_and_encode(req)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(bambora_req))
}
fn build_request(
&self,
req: &types::PaymentsCaptureRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsCaptureType::get_url(self, req, connectors)?)
.headers(types::PaymentsCaptureType::get_headers(
self, req, connectors,
)?)
.body(self.get_request_body(req)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::PaymentsCaptureRouterData,
res: Response,
) -> CustomResult<types::PaymentsCaptureRouterData, errors::ConnectorError> {
let response: bambora::BamboraPaymentsResponse = res
.response
.parse_struct("Bambora PaymentsResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
logger::debug!(bamborapayments_create_response=?response);
types::RouterData::try_from((
types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
},
bambora::PaymentFlow::Capture,
))
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: Response,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl api::PaymentSession for Bambora {}
impl ConnectorIntegration<api::Session, types::PaymentsSessionData, types::PaymentsResponseData>
for Bambora
{
//TODO: implement sessions flow
}
impl api::PaymentAuthorize for Bambora {}
impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::PaymentsResponseData>
for Bambora
{
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> {
Ok(format!("{}{}", self.base_url(_connectors), "/v1/payments"))
}
fn get_request_body(
&self,
req: &types::PaymentsAuthorizeRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let request = bambora::BamboraPaymentsRequest::try_from(req)?;
let bambora_req =
utils::Encode::<bambora::BamboraPaymentsRequest>::encode_to_string_of_json(&request)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(bambora_req))
}
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: bambora::BamboraPaymentsResponse = res
.response
.parse_struct("PaymentIntentResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
logger::debug!(bamborapayments_create_response=?response);
types::RouterData::try_from((
types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
},
get_payment_flow(data.request.is_auto_capture()),
))
.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 Bambora {}
impl api::RefundExecute for Bambora {}
impl api::RefundSync for Bambora {}
impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsResponseData>
for Bambora
{
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> {
let connector_payment_id = req.request.connector_transaction_id.clone();
Ok(format!(
"{}{}{}{}",
self.base_url(connectors),
"/v1/payments/",
connector_payment_id,
"/returns"
))
}
fn get_request_body(
&self,
req: &types::RefundsRouterData<api::Execute>,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let bambora_req = utils::Encode::<bambora::BamboraRefundRequest>::convert_and_encode(req)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(bambora_req))
}
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: bambora::RefundResponse = res
.response
.parse_struct("bambora RefundResponse")
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
types::RefundsRouterData::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 Bambora {
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 _connector_payment_id = req.request.connector_transaction_id.clone();
let connector_refund_id = req.request.get_connector_refund_id()?;
Ok(format!(
"{}{}{}",
self.base_url(connectors),
"/v1/payments/",
connector_refund_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)?)
.body(types::RefundSyncType::get_request_body(self, req)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::RefundSyncRouterData,
res: Response,
) -> CustomResult<types::RefundSyncRouterData, errors::ConnectorError> {
let response: bambora::RefundResponse = res
.response
.parse_struct("bambora 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 Bambora {
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 Bambora {
fn get_flow_type(
&self,
_query_params: &str,
) -> CustomResult<payments::CallConnectorAction, errors::ConnectorError> {
Ok(payments::CallConnectorAction::Trigger)
}
}
pub fn get_payment_flow(is_auto_capture: bool) -> bambora::PaymentFlow {
if is_auto_capture {
bambora::PaymentFlow::Capture
} else {
bambora::PaymentFlow::Authorize
}
}

View File

@ -0,0 +1,453 @@
use base64::Engine;
use masking::Secret;
use serde::{Deserialize, Deserializer, Serialize};
use crate::{
connector::utils::PaymentsAuthorizeRequestData,
consts,
core::errors,
types::{self, api, storage::enums},
};
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
pub struct BamboraCard {
name: Secret<String>,
number: Secret<String, common_utils::pii::CardNumber>,
expiry_month: Secret<String>,
expiry_year: Secret<String>,
cvd: Secret<String>,
complete: bool,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "3d_secure")]
three_d_secure: Option<ThreeDSecure>,
}
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
pub struct ThreeDSecure {
// browser: Option<Browser>, //Needed only in case of 3Ds 2.0. Need to update request for this.
enabled: bool,
}
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
pub struct Browser {
accept_header: String,
java_enabled: String,
language: String,
color_depth: String,
screen_height: i64,
screen_width: i64,
time_zone: i64,
user_agent: String,
javascript_enabled: bool,
}
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
pub struct BamboraPaymentsRequest {
amount: i64,
payment_method: PaymentMethod,
card: BamboraCard,
}
impl TryFrom<&types::PaymentsAuthorizeRouterData> for BamboraPaymentsRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
match item.request.payment_method_data.clone() {
api::PaymentMethodData::Card(req_card) => {
let three_ds = match item.auth_type {
enums::AuthenticationType::ThreeDs => Some(ThreeDSecure { enabled: true }),
enums::AuthenticationType::NoThreeDs => None,
};
let bambora_card = BamboraCard {
name: req_card.card_holder_name,
number: req_card.card_number,
expiry_month: req_card.card_exp_month,
expiry_year: req_card.card_exp_year,
cvd: req_card.card_cvc,
three_d_secure: three_ds,
complete: item.request.is_auto_capture(),
};
Ok(Self {
amount: item.request.amount,
payment_method: PaymentMethod::Card,
card: bambora_card,
})
}
_ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()),
}
}
}
impl TryFrom<&types::PaymentsCancelRouterData> for BamboraPaymentsRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(_item: &types::PaymentsCancelRouterData) -> Result<Self, Self::Error> {
Ok(Self {
amount: 0,
..Default::default()
})
}
}
pub struct BamboraAuthType {
pub(super) api_key: String,
}
impl TryFrom<&types::ConnectorAuthType> for BamboraAuthType {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(auth_type: &types::ConnectorAuthType) -> Result<Self, Self::Error> {
if let types::ConnectorAuthType::BodyKey { api_key, key1 } = auth_type {
let auth_key = format!("{key1}:{api_key}");
let auth_header = format!("Passcode {}", consts::BASE64_ENGINE.encode(auth_key));
Ok(Self {
api_key: auth_header,
})
} else {
Err(errors::ConnectorError::FailedToObtainAuthType)?
}
}
}
pub enum PaymentFlow {
Authorize,
Capture,
Void,
}
// PaymentsResponse
impl<F, T>
TryFrom<(
types::ResponseRouterData<F, BamboraPaymentsResponse, T, types::PaymentsResponseData>,
PaymentFlow,
)> for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
data: (
types::ResponseRouterData<F, BamboraPaymentsResponse, T, types::PaymentsResponseData>,
PaymentFlow,
),
) -> Result<Self, Self::Error> {
let flow = data.1;
let item = data.0;
let pg_response = item.response;
Ok(Self {
status: match pg_response.approved.as_str() {
"0" => match flow {
PaymentFlow::Authorize => enums::AttemptStatus::AuthorizationFailed,
PaymentFlow::Capture => enums::AttemptStatus::Failure,
PaymentFlow::Void => enums::AttemptStatus::VoidFailed,
},
"1" => match flow {
PaymentFlow::Authorize => enums::AttemptStatus::Authorized,
PaymentFlow::Capture => enums::AttemptStatus::Charged,
PaymentFlow::Void => enums::AttemptStatus::Voided,
},
&_ => Err(errors::ConnectorError::ResponseDeserializationFailed)?,
},
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(pg_response.id.to_string()),
redirection_data: None,
mandate_reference: None,
connector_metadata: None,
}),
..item.data
})
}
}
fn str_or_i32<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum StrOrI32 {
Str(String),
I32(i32),
}
let value = StrOrI32::deserialize(deserializer)?;
let res = match value {
StrOrI32::Str(v) => v,
StrOrI32::I32(v) => v.to_string(),
};
Ok(res)
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct BamboraPaymentsResponse {
#[serde(deserialize_with = "str_or_i32")]
id: String,
authorizing_merchant_id: i32,
#[serde(deserialize_with = "str_or_i32")]
approved: String,
#[serde(deserialize_with = "str_or_i32")]
message_id: String,
message: String,
auth_code: String,
created: String,
amount: f32,
order_number: String,
#[serde(rename = "type")]
payment_type: String,
comments: Option<String>,
batch_number: Option<String>,
total_refunds: Option<f32>,
total_completions: Option<f32>,
payment_method: String,
card: CardData,
billing: Option<AddressData>,
shipping: Option<AddressData>,
custom: CustomData,
adjusted_by: Option<Vec<AdjustedBy>>,
links: Vec<Links>,
risk_score: Option<f32>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CardData {
name: Option<String>,
expiry_month: Option<String>,
expiry_year: Option<String>,
card_type: String,
last_four: String,
card_bin: Option<String>,
avs_result: String,
cvd_result: String,
cavv_result: Option<String>,
address_match: Option<i32>,
postal_result: Option<i32>,
avs: Option<AvsObject>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AvsObject {
id: String,
message: String,
processed: bool,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AddressData {
name: String,
address_line1: String,
address_line2: String,
city: String,
province: String,
country: String,
postal_code: String,
phone_number: String,
email_address: String,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CustomData {
ref1: String,
ref2: String,
ref3: String,
ref4: String,
ref5: String,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AdjustedBy {
id: i32,
#[serde(rename = "type")]
adjusted_by_type: String,
approval: i32,
message: String,
amount: f32,
created: String,
url: String,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Links {
rel: String,
href: String,
method: String,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PaymentMethod {
#[default]
Card,
Token,
PaymentProfile,
Cash,
Cheque,
Interac,
ApplePay,
AndroidPay,
#[serde(rename = "3d_secure")]
ThreeDSecure,
ProcessorToken,
}
// Capture
#[derive(Default, Debug, Clone, Serialize, PartialEq)]
pub struct BamboraPaymentsCaptureRequest {
amount: Option<i64>,
payment_method: PaymentMethod,
}
impl TryFrom<&types::PaymentsCaptureRouterData> for BamboraPaymentsCaptureRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsCaptureRouterData) -> Result<Self, Self::Error> {
Ok(Self {
amount: item.request.amount_to_capture,
payment_method: PaymentMethod::Card,
})
}
}
// REFUND :
// Type definition for RefundRequest
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
pub struct BamboraRefundRequest {
amount: i64,
}
impl<F> TryFrom<&types::RefundsRouterData<F>> for BamboraRefundRequest {
type Error = error_stack::Report<errors::ParsingError>;
fn try_from(item: &types::RefundsRouterData<F>) -> Result<Self, Self::Error> {
Ok(Self {
amount: item.request.amount,
})
}
}
// Type definition for Refund Response
#[allow(dead_code)]
#[derive(Debug, Serialize, Default, Deserialize, Clone)]
pub enum RefundStatus {
Succeeded,
Failed,
#[default]
Processing,
}
impl From<RefundStatus> for enums::RefundStatus {
fn from(item: RefundStatus) -> Self {
match item {
RefundStatus::Succeeded => Self::Success,
RefundStatus::Failed => Self::Failure,
RefundStatus::Processing => Self::Pending,
}
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct RefundResponse {
#[serde(deserialize_with = "str_or_i32")]
id: String,
authorizing_merchant_id: i32,
#[serde(deserialize_with = "str_or_i32")]
approved: String,
#[serde(deserialize_with = "str_or_i32")]
message_id: String,
message: String,
auth_code: String,
created: String,
amount: f32,
order_number: String,
#[serde(rename = "type")]
payment_type: String,
comments: Option<String>,
batch_number: Option<String>,
total_refunds: Option<f32>,
total_completions: Option<f32>,
payment_method: String,
card: CardData,
billing: Option<AddressData>,
shipping: Option<AddressData>,
custom: CustomData,
adjusted_by: Option<Vec<AdjustedBy>>,
links: Vec<Links>,
risk_score: Option<f32>,
}
impl TryFrom<types::RefundsResponseRouterData<api::Execute, RefundResponse>>
for types::RefundsRouterData<api::Execute>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::RefundsResponseRouterData<api::Execute, RefundResponse>,
) -> Result<Self, Self::Error> {
let refund_status = match item.response.approved.as_str() {
"0" => enums::RefundStatus::Failure,
"1" => enums::RefundStatus::Success,
&_ => Err(errors::ConnectorError::ResponseDeserializationFailed)?,
};
Ok(Self {
response: Ok(types::RefundsResponseData {
connector_refund_id: item.response.id.to_string(),
refund_status,
}),
..item.data
})
}
}
impl TryFrom<types::RefundsResponseRouterData<api::RSync, RefundResponse>>
for types::RefundsRouterData<api::RSync>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::RefundsResponseRouterData<api::RSync, RefundResponse>,
) -> Result<Self, Self::Error> {
let refund_status = match item.response.approved.as_str() {
"0" => enums::RefundStatus::Failure,
"1" => enums::RefundStatus::Success,
&_ => Err(errors::ConnectorError::ResponseDeserializationFailed)?,
};
Ok(Self {
response: Ok(types::RefundsResponseData {
connector_refund_id: item.response.id.to_string(),
refund_status,
}),
..item.data
})
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct BamboraErrorResponse {
pub code: i32,
pub category: i32,
pub message: String,
pub reference: String,
pub details: Option<Vec<ErrorDetail>>,
pub validation: Option<CardValidation>,
pub card: Option<CardError>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CardError {
pub avs: AVSDetails,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AVSDetails {
pub message: String,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ErrorDetail {
field: String,
message: String,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CardValidation {
id: String,
approved: i32,
message_id: i32,
message: String,
auth_code: String,
trans_date: String,
order_number: String,
type_: String,
amount: f64,
cvd_id: i32,
}

View File

@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
use crate::{ use crate::{
connector::utils::{PaymentsCancelRequestData, RouterData}, connector::utils::{PaymentsCancelRequestData, RouterData},
consts,
core::errors, core::errors,
types::{self, api, storage::enums}, types::{self, api, storage::enums},
}; };
@ -448,6 +449,7 @@ pub struct NuveiPaymentsResponse {
pub payment_option: Option<PaymentOption>, pub payment_option: Option<PaymentOption>,
pub transaction_status: Option<NuveiTransactionStatus>, pub transaction_status: Option<NuveiTransactionStatus>,
pub gw_error_code: Option<i64>, pub gw_error_code: Option<i64>,
pub gw_error_reason: Option<String>,
pub gw_extended_error_code: Option<i64>, pub gw_extended_error_code: Option<i64>,
pub issuer_decline_code: Option<String>, pub issuer_decline_code: Option<String>,
pub issuer_decline_reason: Option<String>, pub issuer_decline_reason: Option<String>,
@ -530,26 +532,47 @@ impl<F, T>
code: item code: item
.response .response
.err_code .err_code
.ok_or(errors::ParsingError)? .map(|c| c.to_string())
.to_string(), .unwrap_or(consts::NO_ERROR_CODE.to_string()),
message: item.response.reason.clone().ok_or(errors::ParsingError)?, message: item
.response
.reason
.unwrap_or(consts::NO_ERROR_MESSAGE.to_string()),
reason: None, reason: None,
status_code: item.http_code, status_code: item.http_code,
}), }),
_ => Ok(types::PaymentsResponseData::TransactionResponse { _ => match item.response.transaction_status {
resource_id: types::ResponseId::ConnectorTransactionId( Some(NuveiTransactionStatus::Error) => Err(types::ErrorResponse {
item.response.transaction_id.ok_or(errors::ParsingError)?, code: item
), .response
redirection_data: None, .gw_error_code
mandate_reference: None, .map(|c| c.to_string())
connector_metadata: Some( .unwrap_or(consts::NO_ERROR_CODE.to_string()),
serde_json::to_value(NuveiMeta { message: item
session_token: item.response.session_token.unwrap_or_default(), .response
}) .gw_error_reason
.into_report() .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()),
.change_context(errors::ParsingError)?, reason: None,
), status_code: item.http_code,
}), }),
_ => Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(
item.response.transaction_id.ok_or(errors::ParsingError)?,
),
redirection_data: None,
mandate_reference: None,
connector_metadata: Some(
serde_json::to_value(NuveiMeta {
session_token: item
.response
.session_token
.ok_or(errors::ParsingError)?,
})
.into_report()
.change_context(errors::ParsingError)?,
),
}),
},
}, },
..item.data ..item.data
}) })
@ -576,13 +599,30 @@ impl TryFrom<types::RefundsResponseRouterData<api::Execute, NuveiPaymentsRespons
let refund_status = item let refund_status = item
.response .response
.transaction_status .transaction_status
.clone()
.map(|a| a.into()) .map(|a| a.into())
.unwrap_or(enums::RefundStatus::Failure); .unwrap_or(enums::RefundStatus::Failure);
Ok(Self { let refund_response = match item.response.status {
response: Ok(types::RefundsResponseData { NuveiPaymentStatus::Error => Err(types::ErrorResponse {
code: item
.response
.err_code
.map(|c| c.to_string())
.unwrap_or(consts::NO_ERROR_CODE.to_string()),
message: item
.response
.reason
.unwrap_or(consts::NO_ERROR_MESSAGE.to_string()),
reason: None,
status_code: item.http_code,
}),
_ => Ok(types::RefundsResponseData {
connector_refund_id: item.response.transaction_id.ok_or(errors::ParsingError)?, connector_refund_id: item.response.transaction_id.ok_or(errors::ParsingError)?,
refund_status, refund_status,
}), }),
};
Ok(Self {
response: refund_response,
..item.data ..item.data
}) })
} }
@ -598,13 +638,30 @@ impl TryFrom<types::RefundsResponseRouterData<api::RSync, NuveiPaymentsResponse>
let refund_status = item let refund_status = item
.response .response
.transaction_status .transaction_status
.clone()
.map(|a| a.into()) .map(|a| a.into())
.unwrap_or(enums::RefundStatus::Failure); .unwrap_or(enums::RefundStatus::Failure);
Ok(Self { let refund_response = match item.response.status {
response: Ok(types::RefundsResponseData { NuveiPaymentStatus::Error => Err(types::ErrorResponse {
code: item
.response
.err_code
.map(|c| c.to_string())
.unwrap_or(consts::NO_ERROR_CODE.to_string()),
message: item
.response
.reason
.unwrap_or(consts::NO_ERROR_MESSAGE.to_string()),
reason: None,
status_code: item.http_code,
}),
_ => Ok(types::RefundsResponseData {
connector_refund_id: item.response.transaction_id.ok_or(errors::ParsingError)?, connector_refund_id: item.response.transaction_id.ok_or(errors::ParsingError)?,
refund_status, refund_status,
}), }),
};
Ok(Self {
response: refund_response,
..item.data ..item.data
}) })
} }

View File

@ -51,6 +51,26 @@ pub trait PaymentsRequestData {
fn get_return_url(&self) -> Result<String, Error>; fn get_return_url(&self) -> Result<String, Error>;
} }
pub trait PaymentsAuthorizeRequestData {
fn is_auto_capture(&self) -> bool;
}
impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData {
fn is_auto_capture(&self) -> bool {
self.capture_method == Some(storage_models::enums::CaptureMethod::Automatic)
}
}
pub trait PaymentsSyncRequestData {
fn is_auto_capture(&self) -> bool;
}
impl PaymentsSyncRequestData for types::PaymentsSyncData {
fn is_auto_capture(&self) -> bool {
self.capture_method == Some(storage_models::enums::CaptureMethod::Automatic)
}
}
pub trait PaymentsCancelRequestData { pub trait PaymentsCancelRequestData {
fn get_amount(&self) -> Result<i64, Error>; fn get_amount(&self) -> Result<i64, Error>;
fn get_currency(&self) -> Result<storage_models::enums::Currency, Error>; fn get_currency(&self) -> Result<storage_models::enums::Currency, Error>;

View File

@ -9,6 +9,7 @@ use storage_models::enums;
use transformers as worldpay; use transformers as worldpay;
use self::{requests::*, response::*}; use self::{requests::*, response::*};
use super::utils::RefundsRequestData;
use crate::{ use crate::{
configs::settings, configs::settings,
core::{ core::{
@ -82,7 +83,7 @@ impl ConnectorCommon for Worldpay {
status_code: res.status_code, status_code: res.status_code,
code: response.error_name, code: response.error_name,
message: response.message, message: response.message,
reason: None, reason: response.validation_errors.map(|e| e.to_string()),
}) })
} }
} }
@ -542,7 +543,7 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse
Ok(format!( Ok(format!(
"{}payments/events/{}", "{}payments/events/{}",
self.base_url(connectors), self.base_url(connectors),
req.request.connector_transaction_id req.request.get_connector_refund_id()?
)) ))
} }

View File

@ -176,8 +176,8 @@ pub struct WalletPayment {
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
pub struct CardExpiryDate { pub struct CardExpiryDate {
pub month: Secret<String>, pub month: i8,
pub year: Secret<String>, pub year: i32,
} }
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]

View File

@ -303,4 +303,5 @@ impl PaymentsResponseScheme {
pub struct WorldpayErrorResponse { pub struct WorldpayErrorResponse {
pub error_name: String, pub error_name: String,
pub message: String, pub message: String,
pub validation_errors: Option<serde_json::Value>,
} }

View File

@ -1,6 +1,7 @@
use base64::Engine; use base64::Engine;
use common_utils::errors::CustomResult; use common_utils::errors::CustomResult;
use error_stack::ResultExt; use error_stack::{IntoReport, ResultExt};
use masking::PeekInterface;
use storage_models::enums; use storage_models::enums;
use super::{requests::*, response::*}; use super::{requests::*, response::*};
@ -16,8 +17,20 @@ fn fetch_payment_instrument(
match payment_method { match payment_method {
api::PaymentMethodData::Card(card) => Ok(PaymentInstrument::Card(CardPayment { api::PaymentMethodData::Card(card) => Ok(PaymentInstrument::Card(CardPayment {
card_expiry_date: CardExpiryDate { card_expiry_date: CardExpiryDate {
month: card.card_exp_month, month: card
year: card.card_exp_year, .card_exp_month
.peek()
.clone()
.parse::<i8>()
.into_report()
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?,
year: card
.card_exp_year
.peek()
.clone()
.parse::<i32>()
.into_report()
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?,
}, },
card_number: card.card_number, card_number: card.card_number,
..CardPayment::default() ..CardPayment::default()
@ -64,7 +77,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for WorldpayPaymentsRequest {
currency: item.request.currency.to_string(), currency: item.request.currency.to_string(),
}, },
narrative: InstructionNarrative { narrative: InstructionNarrative {
line1: item.merchant_id.clone(), line1: item.merchant_id.clone().replace('_', "-"),
..Default::default() ..Default::default()
}, },
payment_instrument: fetch_payment_instrument( payment_instrument: fetch_payment_instrument(
@ -73,7 +86,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for WorldpayPaymentsRequest {
debt_repayment: None, debt_repayment: None,
}, },
merchant: Merchant { merchant: Merchant {
entity: item.payment_id.clone(), entity: item.attempt_id.clone().replace('_', "-"),
..Default::default() ..Default::default()
}, },
transaction_reference: item.attempt_id.clone(), transaction_reference: item.attempt_id.clone(),
@ -91,9 +104,13 @@ impl TryFrom<&types::ConnectorAuthType> for WorldpayAuthType {
type Error = error_stack::Report<errors::ConnectorError>; type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(auth_type: &types::ConnectorAuthType) -> Result<Self, Self::Error> { fn try_from(auth_type: &types::ConnectorAuthType) -> Result<Self, Self::Error> {
match auth_type { match auth_type {
types::ConnectorAuthType::HeaderKey { api_key } => Ok(Self { types::ConnectorAuthType::BodyKey { api_key, key1 } => {
api_key: api_key.to_string(), let auth_key = format!("{key1}:{api_key}");
}), let auth_header = format!("Basic {}", consts::BASE64_ENGINE.encode(auth_key));
Ok(Self {
api_key: auth_header,
})
}
_ => Err(errors::ConnectorError::FailedToObtainAuthType)?, _ => Err(errors::ConnectorError::FailedToObtainAuthType)?,
} }
} }

View File

@ -165,6 +165,7 @@ impl ConnectorData {
"airwallex" => Ok(Box::new(&connector::Airwallex)), "airwallex" => Ok(Box::new(&connector::Airwallex)),
"applepay" => Ok(Box::new(&connector::Applepay)), "applepay" => Ok(Box::new(&connector::Applepay)),
"authorizedotnet" => Ok(Box::new(&connector::Authorizedotnet)), "authorizedotnet" => Ok(Box::new(&connector::Authorizedotnet)),
"bambora" => Ok(Box::new(&connector::Bambora)),
"bluesnap" => Ok(Box::new(&connector::Bluesnap)), "bluesnap" => Ok(Box::new(&connector::Bluesnap)),
"braintree" => Ok(Box::new(&connector::Braintree)), "braintree" => Ok(Box::new(&connector::Braintree)),
"checkout" => Ok(Box::new(&connector::Checkout)), "checkout" => Ok(Box::new(&connector::Checkout)),

View File

@ -0,0 +1,449 @@
use api_models::payments::PaymentMethodData;
use masking::Secret;
use router::types::{self, api, storage::enums};
use crate::{
connector_auth,
utils::{self, ConnectorActions},
};
#[derive(Clone, Copy)]
struct BamboraTest;
impl ConnectorActions for BamboraTest {}
impl utils::Connector for BamboraTest {
fn get_data(&self) -> types::api::ConnectorData {
use router::connector::Bambora;
types::api::ConnectorData {
connector: Box::new(&Bambora),
connector_name: types::Connector::Bambora,
get_token: types::api::GetToken::Connector,
}
}
fn get_auth_token(&self) -> types::ConnectorAuthType {
types::ConnectorAuthType::from(
connector_auth::ConnectorAuthentication::new()
.bambora
.expect("Missing connector authentication configuration"),
)
}
fn get_name(&self) -> String {
"bambora".to_string()
}
}
static CONNECTOR: BamboraTest = BamboraTest {};
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("4030000010001234".to_string()),
card_exp_year: Secret::new("25".to_string()),
card_cvc: Secret::new("123".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
})
}
// Cards Positive Tests
// Creates a payment using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_only_authorize_payment() {
let response = CONNECTOR
.authorize_payment(get_default_payment_authorize_data(), None)
.await
.expect("Authorize payment response");
assert_eq!(response.status, enums::AttemptStatus::Authorized);
}
// Captures a payment using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_capture_authorized_payment() {
let response = CONNECTOR
.authorize_and_capture_payment(get_default_payment_authorize_data(), None, None)
.await
.expect("Capture payment response");
assert_eq!(response.status, enums::AttemptStatus::Charged);
}
// Partially captures a payment using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_partially_capture_authorized_payment() {
let response = CONNECTOR
.authorize_and_capture_payment(
get_default_payment_authorize_data(),
Some(types::PaymentsCaptureData {
amount_to_capture: Some(50),
..utils::PaymentCaptureType::default().0
}),
None,
)
.await
.expect("Capture payment response");
assert_eq!(response.status, enums::AttemptStatus::Charged);
}
// Synchronizes a payment using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_sync_authorized_payment() {
let authorize_response = CONNECTOR
.authorize_payment(get_default_payment_authorize_data(), None)
.await
.expect("Authorize payment response");
let txn_id = utils::get_connector_transaction_id(authorize_response.response);
let response = CONNECTOR
.psync_retry_till_status_matches(
enums::AttemptStatus::Authorized,
Some(types::PaymentsSyncData {
connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(
txn_id.unwrap(),
),
encoded_data: None,
capture_method: None,
}),
None,
)
.await
.expect("PSync response");
assert_eq!(response.status, enums::AttemptStatus::Authorized,);
}
// Voids a payment using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_void_authorized_payment() {
let response = CONNECTOR
.authorize_and_void_payment(
get_default_payment_authorize_data(),
Some(types::PaymentsCancelData {
connector_transaction_id: String::from(""),
cancellation_reason: Some("requested_by_customer".to_string()),
..Default::default()
}),
None,
)
.await
.expect("Void payment response");
assert_eq!(response.status, enums::AttemptStatus::Voided);
}
// Refunds a payment using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_refund_manually_captured_payment() {
let response = CONNECTOR
.capture_payment_and_refund(get_default_payment_authorize_data(), None, None, None)
.await
.unwrap();
assert_eq!(
response.response.unwrap().refund_status,
enums::RefundStatus::Success,
);
}
// Partially refunds a payment using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_partially_refund_manually_captured_payment() {
let response = CONNECTOR
.capture_payment_and_refund(
get_default_payment_authorize_data(),
None,
Some(types::RefundsData {
refund_amount: 50,
..utils::PaymentRefundType::default().0
}),
None,
)
.await
.unwrap();
assert_eq!(
response.response.unwrap().refund_status,
enums::RefundStatus::Success,
);
}
// Synchronizes a refund using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_sync_manually_captured_refund() {
let refund_response = CONNECTOR
.capture_payment_and_refund(get_default_payment_authorize_data(), None, None, None)
.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,
);
}
// 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(), None)
.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(), None)
.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: Some(enums::CaptureMethod::Automatic),
}),
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, None)
.await
.unwrap();
assert_eq!(
response.response.unwrap().refund_status,
enums::RefundStatus::Success,
);
}
// Partially refunds a payment using the automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_partially_refund_succeeded_payment() {
let refund_response = CONNECTOR
.make_payment_and_refund(
get_default_payment_authorize_data(),
Some(types::RefundsData {
refund_amount: 50,
..utils::PaymentRefundType::default().0
}),
None,
)
.await
.unwrap();
assert_eq!(
refund_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, None)
.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 response = CONNECTOR
.make_payment(
Some(types::PaymentsAuthorizeData {
payment_method_data: PaymentMethodData::Card(api::Card {
card_number: Secret::new("1234567891011".to_string()),
card_exp_year: Secret::new("25".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
}),
None,
)
.await
.unwrap();
assert_eq!(
response.response.unwrap_err().message,
"Invalid Card Number".to_string(),
);
}
// Creates a payment with empty card number.
#[actix_web::test]
async fn should_fail_payment_for_empty_card_number() {
let response = CONNECTOR
.make_payment(
Some(types::PaymentsAuthorizeData {
payment_method_data: PaymentMethodData::Card(api::Card {
card_number: Secret::new(String::from("")),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
}),
None,
)
.await
.unwrap();
let x = response.response.unwrap_err();
assert_eq!(
x.reason,
Some(r#"[{"field":"card:number","message":"Invalid Card Number"},{"field":"card:expiry_year","message":"Invalid expiration year"}]"#.to_string()),
);
}
// Creates a payment with incorrect CVC.
#[actix_web::test]
async fn should_fail_payment_for_incorrect_cvc() {
let response = CONNECTOR
.make_payment(
Some(types::PaymentsAuthorizeData {
payment_method_data: PaymentMethodData::Card(api::Card {
card_exp_year: Secret::new("25".to_string()),
card_cvc: Secret::new("12345".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
}),
None,
)
.await
.unwrap();
assert_eq!(
response.response.unwrap_err().reason,
Some(r#"[{"field":"card:cvd","message":"Invalid card CVD"},{"field":"card:cvd","message":"Invalid card CVD"}]"#.to_string())
);
}
// Creates a payment with incorrect expiry month.
#[actix_web::test]
async fn should_fail_payment_for_invalid_exp_month() {
let response = CONNECTOR
.make_payment(
Some(types::PaymentsAuthorizeData {
payment_method_data: PaymentMethodData::Card(api::Card {
card_exp_month: Secret::new("20".to_string()),
card_number: Secret::new("4030000010001234".to_string()),
card_exp_year: Secret::new("25".to_string()),
card_cvc: Secret::new("123".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
}),
None,
)
.await
.unwrap();
assert_eq!(
response.response.unwrap_err().reason,
Some(r#"[{"field":"card:expiry_month","message":"Invalid expiry date"}]"#.to_string())
);
}
// Creates a payment with incorrect expiry year.
#[actix_web::test]
async fn should_fail_payment_for_incorrect_expiry_year() {
let response = CONNECTOR
.make_payment(
Some(types::PaymentsAuthorizeData {
payment_method_data: PaymentMethodData::Card(api::Card {
card_exp_year: Secret::new("2000".to_string()),
card_number: Secret::new("4030000010001234".to_string()),
card_cvc: Secret::new("123".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
}),
None,
)
.await
.unwrap();
assert_eq!(
response.response.unwrap_err().reason,
Some(r#"[{"field":"card:expiry_year","message":"Invalid expiration year"}]"#.to_string())
);
}
// Voids a payment using automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_fail_void_payment_for_auto_capture() {
let authorize_response = CONNECTOR
.make_payment(get_default_payment_authorize_data(), None)
.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 void_response = CONNECTOR
.void_payment(txn_id.unwrap(), None, None)
.await
.unwrap();
assert_eq!(
void_response.response.unwrap_err().message,
"Transaction cannot be adjusted"
);
}
// Captures a payment using invalid connector payment id.
#[actix_web::test]
async fn should_fail_capture_for_invalid_payment() {
let capture_response = CONNECTOR
.capture_payment("123456789".to_string(), None, None)
.await
.unwrap();
assert_eq!(
capture_response.response.unwrap_err().message,
String::from("Missing or invalid payment information - Please validate all required payment information.")
);
}
// Refunds a payment with refund amount higher than payment amount.
#[actix_web::test]
async fn should_succeed_for_refund_amount_higher_than_payment_amount() {
let response = CONNECTOR
.make_payment_and_refund(
get_default_payment_authorize_data(),
Some(types::RefundsData {
refund_amount: 150,
..utils::PaymentRefundType::default().0
}),
None,
)
.await
.unwrap();
assert_eq!(
response.response.unwrap().refund_status,
enums::RefundStatus::Success
);
}

View File

@ -3,14 +3,15 @@ use serde::Deserialize;
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub(crate) struct ConnectorAuthentication { pub(crate) struct ConnectorAuthentication {
pub dlocal: Option<SignatureKey>,
pub aci: Option<BodyKey>, pub aci: Option<BodyKey>,
pub adyen: Option<BodyKey>, pub adyen: Option<BodyKey>,
pub airwallex: Option<BodyKey>, pub airwallex: Option<BodyKey>,
pub authorizedotnet: Option<BodyKey>, pub authorizedotnet: Option<BodyKey>,
pub bambora: Option<BodyKey>,
pub bluesnap: Option<BodyKey>, pub bluesnap: Option<BodyKey>,
pub checkout: Option<BodyKey>, pub checkout: Option<BodyKey>,
pub cybersource: Option<SignatureKey>, pub cybersource: Option<SignatureKey>,
pub dlocal: Option<SignatureKey>,
pub fiserv: Option<SignatureKey>, pub fiserv: Option<SignatureKey>,
pub globalpay: Option<HeaderKey>, pub globalpay: Option<HeaderKey>,
pub nuvei: Option<SignatureKey>, pub nuvei: Option<SignatureKey>,
@ -18,7 +19,7 @@ pub(crate) struct ConnectorAuthentication {
pub rapyd: Option<BodyKey>, pub rapyd: Option<BodyKey>,
pub shift4: Option<HeaderKey>, pub shift4: Option<HeaderKey>,
pub stripe: Option<HeaderKey>, pub stripe: Option<HeaderKey>,
pub worldpay: Option<HeaderKey>, pub worldpay: Option<BodyKey>,
pub worldline: Option<SignatureKey>, pub worldline: Option<SignatureKey>,
} }

View File

@ -4,6 +4,7 @@ mod aci;
mod adyen; mod adyen;
mod airwallex; mod airwallex;
mod authorizedotnet; mod authorizedotnet;
mod bambora;
mod bluesnap; mod bluesnap;
mod checkout; mod checkout;
mod connector_auth; mod connector_auth;

View File

@ -41,7 +41,7 @@ static CONNECTOR: NuveiTest = NuveiTest {};
fn get_payment_data() -> Option<types::PaymentsAuthorizeData> { fn get_payment_data() -> Option<types::PaymentsAuthorizeData> {
Some(types::PaymentsAuthorizeData { Some(types::PaymentsAuthorizeData {
payment_method_data: types::api::PaymentMethodData::Card(api::Card { payment_method_data: types::api::PaymentMethodData::Card(api::Card {
card_number: Secret::new(String::from("4000027891380961")), card_number: Secret::new(String::from("4444 3333 2222 1111")),
..utils::CCardType::default().0 ..utils::CCardType::default().0
}), }),
..utils::PaymentAuthorizeType::default().0 ..utils::PaymentAuthorizeType::default().0
@ -144,7 +144,7 @@ async fn should_refund_manually_captured_payment() {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
response.response.unwrap().refund_status, response.response.unwrap().refund_status,
enums::RefundStatus::Failure, //Nuvei fails refund always enums::RefundStatus::Success,
); );
} }
@ -165,7 +165,7 @@ async fn should_partially_refund_manually_captured_payment() {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
response.response.unwrap().refund_status, response.response.unwrap().refund_status,
enums::RefundStatus::Failure, //Nuvei fails refund always enums::RefundStatus::Success,
); );
} }
@ -220,7 +220,7 @@ async fn should_refund_auto_captured_payment() {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
response.response.unwrap().refund_status, response.response.unwrap().refund_status,
enums::RefundStatus::Failure, enums::RefundStatus::Success,
); );
} }
@ -240,7 +240,7 @@ async fn should_partially_refund_succeeded_payment() {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
refund_response.response.unwrap().refund_status, refund_response.response.unwrap().refund_status,
enums::RefundStatus::Failure, enums::RefundStatus::Success,
); );
} }
@ -395,7 +395,7 @@ async fn should_fail_capture_for_invalid_payment() {
// Refunds a payment with refund amount higher than payment amount. // Refunds a payment with refund amount higher than payment amount.
#[actix_web::test] #[actix_web::test]
async fn should_fail_for_refund_amount_higher_than_payment_amount() { async fn should_accept_refund_amount_higher_than_payment_amount() {
let response = CONNECTOR let response = CONNECTOR
.make_payment_and_refund( .make_payment_and_refund(
get_payment_data(), get_payment_data(),
@ -409,6 +409,6 @@ async fn should_fail_for_refund_amount_higher_than_payment_amount() {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
response.response.unwrap().refund_status, response.response.unwrap().refund_status,
enums::RefundStatus::Failure, enums::RefundStatus::Success,
); );
} }

View File

@ -26,7 +26,8 @@ api_secret = "Secret key"
api_key = "Bearer MyApiKey" api_key = "Bearer MyApiKey"
[worldpay] [worldpay]
api_key = "Bearer MyApiKey" api_key = "api_key"
key1 = "key1"
[payu] [payu]
api_key = "Bearer MyApiKey" api_key = "Bearer MyApiKey"
@ -52,4 +53,13 @@ api_secret = "API Secret Key"
[dlocal] [dlocal]
key1 = "key1" key1 = "key1"
api_key = "api_key" api_key = "api_key"
api_secret = "secret"
[bambora]
api_key = "api_key"
key1= "key1"
[nuvei]
api_key = "api_key"
key1 = "key1"
api_secret = "secret" api_secret = "secret"

View File

@ -97,6 +97,9 @@ base_url = "https://api-demo.airwallex.com/"
[connectors.dlocal] [connectors.dlocal]
base_url = "https://sandbox.dlocal.com/" base_url = "https://sandbox.dlocal.com/"
[connectors.bambora]
base_url = "https://api.na.bambora.com"
[connectors.supported] [connectors.supported]
wallets = ["klarna", "braintree", "applepay"] wallets = ["klarna", "braintree", "applepay"]
cards = [ cards = [
@ -104,6 +107,7 @@ cards = [
"adyen", "adyen",
"airwallex", "airwallex",
"authorizedotnet", "authorizedotnet",
"bambora",
"bluesnap", "bluesnap",
"braintree", "braintree",
"checkout", "checkout",