diff --git a/crates/router/src/connector/bluesnap.rs b/crates/router/src/connector/bluesnap.rs index 1bbe57b2c2..3b2f44ec54 100644 --- a/crates/router/src/connector/bluesnap.rs +++ b/crates/router/src/connector/bluesnap.rs @@ -1094,9 +1094,15 @@ impl services::ConnectorRedirectResponse for Bluesnap { match redirection_result.status.as_str() { "Success" => Ok(payments::CallConnectorAction::Trigger), - _ => Ok(payments::CallConnectorAction::StatusUpdate( - enums::AttemptStatus::AuthenticationFailed, - )), + _ => Ok(payments::CallConnectorAction::StatusUpdate { + status: enums::AttemptStatus::AuthenticationFailed, + error_code: redirection_result.code, + error_message: redirection_result + .info + .as_ref() + .and_then(|info| info.errors.as_ref().and_then(|error| error.first())) + .cloned(), + }), } } } diff --git a/crates/router/src/connector/bluesnap/transformers.rs b/crates/router/src/connector/bluesnap/transformers.rs index ce87a25541..944f9cc3d3 100644 --- a/crates/router/src/connector/bluesnap/transformers.rs +++ b/crates/router/src/connector/bluesnap/transformers.rs @@ -1,6 +1,7 @@ use api_models::enums as api_enums; use base64::Engine; use common_utils::{ + errors::CustomResult, ext_traits::{ByteSliceExt, StringExt, ValueExt}, pii::Email, }; @@ -9,7 +10,7 @@ use masking::ExposeInterface; use serde::{Deserialize, Serialize}; use crate::{ - connector::utils::{self, RouterData}, + connector::utils::{self, AddressDetailsData, PaymentsAuthorizeRequestData, RouterData}, consts, core::errors, pii::Secret, @@ -27,6 +28,15 @@ pub struct BluesnapPaymentsRequest { card_transaction_type: BluesnapTxnType, three_d_secure: Option, transaction_fraud_info: Option, + card_holder_info: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BluesnapCardHolderInfo { + first_name: Secret, + last_name: Secret, + email: Email, } #[derive(Debug, Serialize)] @@ -149,13 +159,16 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for BluesnapPaymentsRequest { Some(enums::CaptureMethod::Manual) => BluesnapTxnType::AuthOnly, _ => BluesnapTxnType::AuthCapture, }; - let payment_method = match item.request.payment_method_data.clone() { - api::PaymentMethodData::Card(ccard) => Ok(PaymentMethodDetails::CreditCard(Card { - card_number: ccard.card_number, - expiration_month: ccard.card_exp_month.clone(), - expiration_year: ccard.card_exp_year.clone(), - security_code: ccard.card_cvc, - })), + let (payment_method, card_holder_info) = match item.request.payment_method_data.clone() { + api::PaymentMethodData::Card(ccard) => Ok(( + PaymentMethodDetails::CreditCard(Card { + card_number: ccard.card_number, + expiration_month: ccard.card_exp_month.clone(), + expiration_year: ccard.card_exp_year.clone(), + security_code: ccard.card_cvc, + }), + get_card_holder_info(item)?, + )), api::PaymentMethodData::Wallet(wallet_data) => match wallet_data { api_models::payments::WalletData::GooglePay(payment_method_data) => { let gpay_object = Encode::::encode_to_string_of_json( @@ -166,10 +179,13 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for BluesnapPaymentsRequest { }, ) .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(PaymentMethodDetails::Wallet(BluesnapWallet { - wallet_type: BluesnapWalletTypes::GooglePay, - encoded_payment_token: consts::BASE64_ENGINE.encode(gpay_object), - })) + Ok(( + PaymentMethodDetails::Wallet(BluesnapWallet { + wallet_type: BluesnapWalletTypes::GooglePay, + encoded_payment_token: consts::BASE64_ENGINE.encode(gpay_object), + }), + None, + )) } api_models::payments::WalletData::ApplePay(payment_method_data) => { let apple_pay_payment_data = consts::BASE64_ENGINE @@ -230,10 +246,13 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for BluesnapPaymentsRequest { ) .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(PaymentMethodDetails::Wallet(BluesnapWallet { - wallet_type: BluesnapWalletTypes::ApplePay, - encoded_payment_token: consts::BASE64_ENGINE.encode(apple_pay_object), - })) + Ok(( + PaymentMethodDetails::Wallet(BluesnapWallet { + wallet_type: BluesnapWalletTypes::ApplePay, + encoded_payment_token: consts::BASE64_ENGINE.encode(apple_pay_object), + }), + None, + )) } _ => Err(errors::ConnectorError::NotImplemented( "Wallets".to_string(), @@ -252,6 +271,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for BluesnapPaymentsRequest { transaction_fraud_info: Some(TransactionFraudInfo { fraud_session_id: item.payment_id.clone(), }), + card_holder_info, }) } } @@ -397,6 +417,7 @@ impl TryFrom<&types::PaymentsCompleteAuthorizeRouterData> for BluesnapPaymentsRe transaction_fraud_info: Some(TransactionFraudInfo { fraud_session_id: item.payment_id.clone(), }), + card_holder_info: None, }) } } @@ -411,6 +432,14 @@ pub struct BluesnapRedirectionResponse { pub struct BluesnapThreeDsResult { three_d_secure: Option, pub status: String, + pub code: Option, + pub info: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RedirectErrorMessage { + pub errors: Option>, } #[derive(Debug, Deserialize)] @@ -759,3 +788,14 @@ pub enum BluesnapErrors { PaymentError(BluesnapErrorResponse), AuthError(BluesnapAuthErrorResponse), } + +fn get_card_holder_info( + item: &types::PaymentsAuthorizeRouterData, +) -> CustomResult, errors::ConnectorError> { + let address = item.get_billing_address()?; + Ok(Some(BluesnapCardHolderInfo { + first_name: address.get_first_name()?.clone(), + last_name: address.get_last_name()?.clone(), + email: item.request.get_email()?, + })) +} diff --git a/crates/router/src/connector/checkout.rs b/crates/router/src/connector/checkout.rs index 888467f44d..d86f354ce4 100644 --- a/crates/router/src/connector/checkout.rs +++ b/crates/router/src/connector/checkout.rs @@ -1199,9 +1199,13 @@ impl services::ConnectorRedirectResponse for Checkout { .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; let connector_action = query .status - .map(|checkout_status| { - payments::CallConnectorAction::StatusUpdate(checkout_status.into()) - }) + .map( + |checkout_status| payments::CallConnectorAction::StatusUpdate { + status: storage_models::enums::AttemptStatus::from(checkout_status), + error_code: None, + error_message: None, + }, + ) .unwrap_or(payments::CallConnectorAction::Trigger); Ok(connector_action) } diff --git a/crates/router/src/connector/globalpay.rs b/crates/router/src/connector/globalpay.rs index 90253691b2..5bd472a2d0 100644 --- a/crates/router/src/connector/globalpay.rs +++ b/crates/router/src/connector/globalpay.rs @@ -914,9 +914,11 @@ impl services::ConnectorRedirectResponse for Globalpay { payments::CallConnectorAction::Trigger, |status| match status { response::GlobalpayPaymentStatus::Captured => { - payments::CallConnectorAction::StatusUpdate( - storage_models::enums::AttemptStatus::from(status), - ) + payments::CallConnectorAction::StatusUpdate { + status: storage_models::enums::AttemptStatus::from(status), + error_code: None, + error_message: None, + } } _ => payments::CallConnectorAction::Trigger, }, diff --git a/crates/router/src/connector/nuvei.rs b/crates/router/src/connector/nuvei.rs index 5026eb78e6..b7d2c4ec3a 100644 --- a/crates/router/src/connector/nuvei.rs +++ b/crates/router/src/connector/nuvei.rs @@ -957,9 +957,11 @@ impl services::ConnectorRedirectResponse for Nuvei { .switch()?; match acs_response.trans_status { None | Some(nuvei::LiabilityShift::Failed) => { - Ok(payments::CallConnectorAction::StatusUpdate( - enums::AttemptStatus::AuthenticationFailed, - )) + Ok(payments::CallConnectorAction::StatusUpdate { + status: enums::AttemptStatus::AuthenticationFailed, + error_code: None, + error_message: None, + }) } _ => Ok(payments::CallConnectorAction::Trigger), } diff --git a/crates/router/src/connector/stripe.rs b/crates/router/src/connector/stripe.rs index 12d38fc1c2..39f1ceea34 100644 --- a/crates/router/src/connector/stripe.rs +++ b/crates/router/src/connector/stripe.rs @@ -1831,9 +1831,11 @@ impl services::ConnectorRedirectResponse for Stripe { transformers::StripePaymentStatus::Failed => { payments::CallConnectorAction::Trigger } - _ => payments::CallConnectorAction::StatusUpdate(enums::AttemptStatus::from( - status, - )), + _ => payments::CallConnectorAction::StatusUpdate { + status: enums::AttemptStatus::from(status), + error_code: None, + error_message: None, + }, }, )) } diff --git a/crates/router/src/connector/trustpay.rs b/crates/router/src/connector/trustpay.rs index 98fce9b4a3..a0c8a95a90 100644 --- a/crates/router/src/connector/trustpay.rs +++ b/crates/router/src/connector/trustpay.rs @@ -802,9 +802,11 @@ impl services::ConnectorRedirectResponse for Trustpay { Ok(query.status.map_or( payments::CallConnectorAction::Trigger, |status| match status.as_str() { - "SuccessOk" => payments::CallConnectorAction::StatusUpdate( - storage_models::enums::AttemptStatus::Charged, - ), + "SuccessOk" => payments::CallConnectorAction::StatusUpdate { + status: storage_models::enums::AttemptStatus::Charged, + error_code: None, + error_message: None, + }, _ => payments::CallConnectorAction::Trigger, }, )) diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 758839287e..202834d622 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -429,6 +429,7 @@ pub trait CardData { delimiter: String, ) -> Secret; fn get_expiry_date_as_yyyymm(&self, delimiter: &str) -> Secret; + fn get_expiry_year_4_digit(&self) -> Secret; } impl CardData for api::Card { @@ -453,17 +454,21 @@ impl CardData for api::Card { )) } fn get_expiry_date_as_yyyymm(&self, delimiter: &str) -> Secret { - let mut x = self.card_exp_year.peek().clone(); - if x.len() == 2 { - x = format!("20{}", x); - } + let year = self.get_expiry_year_4_digit(); Secret::new(format!( "{}{}{}", - x, + year.peek(), delimiter, self.card_exp_month.peek().clone() )) } + fn get_expiry_year_4_digit(&self) -> Secret { + let mut year = self.card_exp_year.peek().clone(); + if year.len() == 2 { + year = format!("20{}", year); + } + Secret::new(year) + } } #[track_caller] diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 5346992506..f8643e2321 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -937,7 +937,11 @@ where pub enum CallConnectorAction { Trigger, Avoid, - StatusUpdate(storage_enums::AttemptStatus), + StatusUpdate { + status: storage_enums::AttemptStatus, + error_code: Option, + error_message: Option, + }, HandleResponse(Vec), } diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 967a60691d..d24daeaec1 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -21,6 +21,7 @@ use self::request::{ContentType, HeaderExt, RequestBuilderExt}; pub use self::request::{Method, Request, RequestBuilder}; use crate::{ configs::settings::Connectors, + consts, core::{ errors::{self, CustomResult}, payments, @@ -190,8 +191,23 @@ where connector_integration.handle_response(req, response) } payments::CallConnectorAction::Avoid => Ok(router_data), - payments::CallConnectorAction::StatusUpdate(status) => { + payments::CallConnectorAction::StatusUpdate { + status, + error_code, + error_message, + } => { router_data.status = status; + let error_response = if error_code.is_some() | error_message.is_some() { + Some(ErrorResponse { + code: error_code.unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: error_message.unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + status_code: 200, // This status code is ignored in redirection response it will override with 302 status code. + reason: None, + }) + } else { + None + }; + router_data.response = error_response.map(Err).unwrap_or(router_data.response); Ok(router_data) } payments::CallConnectorAction::Trigger => { diff --git a/crates/router/tests/connectors/bluesnap.rs b/crates/router/tests/connectors/bluesnap.rs index c51b0d87df..78e12cfb5a 100644 --- a/crates/router/tests/connectors/bluesnap.rs +++ b/crates/router/tests/connectors/bluesnap.rs @@ -1,11 +1,13 @@ use std::str::FromStr; +use api_models::payments::{Address, AddressDetails}; +use common_utils::pii::Email; use masking::Secret; -use router::types::{self, api, storage::enums, ConnectorAuthType}; +use router::types::{self, api, storage::enums, ConnectorAuthType, PaymentAddress}; use crate::{ connector_auth, - utils::{self, ConnectorActions}, + utils::{self, ConnectorActions, PaymentInfo}, }; #[derive(Clone, Copy)] @@ -34,6 +36,28 @@ impl utils::Connector for BluesnapTest { "bluesnap".to_string() } } +fn payment_method_details() -> Option { + Some(types::PaymentsAuthorizeData { + email: Some(Email::from_str("test@gmail.com").unwrap()), + ..utils::PaymentAuthorizeType::default().0 + }) +} +fn get_payment_info() -> Option { + Some(PaymentInfo { + address: Some(PaymentAddress { + billing: Some(Address { + address: Some(AddressDetails { + first_name: Some(Secret::new("joseph".to_string())), + last_name: Some(Secret::new("Doe".to_string())), + ..Default::default() + }), + phone: None, + }), + ..Default::default() + }), + ..Default::default() + }) +} // Cards Positive Tests // Creates a payment using the manual capture flow (Non 3DS). @@ -42,7 +66,7 @@ impl utils::Connector for BluesnapTest { #[actix_web::test] async fn should_only_authorize_payment() { let response = CONNECTOR - .authorize_payment(None, None) + .authorize_payment(payment_method_details(), get_payment_info()) .await .expect("Authorize payment response"); assert_eq!(response.status, enums::AttemptStatus::Authorized); @@ -54,7 +78,7 @@ async fn should_only_authorize_payment() { #[actix_web::test] async fn should_capture_authorized_payment() { let response = CONNECTOR - .authorize_and_capture_payment(None, None, None) + .authorize_and_capture_payment(payment_method_details(), None, get_payment_info()) .await .expect("Capture payment response"); assert_eq!(response.status, enums::AttemptStatus::Charged); @@ -67,12 +91,12 @@ async fn should_capture_authorized_payment() { async fn should_partially_capture_authorized_payment() { let response = CONNECTOR .authorize_and_capture_payment( - None, + payment_method_details(), Some(types::PaymentsCaptureData { amount_to_capture: 50, ..utils::PaymentCaptureType::default().0 }), - None, + get_payment_info(), ) .await .expect("Capture payment response"); @@ -85,7 +109,7 @@ async fn should_partially_capture_authorized_payment() { #[actix_web::test] async fn should_sync_authorized_payment() { let authorize_response = CONNECTOR - .authorize_payment(None, None) + .authorize_payment(payment_method_details(), get_payment_info()) .await .expect("Authorize payment response"); let txn_id = utils::get_connector_transaction_id(authorize_response.response); @@ -112,13 +136,13 @@ async fn should_sync_authorized_payment() { async fn should_void_authorized_payment() { let response = CONNECTOR .authorize_and_void_payment( - None, + payment_method_details(), Some(types::PaymentsCancelData { connector_transaction_id: String::from(""), cancellation_reason: Some("requested_by_customer".to_string()), ..Default::default() }), - None, + get_payment_info(), ) .await .expect("Void payment response"); @@ -131,7 +155,7 @@ async fn should_void_authorized_payment() { #[actix_web::test] async fn should_refund_manually_captured_payment() { let response = CONNECTOR - .capture_payment_and_refund(None, None, None, None) + .capture_payment_and_refund(payment_method_details(), None, None, get_payment_info()) .await .unwrap(); let rsync_response = CONNECTOR @@ -156,13 +180,13 @@ async fn should_refund_manually_captured_payment() { async fn should_partially_refund_manually_captured_payment() { let response = CONNECTOR .capture_payment_and_refund( - None, + payment_method_details(), None, Some(types::RefundsData { refund_amount: 50, ..utils::PaymentRefundType::default().0 }), - None, + get_payment_info(), ) .await .unwrap(); @@ -187,7 +211,7 @@ async fn should_partially_refund_manually_captured_payment() { #[actix_web::test] async fn should_sync_manually_captured_refund() { let refund_response = CONNECTOR - .capture_payment_and_refund(None, None, None, None) + .capture_payment_and_refund(payment_method_details(), None, None, get_payment_info()) .await .unwrap(); let response = CONNECTOR @@ -210,7 +234,10 @@ async fn should_sync_manually_captured_refund() { #[serial_test::serial] #[actix_web::test] async fn should_make_payment() { - let authorize_response = CONNECTOR.make_payment(None, None).await.unwrap(); + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_payment_info()) + .await + .unwrap(); assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); } @@ -219,7 +246,10 @@ async fn should_make_payment() { #[serial_test::serial] #[actix_web::test] async fn should_sync_auto_captured_payment() { - let authorize_response = CONNECTOR.make_payment(None, None).await.unwrap(); + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_payment_info()) + .await + .unwrap(); assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); let txn_id = utils::get_connector_transaction_id(authorize_response.response); assert_ne!(txn_id, None, "Empty connector transaction id"); @@ -245,7 +275,7 @@ async fn should_sync_auto_captured_payment() { #[actix_web::test] async fn should_refund_auto_captured_payment() { let response = CONNECTOR - .make_payment_and_refund(None, None, None) + .make_payment_and_refund(payment_method_details(), None, get_payment_info()) .await .unwrap(); let rsync_response = CONNECTOR @@ -270,12 +300,12 @@ async fn should_refund_auto_captured_payment() { async fn should_partially_refund_succeeded_payment() { let refund_response = CONNECTOR .make_payment_and_refund( - None, + payment_method_details(), Some(types::RefundsData { refund_amount: 50, ..utils::PaymentRefundType::default().0 }), - None, + get_payment_info(), ) .await .unwrap(); @@ -299,7 +329,10 @@ async fn should_partially_refund_succeeded_payment() { #[serial_test::serial] #[actix_web::test] async fn should_refund_succeeded_payment_multiple_times() { - let authorize_response = CONNECTOR.make_payment(None, None).await.unwrap(); + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_payment_info()) + .await + .unwrap(); assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); let transaction_id = utils::get_connector_transaction_id(authorize_response.response).unwrap(); for _x in 0..2 { @@ -337,7 +370,7 @@ async fn should_refund_succeeded_payment_multiple_times() { #[actix_web::test] async fn should_sync_refund() { let refund_response = CONNECTOR - .make_payment_and_refund(None, None, None) + .make_payment_and_refund(payment_method_details(), None, get_payment_info()) .await .unwrap(); let response = CONNECTOR @@ -355,31 +388,6 @@ async fn should_sync_refund() { ); } -// Cards Negative scenerios -// Creates a payment with incorrect card number. - -#[serial_test::serial] -#[actix_web::test] -async fn should_fail_payment_for_incorrect_card_number() { - let response = CONNECTOR - .make_payment( - Some(types::PaymentsAuthorizeData { - payment_method_data: types::api::PaymentMethodData::Card(api::Card { - card_number: cards::CardNumber::from_str("1234567891011").unwrap(), - ..utils::CCardType::default().0 - }), - ..utils::PaymentAuthorizeType::default().0 - }), - None, - ) - .await - .unwrap(); - assert_eq!( - response.response.unwrap_err().message, - "Order creation failure due to problematic input.".to_string(), - ); -} - // Creates a payment with incorrect CVC. #[serial_test::serial] @@ -388,13 +396,15 @@ async fn should_fail_payment_for_incorrect_cvc() { let response = CONNECTOR .make_payment( Some(types::PaymentsAuthorizeData { + email: Some(Email::from_str("test@gmail.com").unwrap()), payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_holder_name: Secret::new("John Doe".to_string()), card_cvc: Secret::new("12345".to_string()), ..utils::CCardType::default().0 }), ..utils::PaymentAuthorizeType::default().0 }), - None, + get_payment_info(), ) .await .unwrap(); @@ -412,13 +422,15 @@ async fn should_fail_payment_for_invalid_exp_month() { let response = CONNECTOR .make_payment( Some(types::PaymentsAuthorizeData { + email: Some(Email::from_str("test@gmail.com").unwrap()), payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_holder_name: Secret::new("John Doe".to_string()), card_exp_month: Secret::new("20".to_string()), ..utils::CCardType::default().0 }), ..utils::PaymentAuthorizeType::default().0 }), - None, + get_payment_info(), ) .await .unwrap(); @@ -436,13 +448,15 @@ async fn should_fail_payment_for_incorrect_expiry_year() { let response = CONNECTOR .make_payment( Some(types::PaymentsAuthorizeData { + email: Some(Email::from_str("test@gmail.com").unwrap()), payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_holder_name: Secret::new("John Doe".to_string()), card_exp_year: Secret::new("2000".to_string()), ..utils::CCardType::default().0 }), ..utils::PaymentAuthorizeType::default().0 }), - None, + get_payment_info(), ) .await .unwrap(); @@ -457,7 +471,10 @@ async fn should_fail_payment_for_incorrect_expiry_year() { #[serial_test::serial] #[actix_web::test] async fn should_fail_void_payment_for_auto_capture() { - let authorize_response = CONNECTOR.make_payment(None, None).await.unwrap(); + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_payment_info()) + .await + .unwrap(); assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); let txn_id = utils::get_connector_transaction_id(authorize_response.response); assert_ne!(txn_id, None, "Empty connector transaction id"); @@ -495,12 +512,12 @@ async fn should_fail_capture_for_invalid_payment() { async fn should_fail_for_refund_amount_higher_than_payment_amount() { let response = CONNECTOR .make_payment_and_refund( - None, + payment_method_details(), Some(types::RefundsData { refund_amount: 150, ..utils::PaymentRefundType::default().0 }), - None, + get_payment_info(), ) .await .unwrap();