feat(connector): add authorize, capture, void, refund, psync, rsync support for Dlocal connector (#650)

Co-authored-by: Venkatesh <inventvenkat@gmail.com>
This commit is contained in:
Arjun Karthik
2023-02-25 19:43:40 +05:30
committed by GitHub
parent 6a487b195b
commit 7792de55ef
17 changed files with 1635 additions and 8 deletions

View File

@ -44,7 +44,7 @@ locker_decryption_key2 = ""
[connectors.supported]
wallets = ["klarna","braintree","applepay"]
cards = ["stripe","adyen","authorizedotnet","checkout","braintree","aci","shift4","cybersource", "worldpay", "globalpay", "fiserv", "payu", "worldline"]
cards = ["stripe","adyen","authorizedotnet","checkout","braintree","aci","shift4","cybersource", "worldpay", "globalpay", "fiserv", "payu", "worldline", "dlocal"]
[refund]
max_attempts = 10
@ -104,6 +104,9 @@ base_url = "https://apis.sandbox.globalpay.com/ucp/"
[connectors.worldline]
base_url = "https://eu.sandbox.api-ingenico.com/"
[connectors.dlocal]
base_url = "https://sandbox.dlocal.com/"
[scheduler]
stream = "SCHEDULER_STREAM"

View File

@ -154,6 +154,9 @@ base_url = "https://try.access.worldpay.com/"
base_url = "https://apis.sandbox.globalpay.com/ucp/"
# This data is used to call respective connectors for wallets and cards
[connectors.dlocal]
base_url = "https://sandbox.dlocal.com/"
[connectors.supported]
wallets = ["klarna", "braintree", "applepay"]
cards = [

View File

@ -105,9 +105,12 @@ base_url = "https://try.access.worldpay.com/"
[connectors.globalpay]
base_url = "https://apis.sandbox.globalpay.com/ucp/"
[connectors.dlocal]
base_url = "https://sandbox.dlocal.com/"
[connectors.supported]
wallets = ["klarna", "braintree", "applepay"]
cards = ["stripe", "adyen", "authorizedotnet", "checkout", "braintree", "shift4", "cybersource", "worldpay", "globalpay", "fiserv"]
cards = ["stripe", "adyen", "authorizedotnet", "checkout", "braintree", "shift4", "cybersource", "worldpay", "globalpay", "fiserv", "dlocal"]
[scheduler]

View File

@ -579,6 +579,7 @@ pub enum Connector {
Cybersource,
#[default]
Dummy,
Dlocal,
Fiserv,
Globalpay,
Klarna,
@ -617,6 +618,7 @@ pub enum RoutableConnectors {
Braintree,
Checkout,
Cybersource,
Dlocal,
Fiserv,
Globalpay,
Klarna,

View File

@ -14,7 +14,12 @@ pub mod validation;
/// Date-time utilities.
pub mod date_time {
use time::{Instant, OffsetDateTime, PrimitiveDateTime};
use std::num::NonZeroU8;
use time::{
format_description::well_known::iso8601::{Config, EncodedConfig, Iso8601, TimePrecision},
Instant, OffsetDateTime, PrimitiveDateTime,
};
/// Struct to represent milliseconds in time sensitive data fields
#[derive(Debug)]
pub struct Milliseconds(i32);
@ -43,6 +48,16 @@ pub mod date_time {
let result = block().await;
(result, start.elapsed().as_seconds_f64() * 1000f64)
}
/// Return the current date and time in UTC with the format [year]-[month]-[day]T[hour]:[minute]:[second].mmmZ Eg: 2023-02-15T13:33:18.898Z
pub fn date_as_yyyymmddthhmmssmmmz() -> Result<String, time::error::Format> {
const ISO_CONFIG: EncodedConfig = Config::DEFAULT
.set_time_precision(TimePrecision::Second {
decimal_digits: NonZeroU8::new(3),
})
.encode();
now().assume_utc().format(&Iso8601::<ISO_CONFIG>)
}
}
/// Generate a nanoid with the given prefix and length

View File

@ -142,6 +142,7 @@ pub struct Connectors {
pub braintree: ConnectorParams,
pub checkout: ConnectorParams,
pub cybersource: ConnectorParams,
pub dlocal: ConnectorParams,
pub fiserv: ConnectorParams,
pub globalpay: ConnectorParams,
pub klarna: ConnectorParams,

View File

@ -16,9 +16,11 @@ pub mod utils;
pub mod worldline;
pub mod worldpay;
pub mod dlocal;
pub use self::{
aci::Aci, adyen::Adyen, applepay::Applepay, authorizedotnet::Authorizedotnet,
braintree::Braintree, checkout::Checkout, cybersource::Cybersource, fiserv::Fiserv,
globalpay::Globalpay, klarna::Klarna, payu::Payu, rapyd::Rapyd, shift4::Shift4, stripe::Stripe,
worldline::Worldline, worldpay::Worldpay,
braintree::Braintree, checkout::Checkout, cybersource::Cybersource, dlocal::Dlocal,
fiserv::Fiserv, globalpay::Globalpay, klarna::Klarna, payu::Payu, rapyd::Rapyd, shift4::Shift4,
stripe::Stripe, worldline::Worldline, worldpay::Worldpay,
};

View File

@ -0,0 +1,597 @@
mod transformers;
use std::fmt::Debug;
use common_utils::{
crypto::{self, SignMessage},
date_time,
};
use error_stack::{IntoReport, ResultExt};
use hex::encode;
use transformers as dlocal;
use crate::{
configs::settings,
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 Dlocal;
impl api::Payment for Dlocal {}
impl api::PaymentSession for Dlocal {}
impl api::ConnectorAccessToken for Dlocal {}
impl api::PreVerify for Dlocal {}
impl api::PaymentAuthorize for Dlocal {}
impl api::PaymentSync for Dlocal {}
impl api::PaymentCapture for Dlocal {}
impl api::PaymentVoid for Dlocal {}
impl api::Refund for Dlocal {}
impl api::RefundExecute for Dlocal {}
impl api::RefundSync for Dlocal {}
impl<Flow, Request, Response> ConnectorCommonExt<Flow, Request, Response> for Dlocal
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 dlocal_req = match self.get_request_body(req)? {
Some(val) => val,
None => "".to_string(),
};
let date = date_time::date_as_yyyymmddthhmmssmmmz()
.into_report()
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
let auth = dlocal::DlocalAuthType::try_from(&req.connector_auth_type)?;
let sign_req: String = format!("{}{}{}", auth.x_login, date, dlocal_req);
let authz = crypto::HmacSha256::sign_message(
&crypto::HmacSha256,
auth.secret.as_bytes(),
sign_req.as_bytes(),
)
.change_context(errors::ConnectorError::RequestEncodingFailed)
.attach_printable("Failed to sign the message")?;
let auth_string: String = format!("V2-HMAC-SHA256, Signature: {}", encode(authz));
let headers = vec![
(headers::AUTHORIZATION.to_string(), auth_string),
(headers::X_LOGIN.to_string(), auth.x_login),
(headers::X_TRANS_KEY.to_string(), auth.x_trans_key),
(headers::X_VERSION.to_string(), "2.1".to_string()),
(headers::X_DATE.to_string(), date),
(
headers::CONTENT_TYPE.to_string(),
Self.get_content_type().to_string(),
),
];
Ok(headers)
}
}
impl ConnectorCommon for Dlocal {
fn id(&self) -> &'static str {
"dlocal"
}
fn common_get_content_type(&self) -> &'static str {
"application/json"
}
fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str {
connectors.dlocal.base_url.as_ref()
}
fn build_error_response(
&self,
res: Response,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
let response: dlocal::DlocalErrorResponse = res
.response
.parse_struct("Dlocal ErrorResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
Ok(ErrorResponse {
status_code: res.status_code,
code: response.code.to_string(),
message: response.message,
reason: response.param,
})
}
}
impl ConnectorIntegration<api::Session, types::PaymentsSessionData, types::PaymentsResponseData>
for Dlocal
{
//TODO: implement sessions flow
}
impl ConnectorIntegration<api::AccessTokenAuth, types::AccessTokenRequestData, types::AccessToken>
for Dlocal
{
}
impl ConnectorIntegration<api::Verify, types::VerifyRequestData, types::PaymentsResponseData>
for Dlocal
{
}
impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::PaymentsResponseData>
for Dlocal
{
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!("{}secure_payments", self.base_url(connectors)))
}
fn get_request_body(
&self,
req: &types::PaymentsAuthorizeRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let dlocal_req = utils::Encode::<dlocal::DlocalPaymentsRequest>::convert_and_encode(req)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(dlocal_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> {
logger::debug!(dlocal_payments_authorize_response=?res);
let response: dlocal::DlocalPaymentsResponse = res
.response
.parse_struct("Dlocal PaymentsAuthorizeResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
}
.try_into()
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: Response,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
for Dlocal
{
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 sync_data = dlocal::DlocalPaymentsSyncRequest::try_from(req)?;
Ok(format!(
"{}payments/{}/status",
self.base_url(connectors),
sync_data.authz_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> {
logger::debug!(dlocal_payment_sync_response=?res);
let response: dlocal::DlocalPaymentsResponse = res
.response
.parse_struct("Dlocal PaymentsSyncResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
}
impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::PaymentsResponseData>
for Dlocal
{
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!("{}payments", self.base_url(connectors)))
}
fn get_request_body(
&self,
req: &types::PaymentsCaptureRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let dlocal_req =
utils::Encode::<dlocal::DlocalPaymentsCaptureRequest>::convert_and_encode(req)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(dlocal_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(types::PaymentsCaptureType::get_request_body(self, req)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::PaymentsCaptureRouterData,
res: Response,
) -> CustomResult<types::PaymentsCaptureRouterData, errors::ConnectorError> {
logger::debug!(dlocal_payments_capture_response=?res);
let response: dlocal::DlocalPaymentsResponse = res
.response
.parse_struct("Dlocal PaymentsCaptureResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
}
.try_into()
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: Response,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl ConnectorIntegration<api::Void, types::PaymentsCancelData, types::PaymentsResponseData>
for Dlocal
{
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 cancel_data = dlocal::DlocalPaymentsCancelRequest::try_from(req)?;
Ok(format!(
"{}payments/{}/cancel",
self.base_url(connectors),
cancel_data.cancel_id
))
}
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)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::PaymentsCancelRouterData,
res: Response,
) -> CustomResult<types::PaymentsCancelRouterData, errors::ConnectorError> {
logger::debug!(dlocal_payments_cancel_response=?res);
let response: dlocal::DlocalPaymentsResponse = res
.response
.parse_struct("Dlocal PaymentsCancelResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
}
.try_into()
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: Response,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsResponseData> for Dlocal {
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> {
Ok(format!("{}refunds", self.base_url(connectors)))
}
fn get_request_body(
&self,
req: &types::RefundsRouterData<api::Execute>,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let dlocal_req = utils::Encode::<dlocal::RefundRequest>::convert_and_encode(req)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(dlocal_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> {
logger::debug!(dlocal_refund_response=?res);
let response: dlocal::RefundResponse =
res.response
.parse_struct("Dlocal RefundResponse")
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
}
.try_into()
.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 Dlocal {
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 sync_data = dlocal::DlocalRefundsSyncRequest::try_from(req)?;
Ok(format!(
"{}refunds/{}/status",
self.base_url(connectors),
sync_data.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)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::RefundSyncRouterData,
res: Response,
) -> CustomResult<types::RefundSyncRouterData, errors::ConnectorError> {
logger::debug!(dlocal_refund_sync_response=?res);
let response: dlocal::RefundResponse = res
.response
.parse_struct("Dlocal RefundSyncResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
}
.try_into()
.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 Dlocal {
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 Dlocal {
fn get_flow_type(
&self,
_query_params: &str,
) -> CustomResult<payments::CallConnectorAction, errors::ConnectorError> {
Ok(payments::CallConnectorAction::Trigger)
}
}

View File

@ -0,0 +1,513 @@
use api_models::payments::AddressDetails;
use common_utils::pii::{self, Email};
use error_stack::ResultExt;
use masking::{PeekInterface, Secret};
use serde::{Deserialize, Serialize};
use url::Url;
use crate::{
connector::utils::{AddressDetailsData, PaymentsRequestData},
core::errors,
services,
types::{self, api, storage::enums},
};
#[derive(Debug, Default, Eq, PartialEq, Serialize)]
pub struct Payer {
pub name: Option<Secret<String>>,
pub email: Option<Secret<String, Email>>,
pub document: Secret<String>,
}
#[derive(Debug, Default, Eq, Clone, PartialEq, Serialize, Deserialize)]
pub struct Card {
pub holder_name: Secret<String>,
pub number: Secret<String, pii::CardNumber>,
pub cvv: Secret<String>,
pub expiration_month: Secret<String>,
pub expiration_year: Secret<String>,
pub capture: String,
pub installments_id: Option<String>,
pub installments: Option<String>,
}
#[derive(Debug, Default, Eq, PartialEq, Serialize)]
pub struct ThreeDSecureReqData {
pub force: bool,
}
#[derive(Debug, Serialize, Default, Deserialize, Clone, Eq, PartialEq)]
#[serde(rename_all = "UPPERCASE")]
pub enum PaymentMethodId {
#[default]
Card,
}
#[derive(Debug, Serialize, Default, Deserialize, Clone, Eq, PartialEq)]
#[serde(rename_all = "UPPERCASE")]
pub enum PaymentMethodFlow {
#[default]
Direct,
ReDirect,
}
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
pub struct DlocalPaymentsRequest {
pub amount: i64,
pub currency: enums::Currency,
pub country: String,
pub payment_method_id: PaymentMethodId,
pub payment_method_flow: PaymentMethodFlow,
pub payer: Payer,
pub card: Option<Card>,
pub order_id: String,
pub three_dsecure: Option<ThreeDSecureReqData>,
pub callback_url: Option<String>,
}
impl TryFrom<&types::PaymentsAuthorizeRouterData> for DlocalPaymentsRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
let email = item.request.email.clone();
let address = item.get_billing_address()?;
let country = address.get_country()?;
let name = get_payer_name(address);
match item.request.payment_method_data {
api::PaymentMethod::Card(ref ccard) => {
let should_capture = matches!(
item.request.capture_method,
Some(enums::CaptureMethod::Automatic)
);
let payment_request = Self {
amount: item.request.amount,
currency: item.request.currency,
payment_method_id: PaymentMethodId::Card,
payment_method_flow: PaymentMethodFlow::Direct,
country: country.to_string(),
payer: Payer {
name,
email,
// [#589]: Allow securely collecting PII from customer in payments request
document: get_doc_from_currency(country.to_string()),
},
card: Some(Card {
holder_name: ccard.card_holder_name.clone(),
number: ccard.card_number.clone(),
cvv: ccard.card_cvc.clone(),
expiration_month: ccard.card_exp_month.clone(),
expiration_year: ccard.card_exp_year.clone(),
capture: should_capture.to_string(),
installments_id: item
.request
.mandate_id
.as_ref()
.map(|ids| ids.mandate_id.clone()),
// [#595[FEATURE] Pass Mandate history information in payment flows/request]
installments: item.request.mandate_id.clone().map(|_| "1".to_string()),
}),
order_id: item.payment_id.clone(),
three_dsecure: match item.auth_type {
storage_models::enums::AuthenticationType::ThreeDs => {
Some(ThreeDSecureReqData { force: true })
}
storage_models::enums::AuthenticationType::NoThreeDs => None,
},
callback_url: item.return_url.clone(),
};
Ok(payment_request)
}
_ => Err(errors::ConnectorError::NotImplemented("Payment Method".to_string()).into()),
}
}
}
fn get_payer_name(address: &AddressDetails) -> Option<Secret<String>> {
let first_name = address
.first_name
.clone()
.map_or("".to_string(), |first_name| first_name.peek().to_string());
let last_name = address
.last_name
.clone()
.map_or("".to_string(), |last_name| last_name.peek().to_string());
let name: String = format!("{} {}", first_name, last_name).trim().to_string();
if !name.is_empty() {
Some(Secret::new(name))
} else {
None
}
}
pub struct DlocalPaymentsSyncRequest {
pub authz_id: String,
}
impl TryFrom<&types::PaymentsSyncRouterData> for DlocalPaymentsSyncRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsSyncRouterData) -> Result<Self, Self::Error> {
Ok(Self {
authz_id: (item
.request
.connector_transaction_id
.get_connector_transaction_id()
.change_context(errors::ConnectorError::MissingConnectorTransactionID)?),
})
}
}
pub struct DlocalPaymentsCancelRequest {
pub cancel_id: String,
}
impl TryFrom<&types::PaymentsCancelRouterData> for DlocalPaymentsCancelRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsCancelRouterData) -> Result<Self, Self::Error> {
Ok(Self {
cancel_id: item.request.connector_transaction_id.clone(),
})
}
}
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
pub struct DlocalPaymentsCaptureRequest {
pub authorization_id: String,
pub amount: i64,
pub currency: String,
pub order_id: String,
}
impl TryFrom<&types::PaymentsCaptureRouterData> for DlocalPaymentsCaptureRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsCaptureRouterData) -> Result<Self, Self::Error> {
let amount_to_capture = match item.request.amount_to_capture {
Some(val) => val,
None => item.request.amount,
};
Ok(Self {
authorization_id: item.request.connector_transaction_id.clone(),
amount: amount_to_capture,
currency: item.request.currency.to_string(),
order_id: item.payment_id.clone(),
})
}
}
// Auth Struct
pub struct DlocalAuthType {
pub(super) x_login: String,
pub(super) x_trans_key: String,
pub(super) secret: String,
}
impl TryFrom<&types::ConnectorAuthType> for DlocalAuthType {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(auth_type: &types::ConnectorAuthType) -> Result<Self, Self::Error> {
if let types::ConnectorAuthType::SignatureKey {
api_key,
key1,
api_secret,
} = auth_type
{
Ok(Self {
x_login: api_key.to_string(),
x_trans_key: key1.to_string(),
secret: api_secret.to_string(),
})
} else {
Err(errors::ConnectorError::FailedToObtainAuthType.into())
}
}
}
#[derive(Debug, Clone, Eq, Default, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "UPPERCASE")]
pub enum DlocalPaymentStatus {
Authorized,
Paid,
Verified,
Cancelled,
#[default]
Pending,
Rejected,
}
impl From<DlocalPaymentStatus> for enums::AttemptStatus {
fn from(item: DlocalPaymentStatus) -> Self {
match item {
DlocalPaymentStatus::Authorized => Self::Authorized,
DlocalPaymentStatus::Verified => Self::Authorized,
DlocalPaymentStatus::Paid => Self::Charged,
DlocalPaymentStatus::Pending => Self::AuthenticationPending,
DlocalPaymentStatus::Cancelled => Self::Voided,
DlocalPaymentStatus::Rejected => Self::AuthenticationFailed,
}
}
}
#[derive(Eq, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ThreeDSecureResData {
pub redirect_url: Option<Url>,
}
#[derive(Debug, Default, Eq, Clone, PartialEq, Serialize, Deserialize)]
pub struct DlocalPaymentsResponse {
status: DlocalPaymentStatus,
id: String,
three_dsecure: Option<ThreeDSecureResData>,
}
impl<F, T>
TryFrom<types::ResponseRouterData<F, DlocalPaymentsResponse, T, types::PaymentsResponseData>>
for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<F, DlocalPaymentsResponse, T, types::PaymentsResponseData>,
) -> Result<Self, Self::Error> {
let redirection_data = item
.response
.three_dsecure
.and_then(|three_secure_data| three_secure_data.redirect_url)
.map(|redirect_url| {
services::RedirectForm::from((redirect_url, services::Method::Get))
});
let response = types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(item.response.id),
redirection_data,
mandate_reference: None,
connector_metadata: None,
};
Ok(Self {
status: enums::AttemptStatus::from(item.response.status),
response: Ok(response),
..item.data
})
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DlocalPaymentsSyncResponse {
status: DlocalPaymentStatus,
id: String,
}
impl<F, T>
TryFrom<
types::ResponseRouterData<F, DlocalPaymentsSyncResponse, T, types::PaymentsResponseData>,
> for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<
F,
DlocalPaymentsSyncResponse,
T,
types::PaymentsResponseData,
>,
) -> Result<Self, Self::Error> {
Ok(Self {
status: enums::AttemptStatus::from(item.response.status),
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(item.response.id),
redirection_data: None,
mandate_reference: None,
connector_metadata: None,
}),
..item.data
})
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DlocalPaymentsCaptureResponse {
status: DlocalPaymentStatus,
id: String,
}
impl<F, T>
TryFrom<
types::ResponseRouterData<F, DlocalPaymentsCaptureResponse, T, types::PaymentsResponseData>,
> for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<
F,
DlocalPaymentsCaptureResponse,
T,
types::PaymentsResponseData,
>,
) -> Result<Self, Self::Error> {
Ok(Self {
status: enums::AttemptStatus::from(item.response.status),
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(item.response.id),
redirection_data: None,
mandate_reference: None,
connector_metadata: None,
}),
..item.data
})
}
}
pub struct DlocalPaymentsCancelResponse {
status: DlocalPaymentStatus,
id: String,
}
impl<F, T>
TryFrom<
types::ResponseRouterData<F, DlocalPaymentsCancelResponse, T, types::PaymentsResponseData>,
> for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<
F,
DlocalPaymentsCancelResponse,
T,
types::PaymentsResponseData,
>,
) -> Result<Self, Self::Error> {
Ok(Self {
status: enums::AttemptStatus::from(item.response.status),
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(item.response.id),
redirection_data: None,
mandate_reference: None,
connector_metadata: None,
}),
..item.data
})
}
}
// REFUND :
#[derive(Default, Debug, Serialize)]
pub struct RefundRequest {
pub amount: String,
pub payment_id: String,
pub currency: enums::Currency,
pub id: String,
}
impl<F> TryFrom<&types::RefundsRouterData<F>> for RefundRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::RefundsRouterData<F>) -> Result<Self, Self::Error> {
let amount_to_refund = item.request.refund_amount.to_string();
Ok(Self {
amount: amount_to_refund,
payment_id: item.request.connector_transaction_id.clone(),
currency: item.request.currency,
id: item.request.refund_id.clone(),
})
}
}
#[allow(dead_code)]
#[derive(Debug, Serialize, Default, Deserialize, Clone)]
#[serde(rename_all = "UPPERCASE")]
pub enum RefundStatus {
Success,
#[default]
Pending,
Rejected,
Cancelled,
}
impl From<RefundStatus> for enums::RefundStatus {
fn from(item: RefundStatus) -> Self {
match item {
RefundStatus::Success => Self::Success,
RefundStatus::Pending => Self::Pending,
RefundStatus::Rejected => Self::ManualReview,
RefundStatus::Cancelled => Self::Failure,
}
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct RefundResponse {
pub id: String,
pub status: RefundStatus,
}
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 = enums::RefundStatus::from(item.response.status);
Ok(Self {
response: Ok(types::RefundsResponseData {
connector_refund_id: item.response.id,
refund_status,
}),
..item.data
})
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct DlocalRefundsSyncRequest {
pub refund_id: String,
}
impl TryFrom<&types::RefundSyncRouterData> for DlocalRefundsSyncRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::RefundSyncRouterData) -> Result<Self, Self::Error> {
let refund_id = match item.request.connector_refund_id.clone() {
Some(val) => val,
None => item.request.refund_id.clone(),
};
Ok(Self {
refund_id: (refund_id),
})
}
}
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 = enums::RefundStatus::from(item.response.status);
Ok(Self {
response: Ok(types::RefundsResponseData {
connector_refund_id: item.response.id,
refund_status,
}),
..item.data
})
}
}
#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
pub struct DlocalErrorResponse {
pub code: i32,
pub message: String,
pub param: Option<String>,
}
fn get_doc_from_currency(country: String) -> Secret<String> {
let doc = match country.as_str() {
"BR" => "91483309223",
"ZA" => "2001014800086",
"BD" | "GT" | "HN" | "PK" | "SN" | "TH" => "1234567890001",
"CR" | "SV" | "VN" => "123456789",
"DO" | "NG" => "12345678901",
"EG" => "12345678901112",
"GH" | "ID" | "RW" | "UG" => "1234567890111123",
"IN" => "NHSTP6374G",
"CI" => "CA124356789",
"JP" | "MY" | "PH" => "123456789012",
"NI" => "1234567890111A",
"TZ" => "12345678912345678900",
_ => "12345678",
};
Secret::new(doc.to_string())
}

