feat(connector): [Shift4] add support for card 3DS payment (#828)

This commit is contained in:
Jagan
2023-04-18 01:01:15 +05:30
committed by GitHub
parent ffaa8da0d2
commit 29999fe51a
17 changed files with 576 additions and 136 deletions

View File

@ -513,13 +513,13 @@ impl
Ok(Some( Ok(Some(
services::RequestBuilder::new() services::RequestBuilder::new()
.method(services::Method::Post) .method(services::Method::Post)
.url(&types::PaymentsComeplteAuthorizeType::get_url( .url(&types::PaymentsCompleteAuthorizeType::get_url(
self, req, connectors, self, req, connectors,
)?) )?)
.headers(types::PaymentsComeplteAuthorizeType::get_headers( .headers(types::PaymentsCompleteAuthorizeType::get_headers(
self, req, connectors, self, req, connectors,
)?) )?)
.body(types::PaymentsComeplteAuthorizeType::get_request_body( .body(types::PaymentsCompleteAuthorizeType::get_request_body(
self, req, self, req,
)?) )?)
.build(), .build(),

View File

@ -267,7 +267,7 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
data: data.clone(), data: data.clone(),
http_code: res.status_code, 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) .change_context(errors::ConnectorError::ResponseHandlingFailed)
} }
@ -440,7 +440,7 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
data: data.clone(), data: data.clone(),
http_code: res.status_code, 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) .change_context(errors::ConnectorError::ResponseHandlingFailed)
} }

View File

