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(
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(),

View File

@ -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)
}

View File

@ -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,

View File

@ -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,

View File

@ -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(),

View File

@ -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(),

View File

@ -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,
};

View File

@ -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(

View File

@ -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((

View File

@ -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() {

View File

@ -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")]

View File

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

View File

@ -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>(

View File

@ -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)),
);
}

View File

@ -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(|| {

View File

@ -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,

View File

@ -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,