View File

@ -38,6 +38,7 @@ pub trait PaymentsRequestData {
fn get_billing(&self) -> Result<&api::Address, Error>;
fn get_billing_country(&self) -> Result<String, Error>;
fn get_billing_phone(&self) -> Result<&api::PhoneDetails, Error>;
fn get_billing_address(&self) -> Result<&api::AddressDetails, Error>;
fn get_card(&self) -> Result<api::Card, Error>;
fn get_return_url(&self) -> Result<String, Error>;
}
@ -79,6 +80,13 @@ impl PaymentsRequestData for types::PaymentsAuthorizeRouterData {
.and_then(|a| a.phone.as_ref())
.ok_or_else(missing_field_err("billing.phone"))
}
fn get_billing_address(&self) -> Result<&api::AddressDetails, Error> {
self.address
.billing
.as_ref()
.and_then(|a| a.address.as_ref())
.ok_or_else(missing_field_err("billing.address"))
}
fn get_billing(&self) -> Result<&api::Address, Error> {
self.address
.billing

View File

@ -50,6 +50,10 @@ pub mod headers {
pub const X_API_VERSION: &str = "X-ApiVersion";
pub const DATE: &str = "Date";
pub const X_MERCHANT_ID: &str = "X-Merchant-Id";
pub const X_LOGIN: &str = "X-Login";
pub const X_TRANS_KEY: &str = "X-Trans-Key";
pub const X_VERSION: &str = "X-Version";
pub const X_DATE: &str = "X-Date";
}
pub mod pii {

View File

@ -167,6 +167,7 @@ impl ConnectorData {
"braintree" => Ok(Box::new(&connector::Braintree)),
"checkout" => Ok(Box::new(&connector::Checkout)),
"cybersource" => Ok(Box::new(&connector::Cybersource)),
"dlocal" => Ok(Box::new(&connector::Dlocal)),
"fiserv" => Ok(Box::new(&connector::Fiserv)),
"globalpay" => Ok(Box::new(&connector::Globalpay)),
"klarna" => Ok(Box::new(&connector::Klarna)),

View File

@ -3,6 +3,7 @@ use serde::Deserialize;
#[derive(Debug, Deserialize, Clone)]
pub(crate) struct ConnectorAuthentication {
pub dlocal: Option<SignatureKey>,
pub aci: Option<BodyKey>,
pub adyen: Option<BodyKey>,
pub authorizedotnet: Option<BodyKey>,

View File

@ -0,0 +1,465 @@
use api_models::payments::Address;
use masking::Secret;
use router::types::{self, api, storage::enums, PaymentAddress};
use crate::{
connector_auth,
utils::{self, ConnectorActions, PaymentInfo},
};
#[derive(Clone, Copy)]
struct DlocalTest;
impl ConnectorActions for DlocalTest {}
impl utils::Connector for DlocalTest {
fn get_data(&self) -> types::api::ConnectorData {
use router::connector::Dlocal;
types::api::ConnectorData {
connector: Box::new(&Dlocal),
connector_name: types::Connector::Dlocal,
get_token: types::api::GetToken::Connector,
}
}
fn get_auth_token(&self) -> types::ConnectorAuthType {
types::ConnectorAuthType::from(
connector_auth::ConnectorAuthentication::new()
.dlocal
.expect("Missing connector authentication configuration"),
)
}
fn get_name(&self) -> String {
"dlocal".to_string()
}
}
static CONNECTOR: DlocalTest = DlocalTest {};
// 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(None, Some(get_payment_info()))
.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(None, None, Some(get_payment_info()))
.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(
None,
Some(types::PaymentsCaptureData {
amount_to_capture: Some(50),
..utils::PaymentCaptureType::default().0
}),
Some(get_payment_info()),
)
.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(None, Some(get_payment_info()))
.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,
}),
Some(get_payment_info()),
)
.await
.expect("PSync response");
println!("{}", response.status);
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(
None,
Some(types::PaymentsCancelData {
connector_transaction_id: String::from(""),
cancellation_reason: Some("requested_by_customer".to_string()),
}),
Some(get_payment_info()),
)
.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(None, None, None, Some(get_payment_info()))
.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(
None,
None,
Some(types::RefundsData {
refund_amount: 50,
..utils::PaymentRefundType::default().0
}),
Some(get_payment_info()),
)
.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(None, None, None, Some(get_payment_info()))
.await
.unwrap();
let response = CONNECTOR
.rsync_retry_till_status_matches(
enums::RefundStatus::Success,
refund_response.response.unwrap().connector_refund_id,
None,
Some(get_payment_info()),
)
.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(None, Some(get_payment_info()))
.await
.unwrap();
assert_eq!(authorize_response.status, enums::AttemptStatus::Charged);
}
// Synchronizes a payment using the automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_sync_auto_captured_payment() {
let authorize_response = CONNECTOR
.make_payment(None, Some(get_payment_info()))
.await
.unwrap();
assert_eq!(authorize_response.status, enums::AttemptStatus::Charged);
let txn_id = utils::get_connector_transaction_id(authorize_response.response);
assert_ne!(txn_id, None, "Empty connector transaction id");
let response = CONNECTOR
.psync_retry_till_status_matches(
enums::AttemptStatus::Charged,
Some(types::PaymentsSyncData {
connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(
txn_id.unwrap(),
),
encoded_data: None,
capture_method: None,
}),
Some(get_payment_info()),
)
.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(None, None, Some(get_payment_info()))
.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(
None,
Some(types::RefundsData {
refund_amount: 50,
..utils::PaymentRefundType::default().0
}),
Some(get_payment_info()),
)
.await
.unwrap();
assert_eq!(
refund_response.response.unwrap().refund_status,
enums::RefundStatus::Success,
);
}
// Creates multiple refunds against a payment using the automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_refund_succeeded_payment_multiple_times() {
CONNECTOR
.make_payment_and_multiple_refund(
None,
Some(types::RefundsData {
refund_amount: 50,
..utils::PaymentRefundType::default().0
}),
Some(get_payment_info()),
)
.await;
}
// 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(None, None, Some(get_payment_info()))
.await
.unwrap();
let response = CONNECTOR
.rsync_retry_till_status_matches(
enums::RefundStatus::Success,
refund_response.response.unwrap().connector_refund_id,
None,
Some(get_payment_info()),
)
.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: types::api::PaymentMethod::Card(api::Card {
card_number: Secret::new("1891011".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
}),
Some(get_payment_info()),
)
.await
.unwrap();
let x = response.response.unwrap_err();
assert_eq!(x.message, "Invalid parameter",);
assert_eq!(x.reason, Some("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: types::api::PaymentMethod::Card(api::Card {
card_number: Secret::new(String::from("")),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
}),
Some(get_payment_info()),
)
.await
.unwrap();
let x = response.response.unwrap_err();
assert_eq!(x.message, "Invalid parameter",);
assert_eq!(x.reason, Some("card.number".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: types::api::PaymentMethod::Card(api::Card {
card_cvc: Secret::new("1ad2345".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
}),
Some(get_payment_info()),
)
.await
.unwrap();
let x = response.response.unwrap_err();
assert_eq!(x.message, "Invalid parameter",);
assert_eq!(x.reason, Some("card.cvv".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: types::api::PaymentMethod::Card(api::Card {
card_exp_month: Secret::new("201".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
}),
Some(get_payment_info()),
)
.await
.unwrap();
let x = response.response.unwrap_err();
assert_eq!(x.message, "Invalid parameter",);
assert_eq!(x.reason, Some("card.expiration_month".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: types::api::PaymentMethod::Card(api::Card {
card_exp_year: Secret::new("20001".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
}),
Some(get_payment_info()),
)
.await
.unwrap();
let x = response.response.unwrap_err();
assert_eq!(x.message, "Invalid parameter",);
assert_eq!(x.reason, Some("card.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(None, Some(get_payment_info()))
.await
.unwrap();
assert_eq!(authorize_response.status, enums::AttemptStatus::Charged);
let txn_id = utils::get_connector_transaction_id(authorize_response.response);
assert_ne!(txn_id, None, "Empty connector transaction id");
let void_response = CONNECTOR
.void_payment(txn_id.unwrap(), None, None)
.await
.unwrap();
let x = void_response.response.unwrap_err();
assert_eq!(x.code, "5021",);
assert_eq!(x.message, "Acquirer could not process the request");
}
// 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("123456sdf789".to_string(), None, Some(get_payment_info()))
.await
.unwrap();
let x = capture_response.response.unwrap_err();
assert_eq!(x.code, "3003",);
}
// Refunds a payment with refund amount higher than payment amount.
#[actix_web::test]
async fn should_fail_for_refund_amount_higher_than_payment_amount() {
let response = CONNECTOR
.make_payment_and_refund(
None,
Some(types::RefundsData {
refund_amount: 150,
..utils::PaymentRefundType::default().0
}),
Some(get_payment_info()),
)
.await
.unwrap();
let x = response.response.unwrap_err();
println!("response from refund amount higher payment");
println!("{}", x.code);
assert_eq!(x.code, "5007",);
assert_eq!(x.message, "Amount exceeded",);
}
pub fn get_payment_info() -> PaymentInfo {
PaymentInfo {
address: Some(PaymentAddress {
shipping: None,
billing: Some(Address {
phone: None,
address: Some(api::AddressDetails {
city: None,
country: Some("PA".to_string()),
line1: None,
line2: None,
line3: None,
zip: None,
state: None,
first_name: None,
last_name: None,
}),
}),
}),
auth_type: None,
access_token: None,
router_return_url: None,
}
}
// Connector dependent test cases goes here
// [#478]: add unit tests for non 3DS, wallets & webhooks in connector tests

View File

@ -6,6 +6,7 @@ mod authorizedotnet;
mod checkout;
mod connector_auth;
mod cybersource;
mod dlocal;
mod fiserv;
mod globalpay;
mod payu;

View File

@ -48,3 +48,8 @@ api_secret = "MySecretKey"
key1 = "Merchant Id"
api_key = "API Key"
api_secret = "API Secret Key"
[dlocal]
key1 = "key1"
api_key = "api_key"
api_secret = "secret"

View File

@ -85,6 +85,9 @@ base_url = "https://apple-pay-gateway.apple.com/"
[connectors.klarna]
base_url = "https://api-na.playground.klarna.com/"
[connectors.dlocal]
base_url = "https://sandbox.dlocal.com/"
[connectors.supported]
wallets = ["klarna", "braintree", "applepay"]
cards = ["stripe", "adyen", "authorizedotnet", "checkout", "braintree", "cybersource", "shift4", "worldpay", "globalpay"]
cards = ["stripe", "adyen", "authorizedotnet", "checkout", "braintree", "cybersource", "shift4", "worldpay", "globalpay", "dlocal"]