mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-27 11:24:45 +08:00
feat(connector): [ACI] Add banking redirect support for EPS, Giropay, iDEAL, and Sofortueberweisung (#890)
Co-authored-by: Arjun Karthik <m.arjunkarthik@gmail.com>
This commit is contained in:
@ -593,6 +593,9 @@ pub enum BankRedirectData {
|
||||
Giropay {
|
||||
/// The billing details for bank redirection
|
||||
billing_details: BankRedirectBilling,
|
||||
/// Bank account details for Giropay
|
||||
bank_account_bic: Option<Secret<String>>,
|
||||
bank_account_iban: Option<Secret<String>>,
|
||||
},
|
||||
Ideal {
|
||||
/// The billing details for bank redirection
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
pub(super) const FAILURE_CODES: [&str; 440] = [
|
||||
pub(super) const FAILURE_CODES: [&str; 499] = [
|
||||
"100.370.100",
|
||||
"100.370.110",
|
||||
"100.370.111",
|
||||
@ -439,6 +439,65 @@ pub(super) const FAILURE_CODES: [&str; 440] = [
|
||||
"100.380.201",
|
||||
"100.380.305",
|
||||
"100.380.306",
|
||||
"800.100.100",
|
||||
"800.100.150",
|
||||
"800.100.151",
|
||||
"800.100.152",
|
||||
"800.100.153",
|
||||
"800.100.154",
|
||||
"800.100.155",
|
||||
"800.100.156",
|
||||
"800.100.157",
|
||||
"800.100.158",
|
||||
"800.100.159",
|
||||
"800.100.160",
|
||||
"800.100.161",
|
||||
"800.100.162",
|
||||
"800.100.163",
|
||||
"800.100.164",
|
||||
"800.100.165",
|
||||
"800.100.166",
|
||||
"800.100.167",
|
||||
"800.100.168",
|
||||
"800.100.169",
|
||||
"800.100.170",
|
||||
"800.100.171",
|
||||
"800.100.172",
|
||||
"800.100.173",
|
||||
"800.100.174",
|
||||
"800.100.175",
|
||||
"800.100.176",
|
||||
"800.100.177",
|
||||
"800.100.178",
|
||||
"800.100.179",
|
||||
"800.100.190",
|
||||
"800.100.191",
|
||||
"800.100.192",
|
||||
"800.100.195",
|
||||
"800.100.196",
|
||||
"800.100.197",
|
||||
"800.100.198",
|
||||
"800.100.199",
|
||||
"800.100.200",
|
||||
"800.100.201",
|
||||
"800.100.202",
|
||||
"800.100.203",
|
||||
"800.100.204",
|
||||
"800.100.205",
|
||||
"800.100.206",
|
||||
"800.100.207",
|
||||
"800.100.208",
|
||||
"800.100.402",
|
||||
"800.100.403",
|
||||
"800.100.500",
|
||||
"800.100.501",
|
||||
"800.700.100",
|
||||
"800.700.101",
|
||||
"800.700.201",
|
||||
"800.700.500",
|
||||
"800.800.102",
|
||||
"800.800.202",
|
||||
"800.800.302",
|
||||
];
|
||||
|
||||
pub(super) const SUCCESSFUL_CODES: [&str; 16] = [
|
||||
|
||||
@ -2,11 +2,13 @@ use std::str::FromStr;
|
||||
|
||||
use error_stack::report;
|
||||
use masking::Secret;
|
||||
use reqwest::Url;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::result_codes::{FAILURE_CODES, PENDING_CODES, SUCCESSFUL_CODES};
|
||||
use crate::{
|
||||
core::errors,
|
||||
services,
|
||||
types::{self, api, storage::enums},
|
||||
};
|
||||
|
||||
@ -47,16 +49,39 @@ pub struct AciCancelRequest {
|
||||
pub payment_type: AciPaymentType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum PaymentDetails {
|
||||
#[serde(rename = "card")]
|
||||
Card(CardDetails),
|
||||
AciCard(Box<CardDetails>),
|
||||
BankRedirect(Box<BankRedirectionPMData>),
|
||||
#[serde(rename = "bank")]
|
||||
Wallet,
|
||||
Klarna,
|
||||
#[serde(rename = "bankRedirect")]
|
||||
BankRedirect,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BankRedirectionPMData {
|
||||
payment_brand: PaymentBrand,
|
||||
#[serde(rename = "bankAccount.country")]
|
||||
bank_account_country: Option<api_models::enums::CountryCode>,
|
||||
#[serde(rename = "bankAccount.bankName")]
|
||||
bank_account_bank_name: Option<String>,
|
||||
#[serde(rename = "bankAccount.bic")]
|
||||
bank_account_bic: Option<Secret<String>>,
|
||||
#[serde(rename = "bankAccount.iban")]
|
||||
bank_account_iban: Option<Secret<String>>,
|
||||
shopper_result_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub enum PaymentBrand {
|
||||
Eps,
|
||||
Ideal,
|
||||
Giropay,
|
||||
Sofortueberweisung,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
|
||||
@ -101,16 +126,64 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for AciPaymentsRequest {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
|
||||
let payment_details: PaymentDetails = match item.request.payment_method_data.clone() {
|
||||
api::PaymentMethodData::Card(ccard) => PaymentDetails::Card(CardDetails {
|
||||
api::PaymentMethodData::Card(ccard) => PaymentDetails::AciCard(Box::new(CardDetails {
|
||||
card_number: ccard.card_number,
|
||||
card_holder: ccard.card_holder_name,
|
||||
card_expiry_month: ccard.card_exp_month,
|
||||
card_expiry_year: ccard.card_exp_year,
|
||||
card_cvv: ccard.card_cvc,
|
||||
}),
|
||||
})),
|
||||
api::PaymentMethodData::PayLater(_) => PaymentDetails::Klarna,
|
||||
api::PaymentMethodData::Wallet(_) => PaymentDetails::Wallet,
|
||||
api::PaymentMethodData::BankRedirect(_) => PaymentDetails::BankRedirect,
|
||||
api::PaymentMethodData::BankRedirect(ref redirect_banking_data) => {
|
||||
match redirect_banking_data {
|
||||
api_models::payments::BankRedirectData::Eps { .. } => {
|
||||
PaymentDetails::BankRedirect(Box::new(BankRedirectionPMData {
|
||||
payment_brand: PaymentBrand::Eps,
|
||||
bank_account_country: Some(api_models::enums::CountryCode::AT),
|
||||
bank_account_bank_name: None,
|
||||
bank_account_bic: None,
|
||||
bank_account_iban: None,
|
||||
shopper_result_url: item.request.router_return_url.clone(),
|
||||
}))
|
||||
}
|
||||
api_models::payments::BankRedirectData::Giropay {
|
||||
bank_account_bic,
|
||||
bank_account_iban,
|
||||
..
|
||||
} => PaymentDetails::BankRedirect(Box::new(BankRedirectionPMData {
|
||||
payment_brand: PaymentBrand::Giropay,
|
||||
bank_account_country: Some(api_models::enums::CountryCode::DE),
|
||||
bank_account_bank_name: None,
|
||||
bank_account_bic: bank_account_bic.clone(),
|
||||
bank_account_iban: bank_account_iban.clone(),
|
||||
shopper_result_url: item.request.router_return_url.clone(),
|
||||
})),
|
||||
api_models::payments::BankRedirectData::Ideal { bank_name, .. } => {
|
||||
PaymentDetails::BankRedirect(Box::new(BankRedirectionPMData {
|
||||
payment_brand: PaymentBrand::Ideal,
|
||||
bank_account_country: Some(api_models::enums::CountryCode::NL),
|
||||
bank_account_bank_name: Some(bank_name.to_string()),
|
||||
bank_account_bic: None,
|
||||
bank_account_iban: None,
|
||||
shopper_result_url: item.request.router_return_url.clone(),
|
||||
}))
|
||||
}
|
||||
api_models::payments::BankRedirectData::Sofort { country, .. } => {
|
||||
PaymentDetails::BankRedirect(Box::new(BankRedirectionPMData {
|
||||
payment_brand: PaymentBrand::Sofortueberweisung,
|
||||
bank_account_country: Some(*country),
|
||||
bank_account_bank_name: None,
|
||||
bank_account_bic: None,
|
||||
bank_account_iban: None,
|
||||
shopper_result_url: item.request.router_return_url.clone(),
|
||||
}))
|
||||
}
|
||||
_ => Err(errors::ConnectorError::NotImplemented(
|
||||
"Payment method".to_string(),
|
||||
))?,
|
||||
}
|
||||
}
|
||||
api::PaymentMethodData::Crypto(_) | api::PaymentMethodData::BankDebit(_) => {
|
||||
Err(errors::ConnectorError::NotSupported {
|
||||
payment_method: format!("{:?}", item.payment_method),
|
||||
@ -152,6 +225,7 @@ pub enum AciPaymentStatus {
|
||||
Failed,
|
||||
#[default]
|
||||
Pending,
|
||||
RedirectShopper,
|
||||
}
|
||||
|
||||
impl From<AciPaymentStatus> for enums::AttemptStatus {
|
||||
@ -160,6 +234,7 @@ impl From<AciPaymentStatus> for enums::AttemptStatus {
|
||||
AciPaymentStatus::Succeeded => Self::Charged,
|
||||
AciPaymentStatus::Failed => Self::Failure,
|
||||
AciPaymentStatus::Pending => Self::Authorizing,
|
||||
AciPaymentStatus::RedirectShopper => Self::AuthenticationPending,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -180,7 +255,7 @@ impl FromStr for AciPaymentStatus {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AciPaymentsResponse {
|
||||
id: String,
|
||||
@ -189,6 +264,21 @@ pub struct AciPaymentsResponse {
|
||||
timestamp: String,
|
||||
build_number: String,
|
||||
pub(super) result: ResultCode,
|
||||
pub(super) redirect: Option<AciRedirectionData>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AciRedirectionData {
|
||||
method: Option<services::Method>,
|
||||
parameters: Vec<Parameters>,
|
||||
url: Url,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
|
||||
pub struct Parameters {
|
||||
name: String,
|
||||
value: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||
@ -214,13 +304,37 @@ impl<F, T>
|
||||
fn try_from(
|
||||
item: types::ResponseRouterData<F, AciPaymentsResponse, T, types::PaymentsResponseData>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
let redirection_data = item.response.redirect.map(|data| {
|
||||
let form_fields = std::collections::HashMap::<_, _>::from_iter(
|
||||
data.parameters
|
||||
.iter()
|
||||
.map(|parameter| (parameter.name.clone(), parameter.value.clone())),
|
||||
);
|
||||
|
||||
// If method is Get, parameters are appended to URL
|
||||
// If method is post, we http Post the method to URL
|
||||
services::RedirectForm::Form {
|
||||
endpoint: data.url.to_string(),
|
||||
// Handles method for Bank redirects currently.
|
||||
// 3DS response have method within preconditions. That would require replacing below line with a function.
|
||||
method: data.method.unwrap_or(services::Method::Post),
|
||||
form_fields,
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
status: enums::AttemptStatus::from(AciPaymentStatus::from_str(
|
||||
&item.response.result.code,
|
||||
)?),
|
||||
status: {
|
||||
if redirection_data.is_some() {
|
||||
enums::AttemptStatus::from(AciPaymentStatus::RedirectShopper)
|
||||
} else {
|
||||
enums::AttemptStatus::from(AciPaymentStatus::from_str(
|
||||
&item.response.result.code,
|
||||
)?)
|
||||
}
|
||||
},
|
||||
response: Ok(types::PaymentsResponseData::TransactionResponse {
|
||||
resource_id: types::ResponseId::ConnectorTransactionId(item.response.id),
|
||||
redirection_data: None,
|
||||
redirection_data,
|
||||
mandate_reference: None,
|
||||
connector_metadata: None,
|
||||
}),
|
||||
|
||||
@ -375,11 +375,7 @@ impl
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &types::RouterData<
|
||||
api::Authorize,
|
||||
types::PaymentsAuthorizeData,
|
||||
types::PaymentsResponseData,
|
||||
>,
|
||||
req: &types::PaymentsAuthorizeRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
||||
Ok(Some(
|
||||
|
||||
@ -571,7 +571,9 @@ impl TryFrom<&payments::BankRedirectData> for StripeBillingAddress {
|
||||
name: Some(billing_details.billing_name.clone()),
|
||||
..Self::default()
|
||||
}),
|
||||
payments::BankRedirectData::Giropay { billing_details } => Ok(Self {
|
||||
payments::BankRedirectData::Giropay {
|
||||
billing_details, ..
|
||||
} => Ok(Self {
|
||||
name: Some(billing_details.billing_name.clone()),
|
||||
..Self::default()
|
||||
}),
|
||||
|
||||
@ -459,6 +459,7 @@ pub struct ConnectorResponse {
|
||||
pub return_url: Option<String>,
|
||||
pub three_ds_form: Option<services::RedirectForm>,
|
||||
}
|
||||
|
||||
pub struct ResponseRouterData<Flow, R, Request, Response> {
|
||||
pub response: R,
|
||||
pub data: RouterData<Flow, Request, Response>,
|
||||
|
||||
Reference in New Issue
Block a user