mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 09:07:09 +08:00
feat(connector): Add support for shift4 connector (#205)
This commit is contained in:
@ -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"
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
92
connector-template/test.rs
Normal file
92
connector-template/test.rs
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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 {}
|
||||
|
||||
@ -494,6 +494,7 @@ pub enum Connector {
|
||||
#[default]
|
||||
Dummy,
|
||||
Klarna,
|
||||
Shift4,
|
||||
Stripe,
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
527
crates/router/src/connector/shift4.rs
Normal file
527
crates/router/src/connector/shift4.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
257
crates/router/src/connector/shift4/transformers.rs
Normal file
257
crates/router/src/connector/shift4/transformers.rs
Normal 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,
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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>,
|
||||
|
||||
@ -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>;
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -1 +0,0 @@
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -2,3 +2,5 @@ mod aci;
|
||||
mod authorizedotnet;
|
||||
mod checkout;
|
||||
mod connector_auth;
|
||||
mod shift4;
|
||||
mod utils;
|
||||
|
||||
@ -12,3 +12,6 @@ key1 = "MyTransactionKey"
|
||||
[checkout]
|
||||
api_key = "Bearer MyApiKey"
|
||||
key1 = "MyProcessingChannelId"
|
||||
|
||||
[shift4]
|
||||
api_key = "Bearer MyApiKey"
|
||||
93
crates/router/tests/connectors/shift4.rs
Normal file
93
crates/router/tests/connectors/shift4.rs
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
197
crates/router/tests/connectors/utils.rs
Normal file
197
crates/router/tests/connectors/utils.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
@ -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
25
scripts/add_connector.sh
Normal 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
|
||||
@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user