feat(connector): [Deutschebank] Implement Card 3ds (#6844)

Co-authored-by: Debarshi Gupta <debarshi.gupta@Debarshi-Gupta-CM92YWDXFD.local>
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Debarshi Gupta
2025-01-13 16:49:50 +05:30
committed by GitHub
parent 6a1f5a8875
commit ac75335276
8 changed files with 990 additions and 111 deletions

View File

@ -3426,6 +3426,10 @@ pub enum RedirectForm {
access_token: String,
step_up_url: String,
},
DeutschebankThreeDSChallengeFlow {
acs_url: String,
creq: String,
},
Payme,
Braintree {
client_token: String,

View File

@ -59,7 +59,7 @@ use crate::{
types::ResponseRouterData,
utils::{
self, PaymentsAuthorizeRequestData, PaymentsCompleteAuthorizeRequestData,
RefundsRequestData,
RefundsRequestData, RouterData as ConnectorRouterData,
},
};
@ -131,7 +131,7 @@ impl ConnectorCommon for Deutschebank {
}
fn get_currency_unit(&self) -> api::CurrencyUnit {
api::CurrencyUnit::Base
api::CurrencyUnit::Minor
}
fn common_get_content_type(&self) -> &'static str {
@ -311,18 +311,30 @@ impl ConnectorIntegration<Authorize, PaymentsAuthorizeData, PaymentsResponseData
req: &PaymentsAuthorizeRouterData,
connectors: &Connectors,
) -> CustomResult<String, errors::ConnectorError> {
if req.request.connector_mandate_id().is_none() {
let event_id = req.connector_request_reference_id.clone();
let tx_action = if req.request.is_auto_capture()? {
"authorization"
} else {
"preauthorization"
};
if req.is_three_ds() && req.request.is_card() {
Ok(format!(
"{}/services/v2.1/headless3DSecure/event/{event_id}/{tx_action}/initialize",
self.base_url(connectors)
))
} else if !req.is_three_ds() && req.request.is_card() {
Err(errors::ConnectorError::NotSupported {
message: "Non-ThreeDs".to_owned(),
connector: "deutschebank",
}
.into())
} else if req.request.connector_mandate_id().is_none() {
Ok(format!(
"{}/services/v2.1/managedmandate",
self.base_url(connectors)
))
} else {
let event_id = req.connector_request_reference_id.clone();
let tx_action = if req.request.is_auto_capture()? {
"authorization"
} else {
"preauthorization"
};
Ok(format!(
"{}/services/v2.1/payment/event/{event_id}/directdebit/{tx_action}",
self.base_url(connectors)
@ -375,7 +387,19 @@ impl ConnectorIntegration<Authorize, PaymentsAuthorizeData, PaymentsResponseData
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<PaymentsAuthorizeRouterData, errors::ConnectorError> {
if data.request.connector_mandate_id().is_none() {
if data.is_three_ds() && data.request.is_card() {
let response: deutschebank::DeutschebankThreeDSInitializeResponse = res
.response
.parse_struct("DeutschebankPaymentsAuthorizeResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
RouterData::try_from(ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
} else if data.request.connector_mandate_id().is_none() {
let response: deutschebank::DeutschebankMandatePostResponse = res
.response
.parse_struct("DeutschebankMandatePostResponse")
@ -437,10 +461,18 @@ impl ConnectorIntegration<CompleteAuthorize, CompleteAuthorizeData, PaymentsResp
} else {
"preauthorization"
};
Ok(format!(
"{}/services/v2.1/payment/event/{event_id}/directdebit/{tx_action}",
self.base_url(connectors)
))
if req.is_three_ds() && matches!(req.payment_method, enums::PaymentMethod::Card) {
Ok(format!(
"{}/services/v2.1//headless3DSecure/event/{event_id}/final",
self.base_url(connectors)
))
} else {
Ok(format!(
"{}/services/v2.1/payment/event/{event_id}/directdebit/{tx_action}",
self.base_url(connectors)
))
}
}
fn get_request_body(
@ -453,10 +485,9 @@ impl ConnectorIntegration<CompleteAuthorize, CompleteAuthorizeData, PaymentsResp
req.request.minor_amount,
req.request.currency,
)?;
let connector_router_data = deutschebank::DeutschebankRouterData::from((amount, req));
let connector_req =
deutschebank::DeutschebankDirectDebitRequest::try_from(&connector_router_data)?;
deutschebank::DeutschebankCompleteAuthorizeRequest::try_from(&connector_router_data)?;
Ok(RequestContent::Json(Box::new(connector_req)))
}
@ -952,10 +983,20 @@ lazy_static! {
deutschebank_supported_payment_methods.add(
enums::PaymentMethod::BankDebit,
enums::PaymentMethodType::Sepa,
PaymentMethodDetails{
mandates: enums::FeatureStatus::Supported,
refunds: enums::FeatureStatus::Supported,
supported_capture_methods: supported_capture_methods.clone(),
}
);
deutschebank_supported_payment_methods.add(
enums::PaymentMethod::Card,
enums::PaymentMethodType::Credit,
PaymentMethodDetails{
mandates: enums::FeatureStatus::NotSupported,
refunds: enums::FeatureStatus::NotSupported,
supported_capture_methods,
refunds: enums::FeatureStatus::Supported,
supported_capture_methods: supported_capture_methods.clone(),
}
);

View File

@ -1,6 +1,7 @@
use std::collections::HashMap;
use common_enums::enums;
use cards::CardNumber;
use common_enums::{enums, PaymentMethod};
use common_utils::{ext_traits::ValueExt, pii::Email, types::MinorUnit};
use error_stack::ResultExt;
use hyperswitch_domain_models::{
@ -22,14 +23,14 @@ use hyperswitch_domain_models::{
PaymentsCompleteAuthorizeRouterData, RefundsRouterData,
},
};
use hyperswitch_interfaces::errors;
use masking::{PeekInterface, Secret};
use hyperswitch_interfaces::{consts, errors};
use masking::{ExposeInterface, PeekInterface, Secret};
use serde::{Deserialize, Serialize};
use crate::{
types::{PaymentsCancelResponseRouterData, RefundsResponseRouterData, ResponseRouterData},
utils::{
self, AddressDetailsData, PaymentsAuthorizeRequestData,
self, AddressDetailsData, CardData, PaymentsAuthorizeRequestData,
PaymentsCompleteAuthorizeRequestData, RefundsRequestData, RouterData as OtherRouterData,
},
};
@ -129,6 +130,75 @@ pub struct DeutschebankMandatePostRequest {
pub enum DeutschebankPaymentsRequest {
MandatePost(DeutschebankMandatePostRequest),
DirectDebit(DeutschebankDirectDebitRequest),
CreditCard(Box<DeutschebankThreeDSInitializeRequest>),
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct DeutschebankThreeDSInitializeRequest {
means_of_payment: DeutschebankThreeDSInitializeRequestMeansOfPayment,
tds_20_data: DeutschebankThreeDSInitializeRequestTds20Data,
amount_total: DeutschebankThreeDSInitializeRequestAmountTotal,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct DeutschebankThreeDSInitializeRequestMeansOfPayment {
credit_card: DeutschebankThreeDSInitializeRequestCreditCard,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct DeutschebankThreeDSInitializeRequestCreditCard {
number: CardNumber,
expiry_date: DeutschebankThreeDSInitializeRequestCreditCardExpiry,
code: Secret<String>,
cardholder: Secret<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct DeutschebankThreeDSInitializeRequestCreditCardExpiry {
year: Secret<String>,
month: Secret<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct DeutschebankThreeDSInitializeRequestAmountTotal {
amount: MinorUnit,
currency: api_models::enums::Currency,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct DeutschebankThreeDSInitializeRequestTds20Data {
communication_data: DeutschebankThreeDSInitializeRequestCommunicationData,
customer_data: DeutschebankThreeDSInitializeRequestCustomerData,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct DeutschebankThreeDSInitializeRequestCommunicationData {
method_notification_url: String,
cres_notification_url: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct DeutschebankThreeDSInitializeRequestCustomerData {
billing_address: DeutschebankThreeDSInitializeRequestCustomerBillingData,
cardholder_email: Email,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct DeutschebankThreeDSInitializeRequestCustomerBillingData {
street: Secret<String>,
postal_code: Secret<String>,
city: String,
state: Secret<String>,
country: String,
}
impl TryFrom<&DeutschebankRouterData<&PaymentsAuthorizeRouterData>>
@ -148,11 +218,9 @@ impl TryFrom<&DeutschebankRouterData<&PaymentsAuthorizeRouterData>>
None => {
// To facilitate one-off payments via SEPA with Deutsche Bank, we are considering not storing the connector mandate ID in our system if future usage is on-session.
// We will only check for customer acceptance to make a one-off payment. we will be storing the connector mandate details only when setup future usage is off-session.
if item.router_data.request.customer_acceptance.is_some() {
match item.router_data.request.payment_method_data.clone() {
PaymentMethodData::BankDebit(BankDebitData::SepaBankDebit {
iban, ..
}) => {
match item.router_data.request.payment_method_data.clone() {
PaymentMethodData::BankDebit(BankDebitData::SepaBankDebit { iban, .. }) => {
if item.router_data.request.customer_acceptance.is_some() {
let billing_address = item.router_data.get_billing_address()?;
Ok(Self::MandatePost(DeutschebankMandatePostRequest {
approval_by: DeutschebankSEPAApproval::Click,
@ -161,17 +229,60 @@ impl TryFrom<&DeutschebankRouterData<&PaymentsAuthorizeRouterData>>
first_name: billing_address.get_first_name()?.clone(),
last_name: billing_address.get_last_name()?.clone(),
}))
} else {
Err(errors::ConnectorError::MissingRequiredField {
field_name: "customer_acceptance",
}
.into())
}
_ => Err(errors::ConnectorError::NotImplemented(
utils::get_unimplemented_payment_method_error_message("deutschebank"),
)
.into()),
}
} else {
Err(errors::ConnectorError::MissingRequiredField {
field_name: "customer_acceptance",
PaymentMethodData::Card(ccard) => {
if !item.router_data.clone().is_three_ds() {
Err(errors::ConnectorError::NotSupported {
message: "Non-ThreeDs".to_owned(),
connector: "deutschebank",
}
.into())
} else {
let billing_address = item.router_data.get_billing_address()?;
Ok(Self::CreditCard(Box::new(DeutschebankThreeDSInitializeRequest {
means_of_payment: DeutschebankThreeDSInitializeRequestMeansOfPayment {
credit_card: DeutschebankThreeDSInitializeRequestCreditCard {
number: ccard.clone().card_number,
expiry_date: DeutschebankThreeDSInitializeRequestCreditCardExpiry {
year: ccard.get_expiry_year_4_digit(),
month: ccard.card_exp_month,
},
code: ccard.card_cvc,
cardholder: item.router_data.get_billing_full_name()?,
}},
amount_total: DeutschebankThreeDSInitializeRequestAmountTotal {
amount: item.amount,
currency: item.router_data.request.currency,
},
tds_20_data: DeutschebankThreeDSInitializeRequestTds20Data {
communication_data: DeutschebankThreeDSInitializeRequestCommunicationData {
method_notification_url: item.router_data.request.get_complete_authorize_url()?,
cres_notification_url: item.router_data.request.get_complete_authorize_url()?,
},
customer_data: DeutschebankThreeDSInitializeRequestCustomerData {
billing_address: DeutschebankThreeDSInitializeRequestCustomerBillingData {
street: billing_address.get_line1()?.clone(),
postal_code: billing_address.get_zip()?.clone(),
city: billing_address.get_city()?.to_string(),
state: billing_address.get_state()?.clone(),
country: item.router_data.get_billing_country()?.to_string(),
},
cardholder_email: item.router_data.request.get_email()?,
}
}
})))
}
}
.into())
_ => Err(errors::ConnectorError::NotImplemented(
utils::get_unimplemented_payment_method_error_message("deutschebank"),
)
.into()),
}
}
Some(api_models::payments::MandateReferenceId::ConnectorMandateId(mandate_data)) => {
@ -209,6 +320,138 @@ impl TryFrom<&DeutschebankRouterData<&PaymentsAuthorizeRouterData>>
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DeutschebankThreeDSInitializeResponse {
outcome: DeutschebankThreeDSInitializeResponseOutcome,
challenge_required: Option<DeutschebankThreeDSInitializeResponseChallengeRequired>,
processed: Option<DeutschebankThreeDSInitializeResponseProcessed>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DeutschebankThreeDSInitializeResponseProcessed {
rc: String,
message: String,
tx_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum DeutschebankThreeDSInitializeResponseOutcome {
Processed,
ChallengeRequired,
MethodRequired,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DeutschebankThreeDSInitializeResponseChallengeRequired {
acs_url: String,
creq: String,
}
impl
TryFrom<
ResponseRouterData<
Authorize,
DeutschebankThreeDSInitializeResponse,
PaymentsAuthorizeData,
PaymentsResponseData,
>,
> for RouterData<Authorize, PaymentsAuthorizeData, PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: ResponseRouterData<
Authorize,
DeutschebankThreeDSInitializeResponse,
PaymentsAuthorizeData,
PaymentsResponseData,
>,
) -> Result<Self, Self::Error> {
match item.response.outcome {
DeutschebankThreeDSInitializeResponseOutcome::Processed => {
match item.response.processed {
Some(processed) => Ok(Self {
status: if is_response_success(&processed.rc) {
match item.data.request.is_auto_capture()? {
true => common_enums::AttemptStatus::Charged,
false => common_enums::AttemptStatus::Authorized,
}
} else {
common_enums::AttemptStatus::AuthenticationFailed
},
response: Ok(PaymentsResponseData::TransactionResponse {
resource_id: ResponseId::ConnectorTransactionId(
processed.tx_id.clone(),
),
redirection_data: Box::new(None),
mandate_reference: Box::new(None),
connector_metadata: None,
network_txn_id: None,
connector_response_reference_id: Some(processed.tx_id.clone()),
incremental_authorization_allowed: None,
charge_id: None,
}),
..item.data
}),
None => {
let response_string = format!("{:?}", item.response);
Err(
errors::ConnectorError::UnexpectedResponseError(bytes::Bytes::from(
response_string,
))
.into(),
)
}
}
}
DeutschebankThreeDSInitializeResponseOutcome::ChallengeRequired => {
match item.response.challenge_required {
Some(challenge) => Ok(Self {
status: common_enums::AttemptStatus::AuthenticationPending,
response: Ok(PaymentsResponseData::TransactionResponse {
resource_id: ResponseId::NoResponseId,
redirection_data: Box::new(Some(
RedirectForm::DeutschebankThreeDSChallengeFlow {
acs_url: challenge.acs_url,
creq: challenge.creq,
},
)),
mandate_reference: Box::new(None),
connector_metadata: None,
network_txn_id: None,
connector_response_reference_id: None,
incremental_authorization_allowed: None,
charge_id: None,
}),
..item.data
}),
None => {
let response_string = format!("{:?}", item.response);
Err(
errors::ConnectorError::UnexpectedResponseError(bytes::Bytes::from(
response_string,
))
.into(),
)
}
}
}
DeutschebankThreeDSInitializeResponseOutcome::MethodRequired => Ok(Self {
status: common_enums::AttemptStatus::Failure,
response: Err(ErrorResponse {
code: consts::NO_ERROR_CODE.to_owned(),
message: "METHOD_REQUIRED Flow not supported for deutschebank 3ds payments".to_owned(),
reason: Some("METHOD_REQUIRED Flow is not currently supported for deutschebank 3ds payments".to_owned()),
status_code: item.http_code,
attempt_status: None,
connector_transaction_id: None,
}),
..item.data
}),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum DeutschebankSEPAMandateStatus {
@ -450,79 +693,117 @@ pub struct DeutschebankDirectDebitRequest {
mandate: DeutschebankMandate,
}
#[derive(Debug, Serialize)]
#[serde(untagged)]
pub enum DeutschebankCompleteAuthorizeRequest {
DeutschebankDirectDebitRequest(DeutschebankDirectDebitRequest),
DeutschebankThreeDSCompleteAuthorizeRequest(DeutschebankThreeDSCompleteAuthorizeRequest),
}
#[derive(Debug, Serialize, PartialEq)]
pub struct DeutschebankThreeDSCompleteAuthorizeRequest {
cres: String,
}
impl TryFrom<&DeutschebankRouterData<&PaymentsCompleteAuthorizeRouterData>>
for DeutschebankDirectDebitRequest
for DeutschebankCompleteAuthorizeRequest
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: &DeutschebankRouterData<&PaymentsCompleteAuthorizeRouterData>,
) -> Result<Self, Self::Error> {
let account_holder = item.router_data.get_billing_address()?.get_full_name()?;
let redirect_response = item.router_data.request.redirect_response.clone().ok_or(
errors::ConnectorError::MissingRequiredField {
field_name: "redirect_response",
},
)?;
let queries_params = redirect_response
.params
.map(|param| {
let mut queries = HashMap::<String, String>::new();
let values = param.peek().split('&').collect::<Vec<&str>>();
for value in values {
let pair = value.split('=').collect::<Vec<&str>>();
queries.insert(
pair.first()
.ok_or(errors::ConnectorError::ResponseDeserializationFailed)?
.to_string(),
pair.get(1)
.ok_or(errors::ConnectorError::ResponseDeserializationFailed)?
.to_string(),
);
}
Ok::<_, errors::ConnectorError>(queries)
})
.transpose()?
.ok_or(errors::ConnectorError::ResponseDeserializationFailed)?;
let reference = Secret::from(
queries_params
.get("reference")
.ok_or(errors::ConnectorError::MissingRequiredField {
field_name: "reference",
})?
.to_owned(),
);
let signed_on = queries_params
.get("signed_on")
.ok_or(errors::ConnectorError::MissingRequiredField {
field_name: "signed_on",
})?
.to_owned();
if matches!(item.router_data.payment_method, PaymentMethod::Card) {
let redirect_response_payload = item
.router_data
.request
.get_redirect_response_payload()?
.expose();
match item.router_data.request.payment_method_data.clone() {
Some(PaymentMethodData::BankDebit(BankDebitData::SepaBankDebit { iban, .. })) => {
Ok(Self {
amount_total: DeutschebankAmount {
amount: item.amount,
currency: item.router_data.request.currency,
},
means_of_payment: DeutschebankMeansOfPayment {
bank_account: DeutschebankBankAccount {
account_holder,
iban: Secret::from(iban.peek().replace(" ", "")),
let cres = redirect_response_payload
.get("cres")
.and_then(|v| v.as_str())
.map(String::from)
.ok_or(errors::ConnectorError::MissingRequiredField { field_name: "cres" })?;
Ok(Self::DeutschebankThreeDSCompleteAuthorizeRequest(
DeutschebankThreeDSCompleteAuthorizeRequest { cres },
))
} else {
match item.router_data.request.payment_method_data.clone() {
Some(PaymentMethodData::BankDebit(BankDebitData::SepaBankDebit {
iban, ..
})) => {
let account_holder = item.router_data.get_billing_address()?.get_full_name()?;
let redirect_response =
item.router_data.request.redirect_response.clone().ok_or(
errors::ConnectorError::MissingRequiredField {
field_name: "redirect_response",
},
)?;
let queries_params = redirect_response
.params
.map(|param| {
let mut queries = HashMap::<String, String>::new();
let values = param.peek().split('&').collect::<Vec<&str>>();
for value in values {
let pair = value.split('=').collect::<Vec<&str>>();
queries.insert(
pair.first()
.ok_or(
errors::ConnectorError::ResponseDeserializationFailed,
)?
.to_string(),
pair.get(1)
.ok_or(
errors::ConnectorError::ResponseDeserializationFailed,
)?
.to_string(),
);
}
Ok::<_, errors::ConnectorError>(queries)
})
.transpose()?
.ok_or(errors::ConnectorError::ResponseDeserializationFailed)?;
let reference = Secret::from(
queries_params
.get("reference")
.ok_or(errors::ConnectorError::MissingRequiredField {
field_name: "reference",
})?
.to_owned(),
);
let signed_on = queries_params
.get("signed_on")
.ok_or(errors::ConnectorError::MissingRequiredField {
field_name: "signed_on",
})?
.to_owned();
Ok(Self::DeutschebankDirectDebitRequest(
DeutschebankDirectDebitRequest {
amount_total: DeutschebankAmount {
amount: item.amount,
currency: item.router_data.request.currency,
},
means_of_payment: DeutschebankMeansOfPayment {
bank_account: DeutschebankBankAccount {
account_holder,
iban: Secret::from(iban.peek().replace(" ", "")),
},
},
mandate: {
DeutschebankMandate {
reference,
signed_on,
}
},
},
},
mandate: {
DeutschebankMandate {
reference,
signed_on,
}
},
})
))
}
_ => Err(errors::ConnectorError::NotImplemented(
utils::get_unimplemented_payment_method_error_message("deutschebank"),
)
.into()),
}
_ => Err(errors::ConnectorError::NotImplemented(
utils::get_unimplemented_payment_method_error_message("deutschebank"),
)
.into()),
}
}
}
@ -636,6 +917,8 @@ impl
#[serde(rename_all = "UPPERCASE")]
pub enum DeutschebankTransactionKind {
Directdebit,
#[serde(rename = "CREDITCARD_3DS20")]
Creditcard3ds20,
}
#[derive(Debug, Serialize, PartialEq)]
@ -649,10 +932,24 @@ impl TryFrom<&DeutschebankRouterData<&PaymentsCaptureRouterData>> for Deutscheba
fn try_from(
item: &DeutschebankRouterData<&PaymentsCaptureRouterData>,
) -> Result<Self, Self::Error> {
Ok(Self {
changed_amount: item.amount,
kind: DeutschebankTransactionKind::Directdebit,
})
if matches!(item.router_data.payment_method, PaymentMethod::BankDebit) {
Ok(Self {
changed_amount: item.amount,
kind: DeutschebankTransactionKind::Directdebit,
})
} else if item.router_data.is_three_ds()
&& matches!(item.router_data.payment_method, PaymentMethod::Card)
{
Ok(Self {
changed_amount: item.amount,
kind: DeutschebankTransactionKind::Creditcard3ds20,
})
} else {
Err(errors::ConnectorError::NotImplemented(
utils::get_unimplemented_payment_method_error_message("deutschebank"),
)
.into())
}
}
}
@ -772,10 +1069,21 @@ pub struct DeutschebankReversalRequest {
impl TryFrom<&PaymentsCancelRouterData> for DeutschebankReversalRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(_item: &PaymentsCancelRouterData) -> Result<Self, Self::Error> {
Ok(Self {
kind: DeutschebankTransactionKind::Directdebit,
})
fn try_from(item: &PaymentsCancelRouterData) -> Result<Self, Self::Error> {
if matches!(item.payment_method, PaymentMethod::BankDebit) {
Ok(Self {
kind: DeutschebankTransactionKind::Directdebit,
})
} else if item.is_three_ds() && matches!(item.payment_method, PaymentMethod::Card) {
Ok(Self {
kind: DeutschebankTransactionKind::Creditcard3ds20,
})
} else {
Err(errors::ConnectorError::NotImplemented(
utils::get_unimplemented_payment_method_error_message("deutschebank"),
)
.into())
}
}
}
@ -815,10 +1123,24 @@ pub struct DeutschebankRefundRequest {
impl<F> TryFrom<&DeutschebankRouterData<&RefundsRouterData<F>>> for DeutschebankRefundRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &DeutschebankRouterData<&RefundsRouterData<F>>) -> Result<Self, Self::Error> {
Ok(Self {
changed_amount: item.amount.to_owned(),
kind: DeutschebankTransactionKind::Directdebit,
})
if matches!(item.router_data.payment_method, PaymentMethod::BankDebit) {
Ok(Self {
changed_amount: item.amount,
kind: DeutschebankTransactionKind::Directdebit,
})
} else if item.router_data.is_three_ds()
&& matches!(item.router_data.payment_method, PaymentMethod::Card)
{
Ok(Self {
changed_amount: item.amount,
kind: DeutschebankTransactionKind::Creditcard3ds20,
})
} else {
Err(errors::ConnectorError::NotImplemented(
utils::get_unimplemented_payment_method_error_message("deutschebank"),
)
.into())
}
}
}

View File

@ -236,6 +236,10 @@ pub enum RedirectForm {
access_token: String,
step_up_url: String,
},
DeutschebankThreeDSChallengeFlow {
acs_url: String,
creq: String,
},
Payme,
Braintree {
client_token: String,
@ -313,6 +317,9 @@ impl From<RedirectForm> for diesel_models::payment_attempt::RedirectForm {
access_token,
step_up_url,
},
RedirectForm::DeutschebankThreeDSChallengeFlow { acs_url, creq } => {
Self::DeutschebankThreeDSChallengeFlow { acs_url, creq }
}
RedirectForm::Payme => Self::Payme,
RedirectForm::Braintree {
client_token,
@ -392,6 +399,9 @@ impl From<diesel_models::payment_attempt::RedirectForm> for RedirectForm {
access_token,
step_up_url,
},
diesel_models::RedirectForm::DeutschebankThreeDSChallengeFlow { acs_url, creq } => {
Self::DeutschebankThreeDSChallengeFlow { acs_url, creq }
}
diesel_models::payment_attempt::RedirectForm::Payme => Self::Payme,
diesel_models::payment_attempt::RedirectForm::Braintree {
client_token,

View File

@ -942,6 +942,129 @@ impl Default for settings::RequiredFields {
),
}
),
(
enums::Connector::Deutschebank,
RequiredFieldFinal {
mandate: HashMap::new(),
non_mandate : HashMap::from(
[
(
"payment_method_data.card.card_number".to_string(),
RequiredFieldInfo {
required_field: "payment_method_data.card.card_number".to_string(),
display_name: "card_number".to_string(),
field_type: enums::FieldType::UserCardNumber,
value: None,
}
),
(
"payment_method_data.card.card_exp_month".to_string(),
RequiredFieldInfo {
required_field: "payment_method_data.card.card_exp_month".to_string(),
display_name: "card_exp_month".to_string(),
field_type: enums::FieldType::UserCardExpiryMonth,
value: None,
}
),
(
"payment_method_data.card.card_exp_year".to_string(),
RequiredFieldInfo {
required_field: "payment_method_data.card.card_exp_year".to_string(),
display_name: "card_exp_year".to_string(),
field_type: enums::FieldType::UserCardExpiryYear,
value: None,
}
),
(
"payment_method_data.card.card_cvc".to_string(),
RequiredFieldInfo {
required_field: "payment_method_data.card.card_cvc".to_string(),
display_name: "card_cvc".to_string(),
field_type: enums::FieldType::UserCardCvc,
value: None,
}
),
(
"email".to_string(),
RequiredFieldInfo {
required_field: "email".to_string(),
display_name: "email".to_string(),
field_type: enums::FieldType::UserEmailAddress,
value: None,
}
),
(
"billing.address.line1".to_string(),
RequiredFieldInfo {
required_field: "payment_method_data.billing.address.line1".to_string(),
display_name: "line1".to_string(),
field_type: enums::FieldType::UserAddressLine1,
value: None,
}
),
(
"billing.address.city".to_string(),
RequiredFieldInfo {
required_field: "payment_method_data.billing.address.city".to_string(),
display_name: "city".to_string(),
field_type: enums::FieldType::UserAddressCity,
value: None,
}
),
(
"billing.address.state".to_string(),
RequiredFieldInfo {
required_field: "payment_method_data.billing.address.state".to_string(),
display_name: "state".to_string(),
field_type: enums::FieldType::UserAddressState,
value: None,
}
),
(
"billing.address.zip".to_string(),
RequiredFieldInfo {
required_field: "payment_method_data.billing.address.zip".to_string(),
display_name: "zip".to_string(),
field_type: enums::FieldType::UserAddressPincode,
value: None,
}
),
(
"billing.address.country".to_string(),
RequiredFieldInfo {
required_field: "payment_method_data.billing.address.country".to_string(),
display_name: "country".to_string(),
field_type: enums::FieldType::UserAddressCountry{
options: vec![
"ALL".to_string(),
]
},
value: None,
}
),
(
"billing.address.first_name".to_string(),
RequiredFieldInfo {
required_field: "payment_method_data.billing.address.first_name".to_string(),
display_name: "first_name".to_string(),
field_type: enums::FieldType::UserFullName,
value: None,
}
),
(
"billing.address.last_name".to_string(),
RequiredFieldInfo {
required_field: "payment_method_data.billing.address.last_name".to_string(),
display_name: "last_name".to_string(),
field_type: enums::FieldType::UserFullName,
value: None,
}
)
]
),
common: HashMap::new(),
}
),
(
enums::Connector::Dlocal,
RequiredFieldFinal {
@ -4139,6 +4262,129 @@ impl Default for settings::RequiredFields {
),
}
),
(
enums::Connector::Deutschebank,
RequiredFieldFinal {
mandate: HashMap::new(),
non_mandate : HashMap::from(
[
(
"payment_method_data.card.card_number".to_string(),
RequiredFieldInfo {
required_field: "payment_method_data.card.card_number".to_string(),
display_name: "card_number".to_string(),
field_type: enums::FieldType::UserCardNumber,
value: None,
}
),
(
"payment_method_data.card.card_exp_month".to_string(),
RequiredFieldInfo {
required_field: "payment_method_data.card.card_exp_month".to_string(),
display_name: "card_exp_month".to_string(),
field_type: enums::FieldType::UserCardExpiryMonth,
value: None,
}
),
(
"payment_method_data.card.card_exp_year".to_string(),
RequiredFieldInfo {
required_field: "payment_method_data.card.card_exp_year".to_string(),
display_name: "card_exp_year".to_string(),
field_type: enums::FieldType::UserCardExpiryYear,
value: None,
}
),
(
"payment_method_data.card.card_cvc".to_string(),
RequiredFieldInfo {
required_field: "payment_method_data.card.card_cvc".to_string(),
display_name: "card_cvc".to_string(),
field_type: enums::FieldType::UserCardCvc,
value: None,
}
),
(
"email".to_string(),
RequiredFieldInfo {
required_field: "email".to_string(),
display_name: "email".to_string(),
field_type: enums::FieldType::UserEmailAddress,
value: None,
}
),
(
"billing.address.line1".to_string(),
RequiredFieldInfo {
required_field: "payment_method_data.billing.address.line1".to_string(),
display_name: "line1".to_string(),
field_type: enums::FieldType::UserAddressLine1,
value: None,
}
),
(
"billing.address.city".to_string(),
RequiredFieldInfo {
required_field: "payment_method_data.billing.address.city".to_string(),
display_name: "city".to_string(),
field_type: enums::FieldType::UserAddressCity,
value: None,
}
),
(
"billing.address.state".to_string(),
RequiredFieldInfo {
required_field: "payment_method_data.billing.address.state".to_string(),
display_name: "state".to_string(),
field_type: enums::FieldType::UserAddressState,
value: None,
}
),
(
"billing.address.zip".to_string(),
RequiredFieldInfo {
required_field: "payment_method_data.billing.address.zip".to_string(),
display_name: "zip".to_string(),
field_type: enums::FieldType::UserAddressPincode,
value: None,
}
),
(
"billing.address.country".to_string(),
RequiredFieldInfo {
required_field: "payment_method_data.billing.address.country".to_string(),
display_name: "country".to_string(),
field_type: enums::FieldType::UserAddressCountry{
options: vec![
"ALL".to_string(),
]
},
value: None,
}
),
(
"billing.address.first_name".to_string(),
RequiredFieldInfo {
required_field: "payment_method_data.billing.address.first_name".to_string(),
display_name: "first_name".to_string(),
field_type: enums::FieldType::UserFullName,
value: None,
}
),
(
"billing.address.last_name".to_string(),
RequiredFieldInfo {
required_field: "payment_method_data.billing.address.last_name".to_string(),
display_name: "last_name".to_string(),
field_type: enums::FieldType::UserFullName,
value: None,
}
)
]
),
common: HashMap::new(),
}
),
(
enums::Connector::Dlocal,
RequiredFieldFinal {

View File

@ -1535,6 +1535,46 @@ pub fn build_redirection_form(
</script>")))
}}
}
RedirectForm::DeutschebankThreeDSChallengeFlow { acs_url, creq } => {
maud::html! {
(maud::DOCTYPE)
html {
head {
meta name="viewport" content="width=device-width, initial-scale=1";
}
body style="background-color: #ffffff; padding: 20px; font-family: Arial, Helvetica, Sans-Serif;" {
div id="loader1" class="lottie" style="height: 150px; display: block; position: relative; margin-top: 150px; margin-left: auto; margin-right: auto;" { "" }
(PreEscaped(r#"<script src="https://cdnjs.cloudflare.com/ajax/libs/bodymovin/5.7.4/lottie.min.js"></script>"#))
(PreEscaped(r#"
<script>
var anime = bodymovin.loadAnimation({
container: document.getElementById('loader1'),
renderer: 'svg',
loop: true,
autoplay: true,
name: 'hyperswitch loader',
animationData: {"v":"4.8.0","meta":{"g":"LottieFiles AE 3.1.1","a":"","k":"","d":"","tc":""},"fr":29.9700012207031,"ip":0,"op":31.0000012626559,"w":400,"h":250,"nm":"loader_shape","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"circle 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[278.25,202.671,0],"ix":2},"a":{"a":0,"k":[23.72,23.72,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[12.935,0],[0,-12.936],[-12.935,0],[0,12.935]],"o":[[-12.952,0],[0,12.935],[12.935,0],[0,-12.936]],"v":[[0,-23.471],[-23.47,0.001],[0,23.471],[23.47,0.001]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.427451010311,0.976470648074,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[10]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":19.99,"s":[100]},{"t":29.9800012211104,"s":[10]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.72,23.721],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48.0000019550801,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"square 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[196.25,201.271,0],"ix":2},"a":{"a":0,"k":[22.028,22.03,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.914,0],[0,0],[0,-1.914],[0,0],[-1.914,0],[0,0],[0,1.914],[0,0]],"o":[[0,0],[-1.914,0],[0,0],[0,1.914],[0,0],[1.914,0],[0,0],[0,-1.914]],"v":[[18.313,-21.779],[-18.312,-21.779],[-21.779,-18.313],[-21.779,18.314],[-18.312,21.779],[18.313,21.779],[21.779,18.314],[21.779,-18.313]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.427451010311,0.976470648074,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":5,"s":[10]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":14.99,"s":[100]},{"t":24.9800010174563,"s":[10]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[22.028,22.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":47.0000019143492,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Triangle 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[116.25,200.703,0],"ix":2},"a":{"a":0,"k":[27.11,21.243,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.558,-0.879],[0,0],[-1.133,0],[0,0],[0.609,0.947],[0,0]],"o":[[-0.558,-0.879],[0,0],[-0.609,0.947],[0,0],[1.133,0],[0,0],[0,0]],"v":[[1.209,-20.114],[-1.192,-20.114],[-26.251,18.795],[-25.051,20.993],[25.051,20.993],[26.251,18.795],[1.192,-20.114]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.427451010311,0.976470648074,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[10]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":9.99,"s":[100]},{"t":19.9800008138021,"s":[10]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[27.11,21.243],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48.0000019550801,"st":0,"bm":0}],"markers":[]}
})
</script>
"#))
h3 style="text-align: center;" { "Please wait while we process your payment..." }
}
(PreEscaped(format!("<form id=\"PaReqForm\" method=\"POST\" action=\"{acs_url}\">
<input type=\"hidden\" name=\"creq\" value=\"{creq}\">
</form>")))
(PreEscaped(format!("<script>
{logging_template}
window.onload = function() {{
var paReqForm = document.querySelector('#PaReqForm'); if(paReqForm) paReqForm.submit();
}}
</script>")))
}
}
}
RedirectForm::Payme => {
maud::html! {
(maud::DOCTYPE)

View File

@ -0,0 +1,214 @@
const successful3DSCardDetails = {
card_number: "4761739090000088",
card_exp_month: "12",
card_exp_year: "2034",
card_holder_name: "John Doe",
card_cvc: "123",
};
export const connectorDetails = {
card_pm: {
PaymentIntent: {
Request: {
currency: "USD",
customer_acceptance: null,
},
Response: {
status: 200,
body: {
status: "requires_payment_method",
},
},
},
"3DSManualCapture": {
Request: {
payment_method: "card",
payment_method_data: {
card: successful3DSCardDetails,
},
currency: "USD",
customer_acceptance: null,
setup_future_usage: "on_session",
},
Response: {
status: 200,
body: {
status: "requires_customer_action",
},
},
},
"3DSAutoCapture": {
Request: {
payment_method: "card",
payment_method_data: {
card: successful3DSCardDetails,
},
currency: "USD",
customer_acceptance: null,
setup_future_usage: "on_session",
},
Response: {
status: 200,
body: {
status: "requires_customer_action",
},
},
},
No3DSManualCapture: {
Request: {
currency: "USD",
payment_method: "card",
payment_method_data: {
card: successful3DSCardDetails,
},
customer_acceptance: null,
setup_future_usage: "on_session",
},
Response: {
status: 400,
body: {
error: {
type: "invalid_request",
message: "Payment method type not supported",
code: "IR_19",
},
},
},
},
No3DSAutoCapture: {
Request: {
currency: "USD",
payment_method: "card",
payment_method_data: {
card: successful3DSCardDetails,
},
customer_acceptance: null,
setup_future_usage: "on_session",
},
Response: {
status: 400,
body: {
error: {
type: "invalid_request",
message: "Payment method type not supported",
code: "IR_19",
},
},
},
},
Capture: {
Request: {
payment_method: "card",
payment_method_data: {
card: successful3DSCardDetails,
},
customer_acceptance: null,
},
Response: {
status: 200,
body: {
status: "succeeded",
amount: 6500,
amount_capturable: 0,
amount_received: 6500,
},
},
},
PartialCapture: {
Request: {},
Response: {
status: 200,
body: {
status: "partially_captured",
amount: 6500,
amount_capturable: 0,
amount_received: 100,
},
},
},
Refund: {
Configs: {
TRIGGER_SKIP: true,
},
Request: {
payment_method: "card",
payment_method_data: {
card: successful3DSCardDetails,
},
customer_acceptance: null,
},
Response: {
status: 200,
body: {
status: "succeeded",
},
},
},
manualPaymentRefund: {
Configs: {
TRIGGER_SKIP: true,
},
Request: {
payment_method: "card",
payment_method_data: {
card: successful3DSCardDetails,
},
customer_acceptance: null,
},
Response: {
status: 200,
body: {
status: "succeeded",
},
},
},
manualPaymentPartialRefund: {
Configs: {
TRIGGER_SKIP: true,
},
Request: {
payment_method: "card",
payment_method_data: {
card: successful3DSCardDetails,
},
customer_acceptance: null,
},
Response: {
status: 200,
body: {
status: "succeeded",
},
},
},
PartialRefund: {
Configs: {
TRIGGER_SKIP: true,
},
Request: {
payment_method: "card",
payment_method_data: {
card: successful3DSCardDetails,
},
customer_acceptance: null,
},
Response: {
status: 200,
body: {
status: "succeeded",
},
},
},
SyncRefund: {
Configs: {
TRIGGER_SKIP: true,
},
Response: {
status: 200,
body: {
status: "succeeded",
},
},
},
},
};

View File

@ -26,6 +26,7 @@ import { connectorDetails as stripeConnectorDetails } from "./Stripe.js";
import { connectorDetails as trustpayConnectorDetails } from "./Trustpay.js";
import { connectorDetails as wellsfargoConnectorDetails } from "./WellsFargo.js";
import { connectorDetails as worldpayConnectorDetails } from "./WorldPay.js";
import { connectorDetails as deutschebankConnectorDetails } from "./Deutschebank.js";
const connectorDetails = {
adyen: adyenConnectorDetails,
@ -34,6 +35,7 @@ const connectorDetails = {
checkout: checkoutConnectorDetails,
commons: CommonConnectorDetails,
cybersource: cybersourceConnectorDetails,
deutschebank: deutschebankConnectorDetails,
fiservemea: fiservemeaConnectorDetails,
iatapay: iatapayConnectorDetails,
itaubank: itaubankConnectorDetails,