feat(connector): Add support for shift4 connector (#205)

This commit is contained in:
Jagan
2022-12-23 22:49:33 +05:30
committed by GitHub
parent 6f62c71ad0
commit a996f0d89b
24 changed files with 1479 additions and 94 deletions

View File

@ -38,7 +38,7 @@ locker_decryption_key2 = ""
[connectors.supported]
wallets = ["klarna","braintree","applepay"]
cards = ["stripe","adyen","authorizedotnet","checkout","braintree","aci"]
cards = ["stripe","adyen","authorizedotnet","checkout","braintree","aci","shift4"]
[eph_key]
validity = 1
@ -67,6 +67,9 @@ base_url = "https://api-na.playground.klarna.com/"
[connectors.applepay]
base_url = "https://apple-pay-gateway.apple.com/"
[connectors.shift4]
base_url = "https://api.shift4.com/"
[scheduler]
stream = "SCHEDULER_STREAM"
consumer_group = "SCHEDULER_GROUP"

View File

@ -119,6 +119,9 @@ base_url = "https://api-na.playground.klarna.com/"
[connectors.applepay]
base_url = "https://apple-pay-gateway.apple.com/"
[connectors.shift4]
base_url = "https://api.shift4.com/"
# This data is used to call respective connectors for wallets and cards
[connectors.supported]
wallets = ["klarna","braintree","applepay"]

View File

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

View File

@ -6,13 +6,16 @@ use bytes::Bytes;
use error_stack::ResultExt;
use crate::{
configs::settings::ConnectorParams,
configs::settings,
utils::{self, BytesExt},
core::errors::{self, CustomResult},
logger, services,
core::{
errors::{self, CustomResult},
payments,
},
headers, logger, services,
types::{
self,
api,
api::{self, ConnectorCommon, ConnectorCommonExt},
ErrorResponse, Response,
}
};
@ -21,15 +24,14 @@ use crate::{
use transformers as {{project-name | downcase}};
#[derive(Debug, Clone)]
pub struct {{project-name | downcase | pascal_case}} {
pub base_url: String,
}
pub struct {{project-name | downcase | pascal_case}};
impl {{project-name | downcase | pascal_case}} {
pub fn make(params: &ConnectorParams) -> Self {
Self {
base_url: params.base_url.to_owned(),
}
impl api::ConnectorCommonExt for {{project-name | downcase | pascal_case}} {
fn build_headers<Flow, Request, Response>(
&self,
req: &types::RouterData<Flow, Request, Response>,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
todo!()
}
}
@ -43,8 +45,8 @@ impl api::ConnectorCommon for {{project-name | downcase | pascal_case}} {
// Ex: "application/x-www-form-urlencoded"
}
fn base_url(&self) -> &str {
&self.base_url
fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str {
connectors.{{project-name}}.base_url.as_ref()
}
fn get_auth_header(&self,_auth_type:&types::ConnectorAuthType)-> CustomResult<Vec<(String,String)>,errors::ConnectorError> {
@ -54,19 +56,151 @@ impl api::ConnectorCommon for {{project-name | downcase | pascal_case}} {
impl api::Payment for {{project-name | downcase | pascal_case}} {}
impl api::PreVerify for {{project-name | downcase | pascal_case}} {}
impl
services::ConnectorIntegration<
api::Verify,
types::VerifyRequestData,
types::PaymentsResponseData,
> for {{project-name | downcase | pascal_case}}
{
}
impl api::PaymentVoid for {{project-name | downcase | pascal_case}} {}
impl
services::ConnectorIntegration<
api::Void,
types::PaymentsCancelData,
types::PaymentsResponseData,
> for {{project-name | downcase | pascal_case}}
{}
impl api::PaymentSync for {{project-name | downcase | pascal_case}} {}
impl
services::ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
for {{project-name | downcase | pascal_case}}
{
fn get_headers(
&self,
req: &types::PaymentsSyncRouterData,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
todo!()
}
fn get_content_type(&self) -> &'static str {
todo!()
}
fn get_url(
&self,
req: &types::PaymentsSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
todo!()
}
fn build_request(
&self,
req: &types::PaymentsSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
todo!()
}
fn get_error_response(
&self,
res: Bytes,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
todo!()
}
fn handle_response(
&self,
data: &types::PaymentsSyncRouterData,
res: Response,
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
todo!()
}
}
impl api::PaymentCapture for {{project-name | downcase | pascal_case}} {}
impl
services::ConnectorIntegration<
api::Capture,
types::PaymentsCaptureData,
types::PaymentsResponseData,
> for {{project-name | downcase | pascal_case}}
{
fn get_headers(
&self,
req: &types::PaymentsCaptureRouterData,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
todo!()
}
fn get_content_type(&self) -> &'static str {
todo!()
}
fn get_request_body(
&self,
_req: &types::PaymentsCaptureRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
todo!()
}
fn build_request(
&self,
req: &types::PaymentsCaptureRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
todo!()
}
fn handle_response(
&self,
data: &types::PaymentsCaptureRouterData,
res: Response,
) -> CustomResult<types::PaymentsCaptureRouterData, errors::ConnectorError> {
todo!()
}
fn get_url(
&self,
req: &types::PaymentsCaptureRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
todo!()
}
fn get_error_response(
&self,
res: Bytes,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
todo!()
}
}
impl api::PaymentSession for {{project-name | downcase | pascal_case}} {}
impl
services::ConnectorIntegration<
api::Session,
types::PaymentsSessionData,
types::PaymentsResponseData,
> for {{project-name | downcase | pascal_case}}
{
//TODO: implement sessions flow
}
impl api::PaymentAuthorize for {{project-name | downcase | pascal_case}} {}
type Authorize = dyn services::ConnectorIntegration<
api::Authorize,
types::PaymentsRequestData,
types::PaymentsResponseData,
>;
impl
services::ConnectorIntegration<
api::Authorize,
types::PaymentsRequestData,
types::PaymentsAuthorizeData,
types::PaymentsResponseData,
> for {{project-name | downcase | pascal_case}} {
fn get_headers(&self, _req: &types::PaymentsAuthorizeRouterData) -> CustomResult<Vec<(String, String)>,errors::ConnectorError> {
@ -77,7 +211,7 @@ impl
todo!()
}
fn get_url(&self, _req: &types::PaymentsAuthorizeRouterData) -> CustomResult<String,errors::ConnectorError> {
fn get_url(&self, _req: &types::PaymentsAuthorizeRouterData, connectors: &settings::Connectors,) -> CustomResult<String,errors::ConnectorError> {
todo!()
}
@ -112,19 +246,13 @@ impl api::Refund for {{project-name | downcase | pascal_case}} {}
impl api::RefundExecute for {{project-name | downcase | pascal_case}} {}
impl api::RefundSync for {{project-name | downcase | pascal_case}} {}
type Execute = dyn services::ConnectorIntegration<
api::Execute,
types::RefundsRequestData,
types::RefundsResponseData,
>;
impl
services::ConnectorIntegration<
api::Execute,
types::RefundsRequestData,
types::RefundsData,
types::RefundsResponseData,
> for {{project-name | downcase | pascal_case}} {
fn get_headers(&self, _req: &types::RefundsRouterData) -> CustomResult<Vec<(String,String)>,errors::ConnectorError> {
fn get_headers(&self, _req: &types::RefundsRouterData<api::Execute>) -> CustomResult<Vec<(String,String)>,errors::ConnectorError> {
todo!()
}
@ -132,32 +260,32 @@ impl
todo!()
}
fn get_url(&self, _req: &types::RefundsRouterData) -> CustomResult<String,errors::ConnectorError> {
fn get_url(&self, _req: &types::RefundsRouterData<api::Execute>, connectors: &settings::Connectors,) -> CustomResult<String,errors::ConnectorError> {
todo!()
}
fn get_request_body(&self, req: &types::RefundsRouterData) -> CustomResult<Option<String>,errors::ConnectorError> {
fn get_request_body(&self, req: &types::RefundsRouterData<api::Execute>) -> CustomResult<Option<String>,errors::ConnectorError> {
let {{project-name | downcase}}_req = utils::Encode::<{{project-name| downcase}}::{{project-name | downcase | pascal_case}}RefundRequest>::convert_and_url_encode(req).change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some({{project-name | downcase}}_req))
}
fn build_request(&self, req: &types::RefundsRouterData) -> CustomResult<Option<services::Request>,errors::ConnectorError> {
fn build_request(&self, req: &types::RefundsRouterData<api::Execute>, connectors: &settings::Connectors,) -> CustomResult<Option<services::Request>,errors::ConnectorError> {
let request = services::RequestBuilder::new()
.method(services::Method::Post)
.url(&Execute::get_url(self, req)?)
.headers(Execute::get_headers(self, req)?)
.body(Execute::get_request_body(self, req)?)
.url(&types::RefundExecuteType::get_url(self, req, connectors)?)
.headers(types::RefundExecuteType::get_headers(self, req)?)
.body(types::RefundExecuteType::get_request_body(self, req)?)
.build();
Ok(Some(request))
}
fn handle_response(
&self,
data: &types::RefundsRouterData,
data: &types::RefundsRouterData<api::Execute>,
res: Response,
) -> CustomResult<types::RefundsRouterData,errors::ConnectorError> {
) -> CustomResult<types::RefundsRouterData<api::Execute>,errors::ConnectorError> {
logger::debug!(target: "router::connector::{{project-name | downcase}}", response=?res);
let response: {{project-name| downcase}}::{{project-name | downcase| pascal_case}}RefundResponse = res.response.parse_struct("{{project-name | downcase}} RefundResponse").change_context(errors::ConnectorError::RequestEncodingFailed)?;
let response: {{project-name| downcase}}::RefundResponse = res.response.parse_struct("{{project-name | downcase}} RefundResponse").change_context(errors::ConnectorError::RequestEncodingFailed)?;
types::ResponseRouterData {
response,
data: data.clone(),
@ -172,14 +300,9 @@ impl
}
}
type RSync = dyn services::ConnectorIntegration<
api::Sync,
types::RefundsRequestData,
types::RefundsResponseData,
>;
impl
services::ConnectorIntegration<api::Sync, types::RefundsRequestData, types::RefundsResponseData> for {{project-name | downcase | pascal_case}} {
fn get_headers(&self, _req: &types::RefundsRouterData) -> CustomResult<Vec<(String, String)>,errors::ConnectorError> {
services::ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponseData> for {{project-name | downcase | pascal_case}} {
fn get_headers(&self, _req: &types::RefundSyncRouterData) -> CustomResult<Vec<(String, String)>,errors::ConnectorError> {
todo!()
}
@ -187,17 +310,17 @@ impl
todo!()
}
fn get_url(&self, _req: &types::RefundsRouterData) -> CustomResult<String,errors::ConnectorError> {
fn get_url(&self, _req: &types::RefundSyncRouterData,_connectors: &settings::Connectors,) -> CustomResult<String,errors::ConnectorError> {
todo!()
}
fn handle_response(
&self,
data: &types::RefundsRouterData,
data: &types::RefundSyncRouterData,
res: Response,
) -> CustomResult<types::RefundsRouterData,errors::ConnectorError> {
) -> CustomResult<types::RefundSyncRouterData,errors::ConnectorError,> {
logger::debug!(target: "router::connector::{{project-name | downcase}}", response=?res);
let response: {{project-name | downcase}}::{{project-name | downcase | pascal_case}}RefundResponse = res.response.parse_struct("{{project-name | downcase}} RefundResponse").change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
let response: {{project-name | downcase}}::RefundResponse = res.response.parse_struct("{{project-name | downcase}} RefundResponse").change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::ResponseRouterData {
response,
data: data.clone(),
@ -224,7 +347,7 @@ impl api::IncomingWebhook for {{project-name | downcase | pascal_case}} {
fn get_webhook_event_type(
&self,
_body: &[u8],
) -> CustomResult<String, errors::ConnectorError> {
) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> {
todo!()
}
@ -235,3 +358,12 @@ impl api::IncomingWebhook for {{project-name | downcase | pascal_case}} {
todo!()
}
}
impl services::ConnectorRedirectResponse for {{project-name | downcase | pascal_case}} {
fn get_flow_type(
&self,
_query_params: &str,
) -> CustomResult<payments::CallConnectorAction, errors::ConnectorError> {
Ok(payments::CallConnectorAction::Trigger)
}
}

View File

@ -0,0 +1,92 @@
use futures::future::OptionFuture;
use masking::Secret;
use router::types::{self, api, storage::enums};
use crate::{
connector_auth,
utils::{self, ConnectorActions},
};
struct {{project-name | downcase | pascal_case}};
impl utils::ConnectorActions for {{project-name | downcase | pascal_case}} {}
impl utils::Connector for {{project-name | downcase | pascal_case}} {
fn get_data(&self) -> types::api::ConnectorData {
use router::connector::{{project-name | downcase | pascal_case}};
types::api::ConnectorData {
connector: Box::new(&{{project-name | downcase | pascal_case}}),
connector_name: types::Connector::{{project-name | downcase | pascal_case}},
get_token: types::api::GetToken::Connector,
}
}
fn get_auth_token(&self) -> types::ConnectorAuthType {
types::ConnectorAuthType::from(
connector_auth::ConnectorAuthentication::new()
.{{project-name | downcase }}
.expect("Missing connector authentication configuration"),
)
}
fn get_name(&self) -> String {
"{{project-name | downcase }}".to_string()
}
}
#[actix_web::test]
async fn should_only_authorize_payment() {
let response = {{project-name | downcase | pascal_case}} {}.authorize_payment(None).await;
assert_eq!(response.status, enums::AttemptStatus::Authorized);
}
#[actix_web::test]
async fn should_authorize_and_capture_payment() {
let response = {{project-name | downcase | pascal_case}} {}.make_payment(None).await;
assert_eq!(response.status, enums::AttemptStatus::Charged);
}
#[actix_web::test]
async fn should_capture_already_authorized_payment() {
let connector = {{project-name | downcase | pascal_case}} {};
let authorize_response = connector.authorize_payment(None).await;
assert_eq!(authorize_response.status, enums::AttemptStatus::Authorized);
let txn_id = utils::get_connector_transaction_id(authorize_response);
let response: OptionFuture<_> = txn_id
.map(|transaction_id| async move {
connector.capture_payment(transaction_id, None).await.status
})
.into();
assert_eq!(response.await, Some(enums::AttemptStatus::Charged));
}
#[actix_web::test]
async fn should_fail_payment_for_incorrect_cvc() {
let response = {{project-name | downcase | pascal_case}} {}.make_payment(Some(types::PaymentsAuthorizeData {
payment_method_data: types::api::PaymentMethod::Card(api::CCard {
card_number: Secret::new("4024007134364842".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
}))
.await;
let x = response.response.unwrap_err();
assert_eq!(
x.message,
"The card's security code failed verification.".to_string(),
);
}
#[actix_web::test]
async fn should_refund_succeeded_payment() {
let connector = {{project-name | downcase | pascal_case}} {};
//make a successful payment
let response = connector.make_payment(None).await;
//try refund for previous payment
if let Some(transaction_id) = utils::get_connector_transaction_id(response) {
let response = connector.refund_payment(transaction_id, None).await;
assert_eq!(
response.response.unwrap().refund_status,
enums::RefundStatus::Success,
);
}
}

View File

@ -1,8 +1,8 @@
use serde::{Deserialize, Serialize};
use crate::{core::errors,types::{self,storage::enums}};
use crate::{core::errors,pii::PeekInterface,types::{self,api, storage::enums}};
//TODO: Fill the struct with respective fields
#[derive(Default, Debug, Serialize, PartialEq)]
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
pub struct {{project-name | downcase | pascal_case}}PaymentsRequest {}
impl TryFrom<&types::PaymentsAuthorizeRouterData> for {{project-name | downcase | pascal_case}}PaymentsRequest {
@ -24,21 +24,15 @@ impl TryFrom<&types::ConnectorAuthType> for {{project-name | downcase | pascal_c
}
// PaymentsResponse
//TODO: Append the remaining status flags
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum {{project-name | downcase | pascal_case}}PaymentStatus {
Succeeded,
Failed,
#[default]
Processing,
}
// Default should be Processing
impl Default for {{project-name | downcase | pascal_case}}PaymentStatus {
fn default() -> Self {
{{project-name | downcase | pascal_case}}PaymentStatus::Processing
}
}
impl From<{{project-name | downcase | pascal_case}}PaymentStatus> for enums::AttemptStatus {
fn from(item: {{project-name | downcase | pascal_case}}PaymentStatus) -> Self {
match item {
@ -66,9 +60,9 @@ impl TryFrom<types::PaymentsResponseRouterData<{{project-name | downcase | pasca
#[derive(Default, Debug, Serialize)]
pub struct {{project-name | downcase | pascal_case}}RefundRequest {}
impl TryFrom<&types::RefundsRouterData> for {{project-name | downcase | pascal_case}}RefundRequest {
impl<F> TryFrom<&types::RefundsRouterData<F>> for {{project-name | downcase | pascal_case}}RefundRequest {
type Error = error_stack::Report<errors::ParsingError>;
fn try_from(_item: &types::RefundsRouterData) -> Result<Self,Self::Error> {
fn try_from(_item: &types::RefundsRouterData<F>) -> Result<Self,Self::Error> {
todo!()
}
}
@ -76,22 +70,16 @@ impl TryFrom<&types::RefundsRouterData> for {{project-name | downcase | pascal_c
// Type definition for Refund Response
#[allow(dead_code)]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Default, Deserialize, Clone)]
pub enum RefundStatus {
Succeeded,
Failed,
#[default]
Processing,
}
// Default should be Processing
impl Default for RefundStatus {
fn default() -> Self {
RefundStatus::Processing
}
}
impl From<RefundStatus> for enums::RefundStatus {
fn from(item: RefundStatus) -> Self {
impl From<self::RefundStatus> for enums::RefundStatus {
fn from(item: self::RefundStatus) -> Self {
match item {
RefundStatus::Succeeded => Self::Success,
RefundStatus::Failed => Self::Failure,
@ -103,15 +91,28 @@ impl From<RefundStatus> for enums::RefundStatus {
//TODO: Fill the struct with respective fields
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct {{project-name | downcase | pascal_case}}RefundResponse {}
pub struct RefundResponse {
}
impl TryFrom<types::RefundsResponseRouterData<{{project-name | downcase | pascal_case}}RefundResponse>> for types::RefundsRouterData {
impl TryFrom<types::RefundsResponseRouterData<api::Execute, RefundResponse>>
for types::RefundsRouterData<api::Execute>
{
type Error = error_stack::Report<errors::ParsingError>;
fn try_from(_item: types::RefundsResponseRouterData<{{project-name | downcase | pascal_case}}RefundResponse>) -> Result<Self,Self::Error> {
fn try_from(
item: types::RefundsResponseRouterData<api::Execute, RefundResponse>,
) -> Result<Self, Self::Error> {
todo!()
}
}
impl TryFrom<types::RefundsResponseRouterData<api::RSync, RefundResponse>> for types::RefundsRouterData<api::RSync>
{
type Error = error_stack::Report<errors::ParsingError>;
fn try_from(_item: types::RefundsResponseRouterData<api::RSync, RefundResponse>) -> Result<Self,Self::Error> {
todo!()
}
}
//TODO: Fill the struct with respective fields
#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
pub struct {{project-name | downcase | pascal_case}}ErrorResponse {}

View File

@ -494,6 +494,7 @@ pub enum Connector {
#[default]
Dummy,
Klarna,
Shift4,
Stripe,
}

View File

@ -110,10 +110,11 @@ pub struct Connectors {
pub aci: ConnectorParams,
pub adyen: ConnectorParams,
pub authorizedotnet: ConnectorParams,
pub checkout: ConnectorParams,
pub stripe: ConnectorParams,
pub braintree: ConnectorParams,
pub checkout: ConnectorParams,
pub klarna: ConnectorParams,
pub shift4: ConnectorParams,
pub stripe: ConnectorParams,
pub supported: SupportedConnectors,
pub applepay: ConnectorParams,
}

View File

@ -7,7 +7,9 @@ pub mod checkout;
pub mod klarna;
pub mod stripe;
pub mod shift4;
pub use self::{
aci::Aci, adyen::Adyen, applepay::Applepay, authorizedotnet::Authorizedotnet,
braintree::Braintree, checkout::Checkout, klarna::Klarna, stripe::Stripe,
braintree::Braintree, checkout::Checkout, klarna::Klarna, shift4::Shift4, stripe::Stripe,
};

View File

@ -0,0 +1,527 @@
mod transformers;
use std::fmt::Debug;
use bytes::Bytes;
use common_utils::ext_traits::ByteSliceExt;
use error_stack::ResultExt;
use transformers as shift4;
use crate::{
configs::settings,
consts,
core::{
errors::{self, CustomResult},
payments,
},
headers, logger,
services::{self, ConnectorIntegration},
types::{
self,
api::{self, ConnectorCommon, ConnectorCommonExt},
ErrorResponse, Response,
},
utils::{self, BytesExt},
};
#[derive(Debug, Clone)]
pub struct Shift4;
impl<Flow, Request, Response> api::ConnectorCommonExt<Flow, Request, Response> for Shift4
where
Self: ConnectorIntegration<Flow, Request, Response>,
{
fn build_headers(
&self,
req: &types::RouterData<Flow, Request, Response>,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
let mut headers = vec![
(
headers::CONTENT_TYPE.to_string(),
self.get_content_type().to_string(),
),
(
headers::ACCEPT.to_string(),
self.get_content_type().to_string(),
),
];
let mut api_key = self.get_auth_header(&req.connector_auth_type)?;
headers.append(&mut api_key);
Ok(headers)
}
}
impl api::ConnectorCommon for Shift4 {
fn id(&self) -> &'static str {
"shift4"
}
fn common_get_content_type(&self) -> &'static str {
"application/json"
}
fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str {
connectors.shift4.base_url.as_ref()
}
fn get_auth_header(
&self,
auth_type: &types::ConnectorAuthType,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
let auth: shift4::Shift4AuthType = auth_type
.try_into()
.change_context(errors::ConnectorError::FailedToObtainAuthType)?;
Ok(vec![(headers::AUTHORIZATION.to_string(), auth.api_key)])
}
fn build_error_response(
&self,
res: Bytes,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
let response: shift4::ErrorResponse = res
.parse_struct("Shift4 ErrorResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
Ok(ErrorResponse {
code: response
.error
.code
.unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()),
message: response.error.message,
reason: None,
})
}
}
impl api::Payment for Shift4 {}
impl api::PreVerify for Shift4 {}
impl
services::ConnectorIntegration<
api::Verify,
types::VerifyRequestData,
types::PaymentsResponseData,
> for Shift4
{
}
impl api::PaymentVoid for Shift4 {}
impl
services::ConnectorIntegration<
api::Void,
types::PaymentsCancelData,
types::PaymentsResponseData,
> for Shift4
{
}
impl api::PaymentSync for Shift4 {}
impl
services::ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
for Shift4
{
fn get_headers(
&self,
req: &types::PaymentsSyncRouterData,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
self.build_headers(req)
}
fn get_content_type(&self) -> &'static str {
self.common_get_content_type()
}
fn get_url(
&self,
req: &types::PaymentsSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
let connector_payment_id = req
.request
.connector_transaction_id
.get_connector_transaction_id()
.change_context(errors::ConnectorError::MissingConnectorTransactionID)?;
Ok(format!(
"{}charges/{}",
self.base_url(connectors),
connector_payment_id
))
}
fn build_request(
&self,
req: &types::PaymentsSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Get)
.url(&types::PaymentsSyncType::get_url(self, req, connectors)?)
.headers(types::PaymentsSyncType::get_headers(self, req)?)
.build(),
))
}
fn get_error_response(
&self,
res: Bytes,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
fn handle_response(
&self,
data: &types::PaymentsSyncRouterData,
res: Response,
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
logger::debug!(payment_sync_response=?res);
let response: shift4::Shift4PaymentsResponse = res
.response
.parse_struct("shift4 PaymentsResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
}
impl api::PaymentCapture for Shift4 {}
impl
services::ConnectorIntegration<
api::Capture,
types::PaymentsCaptureData,
types::PaymentsResponseData,
> for Shift4
{
fn get_headers(
&self,
req: &types::PaymentsCaptureRouterData,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
self.build_headers(req)
}
fn get_content_type(&self) -> &'static str {
self.common_get_content_type()
}
fn build_request(
&self,
req: &types::PaymentsCaptureRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsCaptureType::get_url(self, req, connectors)?)
.headers(types::PaymentsCaptureType::get_headers(self, req)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::PaymentsCaptureRouterData,
res: Response,
) -> CustomResult<types::PaymentsCaptureRouterData, errors::ConnectorError> {
let response: shift4::Shift4PaymentsResponse = res
.response
.parse_struct("Shift4PaymentsResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
logger::debug!(shift4payments_create_response=?response);
types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
}
.try_into()
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_url(
&self,
req: &types::PaymentsCaptureRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
let connector_payment_id = req.request.connector_transaction_id.clone();
Ok(format!(
"{}charges/{}/capture",
self.base_url(connectors),
connector_payment_id
))
}
fn get_error_response(
&self,
res: Bytes,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl api::PaymentSession for Shift4 {}
impl
services::ConnectorIntegration<
api::Session,
types::PaymentsSessionData,
types::PaymentsResponseData,
> for Shift4
{
//TODO: implement sessions flow
}
impl api::PaymentAuthorize for Shift4 {}
impl
services::ConnectorIntegration<
api::Authorize,
types::PaymentsAuthorizeData,
types::PaymentsResponseData,
> for Shift4
{
fn get_headers(
&self,
req: &types::PaymentsAuthorizeRouterData,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
self.build_headers(req)
}
fn get_content_type(&self) -> &'static str {
self.common_get_content_type()
}
fn get_url(
&self,
_req: &types::PaymentsAuthorizeRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!("{}charges", self.base_url(connectors)))
}
fn get_request_body(
&self,
req: &types::PaymentsAuthorizeRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let shift4_req = utils::Encode::<shift4::Shift4PaymentsRequest>::convert_and_encode(req)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(shift4_req))
}
fn build_request(
&self,
req: &types::PaymentsAuthorizeRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsAuthorizeType::get_url(
self, req, connectors,
)?)
.headers(types::PaymentsAuthorizeType::get_headers(self, req)?)
.body(types::PaymentsAuthorizeType::get_request_body(self, req)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::PaymentsAuthorizeRouterData,
res: Response,
) -> CustomResult<types::PaymentsAuthorizeRouterData, errors::ConnectorError> {
let response: shift4::Shift4PaymentsResponse = res
.response
.parse_struct("Shift4PaymentsResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
logger::debug!(shift4payments_create_response=?response);
types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
}
.try_into()
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: Bytes,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl api::Refund for Shift4 {}
impl api::RefundExecute for Shift4 {}
impl api::RefundSync for Shift4 {}
impl services::ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsResponseData>
for Shift4
{
fn get_headers(
&self,
req: &types::RefundsRouterData<api::Execute>,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
self.build_headers(req)
}
fn get_content_type(&self) -> &'static str {
self.common_get_content_type()
}
fn get_url(
&self,
_req: &types::RefundsRouterData<api::Execute>,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!("{}refunds", self.base_url(connectors),))
}
fn get_request_body(
&self,
req: &types::RefundsRouterData<api::Execute>,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let shift4_req = utils::Encode::<shift4::Shift4RefundRequest>::convert_and_encode(req)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(shift4_req))
}
fn build_request(
&self,
req: &types::RefundsRouterData<api::Execute>,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
let request = services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::RefundExecuteType::get_url(self, req, connectors)?)
.headers(types::RefundExecuteType::get_headers(self, req)?)
.body(types::RefundExecuteType::get_request_body(self, req)?)
.build();
Ok(Some(request))
}
fn handle_response(
&self,
data: &types::RefundsRouterData<api::Execute>,
res: Response,
) -> CustomResult<types::RefundsRouterData<api::Execute>, errors::ConnectorError> {
logger::debug!(target: "router::connector::shift4", response=?res);
let response: shift4::RefundResponse = res
.response
.parse_struct("RefundResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
}
.try_into()
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: Bytes,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl services::ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponseData>
for Shift4
{
fn get_headers(
&self,
req: &types::RefundSyncRouterData,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
self.build_headers(req)
}
fn get_content_type(&self) -> &'static str {
self.common_get_content_type()
}
fn get_url(
&self,
_req: &types::RefundSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!("{}refunds", self.base_url(connectors),))
}
fn handle_response(
&self,
data: &types::RefundSyncRouterData,
res: Response,
) -> CustomResult<types::RefundSyncRouterData, errors::ConnectorError> {
logger::debug!(target: "router::connector::shift4", response=?res);
let response: shift4::RefundResponse =
res.response
.parse_struct("shift4 RefundResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
}
.try_into()
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: Bytes,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
#[async_trait::async_trait]
impl api::IncomingWebhook for Shift4 {
fn get_webhook_object_reference_id(
&self,
body: &[u8],
) -> CustomResult<String, errors::ConnectorError> {
let details: shift4::Shift4WebhookObjectId = body
.parse_struct("Shift4WebhookObjectId")
.change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?;
Ok(details.data.id)
}
fn get_webhook_event_type(
&self,
body: &[u8],
) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> {
let details: shift4::Shift4WebhookObjectEventType = body
.parse_struct("Shift4WebhookObjectEventType")
.change_context(errors::ConnectorError::WebhookEventTypeNotFound)?;
Ok(match details.event_type {
shift4::Shift4WebhookEvent::ChargeSucceeded => {
api::IncomingWebhookEvent::PaymentIntentSuccess
}
})
}
fn get_webhook_resource_object(
&self,
body: &[u8],
) -> CustomResult<serde_json::Value, errors::ConnectorError> {
let details: shift4::Shift4WebhookObjectResource = body
.parse_struct("Shift4WebhookObjectResource")
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?;
Ok(details.data)
}
}
impl services::ConnectorRedirectResponse for Shift4 {
fn get_flow_type(
&self,
_query_params: &str,
) -> CustomResult<payments::CallConnectorAction, errors::ConnectorError> {
Ok(payments::CallConnectorAction::Trigger)
}
}

View File

@ -0,0 +1,257 @@
use serde::{Deserialize, Serialize};
use crate::{
core::errors,
pii::PeekInterface,
types::{self, api, storage::enums},
};
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Shift4PaymentsRequest {
amount: String,
card: Card,
currency: String,
description: Option<String>,
captured: bool,
}
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
pub struct DeviceData;
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Card {
number: String,
exp_month: String,
exp_year: String,
cardholder_name: String,
}
impl TryFrom<&types::PaymentsAuthorizeRouterData> for Shift4PaymentsRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
match item.request.payment_method_data {
api::PaymentMethod::Card(ref ccard) => {
let submit_for_settlement = matches!(
item.request.capture_method,
Some(enums::CaptureMethod::Automatic) | None
);
let payment_request = Self {
amount: item.request.amount.to_string(),
card: Card {
number: ccard.card_number.peek().clone(),
exp_month: ccard.card_exp_month.peek().clone(),
exp_year: ccard.card_exp_year.peek().clone(),
cardholder_name: ccard.card_holder_name.peek().clone(),
},
currency: item.request.currency.to_string(),
description: item.description.clone(),
captured: submit_for_settlement,
};
Ok(payment_request)
}
_ => Err(
errors::ConnectorError::NotImplemented("Current Payment Method".to_string()).into(),
),
}
}
}
// Auth Struct
pub struct Shift4AuthType {
pub(super) api_key: String,
}
impl TryFrom<&types::ConnectorAuthType> for Shift4AuthType {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::ConnectorAuthType) -> Result<Self, Self::Error> {
if let types::ConnectorAuthType::HeaderKey { api_key } = item {
Ok(Self {
api_key: api_key.to_string(),
})
} else {
Err(errors::ConnectorError::FailedToObtainAuthType)?
}
}
}
// PaymentsResponse
#[derive(Debug, Clone, Default, Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Shift4PaymentStatus {
Successful,
Failed,
#[default]
Pending,
}
impl From<Shift4PaymentStatus> for enums::AttemptStatus {
fn from(item: Shift4PaymentStatus) -> Self {
match item {
Shift4PaymentStatus::Successful => Self::Charged,
Shift4PaymentStatus::Failed => Self::Failure,
Shift4PaymentStatus::Pending => Self::Pending,
}
}
}
#[derive(Debug, Deserialize)]
pub struct Shift4WebhookObjectEventType {
#[serde(rename = "type")]
pub event_type: Shift4WebhookEvent,
}
#[derive(Debug, Deserialize)]
pub enum Shift4WebhookEvent {
ChargeSucceeded,
}
#[derive(Debug, Deserialize)]
pub struct Shift4WebhookObjectData {
pub id: String,
}
#[derive(Debug, Deserialize)]
pub struct Shift4WebhookObjectId {
pub data: Shift4WebhookObjectData,
}
#[derive(Debug, Deserialize)]
pub struct Shift4WebhookObjectResource {
pub data: serde_json::Value,
}
fn get_payment_status(response: &Shift4PaymentsResponse) -> enums::AttemptStatus {
let is_authorized =
!response.captured && matches!(response.status, Shift4PaymentStatus::Successful);
if is_authorized {
enums::AttemptStatus::Authorized
} else {
enums::AttemptStatus::from(response.status.clone())
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Shift4PaymentsResponse {
id: String,
currency: String,
amount: u32,
status: Shift4PaymentStatus,
captured: bool,
refunded: bool,
}
impl<F, T>
TryFrom<types::ResponseRouterData<F, Shift4PaymentsResponse, T, types::PaymentsResponseData>>
for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<F, Shift4PaymentsResponse, T, types::PaymentsResponseData>,
) -> Result<Self, Self::Error> {
Ok(Self {
status: get_payment_status(&item.response),
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(item.response.id),
redirection_data: None,
redirect: false,
mandate_reference: None,
}),
..item.data
})
}
}
// REFUND :
// Type definition for RefundRequest
#[derive(Default, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Shift4RefundRequest {
charge_id: String,
amount: i64,
}
impl<F> TryFrom<&types::RefundsRouterData<F>> for Shift4RefundRequest {
type Error = error_stack::Report<errors::ParsingError>;
fn try_from(item: &types::RefundsRouterData<F>) -> Result<Self, Self::Error> {
Ok(Self {
charge_id: item.request.connector_transaction_id.clone(),
amount: item.request.amount,
})
}
}
impl From<self::Shift4RefundStatus> for enums::RefundStatus {
fn from(item: self::Shift4RefundStatus) -> Self {
match item {
self::Shift4RefundStatus::Successful => Self::Success,
self::Shift4RefundStatus::Failed => Self::Failure,
self::Shift4RefundStatus::Processing => Self::Pending,
}
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct RefundResponse {
pub id: String,
pub amount: i64,
pub currency: String,
pub charge: String,
pub status: Shift4RefundStatus,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Shift4RefundStatus {
Successful,
Processing,
#[default]
Failed,
}
impl TryFrom<types::RefundsResponseRouterData<api::Execute, RefundResponse>>
for types::RefundsRouterData<api::Execute>
{
type Error = error_stack::Report<errors::ParsingError>;
fn try_from(
item: types::RefundsResponseRouterData<api::Execute, RefundResponse>,
) -> Result<Self, Self::Error> {
let refund_status = enums::RefundStatus::from(item.response.status);
Ok(Self {
response: Ok(types::RefundsResponseData {
connector_refund_id: item.response.id,
refund_status,
}),
..item.data
})
}
}
impl TryFrom<types::RefundsResponseRouterData<api::RSync, RefundResponse>>
for types::RefundsRouterData<api::RSync>
{
type Error = error_stack::Report<errors::ParsingError>;
fn try_from(
item: types::RefundsResponseRouterData<api::RSync, RefundResponse>,
) -> Result<Self, Self::Error> {
let refund_status = enums::RefundStatus::from(item.response.status);
Ok(Self {
response: Ok(types::RefundsResponseData {
connector_refund_id: item.response.id,
refund_status,
}),
..item.data
})
}
}
#[derive(Debug, Default, Deserialize)]
pub struct ErrorResponse {
pub error: ApiErrorResponse,
}
#[derive(Default, Debug, Clone, Deserialize, Eq, PartialEq)]
pub struct ApiErrorResponse {
pub code: Option<String>,
pub message: String,
}

View File

@ -417,10 +417,10 @@ pub async fn validate_and_create_refund(
validator::validate_maximum_refund_against_payment_attempt(&all_refunds)
.change_context(errors::ApiErrorResponse::MaximumRefundCount)?;
let connector = payment_attempt
.connector
.clone()
.ok_or(errors::ApiErrorResponse::InternalServerError)?;
let connector = payment_attempt.connector.clone().ok_or_else(|| {
report!(errors::ApiErrorResponse::InternalServerError)
.attach_printable("connector not populated in payment attempt.")
})?;
refund_create_req = mk_new_refund(
req,

View File

@ -35,11 +35,11 @@ use crate::{
pub type BoxedConnectorIntegration<'a, T, Req, Resp> =
Box<&'a (dyn ConnectorIntegration<T, Req, Resp> + Send + Sync)>;
pub trait ConnectorIntegrationExt<T, Req, Resp>: Send + Sync + 'static {
pub trait ConnectorIntegrationAny<T, Req, Resp>: Send + Sync + 'static {
fn get_connector_integration(&self) -> BoxedConnectorIntegration<T, Req, Resp>;
}
impl<S, T, Req, Resp> ConnectorIntegrationExt<T, Req, Resp> for S
impl<S, T, Req, Resp> ConnectorIntegrationAny<T, Req, Resp> for S
where
S: ConnectorIntegration<T, Req, Resp> + Send + Sync,
{
@ -48,7 +48,7 @@ where
}
}
pub trait ConnectorIntegration<T, Req, Resp>: ConnectorIntegrationExt<T, Req, Resp> {
pub trait ConnectorIntegration<T, Req, Resp>: ConnectorIntegrationAny<T, Req, Resp> {
fn get_headers(
&self,
_req: &types::RouterData<T, Req, Resp>,

View File

@ -7,7 +7,6 @@
// Separation of concerns instead of separation of forms.
pub mod api;
pub mod connector;
pub mod storage;
pub mod transformers;
@ -29,6 +28,8 @@ pub type PaymentsCancelRouterData = RouterData<api::Void, PaymentsCancelData, Pa
pub type PaymentsSessionRouterData =
RouterData<api::Session, PaymentsSessionData, PaymentsResponseData>;
pub type RefundsRouterData<F> = RouterData<F, RefundsData, RefundsResponseData>;
pub type RefundExecuteRouterData = RouterData<api::Execute, RefundsData, RefundsResponseData>;
pub type RefundSyncRouterData = RouterData<api::RSync, RefundsData, RefundsResponseData>;
pub type PaymentsResponseRouterData<R> =
ResponseRouterData<api::Authorize, R, PaymentsAuthorizeData, PaymentsResponseData>;

View File

@ -9,14 +9,16 @@ pub mod webhooks;
use std::{fmt::Debug, marker, str::FromStr};
use bytes::Bytes;
use error_stack::{report, IntoReport, ResultExt};
pub use self::{admin::*, customers::*, payment_methods::*, payments::*, refunds::*, webhooks::*};
use super::ErrorResponse;
use crate::{
configs::settings::Connectors,
connector,
connector, consts,
core::errors::{self, CustomResult},
services::ConnectorRedirectResponse,
services::{ConnectorIntegration, ConnectorRedirectResponse},
types::{self, api::enums as api_enums},
};
@ -43,6 +45,31 @@ pub trait ConnectorCommon {
/// The base URL for interacting with the connector's API.
fn base_url<'a>(&self, connectors: &'a Connectors) -> &'a str;
/// common error response for a connector if it is same in all case
fn build_error_response(
&self,
_res: Bytes,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
Ok(ErrorResponse {
code: consts::NO_ERROR_CODE.to_string(),
message: consts::NO_ERROR_MESSAGE.to_string(),
reason: None,
})
}
}
/// Extended trait for connector common to allow functions with generic type
pub trait ConnectorCommonExt<Flow, Req, Resp>:
ConnectorCommon + ConnectorIntegration<Flow, Req, Resp>
{
/// common header builder when every request for the connector have same headers
fn build_headers(
&self,
_req: &types::RouterData<Flow, Req, Resp>,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
Ok(Vec::new())
}
}
pub trait Router {}
@ -119,6 +146,7 @@ impl ConnectorData {
"braintree" => Ok(Box::new(&connector::Braintree)),
"klarna" => Ok(Box::new(&connector::Klarna)),
"applepay" => Ok(Box::new(&connector::Applepay)),
"shift4" => Ok(Box::new(&connector::Shift4)),
_ => Err(report!(errors::UnexpectedError)
.attach_printable(format!("invalid connector name: {connector_name}")))
.change_context(errors::ConnectorError::InvalidConnectorName)

View File

@ -1 +0,0 @@

View File

@ -6,6 +6,7 @@ pub(crate) struct ConnectorAuthentication {
pub aci: Option<BodyKey>,
pub authorizedotnet: Option<BodyKey>,
pub checkout: Option<BodyKey>,
pub shift4: Option<HeaderKey>,
}
impl ConnectorAuthentication {
@ -18,6 +19,19 @@ impl ConnectorAuthentication {
}
}
#[derive(Debug, Deserialize, Clone)]
pub(crate) struct HeaderKey {
pub api_key: String,
}
impl From<HeaderKey> for ConnectorAuthType {
fn from(key: HeaderKey) -> Self {
ConnectorAuthType::HeaderKey {
api_key: key.api_key,
}
}
}
#[derive(Debug, Deserialize, Clone)]
pub(crate) struct BodyKey {
pub api_key: String,

View File

@ -2,3 +2,5 @@ mod aci;
mod authorizedotnet;
mod checkout;
mod connector_auth;
mod shift4;
mod utils;

View File

@ -12,3 +12,6 @@ key1 = "MyTransactionKey"
[checkout]
api_key = "Bearer MyApiKey"
key1 = "MyProcessingChannelId"
[shift4]
api_key = "Bearer MyApiKey"

View File

@ -0,0 +1,93 @@
use futures::future::OptionFuture;
use masking::Secret;
use router::types::{self, api, storage::enums};
use crate::{
connector_auth,
utils::{self, ConnectorActions},
};
struct Shift4;
impl utils::ConnectorActions for Shift4 {}
impl utils::Connector for Shift4 {
fn get_data(&self) -> types::api::ConnectorData {
use router::connector::Shift4;
types::api::ConnectorData {
connector: Box::new(&Shift4),
connector_name: types::Connector::Shift4,
get_token: types::api::GetToken::Connector,
}
}
fn get_auth_token(&self) -> types::ConnectorAuthType {
types::ConnectorAuthType::from(
connector_auth::ConnectorAuthentication::new()
.shift4
.expect("Missing connector authentication configuration"),
)
}
fn get_name(&self) -> String {
"shift4".to_string()
}
}
#[actix_web::test]
async fn should_only_authorize_payment() {
let response = Shift4 {}.authorize_payment(None).await;
assert_eq!(response.status, enums::AttemptStatus::Authorized);
}
#[actix_web::test]
async fn should_authorize_and_capture_payment() {
let response = Shift4 {}.make_payment(None).await;
assert_eq!(response.status, enums::AttemptStatus::Charged);
}
#[actix_web::test]
async fn should_capture_already_authorized_payment() {
let connector = Shift4 {};
let authorize_response = connector.authorize_payment(None).await;
assert_eq!(authorize_response.status, enums::AttemptStatus::Authorized);
let txn_id = utils::get_connector_transaction_id(authorize_response);
let response: OptionFuture<_> = txn_id
.map(|transaction_id| async move {
connector.capture_payment(transaction_id, None).await.status
})
.into();
assert_eq!(response.await, Some(enums::AttemptStatus::Charged));
}
#[actix_web::test]
async fn should_fail_payment_for_incorrect_cvc() {
let response = Shift4 {}
.make_payment(Some(types::PaymentsAuthorizeData {
payment_method_data: types::api::PaymentMethod::Card(api::CCard {
card_number: Secret::new("4024007134364842".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
}))
.await;
let x = response.response.unwrap_err();
assert_eq!(
x.message,
"The card's security code failed verification.".to_string(),
);
}
#[actix_web::test]
async fn should_refund_succeeded_payment() {
let connector = Shift4 {};
//make a successful payment
let response = connector.make_payment(None).await;
//try refund for previous payment
if let Some(transaction_id) = utils::get_connector_transaction_id(response) {
let response = connector.refund_payment(transaction_id, None).await;
assert_eq!(
response.response.unwrap().refund_status,
enums::RefundStatus::Success,
);
}
}

View File

@ -0,0 +1,197 @@
use std::{fmt::Debug, marker::PhantomData};
use async_trait::async_trait;
use masking::Secret;
use router::{
core::payments,
db::StorageImpl,
routes,
services::{self},
types::{
self,
api::{self},
storage::enums,
PaymentAddress, RouterData,
},
};
pub trait Connector {
fn get_data(&self) -> types::api::ConnectorData;
fn get_auth_token(&self) -> types::ConnectorAuthType;
fn get_name(&self) -> String;
}
#[async_trait]
pub trait ConnectorActions: Connector {
async fn authorize_payment(
&self,
payment_data: Option<types::PaymentsAuthorizeData>,
) -> types::PaymentsAuthorizeRouterData {
let integration = self.get_data().connector.get_connector_integration();
let request = generate_data(
self.get_name(),
self.get_auth_token(),
payment_data.unwrap_or_else(|| types::PaymentsAuthorizeData {
capture_method: Some(storage_models::enums::CaptureMethod::Manual),
..PaymentAuthorizeType::default().0
}),
);
call_connector(request, integration).await
}
async fn make_payment(
&self,
payment_data: Option<types::PaymentsAuthorizeData>,
) -> types::PaymentsAuthorizeRouterData {
let integration = self.get_data().connector.get_connector_integration();
let request = generate_data(
self.get_name(),
self.get_auth_token(),
payment_data.unwrap_or_else(|| PaymentAuthorizeType::default().0),
);
call_connector(request, integration).await
}
async fn capture_payment(
&self,
transaction_id: String,
payment_data: Option<types::PaymentsCaptureData>,
) -> types::PaymentsCaptureRouterData {
let integration = self.get_data().connector.get_connector_integration();
let request = generate_data(
self.get_name(),
self.get_auth_token(),
payment_data.unwrap_or(types::PaymentsCaptureData {
amount_to_capture: Some(100),
connector_transaction_id: transaction_id,
}),
);
call_connector(request, integration).await
}
async fn refund_payment(
&self,
transaction_id: String,
payment_data: Option<types::RefundsData>,
) -> types::RefundExecuteRouterData {
let integration = self.get_data().connector.get_connector_integration();
let request = generate_data(
self.get_name(),
self.get_auth_token(),
payment_data.unwrap_or_else(|| types::RefundsData {
amount: 100,
currency: enums::Currency::USD,
refund_id: uuid::Uuid::new_v4().to_string(),
payment_method_data: types::api::PaymentMethod::Card(CCardType::default().0),
connector_transaction_id: transaction_id,
refund_amount: 100,
}),
);
call_connector(request, integration).await
}
}
async fn call_connector<
T: Debug + Clone + 'static,
Req: Debug + Clone + 'static,
Resp: Debug + Clone + 'static,
>(
request: RouterData<T, Req, Resp>,
integration: services::BoxedConnectorIntegration<'_, T, Req, Resp>,
) -> types::RouterData<T, Req, Resp> {
use router::configs::settings::Settings;
let conf = Settings::new().unwrap();
let state = routes::AppState::with_storage(conf, StorageImpl::PostgresqlTest).await;
services::api::execute_connector_processing_step(
&state,
integration,
&request,
payments::CallConnectorAction::Trigger,
)
.await
.unwrap()
}
pub struct PaymentAuthorizeType(pub types::PaymentsAuthorizeData);
pub struct PaymentRefundType(pub types::RefundsData);
pub struct CCardType(pub api::CCard);
impl Default for CCardType {
fn default() -> Self {
CCardType(api::CCard {
card_number: Secret::new("4200000000000000".to_string()),
card_exp_month: Secret::new("10".to_string()),
card_exp_year: Secret::new("2025".to_string()),
card_holder_name: Secret::new("John Doe".to_string()),
card_cvc: Secret::new("999".to_string()),
})
}
}
impl Default for PaymentAuthorizeType {
fn default() -> Self {
let data = types::PaymentsAuthorizeData {
payment_method_data: types::api::PaymentMethod::Card(CCardType::default().0),
amount: 100,
currency: enums::Currency::USD,
confirm: true,
statement_descriptor_suffix: None,
capture_method: None,
setup_future_usage: None,
mandate_id: None,
off_session: None,
setup_mandate_details: None,
browser_info: None,
order_details: None,
};
PaymentAuthorizeType(data)
}
}
impl Default for PaymentRefundType {
fn default() -> Self {
let data = types::RefundsData {
amount: 1000,
currency: enums::Currency::USD,
refund_id: uuid::Uuid::new_v4().to_string(),
payment_method_data: types::api::PaymentMethod::Card(CCardType::default().0),
connector_transaction_id: String::new(),
refund_amount: 100,
};
PaymentRefundType(data)
}
}
pub fn get_connector_transaction_id(
response: types::PaymentsAuthorizeRouterData,
) -> Option<String> {
match response.response {
Ok(types::PaymentsResponseData::TransactionResponse { resource_id, .. }) => {
resource_id.get_connector_transaction_id().ok()
}
Ok(types::PaymentsResponseData::SessionResponse { .. }) => None,
Err(_) => None,
}
}
fn generate_data<Flow, Req: From<Req>, Res>(
connector: String,
connector_auth_type: types::ConnectorAuthType,
req: Req,
) -> types::RouterData<Flow, Req, Res> {
types::RouterData {
flow: PhantomData,
merchant_id: connector.clone(),
connector,
payment_id: uuid::Uuid::new_v4().to_string(),
status: enums::AttemptStatus::default(),
orca_return_url: None,
auth_type: enums::AuthenticationType::NoThreeDs,
payment_method: enums::PaymentMethodType::Card,
connector_auth_type,
description: Some("This is a test".to_string()),
return_url: None,
request: req,
response: Err(types::ErrorResponse::default()),
payment_method_id: None,
address: PaymentAddress::default(),
connector_meta_data: None,
}
}

View File

@ -22,4 +22,4 @@ aci_api_key: <API-KEY>
aci_key1: <ADDITONAL-KEY>
braintree_api_key: <API-KEY>
braintree_key1: <ADDITIONAL-KEY>
braintree_key1: <ADDITIONAL-KEY>

25
scripts/add_connector.sh Normal file
View File

@ -0,0 +1,25 @@
pg=$1;
pgc="$(tr '[:lower:]' '[:upper:]' <<< ${pg:0:1})${pg:1}"
src="crates/router/src"
conn="$src/connector"
SCRIPT="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
if [[ -z "$pg" ]]; then
echo 'Connector name not present: try "sh add_connector.sh <adyen>"'
exit
fi
cd $SCRIPT/..
rm -rf $conn/$pg $conn/$pg.rs
git checkout $conn.rs $src/types/api.rs scripts/create_connector_account.sh $src/configs/settings.rs
sed -i'' -e "s/pub use self::{/pub mod ${pg};\n\npub use self::{/" $conn.rs
sed -i'' -e "s/};/${pg}::${pgc},\n};/" $conn.rs
sed -i'' -e "s/_ => Err/\"${pg}\" => Ok(Box::new(\&connector::${pgc})),\n\t\t\t_ => Err/" $src/types/api.rs
sed -i'' -e "s/*) echo \"This connector/${pg}) required_connector=\"${pg}\";;\n\t\t*) echo \"This connector/" scripts/create_connector_account.sh
sed -i'' -e "s/pub supported: SupportedConnectors,/pub supported: SupportedConnectors,\n\tpub ${pg}: ConnectorParams,/" $src/configs/settings.rs
rm $conn.rs-e $src/types/api.rs-e scripts/create_connector_account.sh-e $src/configs/settings.rs-e
cd $conn/
cargo gen-pg $pg
mv $pg/mod.rs $pg.rs
mv $pg/test.rs ../../tests/connectors/$pg.rs
sed -i'' -e "s/mod utils;/mod ${pg};\nmod utils;/" ../../tests/connectors/main.rs
rm ../../tests/connectors/main.rs-e
echo "Successfully created connector: try running the tests of "$pg.rs

View File

@ -40,6 +40,7 @@ case "$connector" in
aci) required_connector="aci";;
adyen) required_connector="adyen";;
braintree) required_connector="braintree";;
shift4) required_connector="shift4";;
*) echo "This connector is not supported" 1>&2;exit 1;;
esac