feat(connector): [Stax] Implement Cards for Connector Stax (#1773)

Co-authored-by: Arjun Karthik <m.arjunkarthik@gmail.com>
This commit is contained in:
DEEPANSHU BANSAL
2023-08-01 13:37:28 +05:30
committed by GitHub
parent 9c7ac6246d
commit f492d0a943
9 changed files with 942 additions and 219 deletions

View File

@ -3,10 +3,13 @@ mod transformers;
use std::fmt::Debug;
use error_stack::{IntoReport, ResultExt};
use masking::PeekInterface;
use transformers as stax;
use super::utils::{to_connector_meta, RefundsRequestData};
use crate::{
configs::settings,
consts,
core::errors::{self, CustomResult},
headers,
services::{
@ -38,16 +41,6 @@ impl api::RefundExecute for Stax {}
impl api::RefundSync for Stax {}
impl api::PaymentToken for Stax {}
impl
ConnectorIntegration<
api::PaymentMethodToken,
types::PaymentMethodTokenizationData,
types::PaymentsResponseData,
> for Stax
{
// Not Implemented (R)
}
impl<Flow, Request, Response> ConnectorCommonExt<Flow, Request, Response> for Stax
where
Self: ConnectorIntegration<Flow, Request, Response>,
@ -95,9 +88,10 @@ impl ConnectorCommon for Stax {
) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> {
let auth = stax::StaxAuthType::try_from(auth_type)
.change_context(errors::ConnectorError::FailedToObtainAuthType)?;
Ok(vec![(
headers::AUTHORIZATION.to_string(),
auth.api_key.into_masked(),
format!("Bearer {}", auth.api_key.peek()).into_masked(),
)])
}
@ -105,20 +99,196 @@ impl ConnectorCommon for Stax {
&self,
res: Response,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
let response: stax::StaxErrorResponse = res
.response
.parse_struct("StaxErrorResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
Ok(ErrorResponse {
status_code: res.status_code,
code: response.code,
message: response.message,
reason: response.reason,
code: consts::NO_ERROR_CODE.to_string(),
message: consts::NO_ERROR_MESSAGE.to_string(),
reason: Some(
std::str::from_utf8(&res.response)
.into_report()
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?
.to_owned(),
),
})
}
}
impl api::ConnectorCustomer for Stax {}
impl
ConnectorIntegration<
api::CreateConnectorCustomer,
types::ConnectorCustomerData,
types::PaymentsResponseData,
> for Stax
{
fn get_headers(
&self,
req: &types::ConnectorCustomerRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<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::ConnectorCustomerRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!("{}customer", self.base_url(connectors),))
}
fn get_request_body(
&self,
req: &types::ConnectorCustomerRouterData,
) -> CustomResult<Option<types::RequestBody>, errors::ConnectorError> {
let connector_request = stax::StaxCustomerRequest::try_from(req)?;
let stax_req = types::RequestBody::log_and_get_request_body(
&connector_request,
utils::Encode::<stax::StaxCustomerRequest>::encode_to_string_of_json,
)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(stax_req))
}
fn build_request(
&self,
req: &types::ConnectorCustomerRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::ConnectorCustomerType::get_url(
self, req, connectors,
)?)
.attach_default_headers()
.headers(types::ConnectorCustomerType::get_headers(
self, req, connectors,
)?)
.body(types::ConnectorCustomerType::get_request_body(self, req)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::ConnectorCustomerRouterData,
res: Response,
) -> CustomResult<types::ConnectorCustomerRouterData, errors::ConnectorError>
where
types::PaymentsResponseData: Clone,
{
let response: stax::StaxCustomerResponse = res
.response
.parse_struct("StaxCustomerResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}
fn get_error_response(
&self,
res: Response,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl
ConnectorIntegration<
api::PaymentMethodToken,
types::PaymentMethodTokenizationData,
types::PaymentsResponseData,
> for Stax
{
fn get_headers(
&self,
req: &types::TokenizationRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<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::TokenizationRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!("{}payment-method/", self.base_url(connectors)))
}
fn get_request_body(
&self,
req: &types::TokenizationRouterData,
) -> CustomResult<Option<types::RequestBody>, errors::ConnectorError> {
let connector_request = stax::StaxTokenRequest::try_from(req)?;
let stax_req = types::RequestBody::log_and_get_request_body(
&connector_request,
utils::Encode::<stax::StaxTokenRequest>::encode_to_string_of_json,
)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(stax_req))
}
fn build_request(
&self,
req: &types::TokenizationRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::TokenizationType::get_url(self, req, connectors)?)
.attach_default_headers()
.headers(types::TokenizationType::get_headers(self, req, connectors)?)
.body(types::TokenizationType::get_request_body(self, req)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::TokenizationRouterData,
res: Response,
) -> CustomResult<types::TokenizationRouterData, errors::ConnectorError>
where
types::PaymentsResponseData: Clone,
{
let response: stax::StaxTokenResponse = res
.response
.parse_struct("StaxTokenResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: Response,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl ConnectorIntegration<api::Session, types::PaymentsSessionData, types::PaymentsResponseData>
for Stax
{
@ -153,9 +323,9 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
fn get_url(
&self,
_req: &types::PaymentsAuthorizeRouterData,
_connectors: &settings::Connectors,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into())
Ok(format!("{}charge", self.base_url(connectors),))
}
fn get_request_body(
@ -163,6 +333,7 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
req: &types::PaymentsAuthorizeRouterData,
) -> CustomResult<Option<types::RequestBody>, errors::ConnectorError> {
let req_obj = stax::StaxPaymentsRequest::try_from(req)?;
let stax_req = types::RequestBody::log_and_get_request_body(
&req_obj,
utils::Encode::<stax::StaxPaymentsRequest>::encode_to_string_of_json,
@ -198,7 +369,7 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
) -> CustomResult<types::PaymentsAuthorizeRouterData, errors::ConnectorError> {
let response: stax::StaxPaymentsResponse = res
.response
.parse_struct("Stax PaymentsAuthorizeResponse")
.parse_struct("StaxPaymentsAuthorizeResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
@ -232,10 +403,19 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
fn get_url(
&self,
_req: &types::PaymentsSyncRouterData,
_connectors: &settings::Connectors,
req: &types::PaymentsSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into())
let connector_payment_id = req
.request
.connector_transaction_id
.get_connector_transaction_id()
.change_context(errors::ConnectorError::MissingConnectorTransactionID)?;
Ok(format!(
"{}/transaction/{connector_payment_id}",
self.base_url(connectors),
))
}
fn build_request(
@ -260,7 +440,7 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
let response: stax::StaxPaymentsResponse = res
.response
.parse_struct("stax PaymentsSyncResponse")
.parse_struct("StaxPaymentsSyncResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
@ -294,17 +474,27 @@ impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::Payme
fn get_url(
&self,
_req: &types::PaymentsCaptureRouterData,
_connectors: &settings::Connectors,
req: &types::PaymentsCaptureRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into())
Ok(format!(
"{}/transaction/{}/capture",
self.base_url(connectors),
req.request.connector_transaction_id,
))
}
fn get_request_body(
&self,
_req: &types::PaymentsCaptureRouterData,
req: &types::PaymentsCaptureRouterData,
) -> CustomResult<Option<types::RequestBody>, errors::ConnectorError> {
Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into())
let connector_req = stax::StaxCaptureRequest::try_from(req)?;
let stax_req = types::RequestBody::log_and_get_request_body(
&connector_req,
utils::Encode::<stax::StaxCaptureRequest>::encode_to_string_of_json,
)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(stax_req))
}
fn build_request(
@ -332,7 +522,7 @@ impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::Payme
) -> CustomResult<types::PaymentsCaptureRouterData, errors::ConnectorError> {
let response: stax::StaxPaymentsResponse = res
.response
.parse_struct("Stax PaymentsCaptureResponse")
.parse_struct("StaxPaymentsCaptureResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
@ -352,6 +542,67 @@ impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::Payme
impl ConnectorIntegration<api::Void, types::PaymentsCancelData, types::PaymentsResponseData>
for Stax
{
fn get_headers(
&self,
req: &types::PaymentsCancelRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<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> {
Ok(format!(
"{}/transaction/{}/void-or-refund",
self.base_url(connectors),
req.request.connector_transaction_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)?)
.attach_default_headers()
.headers(types::PaymentsVoidType::get_headers(self, req, connectors)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::PaymentsCancelRouterData,
res: Response,
) -> CustomResult<types::PaymentsCancelRouterData, errors::ConnectorError> {
let response: stax::StaxPaymentsResponse = res
.response
.parse_struct("StaxPaymentsVoidResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}
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 Stax {
@ -369,10 +620,22 @@ impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsRespon
fn get_url(
&self,
_req: &types::RefundsRouterData<api::Execute>,
_connectors: &settings::Connectors,
req: &types::RefundsRouterData<api::Execute>,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into())
let connector_transaction_id = if req.request.connector_metadata.is_some() {
let stax_capture: stax::StaxMetaData =
to_connector_meta(req.request.connector_metadata.clone())?;
stax_capture.capture_id
} else {
req.request.connector_transaction_id.clone()
};
Ok(format!(
"{}/transaction/{}/refund",
self.base_url(connectors),
connector_transaction_id,
))
}
fn get_request_body(
@ -412,7 +675,7 @@ impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsRespon
) -> CustomResult<types::RefundsRouterData<api::Execute>, errors::ConnectorError> {
let response: stax::RefundResponse = res
.response
.parse_struct("stax RefundResponse")
.parse_struct("StaxRefundResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
@ -444,10 +707,14 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse
fn get_url(
&self,
_req: &types::RefundSyncRouterData,
_connectors: &settings::Connectors,
req: &types::RefundSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into())
Ok(format!(
"{}/transaction/{}",
self.base_url(connectors),
req.request.get_connector_refund_id()?,
))
}
fn build_request(
@ -461,7 +728,6 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse
.url(&types::RefundSyncType::get_url(self, req, connectors)?)
.attach_default_headers()
.headers(types::RefundSyncType::get_headers(self, req, connectors)?)
.body(types::RefundSyncType::get_request_body(self, req)?)
.build(),
))
}
@ -471,10 +737,10 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse
data: &types::RefundSyncRouterData,
res: Response,
) -> CustomResult<types::RefundSyncRouterData, errors::ConnectorError> {
let response: stax::RefundResponse =
res.response
.parse_struct("stax RefundSyncResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
let response: stax::RefundResponse = res
.response
.parse_struct("StaxRefundSyncResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),

View File

@ -1,45 +1,40 @@
use masking::Secret;
use common_utils::pii::Email;
use error_stack::IntoReport;
use masking::{ExposeInterface, Secret};
use serde::{Deserialize, Serialize};
use crate::{
connector::utils::PaymentsAuthorizeRequestData,
connector::utils::{CardData, PaymentsAuthorizeRequestData, RouterData},
core::errors,
types::{self, api, storage::enums},
};
//TODO: Fill the struct with respective fields
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
pub struct StaxPaymentsRequest {
amount: i64,
card: StaxCard,
#[derive(Debug, Serialize)]
pub struct StaxPaymentsRequestMetaData {
tax: i64,
}
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
pub struct StaxCard {
name: Secret<String>,
number: cards::CardNumber,
expiry_month: Secret<String>,
expiry_year: Secret<String>,
cvc: Secret<String>,
complete: bool,
#[derive(Debug, Serialize)]
pub struct StaxPaymentsRequest {
payment_method_id: Secret<String>,
total: i64,
is_refundable: bool,
pre_auth: bool,
meta: StaxPaymentsRequestMetaData,
}
impl TryFrom<&types::PaymentsAuthorizeRouterData> for StaxPaymentsRequest {
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 card = StaxCard {
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,
cvc: req_card.card_cvc,
complete: item.request.is_auto_capture()?,
};
api::PaymentMethodData::Card(_) => {
let pre_auth = !item.request.is_auto_capture()?;
Ok(Self {
amount: item.request.amount,
card,
meta: StaxPaymentsRequestMetaData { tax: 0 },
total: item.request.amount,
is_refundable: true,
pre_auth,
payment_method_id: Secret::new(item.get_payment_method_token()?),
})
}
_ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()),
@ -47,7 +42,6 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for StaxPaymentsRequest {
}
}
//TODO: Fill the struct with respective fields
// Auth Struct
pub struct StaxAuthType {
pub(super) api_key: Secret<String>,
@ -64,34 +58,153 @@ impl TryFrom<&types::ConnectorAuthType> for StaxAuthType {
}
}
}
// PaymentsResponse
//TODO: Append the remaining status flags
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum StaxPaymentStatus {
Succeeded,
Failed,
#[default]
Processing,
#[derive(Debug, Serialize)]
pub struct StaxCustomerRequest {
#[serde(skip_serializing_if = "Option::is_none")]
email: Option<Email>,
#[serde(skip_serializing_if = "Option::is_none")]
firstname: Option<String>,
}
impl From<StaxPaymentStatus> for enums::AttemptStatus {
fn from(item: StaxPaymentStatus) -> Self {
match item {
StaxPaymentStatus::Succeeded => Self::Charged,
StaxPaymentStatus::Failed => Self::Failure,
StaxPaymentStatus::Processing => Self::Authorizing,
impl TryFrom<&types::ConnectorCustomerRouterData> for StaxCustomerRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::ConnectorCustomerRouterData) -> Result<Self, Self::Error> {
if item.request.email.is_none() && item.request.name.is_none() {
Err(errors::ConnectorError::MissingRequiredField {
field_name: "email or name",
})
.into_report()
} else {
Ok(Self {
email: item.request.email.to_owned(),
firstname: item.request.name.to_owned(),
})
}
}
}
//TODO: Fill the struct with respective fields
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct StaxPaymentsResponse {
status: StaxPaymentStatus,
#[derive(Debug, Deserialize)]
pub struct StaxCustomerResponse {
id: Secret<String>,
}
impl<F, T>
TryFrom<types::ResponseRouterData<F, StaxCustomerResponse, T, types::PaymentsResponseData>>
for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<F, StaxCustomerResponse, T, types::PaymentsResponseData>,
) -> Result<Self, Self::Error> {
Ok(Self {
response: Ok(types::PaymentsResponseData::ConnectorCustomerResponse {
connector_customer_id: item.response.id.expose(),
}),
..item.data
})
}
}
#[derive(Debug, Serialize)]
pub struct StaxTokenizeData {
person_name: Secret<String>,
card_number: cards::CardNumber,
card_exp: Secret<String>,
card_cvv: Secret<String>,
customer_id: Secret<String>,
}
#[derive(Debug, Serialize)]
#[serde(tag = "method")]
#[serde(rename_all = "lowercase")]
pub enum StaxTokenRequest {
Card(StaxTokenizeData),
}
impl TryFrom<&types::TokenizationRouterData> for StaxTokenRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::TokenizationRouterData) -> Result<Self, Self::Error> {
let customer_id = item.get_connector_customer_id()?;
match item.request.payment_method_data.clone() {
api::PaymentMethodData::Card(card_data) => {
let stax_card_data = StaxTokenizeData {
card_exp: card_data
.get_card_expiry_month_year_2_digit_with_delimiter("".to_string()),
person_name: card_data.card_holder_name,
card_number: card_data.card_number,
card_cvv: card_data.card_cvc,
customer_id: Secret::new(customer_id),
};
Ok(Self::Card(stax_card_data))
}
api::PaymentMethodData::BankDebit(_)
| api::PaymentMethodData::Wallet(_)
| api::PaymentMethodData::PayLater(_)
| api::PaymentMethodData::BankRedirect(_)
| api::PaymentMethodData::BankTransfer(_)
| api::PaymentMethodData::Crypto(_)
| api::PaymentMethodData::MandatePayment
| api::PaymentMethodData::Reward(_)
| api::PaymentMethodData::Voucher(_)
| api::PaymentMethodData::GiftCard(_)
| api::PaymentMethodData::Upi(_) => Err(errors::ConnectorError::NotImplemented(
"Payment Method".to_string(),
))
.into_report(),
}
}
}
#[derive(Debug, Deserialize)]
pub struct StaxTokenResponse {
id: Secret<String>,
}
impl<F, T> TryFrom<types::ResponseRouterData<F, StaxTokenResponse, T, types::PaymentsResponseData>>
for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<F, StaxTokenResponse, T, types::PaymentsResponseData>,
) -> Result<Self, Self::Error> {
Ok(Self {
response: Ok(types::PaymentsResponseData::TokenizationResponse {
token: item.response.id.expose(),
}),
..item.data
})
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StaxPaymentResponseTypes {
Charge,
PreAuth,
}
#[derive(Debug, Deserialize)]
pub struct StaxChildCapture {
id: String,
}
#[derive(Debug, Deserialize)]
pub struct StaxPaymentsResponse {
success: bool,
id: String,
is_captured: i8,
is_voided: bool,
child_captures: Vec<StaxChildCapture>,
#[serde(rename = "type")]
payment_response_type: StaxPaymentResponseTypes,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct StaxMetaData {
pub capture_id: String,
}
impl<F, T>
TryFrom<types::ResponseRouterData<F, StaxPaymentsResponse, T, types::PaymentsResponseData>>
for types::RouterData<F, T, types::PaymentsResponseData>
@ -100,13 +213,36 @@ impl<F, T>
fn try_from(
item: types::ResponseRouterData<F, StaxPaymentsResponse, T, types::PaymentsResponseData>,
) -> Result<Self, Self::Error> {
let mut connector_metadata = None;
let mut status = match item.response.success {
true => match item.response.payment_response_type {
StaxPaymentResponseTypes::Charge => enums::AttemptStatus::Charged,
StaxPaymentResponseTypes::PreAuth => match item.response.is_captured {
0 => enums::AttemptStatus::Authorized,
_ => {
connector_metadata =
item.response.child_captures.first().map(|child_captures| {
serde_json::json!(StaxMetaData {
capture_id: child_captures.id.clone()
})
});
enums::AttemptStatus::Charged
}
},
},
false => enums::AttemptStatus::Failure,
};
if item.response.is_voided {
status = enums::AttemptStatus::Voided;
}
Ok(Self {
status: enums::AttemptStatus::from(item.response.status),
status,
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(item.response.id),
redirection_data: None,
mandate_reference: None,
connector_metadata: None,
connector_metadata,
network_txn_id: None,
connector_response_reference_id: None,
}),
@ -115,50 +251,48 @@ impl<F, T>
}
}
//TODO: Fill the struct with respective fields
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StaxCaptureRequest {
total: Option<i64>,
}
impl TryFrom<&types::PaymentsCaptureRouterData> for StaxCaptureRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsCaptureRouterData) -> Result<Self, Self::Error> {
let total = item.request.amount_to_capture;
Ok(Self { total: Some(total) })
}
}
// REFUND :
// Type definition for RefundRequest
#[derive(Default, Debug, Serialize)]
#[derive(Debug, Serialize)]
pub struct StaxRefundRequest {
pub amount: i64,
pub total: i64,
}
impl<F> TryFrom<&types::RefundsRouterData<F>> for StaxRefundRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::RefundsRouterData<F>) -> Result<Self, Self::Error> {
Ok(Self {
amount: item.request.refund_amount,
total: item.request.refund_amount,
})
}
}
// Type definition for Refund Response
#[allow(dead_code)]
#[derive(Debug, Serialize, Default, Deserialize, Clone)]
pub enum RefundStatus {
Succeeded,
Failed,
#[default]
Processing,
#[derive(Debug, Deserialize)]
pub struct ChildTransactionsInResponse {
id: String,
success: bool,
created_at: String,
total: i64,
}
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,
//TODO: Review mapping
}
}
}
//TODO: Fill the struct with respective fields
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Deserialize)]
pub struct RefundResponse {
id: String,
status: RefundStatus,
success: bool,
child_transactions: Vec<ChildTransactionsInResponse>,
}
impl TryFrom<types::RefundsResponseRouterData<api::Execute, RefundResponse>>
@ -168,10 +302,32 @@ impl TryFrom<types::RefundsResponseRouterData<api::Execute, RefundResponse>>
fn try_from(
item: types::RefundsResponseRouterData<api::Execute, RefundResponse>,
) -> Result<Self, Self::Error> {
let filtered_txn: Vec<&ChildTransactionsInResponse> = item
.response
.child_transactions
.iter()
.filter(|txn| txn.total == item.data.request.refund_amount)
.collect();
let mut refund_txn = filtered_txn
.first()
.ok_or(errors::ConnectorError::ResponseHandlingFailed)?;
for child in filtered_txn.iter() {
if child.created_at > refund_txn.created_at {
refund_txn = child;
}
}
let refund_status = match refund_txn.success {
true => enums::RefundStatus::Success,
false => enums::RefundStatus::Failure,
};
Ok(Self {
response: Ok(types::RefundsResponseData {
connector_refund_id: item.response.id.to_string(),
refund_status: enums::RefundStatus::from(item.response.status),
connector_refund_id: refund_txn.id.clone(),
refund_status,
}),
..item.data
})
@ -185,21 +341,16 @@ impl TryFrom<types::RefundsResponseRouterData<api::RSync, RefundResponse>>
fn try_from(
item: types::RefundsResponseRouterData<api::RSync, RefundResponse>,
) -> Result<Self, Self::Error> {
let refund_status = match item.response.success {
true => enums::RefundStatus::Success,
false => enums::RefundStatus::Failure,
};
Ok(Self {
response: Ok(types::RefundsResponseData {
connector_refund_id: item.response.id.to_string(),
refund_status: enums::RefundStatus::from(item.response.status),
connector_refund_id: item.response.id,
refund_status,
}),
..item.data
})
}
}
//TODO: Fill the struct with respective fields
#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
pub struct StaxErrorResponse {
pub status_code: u16,
pub code: String,
pub message: String,
pub reason: Option<String>,
}

View File

@ -137,7 +137,6 @@ impl<const T: u8>
}
default_imp_for_complete_authorize!(
connector::Stax,
connector::Aci,
connector::Adyen,
connector::Bitpay,
@ -164,6 +163,7 @@ default_imp_for_complete_authorize!(
connector::Payme,
connector::Payu,
connector::Rapyd,
connector::Stax,
connector::Stripe,
connector::Trustpay,
connector::Tsys,
@ -201,7 +201,6 @@ impl<const T: u8>
}
default_imp_for_create_customer!(
connector::Stax,
connector::Aci,
connector::Adyen,
connector::Airwallex,
@ -275,7 +274,6 @@ impl<const T: u8> services::ConnectorRedirectResponse for connector::DummyConnec
}
default_imp_for_connector_redirect_response!(
connector::Stax,
connector::Aci,
connector::Adyen,
connector::Bitpay,
@ -302,6 +300,7 @@ default_imp_for_connector_redirect_response!(
connector::Powertranz,
connector::Rapyd,
connector::Shift4,
connector::Stax,
connector::Tsys,
connector::Wise,
connector::Worldline,
@ -320,7 +319,6 @@ macro_rules! default_imp_for_connector_request_id {
impl<const T: u8> api::ConnectorTransactionId for connector::DummyConnector<T> {}
default_imp_for_connector_request_id!(
connector::Stax,
connector::Aci,
connector::Adyen,
connector::Airwallex,
@ -355,6 +353,7 @@ default_imp_for_connector_request_id!(
connector::Powertranz,
connector::Rapyd,
connector::Shift4,
connector::Stax,
connector::Stripe,
connector::Trustpay,
connector::Tsys,
@ -395,7 +394,6 @@ impl<const T: u8>
}
default_imp_for_accept_dispute!(
connector::Stax,
connector::Aci,
connector::Adyen,
connector::Airwallex,
@ -430,6 +428,7 @@ default_imp_for_accept_dispute!(
connector::Powertranz,
connector::Rapyd,
connector::Shift4,
connector::Stax,
connector::Stripe,
connector::Trustpay,
connector::Tsys,
@ -490,7 +489,6 @@ impl<const T: u8>
}
default_imp_for_file_upload!(
connector::Stax,
connector::Aci,
connector::Adyen,
connector::Airwallex,
@ -525,6 +523,7 @@ default_imp_for_file_upload!(
connector::Powertranz,
connector::Rapyd,
connector::Shift4,
connector::Stax,
connector::Trustpay,
connector::Tsys,
connector::Opennode,
@ -562,7 +561,6 @@ impl<const T: u8>
}
default_imp_for_submit_evidence!(
connector::Stax,
connector::Aci,
connector::Adyen,
connector::Airwallex,
@ -597,6 +595,7 @@ default_imp_for_submit_evidence!(
connector::Powertranz,
connector::Rapyd,
connector::Shift4,
connector::Stax,
connector::Trustpay,
connector::Tsys,
connector::Opennode,
@ -634,7 +633,6 @@ impl<const T: u8>
}
default_imp_for_defend_dispute!(
connector::Stax,
connector::Aci,
connector::Adyen,
connector::Airwallex,
@ -668,8 +666,9 @@ default_imp_for_defend_dispute!(
connector::Payu,
connector::Powertranz,
connector::Rapyd,
connector::Stripe,
connector::Shift4,
connector::Stax,
connector::Stripe,
connector::Trustpay,
connector::Tsys,
connector::Opennode,
@ -707,7 +706,6 @@ impl<const T: u8>
}
default_imp_for_pre_processing_steps!(
connector::Stax,
connector::Aci,
connector::Adyen,
connector::Airwallex,
@ -744,6 +742,7 @@ default_imp_for_pre_processing_steps!(
connector::Powertranz,
connector::Rapyd,
connector::Shift4,
connector::Stax,
connector::Tsys,
connector::Wise,
connector::Worldline,

View File

@ -61,6 +61,8 @@ fn get_default_payment_info() -> Option<utils::PaymentInfo> {
access_token: None,
connector_meta_data: None,
return_url: None,
connector_customer: None,
payment_method_token: None,
country: None,
currency: None,
payout_method_data: None,

View File

@ -1,5 +1,7 @@
use std::{str::FromStr, time::Duration};
use masking::Secret;
use router::types::{self, api, storage::enums};
use router::types::{self, api, storage::enums, PaymentsResponseData};
use test_utils::connector_auth;
use crate::utils::{self, ConnectorActions};
@ -32,12 +34,72 @@ impl utils::Connector for StaxTest {
static CONNECTOR: StaxTest = StaxTest {};
fn get_default_payment_info() -> Option<utils::PaymentInfo> {
None
fn get_default_payment_info(
connector_customer: Option<String>,
payment_method_token: Option<String>,
) -> Option<utils::PaymentInfo> {
Some(utils::PaymentInfo {
address: None,
auth_type: None,
access_token: None,
connector_meta_data: None,
return_url: None,
connector_customer,
payment_method_token,
payout_method_data: None,
currency: None,
country: None,
})
}
fn customer_details() -> Option<types::ConnectorCustomerData> {
Some(types::ConnectorCustomerData {
..utils::CustomerType::default().0
})
}
fn token_details() -> Option<types::PaymentMethodTokenizationData> {
Some(types::PaymentMethodTokenizationData {
payment_method_data: types::api::PaymentMethodData::Card(api::Card {
card_number: cards::CardNumber::from_str("4111111111111111").unwrap(),
card_exp_month: Secret::new("04".to_string()),
card_exp_year: Secret::new("2027".to_string()),
card_cvc: Secret::new("123".to_string()),
..utils::CCardType::default().0
}),
browser_info: None,
})
}
fn payment_method_details() -> Option<types::PaymentsAuthorizeData> {
None
Some(types::PaymentsAuthorizeData {
..utils::PaymentAuthorizeType::default().0
})
}
async fn create_customer_and_get_token() -> Option<String> {
let customer_response = CONNECTOR
.create_connector_customer(customer_details(), get_default_payment_info(None, None))
.await
.expect("Authorize payment response");
let connector_customer_id = match customer_response.response.unwrap() {
PaymentsResponseData::ConnectorCustomerResponse {
connector_customer_id,
} => Some(connector_customer_id),
_ => None,
};
let token_response = CONNECTOR
.create_connector_pm_token(
token_details(),
get_default_payment_info(connector_customer_id, None),
)
.await
.expect("Authorize payment response");
match token_response.response.unwrap() {
PaymentsResponseData::TokenizationResponse { token } => Some(token),
_ => None,
}
}
// Cards Positive Tests
@ -45,7 +107,10 @@ fn payment_method_details() -> Option<types::PaymentsAuthorizeData> {
#[actix_web::test]
async fn should_only_authorize_payment() {
let response = CONNECTOR
.authorize_payment(payment_method_details(), get_default_payment_info())
.authorize_payment(
payment_method_details(),
get_default_payment_info(None, create_customer_and_get_token().await),
)
.await
.expect("Authorize payment response");
assert_eq!(response.status, enums::AttemptStatus::Authorized);
@ -55,7 +120,11 @@ async fn should_only_authorize_payment() {
#[actix_web::test]
async fn should_capture_authorized_payment() {
let response = CONNECTOR
.authorize_and_capture_payment(payment_method_details(), None, get_default_payment_info())
.authorize_and_capture_payment(
payment_method_details(),
None,
get_default_payment_info(None, create_customer_and_get_token().await),
)
.await
.expect("Capture payment response");
assert_eq!(response.status, enums::AttemptStatus::Charged);
@ -71,7 +140,7 @@ async fn should_partially_capture_authorized_payment() {
amount_to_capture: 50,
..utils::PaymentCaptureType::default().0
}),
get_default_payment_info(),
get_default_payment_info(None, create_customer_and_get_token().await),
)
.await
.expect("Capture payment response");
@ -82,7 +151,10 @@ async fn should_partially_capture_authorized_payment() {
#[actix_web::test]
async fn should_sync_authorized_payment() {
let authorize_response = CONNECTOR
.authorize_payment(payment_method_details(), get_default_payment_info())
.authorize_payment(
payment_method_details(),
get_default_payment_info(None, create_customer_and_get_token().await),
)
.await
.expect("Authorize payment response");
let txn_id = utils::get_connector_transaction_id(authorize_response.response);
@ -95,7 +167,7 @@ async fn should_sync_authorized_payment() {
),
..Default::default()
}),
get_default_payment_info(),
get_default_payment_info(None, None),
)
.await
.expect("PSync response");
@ -113,7 +185,7 @@ async fn should_void_authorized_payment() {
cancellation_reason: Some("requested_by_customer".to_string()),
..Default::default()
}),
get_default_payment_info(),
get_default_payment_info(None, create_customer_and_get_token().await),
)
.await
.expect("Void payment response");
@ -123,15 +195,33 @@ async fn should_void_authorized_payment() {
// 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(
let capture_response = CONNECTOR
.authorize_and_capture_payment(
payment_method_details(),
None,
None,
get_default_payment_info(),
Some(types::PaymentsCaptureData {
..utils::PaymentCaptureType::default().0
}),
get_default_payment_info(None, create_customer_and_get_token().await),
)
.await
.expect("Capture payment response");
let refund_txn_id =
utils::get_connector_transaction_id(capture_response.response.clone()).unwrap();
let refund_connector_meta = utils::get_connector_metadata(capture_response.response);
let response = CONNECTOR
.refund_payment(
refund_txn_id,
Some(types::RefundsData {
connector_metadata: refund_connector_meta,
..utils::PaymentRefundType::default().0
}),
get_default_payment_info(None, None),
)
.await
.unwrap();
assert_eq!(
response.response.unwrap().refund_status,
enums::RefundStatus::Success,
@ -141,15 +231,30 @@ async fn should_refund_manually_captured_payment() {
// 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(
let capture_response = CONNECTOR
.authorize_and_capture_payment(
payment_method_details(),
None,
Some(types::PaymentsCaptureData {
..utils::PaymentCaptureType::default().0
}),
get_default_payment_info(None, create_customer_and_get_token().await),
)
.await
.expect("Capture payment response");
let refund_txn_id =
utils::get_connector_transaction_id(capture_response.response.clone()).unwrap();
let refund_connector_meta = utils::get_connector_metadata(capture_response.response);
let response = CONNECTOR
.refund_payment(
refund_txn_id,
Some(types::RefundsData {
refund_amount: 50,
connector_metadata: refund_connector_meta,
..utils::PaymentRefundType::default().0
}),
get_default_payment_info(),
get_default_payment_info(None, None),
)
.await
.unwrap();
@ -162,21 +267,39 @@ async fn should_partially_refund_manually_captured_payment() {
// 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(
let capture_response = CONNECTOR
.authorize_and_capture_payment(
payment_method_details(),
None,
None,
get_default_payment_info(),
Some(types::PaymentsCaptureData {
..utils::PaymentCaptureType::default().0
}),
get_default_payment_info(None, create_customer_and_get_token().await),
)
.await
.expect("Capture payment response");
let refund_txn_id =
utils::get_connector_transaction_id(capture_response.response.clone()).unwrap();
let refund_connector_meta = utils::get_connector_metadata(capture_response.response);
let refund_response = CONNECTOR
.refund_payment(
refund_txn_id,
Some(types::RefundsData {
connector_metadata: refund_connector_meta,
..utils::PaymentRefundType::default().0
}),
get_default_payment_info(None, None),
)
.await
.unwrap();
let response = CONNECTOR
.rsync_retry_till_status_matches(
enums::RefundStatus::Success,
refund_response.response.unwrap().connector_refund_id,
None,
get_default_payment_info(),
get_default_payment_info(None, None),
)
.await
.unwrap();
@ -190,7 +313,10 @@ async fn should_sync_manually_captured_refund() {
#[actix_web::test]
async fn should_make_payment() {
let authorize_response = CONNECTOR
.make_payment(payment_method_details(), get_default_payment_info())
.make_payment(
payment_method_details(),
get_default_payment_info(None, create_customer_and_get_token().await),
)
.await
.unwrap();
assert_eq!(authorize_response.status, enums::AttemptStatus::Charged);
@ -200,7 +326,10 @@ async fn should_make_payment() {
#[actix_web::test]
async fn should_sync_auto_captured_payment() {
let authorize_response = CONNECTOR
.make_payment(payment_method_details(), get_default_payment_info())
.make_payment(
payment_method_details(),
get_default_payment_info(None, create_customer_and_get_token().await),
)
.await
.unwrap();
assert_eq!(authorize_response.status, enums::AttemptStatus::Charged);
@ -216,7 +345,7 @@ async fn should_sync_auto_captured_payment() {
capture_method: Some(enums::CaptureMethod::Automatic),
..Default::default()
}),
get_default_payment_info(),
get_default_payment_info(None, None),
)
.await
.unwrap();
@ -227,7 +356,11 @@ async fn should_sync_auto_captured_payment() {
#[actix_web::test]
async fn should_refund_auto_captured_payment() {
let response = CONNECTOR
.make_payment_and_refund(payment_method_details(), None, get_default_payment_info())
.make_payment_and_refund(
payment_method_details(),
None,
get_default_payment_info(None, create_customer_and_get_token().await),
)
.await
.unwrap();
assert_eq!(
@ -246,7 +379,7 @@ async fn should_partially_refund_succeeded_payment() {
refund_amount: 50,
..utils::PaymentRefundType::default().0
}),
get_default_payment_info(),
get_default_payment_info(None, create_customer_and_get_token().await),
)
.await
.unwrap();
@ -259,23 +392,47 @@ async fn should_partially_refund_succeeded_payment() {
// 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(
let payment_method_token = create_customer_and_get_token().await;
let response = CONNECTOR
.make_payment(
payment_method_details(),
Some(types::RefundsData {
refund_amount: 50,
..utils::PaymentRefundType::default().0
}),
get_default_payment_info(),
get_default_payment_info(None, payment_method_token.clone()),
)
.await;
.await
.unwrap();
//try refund for previous payment
let transaction_id = utils::get_connector_transaction_id(response.response).unwrap();
for _x in 0..2 {
tokio::time::sleep(Duration::from_secs(60)).await; // to avoid 404 error
let refund_response = CONNECTOR
.refund_payment(
transaction_id.clone(),
Some(types::RefundsData {
refund_amount: 50,
..utils::PaymentRefundType::default().0
}),
get_default_payment_info(None, payment_method_token.clone()),
)
.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(payment_method_details(), None, get_default_payment_info())
.make_payment_and_refund(
payment_method_details(),
None,
get_default_payment_info(None, create_customer_and_get_token().await),
)
.await
.unwrap();
let response = CONNECTOR
@ -283,7 +440,7 @@ async fn should_sync_refund() {
enums::RefundStatus::Success,
refund_response.response.unwrap().connector_refund_id,
None,
get_default_payment_info(),
get_default_payment_info(None, None),
)
.await
.unwrap();
@ -297,81 +454,127 @@ async fn should_sync_refund() {
// 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 {
let customer_response = CONNECTOR
.create_connector_customer(customer_details(), get_default_payment_info(None, None))
.await
.expect("Authorize payment response");
let connector_customer_id = match customer_response.response.unwrap() {
PaymentsResponseData::ConnectorCustomerResponse {
connector_customer_id,
} => Some(connector_customer_id),
_ => None,
};
let token_response = CONNECTOR
.create_connector_pm_token(
Some(types::PaymentMethodTokenizationData {
payment_method_data: types::api::PaymentMethodData::Card(api::Card {
card_cvc: Secret::new("12345".to_string()),
card_number: cards::CardNumber::from_str("4111111111111111").unwrap(),
card_exp_month: Secret::new("11".to_string()),
card_exp_year: Secret::new("2027".to_string()),
card_cvc: Secret::new("123456".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
browser_info: None,
}),
get_default_payment_info(),
get_default_payment_info(connector_customer_id, None),
)
.await
.unwrap();
.expect("Authorize payment response");
assert_eq!(
response.response.unwrap_err().message,
"Your card's security code is invalid.".to_string(),
token_response.response.unwrap_err().reason,
Some(r#"{"card_cvv":["The card cvv may not be greater than 99999."]}"#.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 {
let customer_response = CONNECTOR
.create_connector_customer(customer_details(), get_default_payment_info(None, None))
.await
.expect("Authorize payment response");
let connector_customer_id = match customer_response.response.unwrap() {
PaymentsResponseData::ConnectorCustomerResponse {
connector_customer_id,
} => Some(connector_customer_id),
_ => None,
};
let token_response = CONNECTOR
.create_connector_pm_token(
Some(types::PaymentMethodTokenizationData {
payment_method_data: types::api::PaymentMethodData::Card(api::Card {
card_number: cards::CardNumber::from_str("4111111111111111").unwrap(),
card_exp_month: Secret::new("20".to_string()),
card_exp_year: Secret::new("2027".to_string()),
card_cvc: Secret::new("123".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
browser_info: None,
}),
get_default_payment_info(),
get_default_payment_info(connector_customer_id, None),
)
.await
.unwrap();
.expect("Authorize payment response");
assert_eq!(
response.response.unwrap_err().message,
"Your card's expiration month is invalid.".to_string(),
token_response.response.unwrap_err().reason,
Some(r#"{"validation":["Tokenization Validation Errors: Month is invalid"]}"#.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 {
let customer_response = CONNECTOR
.create_connector_customer(customer_details(), get_default_payment_info(None, None))
.await
.expect("Authorize payment response");
let connector_customer_id = match customer_response.response.unwrap() {
PaymentsResponseData::ConnectorCustomerResponse {
connector_customer_id,
} => Some(connector_customer_id),
_ => None,
};
let token_response = CONNECTOR
.create_connector_pm_token(
Some(types::PaymentMethodTokenizationData {
payment_method_data: types::api::PaymentMethodData::Card(api::Card {
card_number: cards::CardNumber::from_str("4111111111111111").unwrap(),
card_exp_month: Secret::new("04".to_string()),
card_exp_year: Secret::new("2000".to_string()),
card_cvc: Secret::new("123".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
browser_info: None,
}),
get_default_payment_info(),
get_default_payment_info(connector_customer_id, None),
)
.await
.unwrap();
.expect("Authorize payment response");
assert_eq!(
response.response.unwrap_err().message,
"Your card's expiration year is invalid.".to_string(),
token_response.response.unwrap_err().reason,
Some(r#"{"validation":["Tokenization Validation Errors: Year is invalid"]}"#.to_string()),
);
}
// Voids a payment using automatic capture flow (Non 3DS).
#[actix_web::test]
#[ignore = "Connector Refunds the payment on Void call for Auto Captured Payment"]
async fn should_fail_void_payment_for_auto_capture() {
let authorize_response = CONNECTOR
.make_payment(payment_method_details(), get_default_payment_info())
.make_payment(
payment_method_details(),
get_default_payment_info(None, create_customer_and_get_token().await),
)
.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, get_default_payment_info())
.void_payment(txn_id.unwrap(), None, get_default_payment_info(None, None))
.await
.unwrap();
assert_eq!(
@ -384,12 +587,16 @@ async fn should_fail_void_payment_for_auto_capture() {
#[actix_web::test]
async fn should_fail_capture_for_invalid_payment() {
let capture_response = CONNECTOR
.capture_payment("123456789".to_string(), None, get_default_payment_info())
.capture_payment(
"123456789".to_string(),
None,
get_default_payment_info(None, create_customer_and_get_token().await),
)
.await
.unwrap();
assert_eq!(
capture_response.response.unwrap_err().message,
String::from("No such payment_intent: '123456789'")
capture_response.response.unwrap_err().reason,
Some(r#"{"id":["The selected id is invalid."]}"#.to_string()),
);
}
@ -403,13 +610,13 @@ async fn should_fail_for_refund_amount_higher_than_payment_amount() {
refund_amount: 150,
..utils::PaymentRefundType::default().0
}),
get_default_payment_info(),
get_default_payment_info(None, create_customer_and_get_token().await),
)
.await
.unwrap();
assert_eq!(
response.response.unwrap_err().message,
"Refund amount (₹1.50) is greater than charge amount (₹1.00)",
response.response.unwrap_err().reason,
Some(r#"{"total":["The total may not be greater than 100."]}"#.to_string()),
);
}

View File

@ -1,6 +1,7 @@
use std::{fmt::Debug, marker::PhantomData, str::FromStr, time::Duration};
use async_trait::async_trait;
use common_utils::pii::Email;
use error_stack::Report;
use masking::Secret;
use router::{
@ -36,6 +37,8 @@ pub struct PaymentInfo {
pub access_token: Option<AccessToken>,
pub connector_meta_data: Option<serde_json::Value>,
pub return_url: Option<String>,
pub connector_customer: Option<String>,
pub payment_method_token: Option<String>,
pub payout_method_data: Option<api::PayoutMethodData>,
pub currency: Option<enums::Currency>,
pub country: Option<enums::CountryAlpha2>,
@ -70,6 +73,52 @@ pub trait ConnectorActions: Connector {
call_connector(request, integration).await
}
async fn create_connector_customer(
&self,
payment_data: Option<types::ConnectorCustomerData>,
payment_info: Option<PaymentInfo>,
) -> Result<types::ConnectorCustomerRouterData, Report<ConnectorError>> {
let integration = self.get_data().connector.get_connector_integration();
let mut request = self.generate_data(
types::ConnectorCustomerData {
..(payment_data.unwrap_or(CustomerType::default().0))
},
payment_info,
);
let tx: oneshot::Sender<()> = oneshot::channel().0;
let state = routes::AppState::with_storage(
Settings::new().unwrap(),
StorageImpl::PostgresqlTest,
tx,
)
.await;
integration.execute_pretasks(&mut request, &state).await?;
call_connector(request, integration).await
}
async fn create_connector_pm_token(
&self,
payment_data: Option<types::PaymentMethodTokenizationData>,
payment_info: Option<PaymentInfo>,
) -> Result<types::TokenizationRouterData, Report<ConnectorError>> {
let integration = self.get_data().connector.get_connector_integration();
let mut request = self.generate_data(
types::PaymentMethodTokenizationData {
..(payment_data.unwrap_or(TokenType::default().0))
},
payment_info,
);
let tx: oneshot::Sender<()> = oneshot::channel().0;
let state = routes::AppState::with_storage(
Settings::new().unwrap(),
StorageImpl::PostgresqlTest,
tx,
)
.await;
integration.execute_pretasks(&mut request, &state).await?;
call_connector(request, integration).await
}
/// For initiating payments when `CaptureMethod` is set to `Automatic`
/// This does complete the transaction without user intervention to Capture the payment
async fn make_payment(
@ -443,8 +492,8 @@ pub trait ConnectorActions: Connector {
access_token: info.clone().and_then(|a| a.access_token),
session_token: None,
reference_id: None,
payment_method_token: None,
connector_customer: None,
payment_method_token: info.clone().and_then(|a| a.payment_method_token),
connector_customer: info.clone().and_then(|a| a.connector_customer),
recurring_mandate_payment_data: None,
preprocessing_id: None,
connector_request_reference_id: uuid::Uuid::new_v4().to_string(),
@ -764,6 +813,8 @@ pub struct PaymentSyncType(pub types::PaymentsSyncData);
pub struct PaymentRefundType(pub types::RefundsData);
pub struct CCardType(pub api::Card);
pub struct BrowserInfoType(pub types::BrowserInformation);
pub struct CustomerType(pub types::ConnectorCustomerData);
pub struct TokenType(pub types::PaymentMethodTokenizationData);
impl Default for CCardType {
fn default() -> Self {
@ -887,6 +938,29 @@ impl Default for PaymentRefundType {
}
}
impl Default for CustomerType {
fn default() -> Self {
let data = types::ConnectorCustomerData {
description: None,
email: Some(Email::from(Secret::new("test@juspay.in".to_string()))),
phone: None,
name: None,
preprocessing_id: None,
};
Self(data)
}
}
impl Default for TokenType {
fn default() -> Self {
let data = types::PaymentMethodTokenizationData {
payment_method_data: types::api::PaymentMethodData::Card(CCardType::default().0),
browser_info: None,
};
Self(data)
}
}
pub fn get_connector_transaction_id(
response: Result<types::PaymentsResponseData, types::ErrorResponse>,
) -> Option<String> {