mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-27 19:46:48 +08:00
feat(connector): Add support for shift4 connector (#205)
This commit is contained in:
@ -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 @@
|
||||
|
||||
Reference in New Issue
Block a user