Skip to content

Commit c86f2c0

Browse files
feat(connector): [ACI] Add banking redirect support for EPS, Giropay, iDEAL, and Sofortueberweisung (#890)
Co-authored-by: Arjun Karthik <[email protected]>
1 parent 4397c8e commit c86f2c0

File tree

6 files changed

+194
-19
lines changed

6 files changed

+194
-19
lines changed

crates/api_models/src/payments.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,9 @@ pub enum BankRedirectData {
593593
Giropay {
594594
/// The billing details for bank redirection
595595
billing_details: BankRedirectBilling,
596+
/// Bank account details for Giropay
597+
bank_account_bic: Option<Secret<String>>,
598+
bank_account_iban: Option<Secret<String>>,
596599
},
597600
Ideal {
598601
/// The billing details for bank redirection

crates/router/src/connector/aci/result_codes.rs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
pub(super) const FAILURE_CODES: [&str; 440] = [
1+
pub(super) const FAILURE_CODES: [&str; 499] = [
22
"100.370.100",
33
"100.370.110",
44
"100.370.111",
@@ -439,6 +439,65 @@ pub(super) const FAILURE_CODES: [&str; 440] = [
439439
"100.380.201",
440440
"100.380.305",
441441
"100.380.306",
442+
"800.100.100",
443+
"800.100.150",
444+
"800.100.151",
445+
"800.100.152",
446+
"800.100.153",
447+
"800.100.154",
448+
"800.100.155",
449+
"800.100.156",
450+
"800.100.157",
451+
"800.100.158",
452+
"800.100.159",
453+
"800.100.160",
454+
"800.100.161",
455+
"800.100.162",
456+
"800.100.163",
457+
"800.100.164",
458+
"800.100.165",
459+
"800.100.166",
460+
"800.100.167",
461+
"800.100.168",
462+
"800.100.169",
463+
"800.100.170",
464+
"800.100.171",
465+
"800.100.172",
466+
"800.100.173",
467+
"800.100.174",
468+
"800.100.175",
469+
"800.100.176",
470+
"800.100.177",
471+
"800.100.178",
472+
"800.100.179",
473+
"800.100.190",
474+
"800.100.191",
475+
"800.100.192",
476+
"800.100.195",
477+
"800.100.196",
478+
"800.100.197",
479+
"800.100.198",
480+
"800.100.199",
481+
"800.100.200",
482+
"800.100.201",
483+
"800.100.202",
484+
"800.100.203",
485+
"800.100.204",
486+
"800.100.205",
487+
"800.100.206",
488+
"800.100.207",
489+
"800.100.208",
490+
"800.100.402",
491+
"800.100.403",
492+
"800.100.500",
493+
"800.100.501",
494+
"800.700.100",
495+
"800.700.101",
496+
"800.700.201",
497+
"800.700.500",
498+
"800.800.102",
499+
"800.800.202",
500+
"800.800.302",
442501
];
443502

444503
pub(super) const SUCCESSFUL_CODES: [&str; 16] = [

crates/router/src/connector/aci/transformers.rs

Lines changed: 126 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ use std::str::FromStr;
22

33
use error_stack::report;
44
use masking::Secret;
5+
use reqwest::Url;
56
use serde::{Deserialize, Serialize};
67

78
use super::result_codes::{FAILURE_CODES, PENDING_CODES, SUCCESSFUL_CODES};
89
use crate::{
910
core::errors,
11+
services,
1012
types::{self, api, storage::enums},
1113
};
1214

@@ -47,16 +49,39 @@ pub struct AciCancelRequest {
4749
pub payment_type: AciPaymentType,
4850
}
4951

50-
#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
52+
#[derive(Debug, Clone, Serialize)]
5153
#[serde(untagged)]
5254
pub enum PaymentDetails {
5355
#[serde(rename = "card")]
54-
Card(CardDetails),
56+
AciCard(Box<CardDetails>),
57+
BankRedirect(Box<BankRedirectionPMData>),
5558
#[serde(rename = "bank")]
5659
Wallet,
5760
Klarna,
58-
#[serde(rename = "bankRedirect")]
59-
BankRedirect,
61+
}
62+
63+
#[derive(Debug, Clone, Serialize)]
64+
#[serde(rename_all = "camelCase")]
65+
pub struct BankRedirectionPMData {
66+
payment_brand: PaymentBrand,
67+
#[serde(rename = "bankAccount.country")]
68+
bank_account_country: Option<api_models::enums::CountryCode>,
69+
#[serde(rename = "bankAccount.bankName")]
70+
bank_account_bank_name: Option<String>,
71+
#[serde(rename = "bankAccount.bic")]
72+
bank_account_bic: Option<Secret<String>>,
73+
#[serde(rename = "bankAccount.iban")]
74+
bank_account_iban: Option<Secret<String>>,
75+
shopper_result_url: Option<String>,
76+
}
77+
78+
#[derive(Debug, Clone, Serialize, Deserialize)]
79+
#[serde(rename_all = "UPPERCASE")]
80+
pub enum PaymentBrand {
81+
Eps,
82+
Ideal,
83+
Giropay,
84+
Sofortueberweisung,
6085
}
6186

6287
#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
@@ -101,16 +126,64 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for AciPaymentsRequest {
101126
type Error = error_stack::Report<errors::ConnectorError>;
102127
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
103128
let payment_details: PaymentDetails = match item.request.payment_method_data.clone() {
104-
api::PaymentMethodData::Card(ccard) => PaymentDetails::Card(CardDetails {
129+
api::PaymentMethodData::Card(ccard) => PaymentDetails::AciCard(Box::new(CardDetails {
105130
card_number: ccard.card_number,
106131
card_holder: ccard.card_holder_name,
107132
card_expiry_month: ccard.card_exp_month,
108133
card_expiry_year: ccard.card_exp_year,
109134
card_cvv: ccard.card_cvc,
110-
}),
135+
})),
111136
api::PaymentMethodData::PayLater(_) => PaymentDetails::Klarna,
112137
api::PaymentMethodData::Wallet(_) => PaymentDetails::Wallet,
113-
api::PaymentMethodData::BankRedirect(_) => PaymentDetails::BankRedirect,
138+
api::PaymentMethodData::BankRedirect(ref redirect_banking_data) => {
139+
match redirect_banking_data {
140+
api_models::payments::BankRedirectData::Eps { .. } => {
141+
PaymentDetails::BankRedirect(Box::new(BankRedirectionPMData {
142+
payment_brand: PaymentBrand::Eps,
143+
bank_account_country: Some(api_models::enums::CountryCode::AT),
144+
bank_account_bank_name: None,
145+
bank_account_bic: None,
146+
bank_account_iban: None,
147+
shopper_result_url: item.request.router_return_url.clone(),
148+
}))
149+
}
150+
api_models::payments::BankRedirectData::Giropay {
151+
bank_account_bic,
152+
bank_account_iban,
153+
..
154+
} => PaymentDetails::BankRedirect(Box::new(BankRedirectionPMData {
155+
payment_brand: PaymentBrand::Giropay,
156+
bank_account_country: Some(api_models::enums::CountryCode::DE),
157+
bank_account_bank_name: None,
158+
bank_account_bic: bank_account_bic.clone(),
159+
bank_account_iban: bank_account_iban.clone(),
160+
shopper_result_url: item.request.router_return_url.clone(),
161+
})),
162+
api_models::payments::BankRedirectData::Ideal { bank_name, .. } => {
163+
PaymentDetails::BankRedirect(Box::new(BankRedirectionPMData {
164+
payment_brand: PaymentBrand::Ideal,
165+
bank_account_country: Some(api_models::enums::CountryCode::NL),
166+
bank_account_bank_name: Some(bank_name.to_string()),
167+
bank_account_bic: None,
168+
bank_account_iban: None,
169+
shopper_result_url: item.request.router_return_url.clone(),
170+
}))
171+
}
172+
api_models::payments::BankRedirectData::Sofort { country, .. } => {
173+
PaymentDetails::BankRedirect(Box::new(BankRedirectionPMData {
174+
payment_brand: PaymentBrand::Sofortueberweisung,
175+
bank_account_country: Some(*country),
176+
bank_account_bank_name: None,
177+
bank_account_bic: None,
178+
bank_account_iban: None,
179+
shopper_result_url: item.request.router_return_url.clone(),
180+
}))
181+
}
182+
_ => Err(errors::ConnectorError::NotImplemented(
183+
"Payment method".to_string(),
184+
))?,
185+
}
186+
}
114187
api::PaymentMethodData::Crypto(_) | api::PaymentMethodData::BankDebit(_) => {
115188
Err(errors::ConnectorError::NotSupported {
116189
payment_method: format!("{:?}", item.payment_method),
@@ -152,6 +225,7 @@ pub enum AciPaymentStatus {
152225
Failed,
153226
#[default]
154227
Pending,
228+
RedirectShopper,
155229
}
156230

157231
impl From<AciPaymentStatus> for enums::AttemptStatus {
@@ -160,6 +234,7 @@ impl From<AciPaymentStatus> for enums::AttemptStatus {
160234
AciPaymentStatus::Succeeded => Self::Charged,
161235
AciPaymentStatus::Failed => Self::Failure,
162236
AciPaymentStatus::Pending => Self::Authorizing,
237+
AciPaymentStatus::RedirectShopper => Self::AuthenticationPending,
163238
}
164239
}
165240
}
@@ -180,7 +255,7 @@ impl FromStr for AciPaymentStatus {
180255
}
181256
}
182257

183-
#[derive(Default, Clone, Deserialize, PartialEq, Eq)]
258+
#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq)]
184259
#[serde(rename_all = "camelCase")]
185260
pub struct AciPaymentsResponse {
186261
id: String,
@@ -189,6 +264,21 @@ pub struct AciPaymentsResponse {
189264
timestamp: String,
190265
build_number: String,
191266
pub(super) result: ResultCode,
267+
pub(super) redirect: Option<AciRedirectionData>,
268+
}
269+
270+
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
271+
#[serde(rename_all = "camelCase")]
272+
pub struct AciRedirectionData {
273+
method: Option<services::Method>,
274+
parameters: Vec<Parameters>,
275+
url: Url,
276+
}
277+
278+
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
279+
pub struct Parameters {
280+
name: String,
281+
value: String,
192282
}
193283

194284
#[derive(Default, Debug, Clone, Deserialize, PartialEq, Eq)]
@@ -214,13 +304,37 @@ impl<F, T>
214304
fn try_from(
215305
item: types::ResponseRouterData<F, AciPaymentsResponse, T, types::PaymentsResponseData>,
216306
) -> Result<Self, Self::Error> {
307+
let redirection_data = item.response.redirect.map(|data| {
308+
let form_fields = std::collections::HashMap::<_, _>::from_iter(
309+
data.parameters
310+
.iter()
311+
.map(|parameter| (parameter.name.clone(), parameter.value.clone())),
312+
);
313+
314+
// If method is Get, parameters are appended to URL
315+
// If method is post, we http Post the method to URL
316+
services::RedirectForm::Form {
317+
endpoint: data.url.to_string(),
318+
// Handles method for Bank redirects currently.
319+
// 3DS response have method within preconditions. That would require replacing below line with a function.
320+
method: data.method.unwrap_or(services::Method::Post),
321+
form_fields,
322+
}
323+
});
324+
217325
Ok(Self {
218-
status: enums::AttemptStatus::from(AciPaymentStatus::from_str(
219-
&item.response.result.code,
220-
)?),
326+
status: {
327+
if redirection_data.is_some() {
328+
enums::AttemptStatus::from(AciPaymentStatus::RedirectShopper)
329+
} else {
330+
enums::AttemptStatus::from(AciPaymentStatus::from_str(
331+
&item.response.result.code,
332+
)?)
333+
}
334+
},
221335
response: Ok(types::PaymentsResponseData::TransactionResponse {
222336
resource_id: types::ResponseId::ConnectorTransactionId(item.response.id),
223-
redirection_data: None,
337+
redirection_data,
224338
mandate_reference: None,
225339
connector_metadata: None,
226340
}),

crates/router/src/connector/adyen.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -375,11 +375,7 @@ impl
375375

376376
fn build_request(
377377
&self,
378-
req: &types::RouterData<
379-
api::Authorize,
380-
types::PaymentsAuthorizeData,
381-
types::PaymentsResponseData,
382-
>,
378+
req: &types::PaymentsAuthorizeRouterData,
383379
connectors: &settings::Connectors,
384380
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
385381
Ok(Some(

crates/router/src/connector/stripe/transformers.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -571,7 +571,9 @@ impl TryFrom<&payments::BankRedirectData> for StripeBillingAddress {
571571
name: Some(billing_details.billing_name.clone()),
572572
..Self::default()
573573
}),
574-
payments::BankRedirectData::Giropay { billing_details } => Ok(Self {
574+
payments::BankRedirectData::Giropay {
575+
billing_details, ..
576+
} => Ok(Self {
575577
name: Some(billing_details.billing_name.clone()),
576578
..Self::default()
577579
}),

crates/router/src/types.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,7 @@ pub struct ConnectorResponse {
459459
pub return_url: Option<String>,
460460
pub three_ds_form: Option<services::RedirectForm>,
461461
}
462+
462463
pub struct ResponseRouterData<Flow, R, Request, Response> {
463464
pub response: R,
464465
pub data: RouterData<Flow, Request, Response>,

0 commit comments

Comments
 (0)