feat(connector): [Tsys] Add cards for Payments and Refunds flow (#1716)

Co-authored-by: Prasunna Soppa <prasunna.soppa@juspay.in>
This commit is contained in:
SamraatBansal
2023-07-17 09:46:29 +05:30
committed by GitHub
parent 95a45e4978
commit 714cd275b3
8 changed files with 618 additions and 171 deletions

View File

@ -108,7 +108,7 @@ pub enum Connector {
Shift4, Shift4,
Stripe, Stripe,
Trustpay, Trustpay,
// Tsys, Tsys,
Worldline, Worldline,
Worldpay, Worldpay,
Zen, Zen,
@ -216,7 +216,7 @@ pub enum RoutableConnectors {
Shift4, Shift4,
Stripe, Stripe,
Trustpay, Trustpay,
// Tsys, Tsys,
Worldline, Worldline,
Worldpay, Worldpay,
Zen, Zen,

View File

@ -3,7 +3,6 @@ mod transformers;
use std::fmt::Debug; use std::fmt::Debug;
use error_stack::{IntoReport, ResultExt}; use error_stack::{IntoReport, ResultExt};
use masking::ExposeInterface;
use transformers as tsys; use transformers as tsys;
use crate::{ use crate::{
@ -12,7 +11,7 @@ use crate::{
headers, headers,
services::{ services::{
self, self,
request::{self, Mask}, request::{self},
ConnectorIntegration, ConnectorIntegration,
}, },
types::{ types::{
@ -54,17 +53,13 @@ where
{ {
fn build_headers( fn build_headers(
&self, &self,
req: &types::RouterData<Flow, Request, Response>, _req: &types::RouterData<Flow, Request, Response>,
_connectors: &settings::Connectors, _connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> { ) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> {
let mut header = vec![( let header = vec![(
headers::CONTENT_TYPE.to_string(), headers::CONTENT_TYPE.to_string(),
types::PaymentsAuthorizeType::get_content_type(self) Self::get_content_type(self).to_string().into(),
.to_string()
.into(),
)]; )];
let mut api_key = self.get_auth_header(&req.connector_auth_type)?;
header.append(&mut api_key);
Ok(header) Ok(header)
} }
} }
@ -81,35 +76,6 @@ impl ConnectorCommon for Tsys {
fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str {
connectors.tsys.base_url.as_ref() connectors.tsys.base_url.as_ref()
} }
fn get_auth_header(
&self,
auth_type: &types::ConnectorAuthType,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> {
let auth = tsys::TsysAuthType::try_from(auth_type)
.change_context(errors::ConnectorError::FailedToObtainAuthType)?;
Ok(vec![(
headers::AUTHORIZATION.to_string(),
auth.api_key.expose().into_masked(),
)])
}
fn build_error_response(
&self,
res: Response,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
let response: tsys::TsysErrorResponse = res
.response
.parse_struct("TsysErrorResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
Ok(ErrorResponse {
status_code: res.status_code,
code: response.code,
message: response.message,
reason: response.reason,
})
}
} }
impl ConnectorIntegration<api::Session, types::PaymentsSessionData, types::PaymentsResponseData> impl ConnectorIntegration<api::Session, types::PaymentsSessionData, types::PaymentsResponseData>
@ -145,9 +111,12 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
fn get_url( fn get_url(
&self, &self,
_req: &types::PaymentsAuthorizeRouterData, _req: &types::PaymentsAuthorizeRouterData,
_connectors: &settings::Connectors, connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> { ) -> CustomResult<String, errors::ConnectorError> {
Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) Ok(format!(
"{}servlets/transnox_api_server",
self.base_url(connectors)
))
} }
fn get_request_body( fn get_request_body(
@ -225,9 +194,25 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
fn get_url( fn get_url(
&self, &self,
_req: &types::PaymentsSyncRouterData, _req: &types::PaymentsSyncRouterData,
_connectors: &settings::Connectors, connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> { ) -> CustomResult<String, errors::ConnectorError> {
Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) Ok(format!(
"{}servlets/Transnox_API_server",
self.base_url(connectors)
))
}
fn get_request_body(
&self,
req: &types::PaymentsSyncRouterData,
) -> CustomResult<Option<types::RequestBody>, errors::ConnectorError> {
let req_obj = tsys::TsysSyncRequest::try_from(req)?;
let tsys_req = types::RequestBody::log_and_get_request_body(
&req_obj,
utils::Encode::<tsys::TsysSyncRequest>::encode_to_string_of_json,
)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(tsys_req))
} }
fn build_request( fn build_request(
@ -237,10 +222,11 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
) -> CustomResult<Option<services::Request>, errors::ConnectorError> { ) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some( Ok(Some(
services::RequestBuilder::new() services::RequestBuilder::new()
.method(services::Method::Get) .method(services::Method::Post)
.url(&types::PaymentsSyncType::get_url(self, req, connectors)?) .url(&types::PaymentsSyncType::get_url(self, req, connectors)?)
.attach_default_headers() .attach_default_headers()
.headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?)
.body(types::PaymentsSyncType::get_request_body(self, req)?)
.build(), .build(),
)) ))
} }
@ -250,7 +236,7 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
data: &types::PaymentsSyncRouterData, data: &types::PaymentsSyncRouterData,
res: Response, res: Response,
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> { ) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
let response: tsys::TsysPaymentsResponse = res let response: tsys::TsysSyncResponse = res
.response .response
.parse_struct("tsys PaymentsSyncResponse") .parse_struct("tsys PaymentsSyncResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?; .change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
@ -287,16 +273,25 @@ impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::Payme
fn get_url( fn get_url(
&self, &self,
_req: &types::PaymentsCaptureRouterData, _req: &types::PaymentsCaptureRouterData,
_connectors: &settings::Connectors, connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> { ) -> CustomResult<String, errors::ConnectorError> {
Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) Ok(format!(
"{}servlets/Transnox_API_server",
self.base_url(connectors)
))
} }
fn get_request_body( fn get_request_body(
&self, &self,
_req: &types::PaymentsCaptureRouterData, req: &types::PaymentsCaptureRouterData,
) -> CustomResult<Option<types::RequestBody>, errors::ConnectorError> { ) -> CustomResult<Option<types::RequestBody>, errors::ConnectorError> {
Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) let connector_req = tsys::TsysPaymentsCaptureRequest::try_from(req)?;
let tsys_req = types::RequestBody::log_and_get_request_body(
&connector_req,
utils::Encode::<tsys::TsysPaymentsCaptureRequest>::encode_to_string_of_json,
)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(tsys_req))
} }
fn build_request( fn build_request(
@ -344,6 +339,77 @@ impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::Payme
impl ConnectorIntegration<api::Void, types::PaymentsCancelData, types::PaymentsResponseData> impl ConnectorIntegration<api::Void, types::PaymentsCancelData, types::PaymentsResponseData>
for Tsys for Tsys
{ {
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!(
"{}servlets/Transnox_API_server",
self.base_url(connectors),
))
}
fn get_request_body(
&self,
req: &types::PaymentsCancelRouterData,
) -> CustomResult<Option<types::RequestBody>, errors::ConnectorError> {
let req_obj = tsys::TsysPaymentsCancelRequest::try_from(req)?;
let tsys_req = types::RequestBody::log_and_get_request_body(
&req_obj,
utils::Encode::<tsys::TsysPaymentsCancelRequest>::encode_to_string_of_json,
)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(tsys_req))
}
fn build_request(
&self,
req: &types::PaymentsCancelRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
let request = services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsVoidType::get_url(self, req, connectors)?)
.headers(types::PaymentsVoidType::get_headers(self, req, connectors)?)
.body(types::PaymentsVoidType::get_request_body(self, req)?)
.build();
Ok(Some(request))
}
fn handle_response(
&self,
data: &types::PaymentsCancelRouterData,
res: Response,
) -> CustomResult<types::PaymentsCancelRouterData, errors::ConnectorError> {
let response: tsys::TsysPaymentsResponse = res
.response
.parse_struct("PaymentCancelResponse")
.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 Tsys { impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsResponseData> for Tsys {
@ -362,9 +428,12 @@ impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsRespon
fn get_url( fn get_url(
&self, &self,
_req: &types::RefundsRouterData<api::Execute>, _req: &types::RefundsRouterData<api::Execute>,
_connectors: &settings::Connectors, connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> { ) -> CustomResult<String, errors::ConnectorError> {
Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) Ok(format!(
"{}servlets/Transnox_API_server",
self.base_url(connectors)
))
} }
fn get_request_body( fn get_request_body(
@ -437,9 +506,25 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse
fn get_url( fn get_url(
&self, &self,
_req: &types::RefundSyncRouterData, _req: &types::RefundSyncRouterData,
_connectors: &settings::Connectors, connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> { ) -> CustomResult<String, errors::ConnectorError> {
Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) Ok(format!(
"{}servlets/Transnox_API_server",
self.base_url(connectors),
))
}
fn get_request_body(
&self,
req: &types::RefundSyncRouterData,
) -> CustomResult<Option<types::RequestBody>, errors::ConnectorError> {
let req_obj = tsys::TsysSyncRequest::try_from(req)?;
let tsys_req = types::RequestBody::log_and_get_request_body(
&req_obj,
utils::Encode::<tsys::TsysSyncRequest>::encode_to_string_of_json,
)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(tsys_req))
} }
fn build_request( fn build_request(
@ -449,7 +534,7 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse
) -> CustomResult<Option<services::Request>, errors::ConnectorError> { ) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some( Ok(Some(
services::RequestBuilder::new() services::RequestBuilder::new()
.method(services::Method::Get) .method(services::Method::Post)
.url(&types::RefundSyncType::get_url(self, req, connectors)?) .url(&types::RefundSyncType::get_url(self, req, connectors)?)
.attach_default_headers() .attach_default_headers()
.headers(types::RefundSyncType::get_headers(self, req, connectors)?) .headers(types::RefundSyncType::get_headers(self, req, connectors)?)
@ -463,10 +548,10 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse
data: &types::RefundSyncRouterData, data: &types::RefundSyncRouterData,
res: Response, res: Response,
) -> CustomResult<types::RefundSyncRouterData, errors::ConnectorError> { ) -> CustomResult<types::RefundSyncRouterData, errors::ConnectorError> {
let response: tsys::RefundResponse = let response: tsys::TsysSyncResponse = res
res.response .response
.parse_struct("tsys RefundSyncResponse") .parse_struct("tsys RefundSyncResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?; .change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData { types::RouterData::try_from(types::ResponseRouterData {
response, response,
data: data.clone(), data: data.clone(),

View File

@ -1,45 +1,68 @@
use error_stack::ResultExt;
use masking::Secret; use masking::Secret;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
connector::utils::PaymentsAuthorizeRequestData, connector::utils::{CardData, PaymentsAuthorizeRequestData, RefundsRequestData},
core::errors, core::errors,
types::{self, api, storage::enums}, types::{
self, api,
storage::{self, enums},
},
}; };
#[derive(Default, Debug, Serialize, Eq, PartialEq)] #[derive(Debug, Serialize)]
pub struct TsysPaymentsRequest { pub enum TsysPaymentsRequest {
amount: i64, Auth(TsysPaymentAuthSaleRequest),
card: TsysCard, Sale(TsysPaymentAuthSaleRequest),
} }
#[derive(Default, Debug, Serialize, Eq, PartialEq)] #[derive(Default, Debug, Serialize)]
pub struct TsysCard { #[serde(rename_all = "camelCase")]
name: Secret<String>, pub struct TsysPaymentAuthSaleRequest {
number: cards::CardNumber, #[serde(rename = "deviceID")]
expiry_month: Secret<String>, device_id: Secret<String>,
expiry_year: Secret<String>, transaction_key: Secret<String>,
cvc: Secret<String>, card_data_source: String,
complete: bool, transaction_amount: String,
currency_code: storage::enums::Currency,
card_number: cards::CardNumber,
expiration_date: Secret<String>,
cvv2: Secret<String>,
terminal_capability: String,
terminal_operating_environment: String,
cardholder_authentication_method: String,
#[serde(rename = "developerID")]
developer_id: Secret<String>,
} }
impl TryFrom<&types::PaymentsAuthorizeRouterData> for TsysPaymentsRequest { impl TryFrom<&types::PaymentsAuthorizeRouterData> for TsysPaymentsRequest {
type Error = error_stack::Report<errors::ConnectorError>; type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> { fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
match item.request.payment_method_data.clone() { match item.request.payment_method_data.clone() {
api::PaymentMethodData::Card(req_card) => { api::PaymentMethodData::Card(ccard) => {
let card = TsysCard { let connector_auth: TsysAuthType =
name: req_card.card_holder_name, TsysAuthType::try_from(&item.connector_auth_type)?;
number: req_card.card_number, let auth_data: TsysPaymentAuthSaleRequest = TsysPaymentAuthSaleRequest {
expiry_month: req_card.card_exp_month, device_id: connector_auth.device_id,
expiry_year: req_card.card_exp_year, transaction_key: connector_auth.transaction_key,
cvc: req_card.card_cvc, card_data_source: "INTERNET".to_string(),
complete: item.request.is_auto_capture()?, transaction_amount: item.request.amount.to_string(),
currency_code: item.request.currency,
card_number: ccard.card_number.clone(),
expiration_date: ccard
.get_card_expiry_month_year_2_digit_with_delimiter("/".to_owned()),
cvv2: ccard.card_cvc,
terminal_capability: "ICC_CHIP_READ_ONLY".to_string(),
terminal_operating_environment: "ON_MERCHANT_PREMISES_ATTENDED".to_string(),
cardholder_authentication_method: "NOT_AUTHENTICATED".to_string(),
developer_id: connector_auth.developer_id,
}; };
Ok(Self { if item.request.is_auto_capture()? {
amount: item.request.amount, Ok(Self::Sale(auth_data))
card, } else {
}) Ok(Self::Auth(auth_data))
}
} }
_ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()),
} }
@ -48,44 +71,152 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for TsysPaymentsRequest {
// Auth Struct // Auth Struct
pub struct TsysAuthType { pub struct TsysAuthType {
pub(super) api_key: Secret<String>, pub(super) device_id: Secret<String>,
pub(super) transaction_key: Secret<String>,
pub(super) developer_id: Secret<String>,
} }
impl TryFrom<&types::ConnectorAuthType> for TsysAuthType { impl TryFrom<&types::ConnectorAuthType> for TsysAuthType {
type Error = error_stack::Report<errors::ConnectorError>; type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(auth_type: &types::ConnectorAuthType) -> Result<Self, Self::Error> { fn try_from(auth_type: &types::ConnectorAuthType) -> Result<Self, Self::Error> {
match auth_type { match auth_type {
types::ConnectorAuthType::HeaderKey { api_key } => Ok(Self { types::ConnectorAuthType::SignatureKey {
api_key: Secret::new(api_key.to_string()), api_key,
key1,
api_secret,
} => Ok(Self {
device_id: Secret::new(api_key.to_string()),
transaction_key: Secret::new(key1.to_string()),
developer_id: Secret::new(api_secret.to_string()),
}), }),
_ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()),
} }
} }
} }
// PaymentsResponse // PaymentsResponse
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "UPPERCASE")]
pub enum TsysPaymentStatus { pub enum TsysPaymentStatus {
Succeeded, Pass,
Failed, Fail,
#[default]
Processing,
} }
impl From<TsysPaymentStatus> for enums::AttemptStatus { #[derive(Debug, Clone, Deserialize)]
fn from(item: TsysPaymentStatus) -> Self { #[serde(rename_all = "UPPERCASE")]
match item { pub enum TsysTransactionStatus {
TsysPaymentStatus::Succeeded => Self::Charged, Approved,
TsysPaymentStatus::Failed => Self::Failure, Declined,
TsysPaymentStatus::Processing => Self::Authorizing, Void,
}
impl From<TsysTransactionDetails> for enums::AttemptStatus {
fn from(item: TsysTransactionDetails) -> Self {
match item.transaction_status {
TsysTransactionStatus::Approved => {
if item.transaction_type.contains("Auth-Only") {
Self::Authorized
} else {
Self::Charged
}
}
TsysTransactionStatus::Void => Self::Voided,
TsysTransactionStatus::Declined => Self::Failure,
} }
} }
} }
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Deserialize)]
pub struct TsysPaymentsResponse { #[serde(rename_all = "camelCase")]
status: TsysPaymentStatus, pub struct TsysErrorResponse {
id: String, pub status: TsysPaymentStatus,
pub response_code: String,
pub response_message: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TsysTransactionDetails {
#[serde(rename = "transactionID")]
transaction_id: String,
transaction_type: String,
transaction_status: TsysTransactionStatus,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TsysPaymentsSyncResponse {
pub status: TsysPaymentStatus,
pub response_code: String,
pub response_message: String,
pub transaction_details: TsysTransactionDetails,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TsysResponse {
pub status: TsysPaymentStatus,
pub response_code: String,
pub response_message: String,
#[serde(rename = "transactionID")]
pub transaction_id: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum TsysResponseTypes {
SuccessResponse(TsysResponse),
ErrorResponse(TsysErrorResponse),
}
#[derive(Debug, Clone, Deserialize)]
#[allow(clippy::enum_variant_names)]
pub enum TsysPaymentsResponse {
AuthResponse(TsysResponseTypes),
SaleResponse(TsysResponseTypes),
CaptureResponse(TsysResponseTypes),
VoidResponse(TsysResponseTypes),
}
fn get_error_response(
connector_error_response: TsysErrorResponse,
status_code: u16,
) -> types::ErrorResponse {
types::ErrorResponse {
code: connector_error_response.response_code,
message: connector_error_response.response_message.clone(),
reason: Some(connector_error_response.response_message),
status_code,
}
}
fn get_payments_response(connector_response: TsysResponse) -> types::PaymentsResponseData {
types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(connector_response.transaction_id),
redirection_data: None,
mandate_reference: None,
connector_metadata: None,
network_txn_id: None,
connector_response_reference_id: None,
}
}
fn get_payments_sync_response(
connector_response: &TsysPaymentsSyncResponse,
) -> types::PaymentsResponseData {
types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(
connector_response
.transaction_details
.transaction_id
.clone(),
),
redirection_data: None,
mandate_reference: None,
connector_metadata: None,
network_txn_id: None,
connector_response_reference_id: None,
}
} }
impl<F, T> impl<F, T>
@ -96,62 +227,253 @@ impl<F, T>
fn try_from( fn try_from(
item: types::ResponseRouterData<F, TsysPaymentsResponse, T, types::PaymentsResponseData>, item: types::ResponseRouterData<F, TsysPaymentsResponse, T, types::PaymentsResponseData>,
) -> Result<Self, Self::Error> { ) -> Result<Self, Self::Error> {
let (response, status) = match item.response {
TsysPaymentsResponse::AuthResponse(resp) => match resp {
TsysResponseTypes::SuccessResponse(auth_response) => (
Ok(get_payments_response(auth_response)),
enums::AttemptStatus::Authorized,
),
TsysResponseTypes::ErrorResponse(connector_error_response) => (
Err(get_error_response(connector_error_response, item.http_code)),
enums::AttemptStatus::AuthorizationFailed,
),
},
TsysPaymentsResponse::SaleResponse(resp) => match resp {
TsysResponseTypes::SuccessResponse(sale_response) => (
Ok(get_payments_response(sale_response)),
enums::AttemptStatus::Charged,
),
TsysResponseTypes::ErrorResponse(connector_error_response) => (
Err(get_error_response(connector_error_response, item.http_code)),
enums::AttemptStatus::Failure,
),
},
TsysPaymentsResponse::CaptureResponse(resp) => match resp {
TsysResponseTypes::SuccessResponse(capture_response) => (
Ok(get_payments_response(capture_response)),
enums::AttemptStatus::Charged,
),
TsysResponseTypes::ErrorResponse(connector_error_response) => (
Err(get_error_response(connector_error_response, item.http_code)),
enums::AttemptStatus::CaptureFailed,
),
},
TsysPaymentsResponse::VoidResponse(resp) => match resp {
TsysResponseTypes::SuccessResponse(void_response) => (
Ok(get_payments_response(void_response)),
enums::AttemptStatus::Voided,
),
TsysResponseTypes::ErrorResponse(connector_error_response) => (
Err(get_error_response(connector_error_response, item.http_code)),
enums::AttemptStatus::VoidFailed,
),
},
};
Ok(Self { Ok(Self {
status: enums::AttemptStatus::from(item.response.status), status,
response: Ok(types::PaymentsResponseData::TransactionResponse { response,
resource_id: types::ResponseId::ConnectorTransactionId(item.response.id),
redirection_data: None,
mandate_reference: None,
connector_metadata: None,
network_txn_id: None,
connector_response_reference_id: None,
}),
..item.data ..item.data
}) })
} }
} }
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TsysSearchTransactionRequest {
#[serde(rename = "deviceID")]
device_id: Secret<String>,
transaction_key: Secret<String>,
#[serde(rename = "transactionID")]
transaction_id: String,
#[serde(rename = "developerID")]
developer_id: Secret<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct TsysSyncRequest {
search_transaction: TsysSearchTransactionRequest,
}
impl TryFrom<&types::PaymentsSyncRouterData> for TsysSyncRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsSyncRouterData) -> Result<Self, Self::Error> {
let connector_auth: TsysAuthType = TsysAuthType::try_from(&item.connector_auth_type)?;
let search_transaction = TsysSearchTransactionRequest {
device_id: connector_auth.device_id,
transaction_key: connector_auth.transaction_key,
transaction_id: item
.request
.connector_transaction_id
.get_connector_transaction_id()
.change_context(errors::ConnectorError::MissingConnectorTransactionID)?,
developer_id: connector_auth.developer_id,
};
Ok(Self { search_transaction })
}
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum SearchResponseTypes {
SuccessResponse(TsysPaymentsSyncResponse),
ErrorResponse(TsysErrorResponse),
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct TsysSyncResponse {
search_transaction_response: SearchResponseTypes,
}
impl<F, T> TryFrom<types::ResponseRouterData<F, TsysSyncResponse, T, types::PaymentsResponseData>>
for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<F, TsysSyncResponse, T, types::PaymentsResponseData>,
) -> Result<Self, Self::Error> {
let tsys_search_response = item.response.search_transaction_response;
let (response, status) = match tsys_search_response {
SearchResponseTypes::SuccessResponse(search_response) => (
Ok(get_payments_sync_response(&search_response)),
enums::AttemptStatus::from(search_response.transaction_details),
),
SearchResponseTypes::ErrorResponse(connector_error_response) => (
Err(get_error_response(connector_error_response, item.http_code)),
item.data.status,
),
};
Ok(Self {
status,
response,
..item.data
})
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TsysCancelRequest {
#[serde(rename = "deviceID")]
device_id: Secret<String>,
transaction_key: Secret<String>,
#[serde(rename = "transactionID")]
transaction_id: String,
#[serde(rename = "developerID")]
developer_id: Secret<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct TsysPaymentsCancelRequest {
void: TsysCancelRequest,
}
impl TryFrom<&types::PaymentsCancelRouterData> for TsysPaymentsCancelRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsCancelRouterData) -> Result<Self, Self::Error> {
let connector_auth: TsysAuthType = TsysAuthType::try_from(&item.connector_auth_type)?;
let void = TsysCancelRequest {
device_id: connector_auth.device_id,
transaction_key: connector_auth.transaction_key,
transaction_id: item.request.connector_transaction_id.clone(),
developer_id: connector_auth.developer_id,
};
Ok(Self { void })
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TsysCaptureRequest {
#[serde(rename = "deviceID")]
device_id: Secret<String>,
transaction_key: Secret<String>,
transaction_amount: String,
#[serde(rename = "transactionID")]
transaction_id: String,
#[serde(rename = "developerID")]
developer_id: Secret<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct TsysPaymentsCaptureRequest {
capture: TsysCaptureRequest,
}
impl TryFrom<&types::PaymentsCaptureRouterData> for TsysPaymentsCaptureRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsCaptureRouterData) -> Result<Self, Self::Error> {
let connector_auth: TsysAuthType = TsysAuthType::try_from(&item.connector_auth_type)?;
let capture = TsysCaptureRequest {
device_id: connector_auth.device_id,
transaction_key: connector_auth.transaction_key,
transaction_id: item.request.connector_transaction_id.clone(),
developer_id: connector_auth.developer_id,
transaction_amount: item.request.amount_to_capture.to_string(),
};
Ok(Self { capture })
}
}
// REFUND : // REFUND :
// Type definition for RefundRequest // Type definition for RefundRequest
#[derive(Default, Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TsysReturnRequest {
#[serde(rename = "deviceID")]
device_id: Secret<String>,
transaction_key: Secret<String>,
transaction_amount: String,
#[serde(rename = "transactionID")]
transaction_id: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct TsysRefundRequest { pub struct TsysRefundRequest {
pub amount: i64, #[serde(rename = "Return")]
return_request: TsysReturnRequest,
} }
impl<F> TryFrom<&types::RefundsRouterData<F>> for TsysRefundRequest { impl<F> TryFrom<&types::RefundsRouterData<F>> for TsysRefundRequest {
type Error = error_stack::Report<errors::ConnectorError>; type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::RefundsRouterData<F>) -> Result<Self, Self::Error> { fn try_from(item: &types::RefundsRouterData<F>) -> Result<Self, Self::Error> {
Ok(Self { let connector_auth: TsysAuthType = TsysAuthType::try_from(&item.connector_auth_type)?;
amount: item.request.refund_amount, let return_request = TsysReturnRequest {
}) device_id: connector_auth.device_id,
transaction_key: connector_auth.transaction_key,
transaction_amount: item.request.refund_amount.to_string(),
transaction_id: item.request.connector_transaction_id.clone(),
};
Ok(Self { return_request })
} }
} }
// Type definition for Refund Response impl From<TsysPaymentStatus> for enums::RefundStatus {
#[allow(dead_code)] fn from(item: TsysPaymentStatus) -> Self {
#[derive(Debug, Serialize, Default, Deserialize, Clone)]
pub enum RefundStatus {
Succeeded,
Failed,
#[default]
Processing,
}
impl From<RefundStatus> for enums::RefundStatus {
fn from(item: RefundStatus) -> Self {
match item { match item {
RefundStatus::Succeeded => Self::Success, TsysPaymentStatus::Pass => Self::Success,
RefundStatus::Failed => Self::Failure, TsysPaymentStatus::Fail => Self::Failure,
RefundStatus::Processing => Self::Pending,
//TODO: Review mapping
} }
} }
} }
#[derive(Default, Debug, Clone, Serialize, Deserialize)] impl From<TsysTransactionDetails> for enums::RefundStatus {
fn from(item: TsysTransactionDetails) -> Self {
match item.transaction_status {
TsysTransactionStatus::Approved => Self::Pending,
TsysTransactionStatus::Void => Self::Success,
TsysTransactionStatus::Declined => Self::Failure,
}
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct RefundResponse { pub struct RefundResponse {
id: String, return_response: TsysResponseTypes,
status: RefundStatus,
} }
impl TryFrom<types::RefundsResponseRouterData<api::Execute, RefundResponse>> impl TryFrom<types::RefundsResponseRouterData<api::Execute, RefundResponse>>
@ -161,37 +483,59 @@ impl TryFrom<types::RefundsResponseRouterData<api::Execute, RefundResponse>>
fn try_from( fn try_from(
item: types::RefundsResponseRouterData<api::Execute, RefundResponse>, item: types::RefundsResponseRouterData<api::Execute, RefundResponse>,
) -> Result<Self, Self::Error> { ) -> Result<Self, Self::Error> {
Ok(Self { let tsys_return_response = item.response.return_response;
response: Ok(types::RefundsResponseData { let response = match tsys_return_response {
connector_refund_id: item.response.id.to_string(), TsysResponseTypes::SuccessResponse(return_response) => Ok(types::RefundsResponseData {
refund_status: enums::RefundStatus::from(item.response.status), connector_refund_id: return_response.transaction_id,
refund_status: enums::RefundStatus::from(return_response.status),
}), }),
TsysResponseTypes::ErrorResponse(connector_error_response) => {
Err(get_error_response(connector_error_response, item.http_code))
}
};
Ok(Self {
response,
..item.data ..item.data
}) })
} }
} }
impl TryFrom<types::RefundsResponseRouterData<api::RSync, RefundResponse>> impl TryFrom<&types::RefundSyncRouterData> for TsysSyncRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::RefundSyncRouterData) -> Result<Self, Self::Error> {
let connector_auth: TsysAuthType = TsysAuthType::try_from(&item.connector_auth_type)?;
let search_transaction = TsysSearchTransactionRequest {
device_id: connector_auth.device_id,
transaction_key: connector_auth.transaction_key,
transaction_id: item.request.get_connector_refund_id()?,
developer_id: connector_auth.developer_id,
};
Ok(Self { search_transaction })
}
}
impl TryFrom<types::RefundsResponseRouterData<api::RSync, TsysSyncResponse>>
for types::RefundsRouterData<api::RSync> for types::RefundsRouterData<api::RSync>
{ {
type Error = error_stack::Report<errors::ConnectorError>; type Error = error_stack::Report<errors::ConnectorError>;
fn try_from( fn try_from(
item: types::RefundsResponseRouterData<api::RSync, RefundResponse>, item: types::RefundsResponseRouterData<api::RSync, TsysSyncResponse>,
) -> Result<Self, Self::Error> { ) -> Result<Self, Self::Error> {
let tsys_search_response = item.response.search_transaction_response;
let response = match tsys_search_response {
SearchResponseTypes::SuccessResponse(search_response) => {
Ok(types::RefundsResponseData {
connector_refund_id: search_response.transaction_details.transaction_id.clone(),
refund_status: enums::RefundStatus::from(search_response.transaction_details),
})
}
SearchResponseTypes::ErrorResponse(connector_error_response) => {
Err(get_error_response(connector_error_response, item.http_code))
}
};
Ok(Self { Ok(Self {
response: Ok(types::RefundsResponseData { response,
connector_refund_id: item.response.id.to_string(),
refund_status: enums::RefundStatus::from(item.response.status),
}),
..item.data ..item.data
}) })
} }
} }
#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
pub struct TsysErrorResponse {
pub status_code: u16,
pub code: String,
pub message: String,
pub reason: Option<String>,
}

View File

@ -253,6 +253,7 @@ impl ConnectorData {
enums::Connector::Nexinets => Ok(Box::new(&connector::Nexinets)), enums::Connector::Nexinets => Ok(Box::new(&connector::Nexinets)),
enums::Connector::Paypal => Ok(Box::new(&connector::Paypal)), enums::Connector::Paypal => Ok(Box::new(&connector::Paypal)),
enums::Connector::Trustpay => Ok(Box::new(&connector::Trustpay)), enums::Connector::Trustpay => Ok(Box::new(&connector::Trustpay)),
enums::Connector::Tsys => Ok(Box::new(&connector::Tsys)),
enums::Connector::Zen => Ok(Box::new(&connector::Zen)), enums::Connector::Zen => Ok(Box::new(&connector::Zen)),
}, },
Err(_) => Err(report!(errors::ConnectorError::InvalidConnectorName) Err(_) => Err(report!(errors::ConnectorError::InvalidConnectorName)

View File

@ -143,13 +143,16 @@ key1 = "key1"
api_key="Classic PMT API Key" api_key="Classic PMT API Key"
key1 = "Evoucher PMT API Key" key1 = "Evoucher PMT API Key"
[tsys]
api_key="device id"
key1 = "transaction key"
api_secret = "developer id"
[globepay] [globepay]
api_key = "Partner code" api_key = "Partner code"
key1 = "Credential code" key1 = "Credential code"
[powertranz] [powertranz]
api_key="PowerTranz-PowerTranzPassword" api_key="PowerTranz-PowerTranzPassword"
key1 = "PowerTranz-PowerTranzId" key1 = "PowerTranz-PowerTranzId"
[tsys]
api_key="API Key"

View File

@ -1,8 +1,13 @@
use std::str::FromStr;
use cards::CardNumber;
use masking::Secret; use masking::Secret;
use router::types::{self, api, storage::enums}; use router::types::{self, api, storage::enums};
use test_utils::connector_auth;
use crate::utils::{self, ConnectorActions}; use crate::{
connector_auth,
utils::{self, ConnectorActions},
};
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
struct TsysTest; struct TsysTest;
@ -12,7 +17,7 @@ impl utils::Connector for TsysTest {
use router::connector::Tsys; use router::connector::Tsys;
types::api::ConnectorData { types::api::ConnectorData {
connector: Box::new(&Tsys), connector: Box::new(&Tsys),
connector_name: types::Connector::DummyConnector1, connector_name: types::Connector::Tsys,
get_token: types::api::GetToken::Connector, get_token: types::api::GetToken::Connector,
} }
} }
@ -20,7 +25,7 @@ impl utils::Connector for TsysTest {
fn get_auth_token(&self) -> types::ConnectorAuthType { fn get_auth_token(&self) -> types::ConnectorAuthType {
types::ConnectorAuthType::from( types::ConnectorAuthType::from(
connector_auth::ConnectorAuthentication::new() connector_auth::ConnectorAuthentication::new()
.dummyconnector .tsys
.expect("Missing connector authentication configuration"), .expect("Missing connector authentication configuration"),
) )
} }
@ -37,7 +42,13 @@ fn get_default_payment_info() -> Option<utils::PaymentInfo> {
} }
fn payment_method_details() -> Option<types::PaymentsAuthorizeData> { fn payment_method_details() -> Option<types::PaymentsAuthorizeData> {
None Some(types::PaymentsAuthorizeData {
payment_method_data: types::api::PaymentMethodData::Card(api::Card {
card_number: CardNumber::from_str("4111111111111111").unwrap(),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
})
} }
// Cards Positive Tests // Cards Positive Tests
@ -301,7 +312,7 @@ async fn should_fail_payment_for_incorrect_cvc() {
.make_payment( .make_payment(
Some(types::PaymentsAuthorizeData { Some(types::PaymentsAuthorizeData {
payment_method_data: types::api::PaymentMethodData::Card(api::Card { payment_method_data: types::api::PaymentMethodData::Card(api::Card {
card_cvc: Secret::new("12345".to_string()), card_cvc: Secret::new("".to_string()),
..utils::CCardType::default().0 ..utils::CCardType::default().0
}), }),
..utils::PaymentAuthorizeType::default().0 ..utils::PaymentAuthorizeType::default().0
@ -312,7 +323,7 @@ async fn should_fail_payment_for_incorrect_cvc() {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
response.response.unwrap_err().message, response.response.unwrap_err().message,
"Your card's security code is invalid.".to_string(), "The value of element cvv2 is not valid.".to_string(),
); );
} }
@ -334,7 +345,7 @@ async fn should_fail_payment_for_invalid_exp_month() {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
response.response.unwrap_err().message, response.response.unwrap_err().message,
"Your card's expiration month is invalid.".to_string(), "The value of element 'expirationDate' is not valid., ".to_string(),
); );
} }
@ -345,7 +356,7 @@ async fn should_fail_payment_for_incorrect_expiry_year() {
.make_payment( .make_payment(
Some(types::PaymentsAuthorizeData { Some(types::PaymentsAuthorizeData {
payment_method_data: types::api::PaymentMethodData::Card(api::Card { payment_method_data: types::api::PaymentMethodData::Card(api::Card {
card_exp_year: Secret::new("2000".to_string()), card_exp_year: Secret::new("abcd".to_string()),
..utils::CCardType::default().0 ..utils::CCardType::default().0
}), }),
..utils::PaymentAuthorizeType::default().0 ..utils::PaymentAuthorizeType::default().0
@ -356,12 +367,13 @@ async fn should_fail_payment_for_incorrect_expiry_year() {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
response.response.unwrap_err().message, response.response.unwrap_err().message,
"Your card's expiration year is invalid.".to_string(), "The value of element 'expirationDate' is not valid., ".to_string(),
); );
} }
// Voids a payment using automatic capture flow (Non 3DS). // Voids a payment using automatic capture flow (Non 3DS).
#[actix_web::test] #[actix_web::test]
#[ignore = "Connector Refunds the payment on Void call for Auto Captured Payment"]
async fn should_fail_void_payment_for_auto_capture() { async fn should_fail_void_payment_for_auto_capture() {
let authorize_response = CONNECTOR let authorize_response = CONNECTOR
.make_payment(payment_method_details(), get_default_payment_info()) .make_payment(payment_method_details(), get_default_payment_info())
@ -389,7 +401,7 @@ async fn should_fail_capture_for_invalid_payment() {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
capture_response.response.unwrap_err().message, capture_response.response.unwrap_err().message,
String::from("No such payment_intent: '123456789'") String::from("Record(s) Not Found.")
); );
} }
@ -409,7 +421,7 @@ async fn should_fail_for_refund_amount_higher_than_payment_amount() {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
response.response.unwrap_err().message, response.response.unwrap_err().message,
"Refund amount (₹1.50) is greater than charge amount (₹1.00)", "Return Not Allowed.",
); );
} }

View File

@ -46,6 +46,7 @@ pub struct ConnectorAuthentication {
pub stripe_au: Option<HeaderKey>, pub stripe_au: Option<HeaderKey>,
pub stripe_uk: Option<HeaderKey>, pub stripe_uk: Option<HeaderKey>,
pub trustpay: Option<SignatureKey>, pub trustpay: Option<SignatureKey>,
pub tsys: Option<SignatureKey>,
pub worldpay: Option<BodyKey>, pub worldpay: Option<BodyKey>,
pub worldline: Option<SignatureKey>, pub worldline: Option<SignatureKey>,
pub zen: Option<HeaderKey>, pub zen: Option<HeaderKey>,

View File

@ -2998,6 +2998,7 @@
"shift4", "shift4",
"stripe", "stripe",
"trustpay", "trustpay",
"tsys",
"worldline", "worldline",
"worldpay", "worldpay",
"zen" "zen"