mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-01 19:42:27 +08:00
feat(connector): [Shift4] add support for card 3DS payment (#828)
This commit is contained in:
@ -513,13 +513,13 @@ impl
|
||||
Ok(Some(
|
||||
services::RequestBuilder::new()
|
||||
.method(services::Method::Post)
|
||||
.url(&types::PaymentsComeplteAuthorizeType::get_url(
|
||||
.url(&types::PaymentsCompleteAuthorizeType::get_url(
|
||||
self, req, connectors,
|
||||
)?)
|
||||
.headers(types::PaymentsComeplteAuthorizeType::get_headers(
|
||||
.headers(types::PaymentsCompleteAuthorizeType::get_headers(
|
||||
self, req, connectors,
|
||||
)?)
|
||||
.body(types::PaymentsComeplteAuthorizeType::get_request_body(
|
||||
.body(types::PaymentsCompleteAuthorizeType::get_request_body(
|
||||
self, req,
|
||||
)?)
|
||||
.build(),
|
||||
|
||||
@ -267,7 +267,7 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
},
|
||||
get_payment_flow(data.request.is_auto_capture()),
|
||||
get_payment_flow(data.request.is_auto_capture()?),
|
||||
))
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||
}
|
||||
@ -440,7 +440,7 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
},
|
||||
get_payment_flow(data.request.is_auto_capture()),
|
||||
get_payment_flow(data.request.is_auto_capture()?),
|
||||
))
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||
}
|
||||
|
||||
@ -64,7 +64,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for BamboraPaymentsRequest {
|
||||
expiry_year: req_card.card_exp_year,
|
||||
cvd: req_card.card_cvc,
|
||||
three_d_secure: three_ds,
|
||||
complete: item.request.is_auto_capture(),
|
||||
complete: item.request.is_auto_capture()?,
|
||||
};
|
||||
Ok(Self {
|
||||
amount: item.request.amount,
|
||||
|
||||
@ -34,7 +34,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for FortePaymentsRequest {
|
||||
expiry_month: req_card.card_exp_month,
|
||||
expiry_year: req_card.card_exp_year,
|
||||
cvc: req_card.card_cvc,
|
||||
complete: item.request.is_auto_capture(),
|
||||
complete: item.request.is_auto_capture()?,
|
||||
};
|
||||
Ok(Self {
|
||||
amount: item.request.amount,
|
||||
|
||||
@ -152,14 +152,14 @@ impl
|
||||
Ok(Some(
|
||||
services::RequestBuilder::new()
|
||||
.method(services::Method::Post)
|
||||
.url(&types::PaymentsComeplteAuthorizeType::get_url(
|
||||
.url(&types::PaymentsCompleteAuthorizeType::get_url(
|
||||
self, req, connectors,
|
||||
)?)
|
||||
.attach_default_headers()
|
||||
.headers(types::PaymentsComeplteAuthorizeType::get_headers(
|
||||
.headers(types::PaymentsCompleteAuthorizeType::get_headers(
|
||||
self, req, connectors,
|
||||
)?)
|
||||
.body(types::PaymentsComeplteAuthorizeType::get_request_body(
|
||||
.body(types::PaymentsCompleteAuthorizeType::get_request_body(
|
||||
self, req,
|
||||
)?)
|
||||
.build(),
|
||||
|
||||
@ -148,14 +148,14 @@ impl
|
||||
Ok(Some(
|
||||
services::RequestBuilder::new()
|
||||
.method(services::Method::Post)
|
||||
.url(&types::PaymentsComeplteAuthorizeType::get_url(
|
||||
.url(&types::PaymentsCompleteAuthorizeType::get_url(
|
||||
self, req, connectors,
|
||||
)?)
|
||||
.attach_default_headers()
|
||||
.headers(types::PaymentsComeplteAuthorizeType::get_headers(
|
||||
.headers(types::PaymentsCompleteAuthorizeType::get_headers(
|
||||
self, req, connectors,
|
||||
)?)
|
||||
.body(types::PaymentsComeplteAuthorizeType::get_request_body(
|
||||
.body(types::PaymentsCompleteAuthorizeType::get_request_body(
|
||||
self, req,
|
||||
)?)
|
||||
.build(),
|
||||
|
||||
@ -80,7 +80,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaypalPaymentsRequest {
|
||||
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
|
||||
match item.request.payment_method_data {
|
||||
api_models::payments::PaymentMethodData::Card(ref ccard) => {
|
||||
let intent = match item.request.is_auto_capture() {
|
||||
let intent = match item.request.is_auto_capture()? {
|
||||
true => PaypalPaymentIntent::Capture,
|
||||
false => PaypalPaymentIntent::Authorize,
|
||||
};
|
||||
|
||||
@ -10,9 +10,12 @@ use super::utils::RefundsRequestData;
|
||||
use crate::{
|
||||
configs::settings,
|
||||
consts,
|
||||
core::errors::{self, CustomResult},
|
||||
headers,
|
||||
services::{self, ConnectorIntegration},
|
||||
core::{
|
||||
errors::{self, CustomResult},
|
||||
payments,
|
||||
},
|
||||
headers, routes,
|
||||
services::{self, request, ConnectorIntegration},
|
||||
types::{
|
||||
self,
|
||||
api::{self, ConnectorCommon, ConnectorCommonExt},
|
||||
@ -93,7 +96,15 @@ impl ConnectorCommon for Shift4 {
|
||||
}
|
||||
|
||||
impl api::Payment for Shift4 {}
|
||||
|
||||
impl api::PaymentVoid for Shift4 {}
|
||||
impl api::PaymentSync for Shift4 {}
|
||||
impl api::PaymentCapture for Shift4 {}
|
||||
impl api::PaymentSession for Shift4 {}
|
||||
impl api::PaymentAuthorize for Shift4 {}
|
||||
impl api::PaymentsCompleteAuthorize for Shift4 {}
|
||||
impl api::Refund for Shift4 {}
|
||||
impl api::RefundExecute for Shift4 {}
|
||||
impl api::RefundSync for Shift4 {}
|
||||
impl api::PaymentToken for Shift4 {}
|
||||
|
||||
impl
|
||||
@ -120,14 +131,128 @@ impl ConnectorIntegration<api::Verify, types::VerifyRequestData, types::Payments
|
||||
{
|
||||
}
|
||||
|
||||
impl api::PaymentVoid for Shift4 {}
|
||||
#[async_trait::async_trait]
|
||||
impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::PaymentsResponseData>
|
||||
for Shift4
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::PaymentsAuthorizeRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
self.build_headers(req, connectors)
|
||||
}
|
||||
|
||||
fn get_content_type(&self) -> &'static str {
|
||||
self.common_get_content_type()
|
||||
}
|
||||
|
||||
fn get_url(
|
||||
&self,
|
||||
_req: &types::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 req_obj = shift4::Shift4PaymentsRequest::try_from(req)?;
|
||||
let req =
|
||||
utils::Encode::<shift4::Shift4PaymentsRequest>::encode_to_string_of_json(&req_obj)
|
||||
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||
Ok(Some(req))
|
||||
}
|
||||
|
||||
async fn execute_pretasks(
|
||||
&self,
|
||||
router_data: &mut types::PaymentsAuthorizeRouterData,
|
||||
app_state: &routes::AppState,
|
||||
) -> CustomResult<(), errors::ConnectorError> {
|
||||
match router_data.auth_type {
|
||||
storage_models::enums::AuthenticationType::ThreeDs => {
|
||||
let integ: Box<
|
||||
&(dyn ConnectorIntegration<
|
||||
api::InitPayment,
|
||||
types::PaymentsAuthorizeData,
|
||||
types::PaymentsResponseData,
|
||||
> + Send
|
||||
+ Sync
|
||||
+ 'static),
|
||||
> = Box::new(&Self);
|
||||
let init_data = &types::PaymentsInitRouterData::from((
|
||||
&router_data,
|
||||
router_data.request.clone(),
|
||||
));
|
||||
let init_resp = services::execute_connector_processing_step(
|
||||
app_state,
|
||||
integ,
|
||||
init_data,
|
||||
payments::CallConnectorAction::Trigger,
|
||||
)
|
||||
.await?;
|
||||
if init_resp.request.enrolled_for_3ds {
|
||||
router_data.response = init_resp.response;
|
||||
router_data.status = init_resp.status;
|
||||
} else {
|
||||
router_data.request.enrolled_for_3ds = false;
|
||||
}
|
||||
}
|
||||
storage_models::enums::AuthenticationType::NoThreeDs => (),
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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, connectors,
|
||||
)?)
|
||||
.body(types::PaymentsAuthorizeType::get_request_body(self, req)?)
|
||||
.build(),
|
||||
))
|
||||
}
|
||||
fn handle_response(
|
||||
&self,
|
||||
data: &types::PaymentsAuthorizeRouterData,
|
||||
res: types::Response,
|
||||
) -> CustomResult<types::PaymentsAuthorizeRouterData, errors::ConnectorError> {
|
||||
let response: shift4::Shift4NonThreeDsResponse = res
|
||||
.response
|
||||
.parse_struct("Shift4NonThreeDsResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
&self,
|
||||
res: types::Response,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::Void, types::PaymentsCancelData, types::PaymentsResponseData>
|
||||
for Shift4
|
||||
{
|
||||
}
|
||||
|
||||
impl api::PaymentSync for Shift4 {}
|
||||
impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
|
||||
for Shift4
|
||||
{
|
||||
@ -187,9 +312,9 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
|
||||
data: &types::PaymentsSyncRouterData,
|
||||
res: types::Response,
|
||||
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
|
||||
let response: shift4::Shift4PaymentsResponse = res
|
||||
let response: shift4::Shift4NonThreeDsResponse = res
|
||||
.response
|
||||
.parse_struct("shift4 PaymentsResponse")
|
||||
.parse_struct("Shift4NonThreeDsResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
@ -200,8 +325,6 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
|
||||
}
|
||||
}
|
||||
|
||||
impl api::PaymentCapture for Shift4 {}
|
||||
|
||||
impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::PaymentsResponseData>
|
||||
for Shift4
|
||||
{
|
||||
@ -239,9 +362,9 @@ impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::Payme
|
||||
data: &types::PaymentsCaptureRouterData,
|
||||
res: types::Response,
|
||||
) -> CustomResult<types::PaymentsCaptureRouterData, errors::ConnectorError> {
|
||||
let response: shift4::Shift4PaymentsResponse = res
|
||||
let response: shift4::Shift4NonThreeDsResponse = res
|
||||
.response
|
||||
.parse_struct("Shift4PaymentsResponse")
|
||||
.parse_struct("Shift4NonThreeDsResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::ResponseRouterData {
|
||||
response,
|
||||
@ -273,25 +396,37 @@ impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::Payme
|
||||
}
|
||||
}
|
||||
|
||||
impl api::PaymentSession for Shift4 {}
|
||||
|
||||
impl ConnectorIntegration<api::Session, types::PaymentsSessionData, types::PaymentsResponseData>
|
||||
for Shift4
|
||||
{
|
||||
//TODO: implement sessions flow
|
||||
}
|
||||
|
||||
impl api::PaymentAuthorize for Shift4 {}
|
||||
|
||||
impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::PaymentsResponseData>
|
||||
for Shift4
|
||||
impl
|
||||
ConnectorIntegration<
|
||||
api::InitPayment,
|
||||
types::PaymentsAuthorizeData,
|
||||
types::PaymentsResponseData,
|
||||
> for Shift4
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::PaymentsAuthorizeRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
req: &types::PaymentsInitRouterData,
|
||||
_connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
self.build_headers(req, connectors)
|
||||
let mut headers = vec![
|
||||
(
|
||||
headers::CONTENT_TYPE.to_string(),
|
||||
"application/x-www-form-urlencoded".to_string(),
|
||||
),
|
||||
(
|
||||
headers::ACCEPT.to_string(),
|
||||
self.common_get_content_type().to_string(),
|
||||
),
|
||||
];
|
||||
let mut api_key = self.get_auth_header(&req.connector_auth_type)?;
|
||||
headers.append(&mut api_key);
|
||||
Ok(headers)
|
||||
}
|
||||
|
||||
fn get_content_type(&self) -> &'static str {
|
||||
@ -300,55 +435,54 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
|
||||
|
||||
fn get_url(
|
||||
&self,
|
||||
_req: &types::PaymentsAuthorizeRouterData,
|
||||
_req: &types::PaymentsInitRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
Ok(format!("{}charges", self.base_url(connectors)))
|
||||
Ok(format!("{}3d-secure", self.base_url(connectors)))
|
||||
}
|
||||
|
||||
fn get_request_body(
|
||||
&self,
|
||||
req: &types::PaymentsAuthorizeRouterData,
|
||||
req: &types::PaymentsInitRouterData,
|
||||
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
||||
let shift4_req = utils::Encode::<shift4::Shift4PaymentsRequest>::convert_and_encode(req)
|
||||
let req_obj = shift4::Shift4PaymentsRequest::try_from(req)?;
|
||||
let req =
|
||||
utils::Encode::<shift4::Shift4PaymentsRequest>::encode_to_string_of_json(&req_obj)
|
||||
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||
Ok(Some(shift4_req))
|
||||
Ok(Some(req))
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &types::PaymentsAuthorizeRouterData,
|
||||
req: &types::PaymentsInitRouterData,
|
||||
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,
|
||||
)?)
|
||||
.url(&types::PaymentsInitType::get_url(self, req, connectors)?)
|
||||
.content_type(request::ContentType::FormUrlEncoded)
|
||||
.attach_default_headers()
|
||||
.headers(types::PaymentsAuthorizeType::get_headers(
|
||||
self, req, connectors,
|
||||
)?)
|
||||
.body(types::PaymentsAuthorizeType::get_request_body(self, req)?)
|
||||
.headers(types::PaymentsInitType::get_headers(self, req, connectors)?)
|
||||
.body(types::PaymentsInitType::get_request_body(self, req)?)
|
||||
.build(),
|
||||
))
|
||||
}
|
||||
|
||||
fn handle_response(
|
||||
&self,
|
||||
data: &types::PaymentsAuthorizeRouterData,
|
||||
data: &types::PaymentsInitRouterData,
|
||||
res: types::Response,
|
||||
) -> CustomResult<types::PaymentsAuthorizeRouterData, errors::ConnectorError> {
|
||||
let response: shift4::Shift4PaymentsResponse = res
|
||||
) -> CustomResult<types::PaymentsInitRouterData, errors::ConnectorError> {
|
||||
let response: shift4::Shift4ThreeDsResponse = res
|
||||
.response
|
||||
.parse_struct("Shift4PaymentsResponse")
|
||||
.parse_struct("Shift4ThreeDsResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::ResponseRouterData {
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
}
|
||||
.try_into()
|
||||
})
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||
}
|
||||
|
||||
@ -360,9 +494,89 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
|
||||
}
|
||||
}
|
||||
|
||||
impl api::Refund for Shift4 {}
|
||||
impl api::RefundExecute for Shift4 {}
|
||||
impl api::RefundSync for Shift4 {}
|
||||
impl
|
||||
ConnectorIntegration<
|
||||
api::CompleteAuthorize,
|
||||
types::CompleteAuthorizeData,
|
||||
types::PaymentsResponseData,
|
||||
> for Shift4
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::PaymentsCompleteAuthorizeRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
self.build_headers(req, connectors)
|
||||
}
|
||||
|
||||
fn get_content_type(&self) -> &'static str {
|
||||
self.common_get_content_type()
|
||||
}
|
||||
|
||||
fn get_url(
|
||||
&self,
|
||||
_req: &types::PaymentsCompleteAuthorizeRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
Ok(format!("{}charges", self.base_url(connectors)))
|
||||
}
|
||||
|
||||
fn get_request_body(
|
||||
&self,
|
||||
req: &types::PaymentsCompleteAuthorizeRouterData,
|
||||
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
||||
let req_obj = shift4::Shift4PaymentsRequest::try_from(req)?;
|
||||
let req =
|
||||
utils::Encode::<shift4::Shift4PaymentsRequest>::encode_to_string_of_json(&req_obj)
|
||||
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||
Ok(Some(req))
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &types::PaymentsCompleteAuthorizeRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
||||
Ok(Some(
|
||||
services::RequestBuilder::new()
|
||||
.method(services::Method::Post)
|
||||
.url(&types::PaymentsCompleteAuthorizeType::get_url(
|
||||
self, req, connectors,
|
||||
)?)
|
||||
.headers(types::PaymentsCompleteAuthorizeType::get_headers(
|
||||
self, req, connectors,
|
||||
)?)
|
||||
.body(types::PaymentsCompleteAuthorizeType::get_request_body(
|
||||
self, req,
|
||||
)?)
|
||||
.build(),
|
||||
))
|
||||
}
|
||||
|
||||
fn handle_response(
|
||||
&self,
|
||||
data: &types::PaymentsCompleteAuthorizeRouterData,
|
||||
res: types::Response,
|
||||
) -> CustomResult<types::PaymentsCompleteAuthorizeRouterData, errors::ConnectorError> {
|
||||
let response: shift4::Shift4NonThreeDsResponse = res
|
||||
.response
|
||||
.parse_struct("Shift4NonThreeDsResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
&self,
|
||||
res: types::Response,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsResponseData> for Shift4 {
|
||||
fn get_headers(
|
||||
|
||||
@ -1,9 +1,15 @@
|
||||
use api_models::payments;
|
||||
use common_utils::pii::SecretSerdeValue;
|
||||
use error_stack::{IntoReport, ResultExt};
|
||||
use masking::Secret;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
connector::utils::{
|
||||
to_connector_meta, PaymentsAuthorizeRequestData, PaymentsCompleteAuthorizeRequestData,
|
||||
RouterData,
|
||||
},
|
||||
core::errors,
|
||||
pii, services,
|
||||
types::{self, api, storage::enums, transformers::ForeignFrom},
|
||||
@ -11,11 +17,33 @@ use crate::{
|
||||
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum Shift4PaymentsRequest {
|
||||
Non3DSRequest(Box<Shift4Non3DSRequest>),
|
||||
ThreeDSRequest(Box<Shift43DSRequest>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Shift4PaymentsRequest {
|
||||
pub struct Shift43DSRequest {
|
||||
amount: String,
|
||||
card: Option<Card>,
|
||||
currency: String,
|
||||
#[serde(rename = "card[number]")]
|
||||
pub card_number: Secret<String, common_utils::pii::CardNumber>,
|
||||
#[serde(rename = "card[expMonth]")]
|
||||
pub card_exp_month: Secret<String>,
|
||||
#[serde(rename = "card[expYear]")]
|
||||
pub card_exp_year: Secret<String>,
|
||||
return_url: String,
|
||||
}
|
||||
|
||||
#[serde_with::skip_serializing_none]
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Shift4Non3DSRequest {
|
||||
amount: String,
|
||||
card: Option<CardPayment>,
|
||||
currency: String,
|
||||
description: Option<String>,
|
||||
payment_method: Option<PaymentMethod>,
|
||||
@ -68,15 +96,26 @@ pub struct DeviceData;
|
||||
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Card {
|
||||
number: Secret<String, common_utils::pii::CardNumber>,
|
||||
exp_month: Secret<String>,
|
||||
exp_year: Secret<String>,
|
||||
cardholder_name: Secret<String>,
|
||||
pub number: Secret<String, common_utils::pii::CardNumber>,
|
||||
pub exp_month: Secret<String>,
|
||||
pub exp_year: Secret<String>,
|
||||
pub cardholder_name: Secret<String>,
|
||||
}
|
||||
|
||||
impl TryFrom<&types::PaymentsAuthorizeRouterData> for Shift4PaymentsRequest {
|
||||
#[derive(Debug, Serialize, Eq, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum CardPayment {
|
||||
RawCard(Box<Card>),
|
||||
CardToken(String),
|
||||
}
|
||||
|
||||
impl<T> TryFrom<&types::RouterData<T, types::PaymentsAuthorizeData, types::PaymentsResponseData>>
|
||||
for Shift4PaymentsRequest
|
||||
{
|
||||
type Error = Error;
|
||||
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
|
||||
fn try_from(
|
||||
item: &types::RouterData<T, types::PaymentsAuthorizeData, types::PaymentsResponseData>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
match &item.request.payment_method_data {
|
||||
api::PaymentMethodData::Card(ccard) => get_card_payment_request(item, ccard),
|
||||
api::PaymentMethodData::BankRedirect(redirect_data) => {
|
||||
@ -87,33 +126,78 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for Shift4PaymentsRequest {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_card_payment_request(
|
||||
item: &types::PaymentsAuthorizeRouterData,
|
||||
impl<T> TryFrom<&types::RouterData<T, types::CompleteAuthorizeData, types::PaymentsResponseData>>
|
||||
for Shift4PaymentsRequest
|
||||
{
|
||||
type Error = Error;
|
||||
fn try_from(
|
||||
item: &types::RouterData<T, types::CompleteAuthorizeData, types::PaymentsResponseData>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
match &item.request.payment_method_data {
|
||||
Some(api::PaymentMethodData::Card(_)) => {
|
||||
let card_token: Shift4CardToken =
|
||||
to_connector_meta(item.request.connector_meta.clone())?;
|
||||
Ok(Self::Non3DSRequest(Box::new(Shift4Non3DSRequest {
|
||||
amount: item.request.amount.to_string(),
|
||||
card: Some(CardPayment::CardToken(card_token.id)),
|
||||
currency: item.request.currency.to_string(),
|
||||
description: item.description.clone(),
|
||||
captured: item.request.is_auto_capture()?,
|
||||
payment_method: None,
|
||||
flow: None,
|
||||
})))
|
||||
}
|
||||
_ => Err(errors::ConnectorError::NotImplemented("Payment Method".to_string()).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_card_payment_request<T>(
|
||||
item: &types::RouterData<T, types::PaymentsAuthorizeData, types::PaymentsResponseData>,
|
||||
card: &api_models::payments::Card,
|
||||
) -> Result<Shift4PaymentsRequest, Error> {
|
||||
let submit_for_settlement = submit_for_settlement(item);
|
||||
let card = Some(Card {
|
||||
let submit_for_settlement = item.request.is_auto_capture()?;
|
||||
let card = Card {
|
||||
number: card.card_number.clone(),
|
||||
exp_month: card.card_exp_month.clone(),
|
||||
exp_year: card.card_exp_year.clone(),
|
||||
cardholder_name: card.card_holder_name.clone(),
|
||||
});
|
||||
Ok(Shift4PaymentsRequest {
|
||||
};
|
||||
if item.is_three_ds() {
|
||||
Ok(Shift4PaymentsRequest::ThreeDSRequest(Box::new(
|
||||
Shift43DSRequest {
|
||||
amount: item.request.amount.to_string(),
|
||||
card,
|
||||
currency: item.request.currency.to_string(),
|
||||
card_number: card.number,
|
||||
card_exp_month: card.exp_month,
|
||||
card_exp_year: card.exp_year,
|
||||
return_url: item
|
||||
.request
|
||||
.complete_authorize_url
|
||||
.clone()
|
||||
.ok_or_else(|| errors::ConnectorError::RequestEncodingFailed)?,
|
||||
},
|
||||
)))
|
||||
} else {
|
||||
Ok(Shift4PaymentsRequest::Non3DSRequest(Box::new(
|
||||
Shift4Non3DSRequest {
|
||||
amount: item.request.amount.to_string(),
|
||||
card: Some(CardPayment::RawCard(Box::new(card))),
|
||||
currency: item.request.currency.to_string(),
|
||||
description: item.description.clone(),
|
||||
captured: submit_for_settlement,
|
||||
payment_method: None,
|
||||
flow: None,
|
||||
})
|
||||
},
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_bank_redirect_request(
|
||||
item: &types::PaymentsAuthorizeRouterData,
|
||||
fn get_bank_redirect_request<T>(
|
||||
item: &types::RouterData<T, types::PaymentsAuthorizeData, types::PaymentsResponseData>,
|
||||
redirect_data: &payments::BankRedirectData,
|
||||
) -> Result<Shift4PaymentsRequest, Error> {
|
||||
let submit_for_settlement = submit_for_settlement(item);
|
||||
let submit_for_settlement = item.request.is_auto_capture()?;
|
||||
let method_type = PaymentMethodType::from(redirect_data);
|
||||
let billing = get_billing(item)?;
|
||||
let payment_method = Some(PaymentMethod {
|
||||
@ -121,7 +205,8 @@ fn get_bank_redirect_request(
|
||||
billing,
|
||||
});
|
||||
let flow = get_flow(item);
|
||||
Ok(Shift4PaymentsRequest {
|
||||
Ok(Shift4PaymentsRequest::Non3DSRequest(Box::new(
|
||||
Shift4Non3DSRequest {
|
||||
amount: item.request.amount.to_string(),
|
||||
card: None,
|
||||
currency: item.request.currency.to_string(),
|
||||
@ -129,7 +214,8 @@ fn get_bank_redirect_request(
|
||||
captured: submit_for_settlement,
|
||||
payment_method,
|
||||
flow: Some(flow),
|
||||
})
|
||||
},
|
||||
)))
|
||||
}
|
||||
|
||||
impl From<&payments::BankRedirectData> for PaymentMethodType {
|
||||
@ -143,13 +229,17 @@ impl From<&payments::BankRedirectData> for PaymentMethodType {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_flow(item: &types::PaymentsAuthorizeRouterData) -> Flow {
|
||||
fn get_flow<T>(
|
||||
item: &types::RouterData<T, types::PaymentsAuthorizeData, types::PaymentsResponseData>,
|
||||
) -> Flow {
|
||||
Flow {
|
||||
return_url: item.request.router_return_url.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_billing(item: &types::PaymentsAuthorizeRouterData) -> Result<Option<Billing>, Error> {
|
||||
fn get_billing<T>(
|
||||
item: &types::RouterData<T, types::PaymentsAuthorizeData, types::PaymentsResponseData>,
|
||||
) -> Result<Option<Billing>, Error> {
|
||||
let billing_address = item
|
||||
.address
|
||||
.billing
|
||||
@ -176,13 +266,6 @@ fn get_address_details(address_details: Option<&payments::AddressDetails>) -> Op
|
||||
})
|
||||
}
|
||||
|
||||
fn submit_for_settlement(item: &types::PaymentsAuthorizeRouterData) -> bool {
|
||||
matches!(
|
||||
item.request.capture_method,
|
||||
Some(enums::CaptureMethod::Automatic) | None
|
||||
)
|
||||
}
|
||||
|
||||
// Auth Struct
|
||||
pub struct Shift4AuthType {
|
||||
pub(super) api_key: String,
|
||||
@ -257,8 +340,7 @@ pub struct Shift4WebhookObjectResource {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Shift4PaymentsResponse {
|
||||
pub struct Shift4NonThreeDsResponse {
|
||||
pub id: String,
|
||||
pub currency: String,
|
||||
pub amount: u32,
|
||||
@ -268,6 +350,45 @@ pub struct Shift4PaymentsResponse {
|
||||
pub flow: Option<FlowResponse>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
pub struct Shift4ThreeDsResponse {
|
||||
pub enrolled: bool,
|
||||
pub version: Option<String>,
|
||||
#[serde(rename = "redirectUrl")]
|
||||
pub redirect_url: Option<Url>,
|
||||
pub token: Token,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
pub struct Token {
|
||||
pub id: String,
|
||||
pub created: i64,
|
||||
#[serde(rename = "objectType")]
|
||||
pub object_type: String,
|
||||
pub first6: String,
|
||||
pub last4: String,
|
||||
pub fingerprint: Secret<String>,
|
||||
pub brand: String,
|
||||
#[serde(rename = "type")]
|
||||
pub token_type: String,
|
||||
pub country: String,
|
||||
pub used: bool,
|
||||
#[serde(rename = "threeDSecureInfo")]
|
||||
pub three_d_secure_info: ThreeDSecureInfo,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
pub struct ThreeDSecureInfo {
|
||||
pub amount: i64,
|
||||
pub currency: String,
|
||||
pub enrolled: bool,
|
||||
#[serde(rename = "liabilityShift")]
|
||||
pub liability_shift: Option<String>,
|
||||
pub version: String,
|
||||
#[serde(rename = "authenticationFlow")]
|
||||
pub authentication_flow: Option<SecretSerdeValue>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FlowResponse {
|
||||
@ -290,13 +411,73 @@ pub enum NextAction {
|
||||
None,
|
||||
}
|
||||
|
||||
impl<F, T>
|
||||
TryFrom<types::ResponseRouterData<F, Shift4PaymentsResponse, T, types::PaymentsResponseData>>
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Shift4CardToken {
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
impl<F>
|
||||
TryFrom<
|
||||
types::ResponseRouterData<
|
||||
F,
|
||||
Shift4ThreeDsResponse,
|
||||
types::PaymentsAuthorizeData,
|
||||
types::PaymentsResponseData,
|
||||
>,
|
||||
> for types::RouterData<F, types::PaymentsAuthorizeData, types::PaymentsResponseData>
|
||||
{
|
||||
type Error = Error;
|
||||
fn try_from(
|
||||
item: types::ResponseRouterData<
|
||||
F,
|
||||
Shift4ThreeDsResponse,
|
||||
types::PaymentsAuthorizeData,
|
||||
types::PaymentsResponseData,
|
||||
>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
let redirection_data = item
|
||||
.response
|
||||
.redirect_url
|
||||
.map(|url| services::RedirectForm::from((url, services::Method::Get)));
|
||||
Ok(Self {
|
||||
status: if redirection_data.is_some() {
|
||||
enums::AttemptStatus::AuthenticationPending
|
||||
} else {
|
||||
enums::AttemptStatus::Pending
|
||||
},
|
||||
request: types::PaymentsAuthorizeData {
|
||||
enrolled_for_3ds: item.response.enrolled,
|
||||
..item.data.request
|
||||
},
|
||||
response: Ok(types::PaymentsResponseData::TransactionResponse {
|
||||
resource_id: types::ResponseId::NoResponseId,
|
||||
redirection_data,
|
||||
mandate_reference: None,
|
||||
connector_metadata: Some(
|
||||
serde_json::to_value(Shift4CardToken {
|
||||
id: item.response.token.id,
|
||||
})
|
||||
.into_report()
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?,
|
||||
),
|
||||
}),
|
||||
..item.data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, F>
|
||||
TryFrom<types::ResponseRouterData<F, Shift4NonThreeDsResponse, T, types::PaymentsResponseData>>
|
||||
for types::RouterData<F, T, types::PaymentsResponseData>
|
||||
{
|
||||
type Error = Error;
|
||||
fn try_from(
|
||||
item: types::ResponseRouterData<F, Shift4PaymentsResponse, T, types::PaymentsResponseData>,
|
||||
item: types::ResponseRouterData<
|
||||
F,
|
||||
Shift4NonThreeDsResponse,
|
||||
T,
|
||||
types::PaymentsResponseData,
|
||||
>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
status: enums::AttemptStatus::foreign_from((
|
||||
|
||||
@ -142,7 +142,7 @@ impl<Flow, Request, Response> RouterData for types::RouterData<Flow, Request, Re
|
||||
}
|
||||
|
||||
pub trait PaymentsAuthorizeRequestData {
|
||||
fn is_auto_capture(&self) -> bool;
|
||||
fn is_auto_capture(&self) -> Result<bool, Error>;
|
||||
fn get_email(&self) -> Result<Secret<String, Email>, Error>;
|
||||
fn get_browser_info(&self) -> Result<types::BrowserInformation, Error>;
|
||||
fn get_card(&self) -> Result<api::Card, Error>;
|
||||
@ -153,8 +153,12 @@ pub trait PaymentsAuthorizeRequestData {
|
||||
}
|
||||
|
||||
impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData {
|
||||
fn is_auto_capture(&self) -> bool {
|
||||
self.capture_method == Some(storage_models::enums::CaptureMethod::Automatic)
|
||||
fn is_auto_capture(&self) -> Result<bool, Error> {
|
||||
match self.capture_method {
|
||||
Some(storage_models::enums::CaptureMethod::Automatic) | None => Ok(true),
|
||||
Some(storage_models::enums::CaptureMethod::Manual) => Ok(false),
|
||||
Some(_) => Err(errors::ConnectorError::CaptureMethodNotSupported.into()),
|
||||
}
|
||||
}
|
||||
fn get_email(&self) -> Result<Secret<String, Email>, Error> {
|
||||
self.email.clone().ok_or_else(missing_field_err("email"))
|
||||
@ -195,14 +199,32 @@ impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PaymentsCompleteAuthorizeRequestData {
|
||||
fn is_auto_capture(&self) -> Result<bool, Error>;
|
||||
}
|
||||
|
||||
impl PaymentsCompleteAuthorizeRequestData for types::CompleteAuthorizeData {
|
||||
fn is_auto_capture(&self) -> Result<bool, Error> {
|
||||
match self.capture_method {
|
||||
Some(storage_models::enums::CaptureMethod::Automatic) | None => Ok(true),
|
||||
Some(storage_models::enums::CaptureMethod::Manual) => Ok(false),
|
||||
Some(_) => Err(errors::ConnectorError::CaptureMethodNotSupported.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PaymentsSyncRequestData {
|
||||
fn is_auto_capture(&self) -> bool;
|
||||
fn is_auto_capture(&self) -> Result<bool, Error>;
|
||||
fn get_connector_transaction_id(&self) -> CustomResult<String, errors::ValidationError>;
|
||||
}
|
||||
|
||||
impl PaymentsSyncRequestData for types::PaymentsSyncData {
|
||||
fn is_auto_capture(&self) -> bool {
|
||||
self.capture_method == Some(storage_models::enums::CaptureMethod::Automatic)
|
||||
fn is_auto_capture(&self) -> Result<bool, Error> {
|
||||
match self.capture_method {
|
||||
Some(storage_models::enums::CaptureMethod::Automatic) | None => Ok(true),
|
||||
Some(storage_models::enums::CaptureMethod::Manual) => Ok(false),
|
||||
Some(_) => Err(errors::ConnectorError::CaptureMethodNotSupported.into()),
|
||||
}
|
||||
}
|
||||
fn get_connector_transaction_id(&self) -> CustomResult<String, errors::ValidationError> {
|
||||
match self.connector_transaction_id.clone() {
|
||||
|
||||
@ -259,6 +259,8 @@ pub enum ConnectorError {
|
||||
},
|
||||
#[error("{flow} flow not supported by {connector} connector")]
|
||||
FlowNotSupported { flow: String, connector: String },
|
||||
#[error("Capture method not supported")]
|
||||
CaptureMethodNotSupported,
|
||||
#[error("Missing connector transaction ID")]
|
||||
MissingConnectorTransactionID,
|
||||
#[error("Missing connector refund ID")]
|
||||
|
||||
@ -105,7 +105,6 @@ default_imp_for_complete_authorize!(
|
||||
connector::Payeezy,
|
||||
connector::Payu,
|
||||
connector::Rapyd,
|
||||
connector::Shift4,
|
||||
connector::Stripe,
|
||||
connector::Trustpay,
|
||||
connector::Worldline,
|
||||
|
||||
@ -117,6 +117,7 @@ impl types::PaymentsAuthorizeRouterData {
|
||||
.execute_pretasks(self, state)
|
||||
.await
|
||||
.map_err(|error| error.to_payment_failed_response())?;
|
||||
if self.should_proceed_with_authorize() {
|
||||
self.decide_authentication_type();
|
||||
let resp = services::execute_connector_processing_step(
|
||||
state,
|
||||
@ -137,6 +138,9 @@ impl types::PaymentsAuthorizeRouterData {
|
||||
.await?;
|
||||
|
||||
Ok(mandate::mandate_procedure(state, resp, maybe_customer, pm_id).await?)
|
||||
} else {
|
||||
Ok(self.clone())
|
||||
}
|
||||
}
|
||||
_ => Ok(self.clone()),
|
||||
}
|
||||
@ -149,6 +153,16 @@ impl types::PaymentsAuthorizeRouterData {
|
||||
self.auth_type = storage_models::enums::AuthenticationType::NoThreeDs
|
||||
}
|
||||
}
|
||||
|
||||
/// to decide if we need to proceed with authorize or not, Eg: If any of the pretask returns `redirection_response` then we should not proceed with authorize call
|
||||
fn should_proceed_with_authorize(&self) -> bool {
|
||||
match &self.response {
|
||||
Ok(types::PaymentsResponseData::TransactionResponse {
|
||||
redirection_data, ..
|
||||
}) => !redirection_data.is_some(),
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn save_payment_method<F: Clone, FData>(
|
||||
|
||||
@ -124,6 +124,7 @@ impl Payments {
|
||||
)
|
||||
.service(
|
||||
web::resource("/{payment_id}/{merchant_id}/redirect/complete/{connector}")
|
||||
.route(web::get().to(payments_complete_authorize))
|
||||
.route(web::post().to(payments_complete_authorize)),
|
||||
);
|
||||
}
|
||||
|
||||
@ -12,9 +12,10 @@ use std::{
|
||||
use actix_web::{body, HttpRequest, HttpResponse, Responder};
|
||||
use common_utils::errors::ReportSwitchExt;
|
||||
use error_stack::{report, IntoReport, Report, ResultExt};
|
||||
use masking::ExposeOptionInterface;
|
||||
use masking::{ExposeOptionInterface, PeekInterface};
|
||||
use router_env::{instrument, tracing, Tag};
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
|
||||
use self::request::{ContentType, HeaderExt, RequestBuilderExt};
|
||||
pub use self::request::{Method, Request, RequestBuilder};
|
||||
@ -311,7 +312,13 @@ async fn send_request(
|
||||
// Currently this is not used remove this if not required
|
||||
// If using this then handle the serde_part
|
||||
Some(ContentType::FormUrlEncoded) => {
|
||||
let url_encoded_payload = serde_urlencoded::to_string(&request.payload)
|
||||
let payload = match request.payload.clone() {
|
||||
Some(req) => serde_json::from_str(req.peek())
|
||||
.into_report()
|
||||
.change_context(errors::ApiClientError::UrlEncodingFailed)?,
|
||||
_ => json!(r#""#),
|
||||
};
|
||||
let url_encoded_payload = serde_urlencoded::to_string(&payload)
|
||||
.into_report()
|
||||
.change_context(errors::ApiClientError::UrlEncodingFailed)
|
||||
.attach_printable_lazy(|| {
|
||||
|
||||
@ -65,7 +65,7 @@ pub type RefundsResponseRouterData<F, R> =
|
||||
|
||||
pub type PaymentsAuthorizeType =
|
||||
dyn services::ConnectorIntegration<api::Authorize, PaymentsAuthorizeData, PaymentsResponseData>;
|
||||
pub type PaymentsComeplteAuthorizeType = dyn services::ConnectorIntegration<
|
||||
pub type PaymentsCompleteAuthorizeType = dyn services::ConnectorIntegration<
|
||||
api::CompleteAuthorize,
|
||||
CompleteAuthorizeData,
|
||||
PaymentsResponseData,
|
||||
|
||||
@ -101,7 +101,7 @@ async fn should_sync_authorized_payment() {
|
||||
txn_id.unwrap(),
|
||||
),
|
||||
encoded_data: None,
|
||||
capture_method: None,
|
||||
capture_method: Some(storage_models::enums::CaptureMethod::Manual),
|
||||
connector_meta: None,
|
||||
}),
|
||||
None,
|
||||
|
||||
Reference in New Issue
Block a user