diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index 03facc2ebab4..a450a30fa760 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -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, diff --git a/crates/hyperswitch_connectors/src/connectors/deutschebank.rs b/crates/hyperswitch_connectors/src/connectors/deutschebank.rs index 3098d27e7ecb..f4d5fe5fdf15 100644 --- a/crates/hyperswitch_connectors/src/connectors/deutschebank.rs +++ b/crates/hyperswitch_connectors/src/connectors/deutschebank.rs @@ -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 CustomResult { - 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, res: Response, ) -> CustomResult { - 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), +} + +#[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, + cardholder: Secret, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct DeutschebankThreeDSInitializeRequestCreditCardExpiry { + year: Secret, + month: Secret, +} + +#[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, + postal_code: Secret, + city: String, + state: Secret, + 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, + processed: Option, +} + +#[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 +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData< + Authorize, + DeutschebankThreeDSInitializeResponse, + PaymentsAuthorizeData, + PaymentsResponseData, + >, + ) -> Result { + 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; fn try_from( item: &DeutschebankRouterData<&PaymentsCompleteAuthorizeRouterData>, ) -> Result { - 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::::new(); - let values = param.peek().split('&').collect::>(); - for value in values { - let pair = value.split('=').collect::>(); - queries.insert( - pair.first() - .ok_or(errors::ConnectorError::ResponseDeserializationFailed)? - .to_string(), - pair.get(1) - .ok_or(errors::ConnectorError::ResponseDeserializationFailed)? - .to_string(), + if matches!(item.router_data.payment_method, PaymentMethod::Card) { + let redirect_response_payload = item + .router_data + .request + .get_redirect_response_payload()? + .expose(); + + 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::::new(); + let values = param.peek().split('&').collect::>(); + for value in values { + let pair = value.split('=').collect::>(); + 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(), ); - } - 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(); - - 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 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 { - 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; - fn try_from(_item: &PaymentsCancelRouterData) -> Result { - Ok(Self { - kind: DeutschebankTransactionKind::Directdebit, - }) + fn try_from(item: &PaymentsCancelRouterData) -> Result { + 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 TryFrom<&DeutschebankRouterData<&RefundsRouterData>> for DeutschebankRefundRequest { type Error = error_stack::Report; fn try_from(item: &DeutschebankRouterData<&RefundsRouterData>) -> Result { - 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()) + } } } diff --git a/crates/hyperswitch_domain_models/src/router_response_types.rs b/crates/hyperswitch_domain_models/src/router_response_types.rs index 61453f36b84e..613797e2988c 100644 --- a/crates/hyperswitch_domain_models/src/router_response_types.rs +++ b/crates/hyperswitch_domain_models/src/router_response_types.rs @@ -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 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 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, diff --git a/crates/router/src/configs/defaults/payment_connector_required_fields.rs b/crates/router/src/configs/defaults/payment_connector_required_fields.rs index 9e42aec4a51d..b4ada6fd8fe9 100644 --- a/crates/router/src/configs/defaults/payment_connector_required_fields.rs +++ b/crates/router/src/configs/defaults/payment_connector_required_fields.rs @@ -941,6 +941,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 { @@ -4138,6 +4261,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 { diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index cac856b2c48b..0c2cc0bb86fd 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -1532,6 +1532,46 @@ pub fn build_redirection_form( "))) }} } + 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#""#)) + + (PreEscaped(r#" + + "#)) + + h3 style="text-align: center;" { "Please wait while we process your payment..." } + } + (PreEscaped(format!("
+ +
"))) + (PreEscaped(format!(""))) + } + } + } RedirectForm::Payme => { maud::html! { (maud::DOCTYPE) diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Deutschebank.js b/cypress-tests/cypress/e2e/PaymentUtils/Deutschebank.js new file mode 100644 index 000000000000..591e6c8869e2 --- /dev/null +++ b/cypress-tests/cypress/e2e/PaymentUtils/Deutschebank.js @@ -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", + }, + }, + }, + }, +}; diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Utils.js b/cypress-tests/cypress/e2e/PaymentUtils/Utils.js index 72bd6451347a..e756c0593de3 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Utils.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Utils.js @@ -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,