mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 00:49:42 +08:00
feat(connector): [Bambora] Add support for cards Authorize, psync, capture, void, refund, Rsync (#677)
This commit is contained in:
@ -49,6 +49,7 @@ cards = [
|
||||
"adyen",
|
||||
"airwallex",
|
||||
"authorizedotnet",
|
||||
"bambora",
|
||||
"bluesnap",
|
||||
"braintree",
|
||||
"checkout",
|
||||
@ -111,7 +112,7 @@ base_url = "https://sandboxapi.rapyd.net"
|
||||
base_url = "https://cert.api.fiservapps.com/"
|
||||
|
||||
[connectors.worldpay]
|
||||
base_url = "http://localhost:9090/"
|
||||
base_url = "https://try.access.worldpay.com/"
|
||||
|
||||
[connectors.payu]
|
||||
base_url = "https://secure.snd.payu.com/"
|
||||
@ -134,6 +135,9 @@ base_url = "https://api-demo.airwallex.com/"
|
||||
[connectors.dlocal]
|
||||
base_url = "https://sandbox.dlocal.com/"
|
||||
|
||||
[connectors.bambora]
|
||||
base_url = "https://api.na.bambora.com"
|
||||
|
||||
[scheduler]
|
||||
stream = "SCHEDULER_STREAM"
|
||||
|
||||
|
||||
@ -166,6 +166,9 @@ base_url = "https://api-demo.airwallex.com/"
|
||||
base_url = "https://sandbox.dlocal.com/"
|
||||
|
||||
# This data is used to call respective connectors for wallets and cards
|
||||
[connectors.bambora]
|
||||
base_url = "https://api.na.bambora.com"
|
||||
|
||||
[connectors.supported]
|
||||
wallets = ["klarna", "braintree", "applepay"]
|
||||
cards = [
|
||||
|
||||
@ -117,12 +117,16 @@ base_url = "https://api-demo.airwallex.com/"
|
||||
[connectors.dlocal]
|
||||
base_url = "https://sandbox.dlocal.com/"
|
||||
|
||||
[connectors.bambora]
|
||||
base_url = "https://api.na.bambora.com"
|
||||
|
||||
[connectors.supported]
|
||||
wallets = ["klarna", "braintree", "applepay"]
|
||||
cards = [
|
||||
"adyen",
|
||||
"airwallex",
|
||||
"authorizedotnet",
|
||||
"bambora",
|
||||
"bluesnap",
|
||||
"braintree",
|
||||
"checkout",
|
||||
|
||||
@ -193,7 +193,7 @@ async fn should_sync_auto_captured_payment() {
|
||||
txn_id.unwrap(),
|
||||
),
|
||||
encoded_data: None,
|
||||
capture_method: None,
|
||||
capture_method: Some(enums::CaptureMethod::Automatic),
|
||||
}),
|
||||
None,
|
||||
)
|
||||
|
||||
@ -550,6 +550,7 @@ pub enum Connector {
|
||||
Cybersource,
|
||||
#[default]
|
||||
Dummy,
|
||||
Bambora,
|
||||
Dlocal,
|
||||
Fiserv,
|
||||
Globalpay,
|
||||
@ -588,6 +589,7 @@ pub enum RoutableConnectors {
|
||||
Adyen,
|
||||
Airwallex,
|
||||
Authorizedotnet,
|
||||
Bambora,
|
||||
Bluesnap,
|
||||
Braintree,
|
||||
Checkout,
|
||||
|
||||
@ -228,6 +228,7 @@ pub struct Connectors {
|
||||
pub airwallex: ConnectorParams,
|
||||
pub applepay: ConnectorParams,
|
||||
pub authorizedotnet: ConnectorParams,
|
||||
pub bambora: ConnectorParams,
|
||||
pub bluesnap: ConnectorParams,
|
||||
pub braintree: ConnectorParams,
|
||||
pub checkout: ConnectorParams,
|
||||
|
||||
@ -3,10 +3,12 @@ pub mod adyen;
|
||||
pub mod airwallex;
|
||||
pub mod applepay;
|
||||
pub mod authorizedotnet;
|
||||
pub mod bambora;
|
||||
pub mod bluesnap;
|
||||
pub mod braintree;
|
||||
pub mod checkout;
|
||||
pub mod cybersource;
|
||||
pub mod dlocal;
|
||||
pub mod fiserv;
|
||||
pub mod globalpay;
|
||||
pub mod klarna;
|
||||
@ -19,12 +21,10 @@ pub mod utils;
|
||||
pub mod worldline;
|
||||
pub mod worldpay;
|
||||
|
||||
pub mod dlocal;
|
||||
|
||||
pub use self::{
|
||||
aci::Aci, adyen::Adyen, airwallex::Airwallex, applepay::Applepay,
|
||||
authorizedotnet::Authorizedotnet, bluesnap::Bluesnap, braintree::Braintree, checkout::Checkout,
|
||||
cybersource::Cybersource, dlocal::Dlocal, fiserv::Fiserv, globalpay::Globalpay, klarna::Klarna,
|
||||
nuvei::Nuvei, payu::Payu, rapyd::Rapyd, shift4::Shift4, stripe::Stripe, worldline::Worldline,
|
||||
worldpay::Worldpay,
|
||||
authorizedotnet::Authorizedotnet, bambora::Bambora, bluesnap::Bluesnap, braintree::Braintree,
|
||||
checkout::Checkout, cybersource::Cybersource, dlocal::Dlocal, fiserv::Fiserv,
|
||||
globalpay::Globalpay, klarna::Klarna, nuvei::Nuvei, payu::Payu, rapyd::Rapyd, shift4::Shift4,
|
||||
stripe::Stripe, worldline::Worldline, worldpay::Worldpay,
|
||||
};
|
||||
|
||||
634
crates/router/src/connector/bambora.rs
Normal file
634
crates/router/src/connector/bambora.rs
Normal 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
|
||||
}
|
||||
}
|
||||
453
crates/router/src/connector/bambora/transformers.rs
Normal file
453
crates/router/src/connector/bambora/transformers.rs
Normal 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,
|
||||
}
|
||||
@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
connector::utils::{PaymentsCancelRequestData, RouterData},
|
||||
consts,
|
||||
core::errors,
|
||||
types::{self, api, storage::enums},
|
||||
};
|
||||
@ -448,6 +449,7 @@ pub struct NuveiPaymentsResponse {
|
||||
pub payment_option: Option<PaymentOption>,
|
||||
pub transaction_status: Option<NuveiTransactionStatus>,
|
||||
pub gw_error_code: Option<i64>,
|
||||
pub gw_error_reason: Option<String>,
|
||||
pub gw_extended_error_code: Option<i64>,
|
||||
pub issuer_decline_code: Option<String>,
|
||||
pub issuer_decline_reason: Option<String>,
|
||||
@ -530,26 +532,47 @@ impl<F, T>
|
||||
code: item
|
||||
.response
|
||||
.err_code
|
||||
.ok_or(errors::ParsingError)?
|
||||
.to_string(),
|
||||
message: item.response.reason.clone().ok_or(errors::ParsingError)?,
|
||||
.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::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.unwrap_or_default(),
|
||||
})
|
||||
.into_report()
|
||||
.change_context(errors::ParsingError)?,
|
||||
),
|
||||
}),
|
||||
_ => match item.response.transaction_status {
|
||||
Some(NuveiTransactionStatus::Error) => Err(types::ErrorResponse {
|
||||
code: item
|
||||
.response
|
||||
.gw_error_code
|
||||
.map(|c| c.to_string())
|
||||
.unwrap_or(consts::NO_ERROR_CODE.to_string()),
|
||||
message: item
|
||||
.response
|
||||
.gw_error_reason
|
||||
.unwrap_or(consts::NO_ERROR_MESSAGE.to_string()),
|
||||
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
|
||||
})
|
||||
@ -576,13 +599,30 @@ impl TryFrom<types::RefundsResponseRouterData<api::Execute, NuveiPaymentsRespons
|
||||
let refund_status = item
|
||||
.response
|
||||
.transaction_status
|
||||
.clone()
|
||||
.map(|a| a.into())
|
||||
.unwrap_or(enums::RefundStatus::Failure);
|
||||
Ok(Self {
|
||||
response: Ok(types::RefundsResponseData {
|
||||
let refund_response = match item.response.status {
|
||||
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)?,
|
||||
refund_status,
|
||||
}),
|
||||
};
|
||||
Ok(Self {
|
||||
response: refund_response,
|
||||
..item.data
|
||||
})
|
||||
}
|
||||
@ -598,13 +638,30 @@ impl TryFrom<types::RefundsResponseRouterData<api::RSync, NuveiPaymentsResponse>
|
||||
let refund_status = item
|
||||
.response
|
||||
.transaction_status
|
||||
.clone()
|
||||
.map(|a| a.into())
|
||||
.unwrap_or(enums::RefundStatus::Failure);
|
||||
Ok(Self {
|
||||
response: Ok(types::RefundsResponseData {
|
||||
let refund_response = match item.response.status {
|
||||
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)?,
|
||||
refund_status,
|
||||
}),
|
||||
};
|
||||
Ok(Self {
|
||||
response: refund_response,
|
||||
..item.data
|
||||
})
|
||||
}
|
||||
|
||||
@ -51,6 +51,26 @@ pub trait PaymentsRequestData {
|
||||
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 {
|
||||
fn get_amount(&self) -> Result<i64, Error>;
|
||||
fn get_currency(&self) -> Result<storage_models::enums::Currency, Error>;
|
||||
|
||||
@ -9,6 +9,7 @@ use storage_models::enums;
|
||||
use transformers as worldpay;
|
||||
|
||||
use self::{requests::*, response::*};
|
||||
use super::utils::RefundsRequestData;
|
||||
use crate::{
|
||||
configs::settings,
|
||||
core::{
|
||||
@ -82,7 +83,7 @@ impl ConnectorCommon for Worldpay {
|
||||
status_code: res.status_code,
|
||||
code: response.error_name,
|
||||
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!(
|
||||
"{}payments/events/{}",
|
||||
self.base_url(connectors),
|
||||
req.request.connector_transaction_id
|
||||
req.request.get_connector_refund_id()?
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@ -176,8 +176,8 @@ pub struct WalletPayment {
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
||||
pub struct CardExpiryDate {
|
||||
pub month: Secret<String>,
|
||||
pub year: Secret<String>,
|
||||
pub month: i8,
|
||||
pub year: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
||||
|
||||
@ -303,4 +303,5 @@ impl PaymentsResponseScheme {
|
||||
pub struct WorldpayErrorResponse {
|
||||
pub error_name: String,
|
||||
pub message: String,
|
||||
pub validation_errors: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use base64::Engine;
|
||||
use common_utils::errors::CustomResult;
|
||||
use error_stack::ResultExt;
|
||||
use error_stack::{IntoReport, ResultExt};
|
||||
use masking::PeekInterface;
|
||||
use storage_models::enums;
|
||||
|
||||
use super::{requests::*, response::*};
|
||||
@ -16,8 +17,20 @@ fn fetch_payment_instrument(
|
||||
match payment_method {
|
||||
api::PaymentMethodData::Card(card) => Ok(PaymentInstrument::Card(CardPayment {
|
||||
card_expiry_date: CardExpiryDate {
|
||||
month: card.card_exp_month,
|
||||
year: card.card_exp_year,
|
||||
month: card
|
||||
.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,
|
||||
..CardPayment::default()
|
||||
@ -64,7 +77,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for WorldpayPaymentsRequest {
|
||||
currency: item.request.currency.to_string(),
|
||||
},
|
||||
narrative: InstructionNarrative {
|
||||
line1: item.merchant_id.clone(),
|
||||
line1: item.merchant_id.clone().replace('_', "-"),
|
||||
..Default::default()
|
||||
},
|
||||
payment_instrument: fetch_payment_instrument(
|
||||
@ -73,7 +86,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for WorldpayPaymentsRequest {
|
||||
debt_repayment: None,
|
||||
},
|
||||
merchant: Merchant {
|
||||
entity: item.payment_id.clone(),
|
||||
entity: item.attempt_id.clone().replace('_', "-"),
|
||||
..Default::default()
|
||||
},
|
||||
transaction_reference: item.attempt_id.clone(),
|
||||
@ -91,9 +104,13 @@ impl TryFrom<&types::ConnectorAuthType> for WorldpayAuthType {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(auth_type: &types::ConnectorAuthType) -> Result<Self, Self::Error> {
|
||||
match auth_type {
|
||||
types::ConnectorAuthType::HeaderKey { api_key } => Ok(Self {
|
||||
api_key: api_key.to_string(),
|
||||
}),
|
||||
types::ConnectorAuthType::BodyKey { api_key, key1 } => {
|
||||
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)?,
|
||||
}
|
||||
}
|
||||
|
||||
@ -165,6 +165,7 @@ impl ConnectorData {
|
||||
"airwallex" => Ok(Box::new(&connector::Airwallex)),
|
||||
"applepay" => Ok(Box::new(&connector::Applepay)),
|
||||
"authorizedotnet" => Ok(Box::new(&connector::Authorizedotnet)),
|
||||
"bambora" => Ok(Box::new(&connector::Bambora)),
|
||||
"bluesnap" => Ok(Box::new(&connector::Bluesnap)),
|
||||
"braintree" => Ok(Box::new(&connector::Braintree)),
|
||||
"checkout" => Ok(Box::new(&connector::Checkout)),
|
||||
|
||||
449
crates/router/tests/connectors/bambora.rs
Normal file
449
crates/router/tests/connectors/bambora.rs
Normal 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
|
||||
);
|
||||
}
|
||||
@ -3,14 +3,15 @@ use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub(crate) struct ConnectorAuthentication {
|
||||
pub dlocal: Option<SignatureKey>,
|
||||
pub aci: Option<BodyKey>,
|
||||
pub adyen: Option<BodyKey>,
|
||||
pub airwallex: Option<BodyKey>,
|
||||
pub authorizedotnet: Option<BodyKey>,
|
||||
pub bambora: Option<BodyKey>,
|
||||
pub bluesnap: Option<BodyKey>,
|
||||
pub checkout: Option<BodyKey>,
|
||||
pub cybersource: Option<SignatureKey>,
|
||||
pub dlocal: Option<SignatureKey>,
|
||||
pub fiserv: Option<SignatureKey>,
|
||||
pub globalpay: Option<HeaderKey>,
|
||||
pub nuvei: Option<SignatureKey>,
|
||||
@ -18,7 +19,7 @@ pub(crate) struct ConnectorAuthentication {
|
||||
pub rapyd: Option<BodyKey>,
|
||||
pub shift4: Option<HeaderKey>,
|
||||
pub stripe: Option<HeaderKey>,
|
||||
pub worldpay: Option<HeaderKey>,
|
||||
pub worldpay: Option<BodyKey>,
|
||||
pub worldline: Option<SignatureKey>,
|
||||
}
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ mod aci;
|
||||
mod adyen;
|
||||
mod airwallex;
|
||||
mod authorizedotnet;
|
||||
mod bambora;
|
||||
mod bluesnap;
|
||||
mod checkout;
|
||||
mod connector_auth;
|
||||
|
||||
@ -41,7 +41,7 @@ static CONNECTOR: NuveiTest = NuveiTest {};
|
||||
fn get_payment_data() -> Option<types::PaymentsAuthorizeData> {
|
||||
Some(types::PaymentsAuthorizeData {
|
||||
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::PaymentAuthorizeType::default().0
|
||||
@ -144,7 +144,7 @@ async fn should_refund_manually_captured_payment() {
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
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();
|
||||
assert_eq!(
|
||||
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();
|
||||
assert_eq!(
|
||||
response.response.unwrap().refund_status,
|
||||
enums::RefundStatus::Failure,
|
||||
enums::RefundStatus::Success,
|
||||
);
|
||||
}
|
||||
|
||||
@ -240,7 +240,7 @@ async fn should_partially_refund_succeeded_payment() {
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
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.
|
||||
#[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
|
||||
.make_payment_and_refund(
|
||||
get_payment_data(),
|
||||
@ -409,6 +409,6 @@ async fn should_fail_for_refund_amount_higher_than_payment_amount() {
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.response.unwrap().refund_status,
|
||||
enums::RefundStatus::Failure,
|
||||
enums::RefundStatus::Success,
|
||||
);
|
||||
}
|
||||
|
||||
@ -26,7 +26,8 @@ api_secret = "Secret key"
|
||||
api_key = "Bearer MyApiKey"
|
||||
|
||||
[worldpay]
|
||||
api_key = "Bearer MyApiKey"
|
||||
api_key = "api_key"
|
||||
key1 = "key1"
|
||||
|
||||
[payu]
|
||||
api_key = "Bearer MyApiKey"
|
||||
@ -52,4 +53,13 @@ api_secret = "API Secret Key"
|
||||
[dlocal]
|
||||
key1 = "key1"
|
||||
api_key = "api_key"
|
||||
api_secret = "secret"
|
||||
|
||||
[bambora]
|
||||
api_key = "api_key"
|
||||
key1= "key1"
|
||||
|
||||
[nuvei]
|
||||
api_key = "api_key"
|
||||
key1 = "key1"
|
||||
api_secret = "secret"
|
||||
@ -97,6 +97,9 @@ base_url = "https://api-demo.airwallex.com/"
|
||||
[connectors.dlocal]
|
||||
base_url = "https://sandbox.dlocal.com/"
|
||||
|
||||
[connectors.bambora]
|
||||
base_url = "https://api.na.bambora.com"
|
||||
|
||||
[connectors.supported]
|
||||
wallets = ["klarna", "braintree", "applepay"]
|
||||
cards = [
|
||||
@ -104,6 +107,7 @@ cards = [
|
||||
"adyen",
|
||||
"airwallex",
|
||||
"authorizedotnet",
|
||||
"bambora",
|
||||
"bluesnap",
|
||||
"braintree",
|
||||
"checkout",
|
||||
|
||||
Reference in New Issue
Block a user