mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-02 12:06:56 +08:00
feat(connector): [Shift4] add eps, sofort, giropay, ideal support for shift4 connector (#810)
This commit is contained in:
@ -1,19 +1,65 @@
|
||||
use api_models::payments;
|
||||
use masking::Secret;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
core::errors,
|
||||
types::{self, api, storage::enums},
|
||||
pii, services,
|
||||
types::{self, api, storage::enums, transformers::ForeignFrom},
|
||||
};
|
||||
|
||||
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Shift4PaymentsRequest {
|
||||
amount: String,
|
||||
card: Card,
|
||||
card: Option<Card>,
|
||||
currency: String,
|
||||
description: Option<String>,
|
||||
payment_method: Option<PaymentMethod>,
|
||||
captured: bool,
|
||||
flow: Option<Flow>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Flow {
|
||||
pub return_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum PaymentMethodType {
|
||||
Eps,
|
||||
Giropay,
|
||||
Ideal,
|
||||
Sofort,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PaymentMethod {
|
||||
#[serde(rename = "type")]
|
||||
method_type: PaymentMethodType,
|
||||
billing: Option<Billing>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Billing {
|
||||
name: Option<Secret<String>>,
|
||||
email: Option<Secret<String, pii::Email>>,
|
||||
address: Option<Address>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Address {
|
||||
line1: Option<Secret<String>>,
|
||||
line2: Option<Secret<String>>,
|
||||
zip: Option<Secret<String>>,
|
||||
state: Option<Secret<String>>,
|
||||
city: Option<String>,
|
||||
country: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||
@ -29,42 +75,121 @@ pub struct Card {
|
||||
}
|
||||
|
||||
impl TryFrom<&types::PaymentsAuthorizeRouterData> for Shift4PaymentsRequest {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
type Error = Error;
|
||||
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
|
||||
match item.request.payment_method_data.clone() {
|
||||
api::PaymentMethodData::Card(ccard) => {
|
||||
let submit_for_settlement = matches!(
|
||||
item.request.capture_method,
|
||||
Some(enums::CaptureMethod::Automatic) | None
|
||||
);
|
||||
let payment_request = Self {
|
||||
amount: item.request.amount.to_string(),
|
||||
card: Card {
|
||||
number: ccard.card_number,
|
||||
exp_month: ccard.card_exp_month,
|
||||
exp_year: ccard.card_exp_year,
|
||||
cardholder_name: ccard.card_holder_name,
|
||||
},
|
||||
currency: item.request.currency.to_string(),
|
||||
description: item.description.clone(),
|
||||
captured: submit_for_settlement,
|
||||
};
|
||||
Ok(payment_request)
|
||||
match &item.request.payment_method_data {
|
||||
api::PaymentMethodData::Card(ccard) => get_card_payment_request(item, ccard),
|
||||
api::PaymentMethodData::BankRedirect(redirect_data) => {
|
||||
get_bank_redirect_request(item, redirect_data)
|
||||
}
|
||||
_ => Err(
|
||||
errors::ConnectorError::NotImplemented("Current Payment Method".to_string()).into(),
|
||||
),
|
||||
_ => Err(errors::ConnectorError::NotImplemented("Payment Method".to_string()).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_card_payment_request(
|
||||
item: &types::PaymentsAuthorizeRouterData,
|
||||
card: &api_models::payments::Card,
|
||||
) -> Result<Shift4PaymentsRequest, Error> {
|
||||
let submit_for_settlement = submit_for_settlement(item);
|
||||
let card = Some(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 {
|
||||
amount: item.request.amount.to_string(),
|
||||
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,
|
||||
redirect_data: &payments::BankRedirectData,
|
||||
) -> Result<Shift4PaymentsRequest, Error> {
|
||||
let submit_for_settlement = submit_for_settlement(item);
|
||||
let method_type = PaymentMethodType::from(redirect_data);
|
||||
let billing = get_billing(item)?;
|
||||
let payment_method = Some(PaymentMethod {
|
||||
method_type,
|
||||
billing,
|
||||
});
|
||||
let flow = get_flow(item);
|
||||
Ok(Shift4PaymentsRequest {
|
||||
amount: item.request.amount.to_string(),
|
||||
card: None,
|
||||
currency: item.request.currency.to_string(),
|
||||
description: item.description.clone(),
|
||||
captured: submit_for_settlement,
|
||||
payment_method,
|
||||
flow: Some(flow),
|
||||
})
|
||||
}
|
||||
|
||||
impl From<&payments::BankRedirectData> for PaymentMethodType {
|
||||
fn from(value: &payments::BankRedirectData) -> Self {
|
||||
match value {
|
||||
payments::BankRedirectData::Eps { .. } => Self::Eps,
|
||||
payments::BankRedirectData::Giropay { .. } => Self::Giropay,
|
||||
payments::BankRedirectData::Ideal { .. } => Self::Ideal,
|
||||
payments::BankRedirectData::Sofort { .. } => Self::Sofort,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_flow(item: &types::PaymentsAuthorizeRouterData) -> Flow {
|
||||
Flow {
|
||||
return_url: item.router_return_url.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_billing(item: &types::PaymentsAuthorizeRouterData) -> Result<Option<Billing>, Error> {
|
||||
let billing_address = item
|
||||
.address
|
||||
.billing
|
||||
.as_ref()
|
||||
.and_then(|billing| billing.address.as_ref());
|
||||
let address = get_address_details(billing_address);
|
||||
Ok(Some(Billing {
|
||||
name: billing_address.map(|billing| {
|
||||
Secret::new(format!("{:?} {:?}", billing.first_name, billing.last_name))
|
||||
}),
|
||||
email: item.request.email.clone(),
|
||||
address,
|
||||
}))
|
||||
}
|
||||
|
||||
fn get_address_details(address_details: Option<&payments::AddressDetails>) -> Option<Address> {
|
||||
address_details.map(|address| Address {
|
||||
line1: address.line1.clone(),
|
||||
line2: address.line1.clone(),
|
||||
zip: address.zip.clone(),
|
||||
state: address.state.clone(),
|
||||
city: address.city.clone(),
|
||||
country: address.country.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
impl TryFrom<&types::ConnectorAuthType> for Shift4AuthType {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
type Error = Error;
|
||||
fn try_from(item: &types::ConnectorAuthType) -> Result<Self, Self::Error> {
|
||||
if let types::ConnectorAuthType::HeaderKey { api_key } = item {
|
||||
Ok(Self {
|
||||
@ -85,12 +210,22 @@ pub enum Shift4PaymentStatus {
|
||||
Pending,
|
||||
}
|
||||
|
||||
impl From<Shift4PaymentStatus> for enums::AttemptStatus {
|
||||
fn from(item: Shift4PaymentStatus) -> Self {
|
||||
match item {
|
||||
Shift4PaymentStatus::Successful => Self::Charged,
|
||||
impl ForeignFrom<(bool, Option<&NextAction>, Shift4PaymentStatus)> for enums::AttemptStatus {
|
||||
fn foreign_from(item: (bool, Option<&NextAction>, Shift4PaymentStatus)) -> Self {
|
||||
let (captured, next_action, payment_status) = item;
|
||||
match payment_status {
|
||||
Shift4PaymentStatus::Successful => {
|
||||
if captured {
|
||||
Self::Charged
|
||||
} else {
|
||||
Self::Authorized
|
||||
}
|
||||
}
|
||||
Shift4PaymentStatus::Failed => Self::Failure,
|
||||
Shift4PaymentStatus::Pending => Self::Pending,
|
||||
Shift4PaymentStatus::Pending => match next_action {
|
||||
Some(NextAction::Redirect) => Self::AuthenticationPending,
|
||||
Some(NextAction::Wait) | Some(NextAction::None) | None => Self::Pending,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -121,39 +256,65 @@ pub struct Shift4WebhookObjectResource {
|
||||
pub data: serde_json::Value,
|
||||
}
|
||||
|
||||
fn get_payment_status(response: &Shift4PaymentsResponse) -> enums::AttemptStatus {
|
||||
let is_authorized =
|
||||
!response.captured && matches!(response.status, Shift4PaymentStatus::Successful);
|
||||
if is_authorized {
|
||||
enums::AttemptStatus::Authorized
|
||||
} else {
|
||||
enums::AttemptStatus::from(response.status.clone())
|
||||
}
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Shift4PaymentsResponse {
|
||||
pub id: String,
|
||||
pub currency: String,
|
||||
pub amount: u32,
|
||||
pub status: Shift4PaymentStatus,
|
||||
pub captured: bool,
|
||||
pub refunded: bool,
|
||||
pub flow: Option<FlowResponse>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct Shift4PaymentsResponse {
|
||||
id: String,
|
||||
currency: String,
|
||||
amount: u32,
|
||||
status: Shift4PaymentStatus,
|
||||
captured: bool,
|
||||
refunded: bool,
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FlowResponse {
|
||||
pub next_action: Option<NextAction>,
|
||||
pub redirect: Option<Redirect>,
|
||||
pub return_url: Option<Url>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Redirect {
|
||||
pub redirect_url: Option<Url>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum NextAction {
|
||||
Redirect,
|
||||
Wait,
|
||||
None,
|
||||
}
|
||||
|
||||
impl<F, T>
|
||||
TryFrom<types::ResponseRouterData<F, Shift4PaymentsResponse, T, types::PaymentsResponseData>>
|
||||
for types::RouterData<F, T, types::PaymentsResponseData>
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
type Error = Error;
|
||||
fn try_from(
|
||||
item: types::ResponseRouterData<F, Shift4PaymentsResponse, T, types::PaymentsResponseData>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
status: get_payment_status(&item.response),
|
||||
status: enums::AttemptStatus::foreign_from((
|
||||
item.response.captured,
|
||||
item.response
|
||||
.flow
|
||||
.as_ref()
|
||||
.and_then(|flow| flow.next_action.as_ref()),
|
||||
item.response.status,
|
||||
)),
|
||||
response: Ok(types::PaymentsResponseData::TransactionResponse {
|
||||
resource_id: types::ResponseId::ConnectorTransactionId(item.response.id),
|
||||
redirection_data: None,
|
||||
redirection_data: item
|
||||
.response
|
||||
.flow
|
||||
.and_then(|flow| flow.redirect)
|
||||
.and_then(|redirect| redirect.redirect_url)
|
||||
.map(|url| services::RedirectForm::from((url, services::Method::Get))),
|
||||
mandate_reference: None,
|
||||
connector_metadata: None,
|
||||
}),
|
||||
@ -172,7 +333,7 @@ pub struct Shift4RefundRequest {
|
||||
}
|
||||
|
||||
impl<F> TryFrom<&types::RefundsRouterData<F>> for Shift4RefundRequest {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
type Error = Error;
|
||||
fn try_from(item: &types::RefundsRouterData<F>) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
charge_id: item.request.connector_transaction_id.clone(),
|
||||
@ -212,7 +373,7 @@ pub enum Shift4RefundStatus {
|
||||
impl TryFrom<types::RefundsResponseRouterData<api::Execute, RefundResponse>>
|
||||
for types::RefundsRouterData<api::Execute>
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
type Error = Error;
|
||||
fn try_from(
|
||||
item: types::RefundsResponseRouterData<api::Execute, RefundResponse>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
@ -230,7 +391,7 @@ impl TryFrom<types::RefundsResponseRouterData<api::Execute, RefundResponse>>
|
||||
impl TryFrom<types::RefundsResponseRouterData<api::RSync, RefundResponse>>
|
||||
for types::RefundsRouterData<api::RSync>
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
type Error = Error;
|
||||
fn try_from(
|
||||
item: types::RefundsResponseRouterData<api::RSync, RefundResponse>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
|
||||
Reference in New Issue
Block a user