@ -64,7 +64,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for BamboraPaymentsRequest {
expiry_year: req_card.card_exp_year, expiry_year: req_card.card_exp_year,
cvd: req_card.card_cvc, cvd: req_card.card_cvc,
three_d_secure: three_ds, three_d_secure: three_ds,
complete: item.request.is_auto_capture(), complete: item.request.is_auto_capture()?,
}; };
Ok(Self { Ok(Self {
amount: item.request.amount, amount: item.request.amount,

View File

@ -34,7 +34,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for FortePaymentsRequest {
expiry_month: req_card.card_exp_month, expiry_month: req_card.card_exp_month,
expiry_year: req_card.card_exp_year, expiry_year: req_card.card_exp_year,
cvc: req_card.card_cvc, cvc: req_card.card_cvc,
complete: item.request.is_auto_capture(), complete: item.request.is_auto_capture()?,
}; };
Ok(Self { Ok(Self {
amount: item.request.amount, amount: item.request.amount,

View File

@ -152,14 +152,14 @@ impl
Ok(Some( Ok(Some(
services::RequestBuilder::new() services::RequestBuilder::new()
.method(services::Method::Post) .method(services::Method::Post)
.url(&types::PaymentsComeplteAuthorizeType::get_url( .url(&types::PaymentsCompleteAuthorizeType::get_url(
self, req, connectors, self, req, connectors,
)?) )?)
.attach_default_headers() .attach_default_headers()
.headers(types::PaymentsComeplteAuthorizeType::get_headers( .headers(types::PaymentsCompleteAuthorizeType::get_headers(
self, req, connectors, self, req, connectors,
)?) )?)
.body(types::PaymentsComeplteAuthorizeType::get_request_body( .body(types::PaymentsCompleteAuthorizeType::get_request_body(
self, req, self, req,
)?) )?)
.build(), .build(),

View File

@ -148,14 +148,14 @@ impl
Ok(Some( Ok(Some(
services::RequestBuilder::new() services::RequestBuilder::new()
.method(services::Method::Post) .method(services::Method::Post)
.url(&types::PaymentsComeplteAuthorizeType::get_url( .url(&types::PaymentsCompleteAuthorizeType::get_url(
self, req, connectors, self, req, connectors,
)?) )?)
.attach_default_headers() .attach_default_headers()
.headers(types::PaymentsComeplteAuthorizeType::get_headers( .headers(types::PaymentsCompleteAuthorizeType::get_headers(
self, req, connectors, self, req, connectors,
)?) )?)
.body(types::PaymentsComeplteAuthorizeType::get_request_body( .body(types::PaymentsCompleteAuthorizeType::get_request_body(
self, req, self, req,
)?) )?)
.build(), .build(),

View File

@ -80,7 +80,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaypalPaymentsRequest {
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> { fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
match item.request.payment_method_data { match item.request.payment_method_data {
api_models::payments::PaymentMethodData::Card(ref ccard) => { 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, true => PaypalPaymentIntent::Capture,
false => PaypalPaymentIntent::Authorize, false => PaypalPaymentIntent::Authorize,
}; };

View File

@ -10,9 +10,12 @@ use super::utils::RefundsRequestData;
use crate::{ use crate::{
configs::settings, configs::settings,
consts, consts,
core::errors::{self, CustomResult}, core::{
headers, errors::{self, CustomResult},
services::{self, ConnectorIntegration}, payments,
},
headers, routes,
services::{self, request, ConnectorIntegration},
types::{ types::{
self, self,
api::{self, ConnectorCommon, ConnectorCommonExt}, api::{self, ConnectorCommon, ConnectorCommonExt},
@ -93,7 +96,15 @@ impl ConnectorCommon for Shift4 {
} }
impl api::Payment 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 api::PaymentToken for Shift4 {}
impl 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> impl ConnectorIntegration<api::Void, types::PaymentsCancelData, types::PaymentsResponseData>
for Shift4 for Shift4
{ {
} }
impl api::PaymentSync for Shift4 {}
impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData> impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
for Shift4 for Shift4
{ {
@ -187,9 +312,9 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
data: &types::PaymentsSyncRouterData, data: &types::PaymentsSyncRouterData,
res: types::Response, res: types::Response,
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> { ) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
let response: shift4::Shift4PaymentsResponse = res let response: shift4::Shift4NonThreeDsResponse = res
.response .response
.parse_struct("shift4 PaymentsResponse") .parse_struct("Shift4NonThreeDsResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?; .change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData { types::RouterData::try_from(types::ResponseRouterData {
response, 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> impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::PaymentsResponseData>
for Shift4 for Shift4
{ {
@ -239,9 +362,9 @@ impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::Payme
data: &types::PaymentsCaptureRouterData, data: &types::PaymentsCaptureRouterData,
res: types::Response, res: types::Response,
) -> CustomResult<types::PaymentsCaptureRouterData, errors::ConnectorError> { ) -> CustomResult<types::PaymentsCaptureRouterData, errors::ConnectorError> {
let response: shift4::Shift4PaymentsResponse = res let response: shift4::Shift4NonThreeDsResponse = res
.response .response
.parse_struct("Shift4PaymentsResponse") .parse_struct("Shift4NonThreeDsResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?; .change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::ResponseRouterData { types::ResponseRouterData {
response, 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> impl ConnectorIntegration<api::Session, types::PaymentsSessionData, types::PaymentsResponseData>
for Shift4 for Shift4
{ {
//TODO: implement sessions flow //TODO: implement sessions flow
} }
impl api::PaymentAuthorize for Shift4 {} impl
ConnectorIntegration<
impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::PaymentsResponseData> api::InitPayment,
for Shift4 types::PaymentsAuthorizeData,
types::PaymentsResponseData,
> for Shift4
{ {
fn get_headers( fn get_headers(
&self, &self,
req: &types::PaymentsAuthorizeRouterData, req: &types::PaymentsInitRouterData,
connectors: &settings::Connectors, _connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> { ) -> 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 { fn get_content_type(&self) -> &'static str {
@ -300,55 +435,54 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
fn get_url( fn get_url(
&self, &self,
_req: &types::PaymentsAuthorizeRouterData, _req: &types::PaymentsInitRouterData,
connectors: &settings::Connectors, connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> { ) -> CustomResult<String, errors::ConnectorError> {
Ok(format!("{}charges", self.base_url(connectors))) Ok(format!("{}3d-secure", self.base_url(connectors)))
} }
fn get_request_body( fn get_request_body(
&self, &self,
req: &types::PaymentsAuthorizeRouterData, req: &types::PaymentsInitRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> { ) -> 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)?; .change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(shift4_req)) Ok(Some(req))
} }
fn build_request( fn build_request(
&self, &self,
req: &types::PaymentsAuthorizeRouterData, req: &types::PaymentsInitRouterData,
connectors: &settings::Connectors, connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> { ) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some( Ok(Some(
services::RequestBuilder::new() services::RequestBuilder::new()
.method(services::Method::Post) .method(services::Method::Post)
.url(&types::PaymentsAuthorizeType::get_url( .url(&types::PaymentsInitType::get_url(self, req, connectors)?)
self, req, connectors, .content_type(request::ContentType::FormUrlEncoded)
)?)
.attach_default_headers() .attach_default_headers()
.headers(types::PaymentsAuthorizeType::get_headers( .headers(types::PaymentsInitType::get_headers(self, req, connectors)?)
self, req, connectors, .body(types::PaymentsInitType::get_request_body(self, req)?)
)?)
.body(types::PaymentsAuthorizeType::get_request_body(self, req)?)
.build(), .build(),
)) ))
} }
fn handle_response( fn handle_response(
&self, &self,
data: &types::PaymentsAuthorizeRouterData, data: &types::PaymentsInitRouterData,
res: types::Response, res: types::Response,
) -> CustomResult<types::PaymentsAuthorizeRouterData, errors::ConnectorError> { ) -> CustomResult<types::PaymentsInitRouterData, errors::ConnectorError> {
let response: shift4::Shift4PaymentsResponse = res let response: shift4::Shift4ThreeDsResponse = res
.response .response
.parse_struct("Shift4PaymentsResponse") .parse_struct("Shift4ThreeDsResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?; .change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::ResponseRouterData { types::RouterData::try_from(types::ResponseRouterData {
response, response,
data: data.clone(), data: data.clone(),
http_code: res.status_code, http_code: res.status_code,
} })
.try_into()
.change_context(errors::ConnectorError::ResponseHandlingFailed) .change_context(errors::ConnectorError::ResponseHandlingFailed)
} }
@ -360,9 +494,89 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
} }
} }
impl api::Refund for Shift4 {} impl
impl api::RefundExecute for Shift4 {} ConnectorIntegration<
impl api::RefundSync for Shift4 {} 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 { impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsResponseData> for Shift4 {
fn get_headers( fn get_headers(

View File

@ -1,9 +1,15 @@
use api_models::payments; use api_models::payments;
use common_utils::pii::SecretSerdeValue;
use error_stack::{IntoReport, ResultExt};
use masking::Secret; use masking::Secret;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url; use url::Url;
use crate::{ use crate::{
connector::utils::{
to_connector_meta, PaymentsAuthorizeRequestData, PaymentsCompleteAuthorizeRequestData,
RouterData,
},
core::errors, core::errors,
pii, services, pii, services,
types::{self, api, storage::enums, transformers::ForeignFrom}, types::{self, api, storage::enums, transformers::ForeignFrom},
@ -11,11 +17,33 @@ use crate::{
type Error = error_stack::Report<errors::ConnectorError>; type Error = error_stack::Report<errors::ConnectorError>;
#[derive(Debug, Serialize)]
#[serde(untagged)]
pub enum Shift4PaymentsRequest {
Non3DSRequest(Box<Shift4Non3DSRequest>),
ThreeDSRequest(Box<Shift43DSRequest>),
}
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Shift4PaymentsRequest { pub struct Shift43DSRequest {
amount: String, 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, currency: String,
description: Option<String>, description: Option<String>,
payment_method: Option<PaymentMethod>, payment_method: Option<PaymentMethod>,
@ -68,15 +96,26 @@ pub struct DeviceData;
#[derive(Default, Debug, Serialize, Eq, PartialEq)] #[derive(Default, Debug, Serialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Card { pub struct Card {
number: Secret<String, common_utils::pii::CardNumber>, pub number: Secret<String, common_utils::pii::CardNumber>,
exp_month: Secret<String>, pub exp_month: Secret<String>,
exp_year: Secret<String>, pub exp_year: Secret<String>,
cardholder_name: 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; 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 { match &item.request.payment_method_data {
api::PaymentMethodData::Card(ccard) => get_card_payment_request(item, ccard), api::PaymentMethodData::Card(ccard) => get_card_payment_request(item, ccard),
api::PaymentMethodData::BankRedirect(redirect_data) => { api::PaymentMethodData::BankRedirect(redirect_data) => {
@ -87,33 +126,78 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for Shift4PaymentsRequest {
} }
} }
fn get_card_payment_request( impl<T> TryFrom<&types::RouterData<T, types::CompleteAuthorizeData, types::PaymentsResponseData>>
item: &types::PaymentsAuthorizeRouterData, 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, card: &api_models::payments::Card,
) -> Result<Shift4PaymentsRequest, Error> { ) -> Result<Shift4PaymentsRequest, Error> {
let submit_for_settlement = submit_for_settlement(item); let submit_for_settlement = item.request.is_auto_capture()?;
let card = Some(Card { let card = Card {
number: card.card_number.clone(), number: card.card_number.clone(),
exp_month: card.card_exp_month.clone(), exp_month: card.card_exp_month.clone(),
exp_year: card.card_exp_year.clone(), exp_year: card.card_exp_year.clone(),
cardholder_name: card.card_holder_name.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(), 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(), currency: item.request.currency.to_string(),
description: item.description.clone(), description: item.description.clone(),
captured: submit_for_settlement, captured: submit_for_settlement,
payment_method: None, payment_method: None,
flow: None, flow: None,
}) },
)))
}
} }
fn get_bank_redirect_request( fn get_bank_redirect_request<T>(
item: &types::PaymentsAuthorizeRouterData, item: &types::RouterData<T, types::PaymentsAuthorizeData, types::PaymentsResponseData>,
redirect_data: &payments::BankRedirectData, redirect_data: &payments::BankRedirectData,
) -> Result<Shift4PaymentsRequest, Error> { ) -> 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 method_type = PaymentMethodType::from(redirect_data);
let billing = get_billing(item)?; let billing = get_billing(item)?;
let payment_method = Some(PaymentMethod { let payment_method = Some(PaymentMethod {
@ -121,7 +205,8 @@ fn get_bank_redirect_request(
billing, billing,
}); });
let flow = get_flow(item); let flow = get_flow(item);
Ok(Shift4PaymentsRequest { Ok(Shift4PaymentsRequest::Non3DSRequest(Box::new(
Shift4Non3DSRequest {
amount: item.request.amount.to_string(), amount: item.request.amount.to_string(),
card: None, card: None,
currency: item.request.currency.to_string(), currency: item.request.currency.to_string(),
@ -129,7 +214,8 @@ fn get_bank_redirect_request(
captured: submit_for_settlement, captured: submit_for_settlement,
payment_method, payment_method,
flow: Some(flow), flow: Some(flow),
}) },
)))
} }
impl From<&payments::BankRedirectData> for PaymentMethodType { 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 { Flow {
return_url: item.request.router_return_url.clone(), 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 let billing_address = item
.address .address
.billing .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 // Auth Struct
pub struct Shift4AuthType { pub struct Shift4AuthType {
pub(super) api_key: String, pub(super) api_key: String,
@ -257,8 +340,7 @@ pub struct Shift4WebhookObjectResource {
} }
#[derive(Default, Debug, Deserialize)] #[derive(Default, Debug, Deserialize)]
#[serde(rename_all = "camelCase")] pub struct Shift4NonThreeDsResponse {
pub struct Shift4PaymentsResponse {
pub id: String, pub id: String,
pub currency: String, pub currency: String,
pub amount: u32, pub amount: u32,
@ -268,6 +350,45 @@ pub struct Shift4PaymentsResponse {
pub flow: Option<FlowResponse>, 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)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct FlowResponse { pub struct FlowResponse {
@ -290,13 +411,73 @@ pub enum NextAction {
None, None,
} }
impl<F, T> #[derive(Debug, Serialize, Deserialize)]
TryFrom<types::ResponseRouterData<F, Shift4PaymentsResponse, T, types::PaymentsResponseData>> 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> for types::RouterData<F, T, types::PaymentsResponseData>
{ {
type Error = Error; type Error = Error;
fn try_from( fn try_from(
item: types::ResponseRouterData<F, Shift4PaymentsResponse, T, types::PaymentsResponseData>, item: types::ResponseRouterData<
F,
Shift4NonThreeDsResponse,
T,
types::PaymentsResponseData,
>,
) -> Result<Self, Self::Error> { ) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
status: enums::AttemptStatus::foreign_from(( status: enums::AttemptStatus::foreign_from((

View File

@ -142,7 +142,7 @@ impl<Flow, Request, Response> RouterData for types::RouterData<Flow, Request, Re
} }
pub trait PaymentsAuthorizeRequestData { 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_email(&self) -> Result<Secret<String, Email>, Error>;
fn get_browser_info(&self) -> Result<types::BrowserInformation, Error>; fn get_browser_info(&self) -> Result<types::BrowserInformation, Error>;
fn get_card(&self) -> Result<api::Card, Error>; fn get_card(&self) -> Result<api::Card, Error>;
@ -153,8 +153,12 @@ pub trait PaymentsAuthorizeRequestData {
} }
impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData { impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData {
fn is_auto_capture(&self) -> bool { fn is_auto_capture(&self) -> Result<bool, Error> {
self.capture_method == Some(storage_models::enums::CaptureMethod::Automatic) 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> { fn get_email(&self) -> Result<Secret<String, Email>, Error> {
self.email.clone().ok_or_else(missing_field_err("email")) 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 { 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>; fn get_connector_transaction_id(&self) -> CustomResult<String, errors::ValidationError>;
} }
impl PaymentsSyncRequestData for types::PaymentsSyncData { impl PaymentsSyncRequestData for types::PaymentsSyncData {
fn is_auto_capture(&self) -> bool { fn is_auto_capture(&self) -> Result<bool, Error> {
self.capture_method == Some(storage_models::enums::CaptureMethod::Automatic) 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> { fn get_connector_transaction_id(&self) -> CustomResult<String, errors::ValidationError> {
match self.connector_transaction_id.clone() { match self.connector_transaction_id.clone() {

View File

@ -259,6 +259,8 @@ pub enum ConnectorError {
}, },
#[error("{flow} flow not supported by {connector} connector")] #[error("{flow} flow not supported by {connector} connector")]
FlowNotSupported { flow: String, connector: String }, FlowNotSupported { flow: String, connector: String },
#[error("Capture method not supported")]
CaptureMethodNotSupported,
#[error("Missing connector transaction ID")] #[error("Missing connector transaction ID")]
MissingConnectorTransactionID, MissingConnectorTransactionID,
#[error("Missing connector refund ID")] #[error("Missing connector refund ID")]

View File

@ -105,7 +105,6 @@ default_imp_for_complete_authorize!(
connector::Payeezy, connector::Payeezy,
connector::Payu, connector::Payu,
connector::Rapyd, connector::Rapyd,
connector::Shift4,
connector::Stripe, connector::Stripe,
connector::Trustpay, connector::Trustpay,
connector::Worldline, connector::Worldline,

View File

@ -117,6 +117,7 @@ impl types::PaymentsAuthorizeRouterData {
.execute_pretasks(self, state) .execute_pretasks(self, state)
.await .await
.map_err(|error| error.to_payment_failed_response())?; .map_err(|error| error.to_payment_failed_response())?;
if self.should_proceed_with_authorize() {
self.decide_authentication_type(); self.decide_authentication_type();
let resp = services::execute_connector_processing_step( let resp = services::execute_connector_processing_step(
state, state,
@ -137,6 +138,9 @@ impl types::PaymentsAuthorizeRouterData {
.await?; .await?;
Ok(mandate::mandate_procedure(state, resp, maybe_customer, pm_id).await?) Ok(mandate::mandate_procedure(state, resp, maybe_customer, pm_id).await?)
} else {
Ok(self.clone())
}
} }
_ => Ok(self.clone()), _ => Ok(self.clone()),
} }
@ -149,6 +153,16 @@ impl types::PaymentsAuthorizeRouterData {
self.auth_type = storage_models::enums::AuthenticationType::NoThreeDs 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>( pub async fn save_payment_method<F: Clone, FData>(

View File

@ -124,6 +124,7 @@ impl Payments {
) )
.service( .service(
web::resource("/{payment_id}/{merchant_id}/redirect/complete/{connector}") web::resource("/{payment_id}/{merchant_id}/redirect/complete/{connector}")
.route(web::get().to(payments_complete_authorize))
.route(web::post().to(payments_complete_authorize)), .route(web::post().to(payments_complete_authorize)),
); );
} }

View File

@ -12,9 +12,10 @@ use std::{
use actix_web::{body, HttpRequest, HttpResponse, Responder}; use actix_web::{body, HttpRequest, HttpResponse, Responder};
use common_utils::errors::ReportSwitchExt; use common_utils::errors::ReportSwitchExt;
use error_stack::{report, IntoReport, Report, ResultExt}; use error_stack::{report, IntoReport, Report, ResultExt};
use masking::ExposeOptionInterface; use masking::{ExposeOptionInterface, PeekInterface};
use router_env::{instrument, tracing, Tag}; use router_env::{instrument, tracing, Tag};
use serde::Serialize; use serde::Serialize;
use serde_json::json;
use self::request::{ContentType, HeaderExt, RequestBuilderExt}; use self::request::{ContentType, HeaderExt, RequestBuilderExt};
pub use self::request::{Method, Request, RequestBuilder}; 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 // Currently this is not used remove this if not required
// If using this then handle the serde_part // If using this then handle the serde_part
Some(ContentType::FormUrlEncoded) => { 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() .into_report()
.change_context(errors::ApiClientError::UrlEncodingFailed) .change_context(errors::ApiClientError::UrlEncodingFailed)
.attach_printable_lazy(|| { .attach_printable_lazy(|| {

View File

@ -65,7 +65,7 @@ pub type RefundsResponseRouterData<F, R> =
pub type PaymentsAuthorizeType = pub type PaymentsAuthorizeType =
dyn services::ConnectorIntegration<api::Authorize, PaymentsAuthorizeData, PaymentsResponseData>; dyn services::ConnectorIntegration<api::Authorize, PaymentsAuthorizeData, PaymentsResponseData>;
pub type PaymentsComeplteAuthorizeType = dyn services::ConnectorIntegration< pub type PaymentsCompleteAuthorizeType = dyn services::ConnectorIntegration<
api::CompleteAuthorize, api::CompleteAuthorize,
CompleteAuthorizeData, CompleteAuthorizeData,
PaymentsResponseData, PaymentsResponseData,

View File

@ -101,7 +101,7 @@ async fn should_sync_authorized_payment() {
txn_id.unwrap(), txn_id.unwrap(),
), ),
encoded_data: None, encoded_data: None,
capture_method: None, capture_method: Some(storage_models::enums::CaptureMethod::Manual),
connector_meta: None, connector_meta: None,
}), }),
None, None,