diff --git a/connector-template/mod.rs b/connector-template/mod.rs index ce7dc0777b..8282f7d169 100644 --- a/connector-template/mod.rs +++ b/connector-template/mod.rs @@ -1,8 +1,6 @@ mod transformers; use std::fmt::Debug; - -use bytes::Bytes; use error_stack::{ResultExt, IntoReport}; use crate::{ @@ -52,8 +50,8 @@ impl ConnectorCommon for {{project-name | downcase | pascal_case}} { connectors.{{project-name}}.base_url.as_ref() } - fn get_auth_header(&self,_auth_type:&types::ConnectorAuthType)-> CustomResult,errors::ConnectorError> { - let auth: {{project-name | downcase | pascal_case}}::{{project-name | downcase | pascal_case}}AuthType = auth_type + fn get_auth_header(&self, auth_type:&types::ConnectorAuthType)-> CustomResult,errors::ConnectorError> { + let auth: {{project-name | downcase}}::{{project-name | downcase | pascal_case}}AuthType = auth_type .try_into() .change_context(errors::ConnectorError::FailedToObtainAuthType)?; Ok(vec![(headers::AUTHORIZATION.to_string(), auth.api_key)]) @@ -82,6 +80,13 @@ impl > for {{project-name | downcase | pascal_case}} {} +impl api::ConnectorAccessToken for {{project-name | downcase | pascal_case}} {} + +impl ConnectorIntegration + for {{project-name | downcase | pascal_case}} +{ +} + impl api::PaymentSync for {{project-name | downcase | pascal_case}} {} impl ConnectorIntegration @@ -109,8 +114,8 @@ impl fn build_request( &self, - _req: &types::PaymentsSyncRouterData, - _connectors: &settings::Connectors, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { Ok(Some( services::RequestBuilder::new() @@ -123,15 +128,15 @@ impl fn get_error_response( &self, - _res: types::Response, + res: Response, ) -> CustomResult { self.build_error_response(res) } fn handle_response( &self, - _data: &types::PaymentsSyncRouterData, - _res: Response, + data: &types::PaymentsSyncRouterData, + res: Response, ) -> CustomResult { logger::debug!(payment_sync_response=?res); let response: {{project-name | downcase}}:: {{project-name | downcase | pascal_case}}PaymentsResponse = res @@ -185,8 +190,8 @@ impl fn build_request( &self, - _req: &types::PaymentsCaptureRouterData, - _connectors: &settings::Connectors, + req: &types::PaymentsCaptureRouterData, + connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { Ok(Some( services::RequestBuilder::new() @@ -201,8 +206,8 @@ impl fn handle_response( &self, - _data: &types::PaymentsCaptureRouterData, - _res: Response, + data: &types::PaymentsCaptureRouterData, + res: Response, ) -> CustomResult { let response: {{project-name | downcase }}::{{project-name | downcase | pascal_case}}PaymentsResponse = res .response @@ -220,7 +225,7 @@ impl fn get_error_response( &self, - _res: types::Response, + res: Response, ) -> CustomResult { self.build_error_response(res) } @@ -299,7 +304,7 @@ impl .change_context(errors::ConnectorError::ResponseHandlingFailed) } - fn get_error_response(&self, res: types::Response) -> CustomResult { + fn get_error_response(&self, res: Response) -> CustomResult { self.build_error_response(res) } } @@ -357,7 +362,7 @@ impl .change_context(errors::ConnectorError::ResponseHandlingFailed) } - fn get_error_response(&self, res: types::Response) -> CustomResult { + fn get_error_response(&self, res: Response) -> CustomResult { self.build_error_response(res) } } @@ -407,7 +412,7 @@ impl .change_context(errors::ConnectorError::ResponseHandlingFailed) } - fn get_error_response(&self, res: types::Response) -> CustomResult { + fn get_error_response(&self, res: Response) -> CustomResult { self.build_error_response(res) } } diff --git a/connector-template/test.rs b/connector-template/test.rs index 5926584c52..c9c5fe0b5f 100644 --- a/connector-template/test.rs +++ b/connector-template/test.rs @@ -79,7 +79,7 @@ async fn should_sync_authorized_payment() { .authorize_payment(None, None) .await .expect("Authorize payment response"); - let txn_id = utils::get_connector_transaction_id(authorize_response); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); let response = CONNECTOR .psync_retry_till_status_matches( enums::AttemptStatus::Authorized, @@ -182,7 +182,7 @@ async fn should_make_payment() { async fn should_sync_auto_captured_payment() { let authorize_response = CONNECTOR.make_payment(None, None).await.unwrap(); assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); - let txn_id = utils::get_connector_transaction_id(authorize_response); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); assert_ne!(txn_id, None, "Empty connector transaction id"); let response = CONNECTOR .psync_retry_till_status_matches( @@ -388,7 +388,7 @@ async fn should_fail_payment_for_incorrect_expiry_year() { async fn should_fail_void_payment_for_auto_capture() { let authorize_response = CONNECTOR.make_payment(None, None).await.unwrap(); assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); - let txn_id = utils::get_connector_transaction_id(authorize_response); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); assert_ne!(txn_id, None, "Empty connector transaction id"); let void_response = CONNECTOR .void_payment(txn_id.unwrap(), None, None) diff --git a/connector-template/transformers.rs b/connector-template/transformers.rs index 2cf351f47f..ab51bbff3c 100644 --- a/connector-template/transformers.rs +++ b/connector-template/transformers.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use crate::{core::errors,pii::PeekInterface,types::{self,api, storage::enums}}; +use crate::{core::errors,types::{self,api, storage::enums}}; //TODO: Fill the struct with respective fields #[derive(Default, Debug, Serialize, Eq, PartialEq)] @@ -14,7 +14,9 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for {{project-name | downcase //TODO: Fill the struct with respective fields // Auth Struct -pub struct {{project-name | downcase | pascal_case}}AuthType {} +pub struct {{project-name | downcase | pascal_case}}AuthType { + pub(super) api_key: String +} impl TryFrom<&types::ConnectorAuthType> for {{project-name | downcase | pascal_case}}AuthType { type Error = error_stack::Report; @@ -45,7 +47,10 @@ impl From<{{project-name | downcase | pascal_case}}PaymentStatus> for enums::Att //TODO: Fill the struct with respective fields #[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct {{project-name | downcase | pascal_case}}PaymentsResponse {} +pub struct {{project-name | downcase | pascal_case}}PaymentsResponse { + status: {{project-name | downcase | pascal_case}}PaymentStatus, + id: String, +} impl TryFrom> for types::RouterData { type Error = error_stack::Report; @@ -57,6 +62,7 @@ impl TryFrom for enums::RefundStatus { - fn from(item: self::RefundStatus) -> Self { +impl From for enums::RefundStatus { + fn from(item: RefundStatus) -> Self { match item { RefundStatus::Succeeded => Self::Success, RefundStatus::Failed => Self::Failure, @@ -108,7 +114,7 @@ impl TryFrom> { type Error = error_stack::Report; fn try_from( - item: types::RefundsResponseRouterData, + _item: types::RefundsResponseRouterData, ) -> Result { todo!() } diff --git a/crates/router/src/connector/authorizedotnet/transformers.rs b/crates/router/src/connector/authorizedotnet/transformers.rs index 05630a7005..1d0de718a0 100644 --- a/crates/router/src/connector/authorizedotnet/transformers.rs +++ b/crates/router/src/connector/authorizedotnet/transformers.rs @@ -3,6 +3,7 @@ use error_stack::ResultExt; use serde::{Deserialize, Serialize}; use crate::{ + connector::utils::RefundsRequestData, core::errors, pii::PeekInterface, types::{self, api, storage::enums}, @@ -466,12 +467,7 @@ impl TryFrom<&types::RefundsRouterData> for AuthorizedotnetCreateSyncReque type Error = error_stack::Report; fn try_from(item: &types::RefundsRouterData) -> Result { - let transaction_id = item - .request - .connector_refund_id - .clone() - .get_required_value("connector_refund_id") - .change_context(errors::ConnectorError::MissingConnectorRefundID)?; + let transaction_id = item.request.get_connector_refund_id()?; let merchant_authentication = MerchantAuthentication::try_from(&item.connector_auth_type)?; let payload = Self { diff --git a/crates/router/src/connector/checkout.rs b/crates/router/src/connector/checkout.rs index f1e92ef155..125ea21420 100644 --- a/crates/router/src/connector/checkout.rs +++ b/crates/router/src/connector/checkout.rs @@ -7,6 +7,7 @@ use std::fmt::Debug; use error_stack::{IntoReport, ResultExt}; use self::transformers as checkout; +use super::utils::RefundsRequestData; use crate::{ configs::settings, consts, @@ -19,7 +20,7 @@ use crate::{ self, api::{self, ConnectorCommon}, }, - utils::{self, BytesExt, OptionExt}, + utils::{self, BytesExt}, }; #[derive(Debug, Clone)] @@ -674,12 +675,7 @@ impl services::ConnectorIntegration, res: types::Response, ) -> CustomResult, errors::ConnectorError> { - let refund_action_id = data - .request - .connector_refund_id - .clone() - .get_required_value("connector_refund_id") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let refund_action_id = data.request.get_connector_refund_id()?; let response: Vec = res .response diff --git a/crates/router/src/connector/cybersource.rs b/crates/router/src/connector/cybersource.rs index 3fd44571f6..a932a3fe82 100644 --- a/crates/router/src/connector/cybersource.rs +++ b/crates/router/src/connector/cybersource.rs @@ -11,6 +11,7 @@ use url::Url; use crate::{ configs::settings, + connector::utils::RefundsRequestData, consts, core::errors::{self, CustomResult}, headers, logger, @@ -19,7 +20,7 @@ use crate::{ self, api::{self, ConnectorCommon, ConnectorCommonExt}, }, - utils::{self, BytesExt, OptionExt}, + utils::{self, BytesExt}, }; #[derive(Debug, Clone)] @@ -95,8 +96,19 @@ impl ConnectorCommon for Cybersource { Ok(types::ErrorResponse { status_code: res.status_code, code: consts::NO_ERROR_CODE.to_string(), - message: response.details.to_string(), - reason: None, + message: response + .message + .map(|m| { + format!( + "{} {}", + m, + response.details.map(|d| d.to_string()).unwrap_or_default() + ) + .trim() + .to_string() + }) + .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), + reason: response.reason, }) } } @@ -633,13 +645,7 @@ impl ConnectorIntegration CustomResult { - let refund_id = req - .response - .clone() - .ok() - .get_required_value("response") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)? - .connector_refund_id; + let refund_id = req.request.get_connector_refund_id()?; Ok(format!( "{}tss/v2/transactions/{}", self.base_url(connectors), diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index d67cbd2205..acec267d9f 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -260,7 +260,9 @@ impl From for enums::AttemptStatus { impl From for enums::RefundStatus { fn from(item: CybersourcePaymentStatus) -> Self { match item { - CybersourcePaymentStatus::Succeeded => Self::Success, + CybersourcePaymentStatus::Succeeded | CybersourcePaymentStatus::Transmitted => { + Self::Success + } CybersourcePaymentStatus::Failed => Self::Failure, _ => Self::Pending, } @@ -390,9 +392,10 @@ impl #[serde(rename_all = "camelCase")] pub struct ErrorResponse { pub error_information: Option, - pub status: String, + pub status: Option, pub message: Option, - pub details: serde_json::Value, + pub reason: Option, + pub details: Option, } #[derive(Debug, Default, Deserialize)] @@ -413,7 +416,7 @@ impl TryFrom<&types::RefundsRouterData> for CybersourceRefundRequest { Ok(Self { order_information: OrderInformation { amount_details: Amount { - total_amount: item.request.amount.to_string(), + total_amount: item.request.refund_amount.to_string(), currency: item.request.currency.to_string(), }, }, diff --git a/crates/router/src/connector/shift4.rs b/crates/router/src/connector/shift4.rs index 7f2d5ec128..470eeb2502 100644 --- a/crates/router/src/connector/shift4.rs +++ b/crates/router/src/connector/shift4.rs @@ -6,6 +6,7 @@ use common_utils::ext_traits::ByteSliceExt; use error_stack::ResultExt; use transformers as shift4; +use super::utils::RefundsRequestData; use crate::{ configs::settings, consts, @@ -20,7 +21,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, }, - utils::{self, BytesExt, OptionExt}, + utils::{self, BytesExt}, }; #[derive(Debug, Clone)] @@ -444,12 +445,7 @@ impl ConnectorIntegration CustomResult { - let refund_id = req - .request - .connector_refund_id - .clone() - .get_required_value("connector_refund_id") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let refund_id = req.request.get_connector_refund_id()?; Ok(format!( "{}refunds/{}", self.base_url(connectors), diff --git a/crates/router/src/connector/shift4/transformers.rs b/crates/router/src/connector/shift4/transformers.rs index c3dac73b97..ea1ebce8c5 100644 --- a/crates/router/src/connector/shift4/transformers.rs +++ b/crates/router/src/connector/shift4/transformers.rs @@ -177,7 +177,7 @@ impl TryFrom<&types::RefundsRouterData> for Shift4RefundRequest { fn try_from(item: &types::RefundsRouterData) -> Result { Ok(Self { charge_id: item.request.connector_transaction_id.clone(), - amount: item.request.amount, + amount: item.request.refund_amount, }) } } diff --git a/crates/router/src/connector/stripe.rs b/crates/router/src/connector/stripe.rs index d6841c257c..fecedb31f1 100644 --- a/crates/router/src/connector/stripe.rs +++ b/crates/router/src/connector/stripe.rs @@ -6,6 +6,7 @@ use error_stack::{IntoReport, ResultExt}; use router_env::{instrument, tracing}; use self::transformers as stripe; +use super::utils::RefundsRequestData; use crate::{ configs::settings, consts, @@ -19,7 +20,7 @@ use crate::{ self, api::{self, ConnectorCommon}, }, - utils::{self, crypto, ByteSliceExt, BytesExt, OptionExt}, + utils::{self, crypto, ByteSliceExt, BytesExt}, }; #[derive(Debug, Clone)] @@ -779,12 +780,7 @@ impl services::ConnectorIntegration, connectors: &settings::Connectors, ) -> CustomResult { - let id = req - .request - .connector_refund_id - .clone() - .get_required_value("connector_refund_id") - .change_context(errors::ConnectorError::FailedToObtainIntegrationUrl)?; + let id = req.request.get_connector_refund_id()?; Ok(format!("{}v1/refunds/{}", self.base_url(connectors), id)) } diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index cd2d9db446..20238844ea 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -1,9 +1,11 @@ +use error_stack::ResultExt; use masking::Secret; use crate::{ core::errors, pii::PeekInterface, types::{self, api}, + utils::OptionExt, }; pub fn missing_field_err( @@ -40,6 +42,19 @@ pub trait PaymentsRequestData { fn get_card(&self) -> Result; } +pub trait RefundsRequestData { + fn get_connector_refund_id(&self) -> Result; +} + +impl RefundsRequestData for types::RefundsData { + fn get_connector_refund_id(&self) -> Result { + self.connector_refund_id + .clone() + .get_required_value("connector_refund_id") + .change_context(errors::ConnectorError::MissingConnectorTransactionID) + } +} + impl PaymentsRequestData for types::PaymentsAuthorizeRouterData { fn get_attempt_id(&self) -> Result { self.attempt_id diff --git a/crates/router/src/connector/worldline.rs b/crates/router/src/connector/worldline.rs index ceb72f93cd..7d6f214e96 100644 --- a/crates/router/src/connector/worldline.rs +++ b/crates/router/src/connector/worldline.rs @@ -9,6 +9,7 @@ use storage_models::enums; use time::{format_description, OffsetDateTime}; use transformers as worldline; +use super::utils::RefundsRequestData; use crate::{ configs::settings::Connectors, consts, @@ -20,7 +21,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, }, - utils::{self, BytesExt, OptionExt}, + utils::{self, BytesExt}, }; #[derive(Debug, Clone)] @@ -600,12 +601,7 @@ impl ConnectorIntegration CustomResult { - let refund_id = req - .request - .connector_refund_id - .clone() - .get_required_value("connector_refund_id") - .change_context(errors::ConnectorError::FailedToObtainIntegrationUrl)?; + let refund_id = req.request.get_connector_refund_id()?; let base_url = self.base_url(connectors); let auth: worldline::AuthType = worldline::AuthType::try_from(&req.connector_auth_type)?; let merchant_account_id = auth.merchant_account_id; diff --git a/crates/router/tests/connectors/cybersource.rs b/crates/router/tests/connectors/cybersource.rs index 51ba6e3973..f960e3ea2d 100644 --- a/crates/router/tests/connectors/cybersource.rs +++ b/crates/router/tests/connectors/cybersource.rs @@ -1,12 +1,13 @@ -use futures::future::OptionFuture; use masking::Secret; -use router::types::{self, api, storage::enums}; +use router::types::{ + self, api, + storage::{self, enums}, +}; use crate::{ connector_auth, utils::{self, ConnectorActions, PaymentAuthorizeType}, }; - struct Cybersource; impl ConnectorActions for Cybersource {} impl utils::Connector for Cybersource { @@ -18,7 +19,6 @@ impl utils::Connector for Cybersource { get_token: types::api::GetToken::Connector, } } - fn get_auth_token(&self) -> types::ConnectorAuthType { types::ConnectorAuthType::from( connector_auth::ConnectorAuthentication::new() @@ -26,7 +26,6 @@ impl utils::Connector for Cybersource { .expect("Missing connector authentication configuration"), ) } - fn get_name(&self) -> String { "cybersource".to_string() } @@ -56,9 +55,9 @@ fn get_default_payment_info() -> Option { ..Default::default() }) } - fn get_default_payment_authorize_data() -> Option { Some(types::PaymentsAuthorizeData { + currency: storage::enums::Currency::USD, email: Some(Secret::new("abc@gmail.com".to_string())), ..PaymentAuthorizeType::default().0 }) @@ -74,130 +73,221 @@ async fn should_only_authorize_payment() { .unwrap(); assert_eq!(response.status, enums::AttemptStatus::Authorized); } - #[actix_web::test] -async fn should_authorize_and_capture_payment() { - let connector = Cybersource {}; - let response = connector +async fn should_make_payment() { + let response = Cybersource {} .make_payment( get_default_payment_authorize_data(), get_default_payment_info(), ) - .await; - let sync_response = connector - .sync_payment( - Some(types::PaymentsSyncData { - connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( - utils::get_connector_transaction_id(response.unwrap()).unwrap(), - ), - encoded_data: None, - capture_method: None, - }), - get_default_payment_info(), - ) .await .unwrap(); - //cybersource takes sometime to settle the transaction,so it will be in pending for long time - assert_eq!(sync_response.status, enums::AttemptStatus::Pending); + assert_eq!(response.status, enums::AttemptStatus::Pending); } - -#[actix_web::test] -async fn should_sync_capture_payment() { - let sync_response = Cybersource {} - .sync_payment( - Some(types::PaymentsSyncData { - connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( - "6736046645576085004953".to_string(), - ), - encoded_data: None, - capture_method: None, - }), - get_default_payment_info(), - ) - .await - .unwrap(); - assert_eq!(sync_response.status, enums::AttemptStatus::Charged); -} - #[actix_web::test] async fn should_capture_already_authorized_payment() { let connector = Cybersource {}; - let authorize_response = connector - .authorize_payment( + let response = connector + .authorize_and_capture_payment( get_default_payment_authorize_data(), + None, get_default_payment_info(), ) + .await; + assert_eq!(response.unwrap().status, enums::AttemptStatus::Pending); +} +#[actix_web::test] +async fn should_partially_capture_already_authorized_payment() { + let connector = Cybersource {}; + let response = connector + .authorize_and_capture_payment( + get_default_payment_authorize_data(), + Some(types::PaymentsCaptureData { + amount_to_capture: Some(50), + ..utils::PaymentCaptureType::default().0 + }), + get_default_payment_info(), + ) + .await; + assert_eq!(response.unwrap().status, enums::AttemptStatus::Pending); +} + +#[actix_web::test] +async fn should_sync_payment() { + let connector = Cybersource {}; + let response = connector + .psync_retry_till_status_matches( + enums::AttemptStatus::Charged, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + "6699597903496176903954".to_string(), + ), + encoded_data: None, + capture_method: None, + }), + None, + ) .await .unwrap(); - assert_eq!(authorize_response.status, enums::AttemptStatus::Authorized); - let txn_id = utils::get_connector_transaction_id(authorize_response); - let response: OptionFuture<_> = txn_id - .map(|transaction_id| async move { - connector - .capture_payment(transaction_id, None, get_default_payment_info()) - .await - .unwrap() - .status - }) - .into(); - //cybersource takes sometime to settle the transaction,so it will be in pending for long time - assert_eq!(response.await, Some(enums::AttemptStatus::Pending)); + assert_eq!(response.status, enums::AttemptStatus::Charged,); } - #[actix_web::test] async fn should_void_already_authorized_payment() { let connector = Cybersource {}; - let authorize_response = connector - .authorize_payment( + let response = connector + .authorize_and_void_payment( get_default_payment_authorize_data(), + Some(types::PaymentsCancelData { + connector_transaction_id: "".to_string(), + cancellation_reason: Some("requested_by_customer".to_string()), + }), + get_default_payment_info(), + ) + .await; + assert_eq!(response.unwrap().status, enums::AttemptStatus::Voided); +} +#[actix_web::test] +async fn should_fail_payment_for_incorrect_card_number() { + let response = Cybersource {} + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethod::Card(api::Card { + card_number: Secret::new("4024007134364111".to_string()), + ..utils::CCardType::default().0 + }), + ..get_default_payment_authorize_data().unwrap() + }), get_default_payment_info(), ) .await .unwrap(); - assert_eq!(authorize_response.status, enums::AttemptStatus::Authorized); - let txn_id = utils::get_connector_transaction_id(authorize_response); - let response: OptionFuture<_> = txn_id - .map(|transaction_id| async move { - connector - .void_payment(transaction_id, None, get_default_payment_info()) - .await - .unwrap() - .status - }) - .into(); - assert_eq!(response.await, Some(enums::AttemptStatus::Voided)); + let x = response.response.unwrap_err(); + assert_eq!(x.message, "Decline - Invalid account number",); } - #[actix_web::test] -async fn should_refund_succeeded_payment() { +async fn should_fail_payment_for_no_card_number() { + let response = Cybersource {} + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethod::Card(api::Card { + card_number: Secret::new("".to_string()), + ..utils::CCardType::default().0 + }), + ..get_default_payment_authorize_data().unwrap() + }), + get_default_payment_info(), + ) + .await + .unwrap(); + let x = response.response.unwrap_err(); + assert_eq!(x.message, "The order has been rejected by Decision Manager",); +} +#[actix_web::test] +async fn should_fail_payment_for_invalid_exp_month() { + let response = Cybersource {} + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethod::Card(api::Card { + card_exp_month: Secret::new("13".to_string()), + ..utils::CCardType::default().0 + }), + ..get_default_payment_authorize_data().unwrap() + }), + get_default_payment_info(), + ) + .await + .unwrap(); + let x = response.response.unwrap_err(); + assert_eq!( + x.message, + r#"Declined - One or more fields in the request contains invalid data [{"field":"paymentInformation.card.expirationMonth","reason":"INVALID_DATA"}]"#, + ); +} +#[actix_web::test] +async fn should_fail_payment_for_invalid_exp_year() { + let response = Cybersource {} + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethod::Card(api::Card { + card_exp_year: Secret::new("2022".to_string()), + ..utils::CCardType::default().0 + }), + ..get_default_payment_authorize_data().unwrap() + }), + get_default_payment_info(), + ) + .await + .unwrap(); + let x = response.response.unwrap_err(); + assert_eq!(x.message, "Decline - Expired card. You might also receive this if the expiration date you provided does not match the date the issuing bank has on file.",); +} +#[actix_web::test] +async fn should_fail_payment_for_invalid_card_cvc() { + let response = Cybersource {} + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethod::Card(api::Card { + card_cvc: Secret::new("2131233213".to_string()), + ..utils::CCardType::default().0 + }), + ..get_default_payment_authorize_data().unwrap() + }), + get_default_payment_info(), + ) + .await + .unwrap(); + let x = response.response.unwrap_err(); + assert_eq!( + x.message, + r#"Declined - One or more fields in the request contains invalid data [{"field":"paymentInformation.card.securityCode","reason":"INVALID_DATA"}]"#, + ); +} +// Voids a payment using automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_fail_void_payment_for_reversed_payment() { let connector = Cybersource {}; - //make a successful payment - let response = connector + // Authorize + let authorize_response = connector .make_payment( get_default_payment_authorize_data(), get_default_payment_info(), ) .await .unwrap(); - - //try refund for previous payment - let transaction_id = utils::get_connector_transaction_id(response).unwrap(); - let response = connector - .refund_payment(transaction_id, None, get_default_payment_info()) + assert_eq!(authorize_response.status, enums::AttemptStatus::Pending); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + assert_ne!(txn_id, None, "Empty connector transaction id"); + // Void + let void_response = connector + .void_payment("6736046645576085004953".to_string(), None, None) .await .unwrap(); + let res = void_response.response.unwrap_err(); assert_eq!( - response.response.unwrap().refund_status, - enums::RefundStatus::Pending, //cybersource takes sometime to refund the transaction,so it will be in pending state for long time + res.message, + "Decline - The authorization has already been reversed." ); } - #[actix_web::test] -async fn should_sync_refund() { +async fn should_fail_capture_for_invalid_payment() { let connector = Cybersource {}; let response = connector - .sync_refund( - "6738063831816571404953".to_string(), + .capture_payment("12345".to_string(), None, get_default_payment_info()) + .await + .unwrap(); + let err = response.response.unwrap_err(); + assert_eq!( + err.message, + r#"Declined - One or more fields in the request contains invalid data [{"field":"id","reason":"INVALID_DATA"}]"# + ); + assert_eq!(err.code, "No error code".to_string()); +} +#[actix_web::test] +async fn should_refund_succeeded_payment() { + let connector = Cybersource {}; + let response = connector + .make_payment_and_refund( + get_default_payment_authorize_data(), None, get_default_payment_info(), ) @@ -205,49 +295,98 @@ async fn should_sync_refund() { .unwrap(); assert_eq!( response.response.unwrap().refund_status, - enums::RefundStatus::Pending, //cybersource takes sometime to refund the transaction,so it will be in pending state for long time + enums::RefundStatus::Pending, ); } - #[actix_web::test] -async fn should_fail_payment_for_incorrect_card_number() { - let response = Cybersource {} - .make_payment( - Some(types::PaymentsAuthorizeData { - payment_method_data: types::api::PaymentMethod::Card(api::Card { - card_number: Secret::new("424242442424242".to_string()), - ..utils::CCardType::default().0 - }), - ..get_default_payment_authorize_data().unwrap() +async fn should_refund_manually_captured_payment() { + let connector = Cybersource {}; + let response = connector + .auth_capture_and_refund( + get_default_payment_authorize_data(), + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Pending, + ); +} +#[actix_web::test] +async fn should_partially_refund_succeeded_payment() { + let connector = Cybersource {}; + let refund_response = connector + .make_payment_and_refund( + get_default_payment_authorize_data(), + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 }), get_default_payment_info(), ) .await .unwrap(); - assert_eq!(response.status, enums::AttemptStatus::Failure); - let x = response.response.unwrap_err(); - assert_eq!(x.message, "Decline - Invalid account number".to_string(),); + assert_eq!( + refund_response.response.unwrap().refund_status, + enums::RefundStatus::Pending, + ); } #[actix_web::test] -async fn should_fail_payment_for_incorrect_exp_month() { - let response = Cybersource {} - .make_payment( - Some(types::PaymentsAuthorizeData { - payment_method_data: types::api::PaymentMethod::Card(api::Card { - card_number: Secret::new("4242424242424242".to_string()), - card_exp_month: Secret::new("101".to_string()), - ..utils::CCardType::default().0 - }), - ..get_default_payment_authorize_data().unwrap() +async fn should_partially_refund_manually_captured_payment() { + let connector = Cybersource {}; + let response = connector + .auth_capture_and_refund( + get_default_payment_authorize_data(), + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 }), get_default_payment_info(), ) - .await; - let x = response.unwrap().response.unwrap_err(); + .await + .unwrap(); assert_eq!( - x.message, - r#"[{"field":"paymentInformation.card.expirationMonth","reason":"INVALID_DATA"}]"# - .to_string(), + response.response.unwrap().refund_status, + enums::RefundStatus::Pending, + ); +} + +#[actix_web::test] +async fn should_fail_refund_for_invalid_amount() { + let connector = Cybersource {}; + let response = connector + .make_payment_and_refund( + get_default_payment_authorize_data(), + Some(types::RefundsData { + refund_amount: 15000, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Pending, + ); +} +#[actix_web::test] +async fn should_sync_refund() { + let connector = Cybersource {}; + let response = connector + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + "6699597076726585603955".to_string(), + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, ); } diff --git a/crates/router/tests/connectors/fiserv.rs b/crates/router/tests/connectors/fiserv.rs index 25d2539fa6..1edbb45190 100644 --- a/crates/router/tests/connectors/fiserv.rs +++ b/crates/router/tests/connectors/fiserv.rs @@ -101,7 +101,7 @@ async fn should_capture_already_authorized_payment() { .await .unwrap(); assert_eq!(authorize_response.status, enums::AttemptStatus::Authorized); - let txn_id = utils::get_connector_transaction_id(authorize_response); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); let response: OptionFuture<_> = txn_id .map(|transaction_id| async move { connector @@ -144,7 +144,7 @@ async fn should_refund_succeeded_payment() { let response = connector.make_payment(None, None).await.unwrap(); //try refund for previous payment - if let Some(transaction_id) = utils::get_connector_transaction_id(response) { + if let Some(transaction_id) = utils::get_connector_transaction_id(response.response) { let response = connector .refund_payment(transaction_id, None, None) .await diff --git a/crates/router/tests/connectors/globalpay.rs b/crates/router/tests/connectors/globalpay.rs index 3f8330b4e1..971f42f3bc 100644 --- a/crates/router/tests/connectors/globalpay.rs +++ b/crates/router/tests/connectors/globalpay.rs @@ -1,6 +1,5 @@ use std::{thread::sleep, time::Duration}; -use futures::future::OptionFuture; use masking::Secret; use router::types::{ self, @@ -55,8 +54,7 @@ fn get_default_payment_info() -> Option { }), ..Default::default() }), - auth_type: None, - access_token: None, + ..Default::default() }) } @@ -81,22 +79,17 @@ async fn should_authorize_and_capture_payment() { #[actix_web::test] async fn should_capture_already_authorized_payment() { let connector = Globalpay {}; - let authorize_response = connector - .authorize_payment(None, get_default_payment_info()) - .await - .unwrap(); - assert_eq!(authorize_response.status, enums::AttemptStatus::Authorized); - let txn_id = utils::get_connector_transaction_id(authorize_response); - let response: OptionFuture<_> = txn_id - .map(|transaction_id| async move { - connector - .capture_payment(transaction_id, None, get_default_payment_info()) - .await - .unwrap() - .status - }) - .into(); - assert_eq!(response.await, Some(enums::AttemptStatus::Charged)); + let response = connector + .authorize_and_capture_payment( + None, + Some(types::PaymentsCaptureData { + amount_to_capture: Some(50), + ..utils::PaymentCaptureType::default().0 + }), + get_default_payment_info(), + ) + .await; + assert_eq!(response.unwrap().status, enums::AttemptStatus::Charged); } #[actix_web::test] @@ -106,10 +99,11 @@ async fn should_sync_payment() { .authorize_payment(None, get_default_payment_info()) .await .unwrap(); - let txn_id = utils::get_connector_transaction_id(authorize_response); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); sleep(Duration::from_secs(5)); // to avoid 404 error as globalpay takes some time to process the new transaction let response = connector - .sync_payment( + .psync_retry_till_status_matches( + enums::AttemptStatus::Authorized, Some(types::PaymentsSyncData { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( txn_id.unwrap(), @@ -146,16 +140,8 @@ async fn should_fail_payment_for_incorrect_cvc() { #[actix_web::test] async fn should_refund_succeeded_payment() { let connector = Globalpay {}; - //make a successful payment let response = connector - .make_payment(None, get_default_payment_info()) - .await - .unwrap(); - - //try refund for previous payment - let transaction_id = utils::get_connector_transaction_id(response).unwrap(); - let response = connector - .refund_payment(transaction_id, None, get_default_payment_info()) + .make_payment_and_refund(None, None, get_default_payment_info()) .await .unwrap(); assert_eq!( @@ -167,39 +153,26 @@ async fn should_refund_succeeded_payment() { #[actix_web::test] async fn should_void_already_authorized_payment() { let connector = Globalpay {}; - let authorize_response = connector - .authorize_payment(None, get_default_payment_info()) - .await - .unwrap(); - assert_eq!(authorize_response.status, enums::AttemptStatus::Authorized); - let txn_id = utils::get_connector_transaction_id(authorize_response); - let response: OptionFuture<_> = txn_id - .map(|transaction_id| async move { - connector - .void_payment(transaction_id, None, None) - .await - .unwrap() - .status - }) - .into(); - assert_eq!(response.await, Some(enums::AttemptStatus::Voided)); + let response = connector + .authorize_and_void_payment(None, None, get_default_payment_info()) + .await; + assert_eq!(response.unwrap().status, enums::AttemptStatus::Voided); } #[actix_web::test] async fn should_sync_refund() { let connector = Globalpay {}; - let response = connector - .make_payment(None, get_default_payment_info()) + let refund_response = connector + .make_payment_and_refund(None, None, get_default_payment_info()) .await .unwrap(); - let transaction_id = utils::get_connector_transaction_id(response).unwrap(); - connector - .refund_payment(transaction_id.clone(), None, get_default_payment_info()) - .await - .unwrap(); - sleep(Duration::from_secs(5)); // to avoid 404 error as globalpay takes some time to process the new transaction let response = connector - .sync_refund(transaction_id, None, get_default_payment_info()) + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + refund_response.response.unwrap().connector_refund_id, + None, + get_default_payment_info(), + ) .await .unwrap(); assert_eq!( diff --git a/crates/router/tests/connectors/payu.rs b/crates/router/tests/connectors/payu.rs index d610119a65..5b6523dcdf 100644 --- a/crates/router/tests/connectors/payu.rs +++ b/crates/router/tests/connectors/payu.rs @@ -41,9 +41,8 @@ fn get_access_token() -> Option { } fn get_default_payment_info() -> Option { Some(utils::PaymentInfo { - address: None, - auth_type: None, access_token: get_access_token(), + ..Default::default() }) } @@ -63,7 +62,7 @@ async fn should_authorize_card_payment() { .unwrap(); // in Payu need Psync to get status therefore set to pending assert_eq!(authorize_response.status, enums::AttemptStatus::Pending); - if let Some(transaction_id) = utils::get_connector_transaction_id(authorize_response) { + if let Some(transaction_id) = utils::get_connector_transaction_id(authorize_response.response) { let sync_response = Payu {} .psync_retry_till_status_matches( enums::AttemptStatus::Authorized, @@ -95,7 +94,7 @@ async fn should_authorize_gpay_payment() { ..PaymentAuthorizeType::default().0 }), get_default_payment_info()).await.unwrap(); assert_eq!(authorize_response.status, enums::AttemptStatus::Pending); - if let Some(transaction_id) = utils::get_connector_transaction_id(authorize_response) { + if let Some(transaction_id) = utils::get_connector_transaction_id(authorize_response.response) { let sync_response = Payu {} .sync_payment( Some(types::PaymentsSyncData { @@ -128,7 +127,7 @@ async fn should_capture_already_authorized_payment() { .await .unwrap(); assert_eq!(authorize_response.status, enums::AttemptStatus::Pending); - if let Some(transaction_id) = utils::get_connector_transaction_id(authorize_response) { + if let Some(transaction_id) = utils::get_connector_transaction_id(authorize_response.response) { let sync_response = connector .psync_retry_till_status_matches( enums::AttemptStatus::Authorized, @@ -184,7 +183,7 @@ async fn should_sync_payment() { .unwrap(); assert_eq!(authorize_response.status, enums::AttemptStatus::Pending); - if let Some(transaction_id) = utils::get_connector_transaction_id(authorize_response) { + if let Some(transaction_id) = utils::get_connector_transaction_id(authorize_response.response) { // Sync the Payment Data let response = connector .psync_retry_till_status_matches( @@ -223,7 +222,7 @@ async fn should_void_already_authorized_payment() { assert_eq!(authorize_response.status, enums::AttemptStatus::Pending); //try CANCEL for previous payment - if let Some(transaction_id) = utils::get_connector_transaction_id(authorize_response) { + if let Some(transaction_id) = utils::get_connector_transaction_id(authorize_response.response) { let void_response = connector .void_payment(transaction_id.clone(), None, get_default_payment_info()) .await @@ -264,7 +263,7 @@ async fn should_refund_succeeded_payment() { .unwrap(); assert_eq!(authorize_response.status, enums::AttemptStatus::Pending); - if let Some(transaction_id) = utils::get_connector_transaction_id(authorize_response) { + if let Some(transaction_id) = utils::get_connector_transaction_id(authorize_response.response) { let sync_response = connector .psync_retry_till_status_matches( enums::AttemptStatus::Authorized, diff --git a/crates/router/tests/connectors/rapyd.rs b/crates/router/tests/connectors/rapyd.rs index c17a0be577..bf47e06411 100644 --- a/crates/router/tests/connectors/rapyd.rs +++ b/crates/router/tests/connectors/rapyd.rs @@ -81,7 +81,7 @@ async fn should_capture_already_authorized_payment() { let connector = Rapyd {}; let authorize_response = connector.authorize_payment(None, None).await.unwrap(); assert_eq!(authorize_response.status, enums::AttemptStatus::Authorized); - let txn_id = utils::get_connector_transaction_id(authorize_response); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); let response: OptionFuture<_> = txn_id .map(|transaction_id| async move { connector @@ -100,7 +100,7 @@ async fn voiding_already_authorized_payment_fails() { let connector = Rapyd {}; let authorize_response = connector.authorize_payment(None, None).await.unwrap(); assert_eq!(authorize_response.status, enums::AttemptStatus::Authorized); - let txn_id = utils::get_connector_transaction_id(authorize_response); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); let response: OptionFuture<_> = txn_id .map(|transaction_id| async move { connector @@ -120,7 +120,7 @@ async fn should_refund_succeeded_payment() { let response = connector.make_payment(None, None).await.unwrap(); //try refund for previous payment - if let Some(transaction_id) = utils::get_connector_transaction_id(response) { + if let Some(transaction_id) = utils::get_connector_transaction_id(response.response) { let response = connector .refund_payment(transaction_id, None, None) .await diff --git a/crates/router/tests/connectors/shift4.rs b/crates/router/tests/connectors/shift4.rs index 35209ebdf6..66f24aaf2f 100644 --- a/crates/router/tests/connectors/shift4.rs +++ b/crates/router/tests/connectors/shift4.rs @@ -1,4 +1,3 @@ -use futures::future::OptionFuture; use masking::Secret; use router::types::{self, api, storage::enums}; @@ -7,9 +6,10 @@ use crate::{ utils::{self, ConnectorActions}, }; -struct Shift4; -impl ConnectorActions for Shift4 {} -impl utils::Connector for Shift4 { +#[derive(Clone, Copy)] +struct Shift4Test; +impl ConnectorActions for Shift4Test {} +impl utils::Connector for Shift4Test { fn get_data(&self) -> types::api::ConnectorData { use router::connector::Shift4; types::api::ConnectorData { @@ -32,43 +32,146 @@ impl utils::Connector for Shift4 { } } +static CONNECTOR: Shift4Test = Shift4Test {}; + +// Cards Positive Tests +// Creates a payment using the manual capture flow (Non 3DS). #[actix_web::test] async fn should_only_authorize_payment() { - let response = Shift4 {}.authorize_payment(None, None).await.unwrap(); + let response = CONNECTOR.authorize_payment(None, None).await.unwrap(); assert_eq!(response.status, enums::AttemptStatus::Authorized); } +// Creates a payment using the automatic capture flow (Non 3DS). #[actix_web::test] -async fn should_authorize_and_capture_payment() { - let response = Shift4 {}.make_payment(None, None).await.unwrap(); - assert_eq!(response.status, enums::AttemptStatus::Charged); +async fn should_make_payment() { + let authorize_response = CONNECTOR.make_payment(None, None).await.unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); } +// Captures a payment using the manual capture flow (Non 3DS). #[actix_web::test] -async fn should_capture_already_authorized_payment() { - let connector = Shift4 {}; +async fn should_capture_authorized_payment() { + let connector = CONNECTOR; + let response = connector + .authorize_and_capture_payment(None, None, None) + .await; + assert_eq!(response.unwrap().status, enums::AttemptStatus::Charged); +} + +// Partially captures a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_partially_capture_authorized_payment() { + let connector = CONNECTOR; + let response = connector + .authorize_and_capture_payment( + None, + Some(types::PaymentsCaptureData { + amount_to_capture: Some(50), + ..utils::PaymentCaptureType::default().0 + }), + None, + ) + .await; + assert_eq!(response.unwrap().status, enums::AttemptStatus::Charged); +} + +// Synchronizes a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_authorized_payment() { + let connector = CONNECTOR; let authorize_response = connector.authorize_payment(None, None).await.unwrap(); - assert_eq!(authorize_response.status, enums::AttemptStatus::Authorized); - let txn_id = utils::get_connector_transaction_id(authorize_response); - let response: OptionFuture<_> = txn_id - .map(|transaction_id| async move { - connector - .capture_payment(transaction_id, None, None) - .await - .unwrap() - .status - }) - .into(); - assert_eq!(response.await, Some(enums::AttemptStatus::Charged)); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + let response = connector + .psync_retry_till_status_matches( + enums::AttemptStatus::Authorized, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + txn_id.unwrap(), + ), + encoded_data: None, + capture_method: None, + }), + None, + ) + .await + .unwrap(); + assert_eq!(response.status, enums::AttemptStatus::Authorized,); } +// Synchronizes a payment using the automatic capture flow (Non 3DS). #[actix_web::test] -async fn should_fail_payment_for_incorrect_cvc() { - let response = Shift4 {} +async fn should_sync_auto_captured_payment() { + // Authorize + let authorize_response = CONNECTOR.make_payment(None, None).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"); + let response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Charged, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + txn_id.unwrap(), + ), + encoded_data: None, + capture_method: None, + }), + None, + ) + .await + .unwrap(); + assert_eq!(response.status, enums::AttemptStatus::Charged,); +} + +// Voids a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_void_authorized_payment() { + let connector = CONNECTOR; + let response = connector + .authorize_and_void_payment( + None, + Some(types::PaymentsCancelData { + connector_transaction_id: "".to_string(), + cancellation_reason: Some("requested_by_customer".to_string()), + }), + None, + ) + .await; + assert_eq!(response.unwrap().status, enums::AttemptStatus::Pending); //shift4 doesn't allow voiding a payment +} + +// Cards Negative scenerios +// Creates a payment with incorrect card number. +#[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::PaymentMethod::Card(api::Card { - card_number: Secret::new("4024007134364842".to_string()), + card_number: Secret::new("1234567891011".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Your request was in test mode, but used a non test card. For a list of valid test cards, visit our doc site.".to_string(), + ); +} + +// Creates a payment with empty card number. +#[actix_web::test] +async fn should_fail_payment_for_empty_card_number() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethod::Card(api::Card { + card_number: Secret::new("".to_string()), ..utils::CCardType::default().0 }), ..utils::PaymentAuthorizeType::default().0 @@ -80,25 +183,251 @@ async fn should_fail_payment_for_incorrect_cvc() { let x = response.response.unwrap_err(); assert_eq!( x.message, - "The card's security code failed verification.".to_string(), + "The card number is not a valid credit card number.", + ); +} + +// Creates a payment with incorrect CVC. +#[actix_web::test] +async fn should_succeed_payment_for_incorrect_cvc() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethod::Card(api::Card { + card_cvc: Secret::new("asdasd".to_string()), //shift4 accept invalid CVV as it doesn't accept CVV + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!(response.status, enums::AttemptStatus::Charged); +} + +// Creates a payment with incorrect expiry month. +#[actix_web::test] +async fn should_fail_payment_for_invalid_exp_month() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethod::Card(api::Card { + card_exp_month: Secret::new("20".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "The card's expiration month is invalid.".to_string(), + ); +} + +// Creates a payment with incorrect expiry year. +#[actix_web::test] +async fn should_fail_payment_for_incorrect_expiry_year() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethod::Card(api::Card { + card_exp_year: Secret::new("2000".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "The card has expired.".to_string(), + ); +} + +// Voids a payment using automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_fail_void_payment_for_auto_capture() { + // Authorize + let authorize_response = CONNECTOR.make_payment(None, None).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"); + + // Void + let void_response = CONNECTOR + .void_payment(txn_id.unwrap(), None, None) + .await + .unwrap(); + assert_eq!(void_response.status, enums::AttemptStatus::Pending); //shift4 doesn't allow voiding a payment +} + +// Captures a payment using invalid connector payment id. +#[actix_web::test] +async fn should_fail_capture_for_invalid_payment() { + // Capture + let capture_response = CONNECTOR + .capture_payment("123456789".to_string(), None, None) + .await + .unwrap(); + assert_eq!( + capture_response.response.unwrap_err().message, + String::from("Charge '123456789' does not exist") + ); +} + +// Refunds a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_refund_auto_captured_payment() { + let connector = CONNECTOR; + let response = connector + .make_payment_and_refund(None, None, None) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Refunds a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_refund_manually_captured_payment() { + let connector = CONNECTOR; + let response = connector + .auth_capture_and_refund(None, None, None) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, ); } #[actix_web::test] -async fn should_refund_succeeded_payment() { - let connector = Shift4 {}; - //make a successful payment - let response = connector.make_payment(None, None).await.unwrap(); - - //try refund for previous payment - if let Some(transaction_id) = utils::get_connector_transaction_id(response) { - let response = connector - .refund_payment(transaction_id, None, None) - .await - .unwrap(); - assert_eq!( - response.response.unwrap().refund_status, - enums::RefundStatus::Success, - ); - } +async fn should_partially_refund_succeeded_payment() { + let connector = CONNECTOR; + let refund_response = connector + .make_payment_and_refund( + None, + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + refund_response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Partially refunds a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_partially_refund_manually_captured_payment() { + let connector = CONNECTOR; + let response = connector + .auth_capture_and_refund( + None, + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +#[actix_web::test] +async fn should_refund_succeeded_payment_multiple_times() { + let connector = CONNECTOR; + connector + .make_payment_and_multiple_refund( + None, + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + None, + ) + .await; +} + +// Refunds a payment with refund amount higher than payment amount. +#[actix_web::test] +async fn should_fail_for_refund_amount_higher_than_payment_amount() { + let connector = CONNECTOR; + let response = connector + .make_payment_and_refund( + None, + Some(types::RefundsData { + refund_amount: 150, + ..utils::PaymentRefundType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Invalid Refund data", + ); +} + +// Synchronizes a refund using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_refund() { + let connector = CONNECTOR; + let refund_response = connector + .make_payment_and_refund(None, None, None) + .await + .unwrap(); + let response = connector + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + refund_response.response.unwrap().connector_refund_id, + None, + None, + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Synchronizes a refund using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_manually_captured_refund() { + let connector = CONNECTOR; + let refund_response = connector + .auth_capture_and_refund(None, None, None) + .await + .unwrap(); + let response = connector + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + refund_response.response.unwrap().connector_refund_id, + None, + None, + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); } diff --git a/crates/router/tests/connectors/stripe.rs b/crates/router/tests/connectors/stripe.rs index 0537ac45c0..04fa9166ad 100644 --- a/crates/router/tests/connectors/stripe.rs +++ b/crates/router/tests/connectors/stripe.rs @@ -1,5 +1,3 @@ -use std::time::Duration; - use masking::Secret; use router::types::{self, api, storage::enums}; @@ -53,7 +51,7 @@ async fn should_only_authorize_payment() { } #[actix_web::test] -async fn should_authorize_and_capture_payment() { +async fn should_make_payment() { let response = Stripe {} .make_payment(get_payment_authorize_data(), None) .await @@ -87,13 +85,13 @@ async fn should_partially_capture_already_authorized_payment() { } #[actix_web::test] -async fn should_sync_payment() { +async fn should_sync_authorized_payment() { let connector = Stripe {}; let authorize_response = connector .authorize_payment(get_payment_authorize_data(), None) .await .unwrap(); - let txn_id = utils::get_connector_transaction_id(authorize_response); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); let response = connector .psync_retry_till_status_matches( enums::AttemptStatus::Authorized, @@ -111,6 +109,31 @@ async fn should_sync_payment() { assert_eq!(response.status, enums::AttemptStatus::Authorized,); } +#[actix_web::test] +async fn should_sync_payment() { + let connector = Stripe {}; + let authorize_response = connector + .make_payment(get_payment_authorize_data(), None) + .await + .unwrap(); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + let response = connector + .psync_retry_till_status_matches( + enums::AttemptStatus::Charged, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + txn_id.unwrap(), + ), + encoded_data: None, + capture_method: None, + }), + None, + ) + .await + .unwrap(); + assert_eq!(response.status, enums::AttemptStatus::Charged,); +} + #[actix_web::test] async fn should_void_already_authorized_payment() { let connector = Stripe {}; @@ -118,7 +141,7 @@ async fn should_void_already_authorized_payment() { .authorize_and_void_payment( get_payment_authorize_data(), Some(types::PaymentsCancelData { - connector_transaction_id: "".to_string(), + connector_transaction_id: "".to_string(), // this connector_transaction_id will be ignored and the transaction_id from payment authorize data will be used for void cancellation_reason: Some("requested_by_customer".to_string()), }), None, @@ -228,15 +251,33 @@ async fn should_fail_payment_for_invalid_card_cvc() { assert_eq!(x.message, "Your card's security code is invalid.",); } +// Voids a payment using automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_fail_void_payment_for_auto_capture() { + let connector = Stripe {}; + // Authorize + let authorize_response = connector + .make_payment(get_payment_authorize_data(), None) + .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"); + + // Void + let void_response = connector + .void_payment(txn_id.unwrap(), None, None) + .await + .unwrap(); + assert_eq!( + void_response.response.unwrap_err().message, + "You cannot cancel this PaymentIntent because it has a status of succeeded. Only a PaymentIntent with one of the following statuses may be canceled: requires_payment_method, requires_capture, requires_confirmation, requires_action, processing." + ); +} + #[actix_web::test] async fn should_fail_capture_for_invalid_payment() { let connector = Stripe {}; - let authorize_response = connector - .authorize_payment(get_payment_authorize_data(), None) - .await - .unwrap(); - assert_eq!(authorize_response.status, enums::AttemptStatus::Authorized); - tokio::time::sleep(Duration::from_secs(5)).await; // to avoid 404 error as stripe takes some time to process the new transaction let response = connector .capture_payment("12345".to_string(), None, None) .await @@ -259,6 +300,19 @@ async fn should_refund_succeeded_payment() { ); } +#[actix_web::test] +async fn should_refund_manually_captured_payment() { + let connector = Stripe {}; + let response = connector + .auth_capture_and_refund(get_payment_authorize_data(), None, None) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + #[actix_web::test] async fn should_partially_refund_succeeded_payment() { let connector = Stripe {}; @@ -279,6 +333,26 @@ async fn should_partially_refund_succeeded_payment() { ); } +#[actix_web::test] +async fn should_partially_refund_manually_captured_payment() { + let connector = Stripe {}; + let response = connector + .auth_capture_and_refund( + get_payment_authorize_data(), + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + #[actix_web::test] async fn should_refund_succeeded_payment_multiple_times() { let connector = Stripe {}; @@ -335,3 +409,25 @@ async fn should_sync_refund() { enums::RefundStatus::Success, ); } + +#[actix_web::test] +async fn should_sync_manually_captured_refund() { + let connector = Stripe {}; + let refund_response = connector + .auth_capture_and_refund(get_payment_authorize_data(), None, None) + .await + .unwrap(); + let response = connector + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + refund_response.response.unwrap().connector_refund_id, + None, + None, + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index b46db3f6e0..32ab2637bf 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -29,6 +29,7 @@ pub struct PaymentInfo { pub address: Option, pub auth_type: Option, pub access_token: Option, + pub router_return_url: Option, } #[async_trait] @@ -41,7 +42,7 @@ pub trait ConnectorActions: Connector { let integration = self.get_data().connector.get_connector_integration(); let request = self.generate_data( types::PaymentsAuthorizeData { - confirm: false, + confirm: true, capture_method: Some(storage_models::enums::CaptureMethod::Manual), ..(payment_data.unwrap_or(PaymentAuthorizeType::default().0)) }, @@ -57,7 +58,11 @@ pub trait ConnectorActions: Connector { ) -> Result> { let integration = self.get_data().connector.get_connector_integration(); let request = self.generate_data( - payment_data.unwrap_or_else(|| PaymentAuthorizeType::default().0), + types::PaymentsAuthorizeData { + confirm: true, + capture_method: Some(storage_models::enums::CaptureMethod::Automatic), + ..(payment_data.unwrap_or(PaymentAuthorizeType::default().0)) + }, payment_info, ); call_connector(request, integration).await @@ -125,7 +130,7 @@ pub trait ConnectorActions: Connector { .await .unwrap(); assert_eq!(authorize_response.status, enums::AttemptStatus::Authorized); - let txn_id = get_connector_transaction_id(authorize_response); + let txn_id = get_connector_transaction_id(authorize_response.response); let response = self .capture_payment(txn_id.unwrap(), capture_data, payment_info) .await @@ -161,7 +166,7 @@ pub trait ConnectorActions: Connector { .await .unwrap(); assert_eq!(authorize_response.status, enums::AttemptStatus::Authorized); - let txn_id = get_connector_transaction_id(authorize_response); + let txn_id = get_connector_transaction_id(authorize_response.response); tokio::time::sleep(Duration::from_secs(self.get_request_interval())).await; // to avoid 404 error let response = self .void_payment(txn_id.unwrap(), void_data, payment_info) @@ -222,7 +227,28 @@ pub trait ConnectorActions: Connector { .unwrap(); //try refund for previous payment - let transaction_id = get_connector_transaction_id(response).unwrap(); + let transaction_id = get_connector_transaction_id(response.response).unwrap(); + tokio::time::sleep(Duration::from_secs(self.get_request_interval())).await; // to avoid 404 error + Ok(self + .refund_payment(transaction_id, refund_data, payment_info) + .await + .unwrap()) + } + + async fn auth_capture_and_refund( + &self, + authorize_data: Option, + refund_data: Option, + payment_info: Option, + ) -> Result> { + //make a successful payment + let response = self + .authorize_and_capture_payment(authorize_data, None, payment_info.clone()) + .await + .unwrap(); + + //try refund for previous payment + let transaction_id = get_connector_transaction_id(response.response).unwrap(); tokio::time::sleep(Duration::from_secs(self.get_request_interval())).await; // to avoid 404 error Ok(self .refund_payment(transaction_id, refund_data, payment_info) @@ -243,7 +269,7 @@ pub trait ConnectorActions: Connector { .unwrap(); //try refund for previous payment - let transaction_id = get_connector_transaction_id(response).unwrap(); + let transaction_id = get_connector_transaction_id(response.response).unwrap(); for _x in 0..2 { tokio::time::sleep(Duration::from_secs(self.get_request_interval())).await; // to avoid 404 error let refund_response = self @@ -324,7 +350,7 @@ pub trait ConnectorActions: Connector { payment_id: uuid::Uuid::new_v4().to_string(), attempt_id: Some(uuid::Uuid::new_v4().to_string()), status: enums::AttemptStatus::default(), - router_return_url: None, + router_return_url: info.clone().and_then(|a| a.router_return_url), auth_type: info .clone() .map_or(enums::AuthenticationType::NoThreeDs, |a| { @@ -516,9 +542,9 @@ impl Default for PaymentRefundType { } pub fn get_connector_transaction_id( - response: types::PaymentsAuthorizeRouterData, + response: Result, ) -> Option { - match response.response { + match response { Ok(types::PaymentsResponseData::TransactionResponse { resource_id, .. }) => { resource_id.get_connector_transaction_id().ok() } diff --git a/crates/router/tests/connectors/worldline.rs b/crates/router/tests/connectors/worldline.rs index 8ae7ded8de..08bcbad43f 100644 --- a/crates/router/tests/connectors/worldline.rs +++ b/crates/router/tests/connectors/worldline.rs @@ -49,8 +49,7 @@ impl WorldlineTest { }), ..Default::default() }), - auth_type: None, - access_token: None, + ..Default::default() }) } @@ -196,7 +195,8 @@ async fn should_sync_manual_auth_payment() { .await .unwrap(); assert_eq!(response.status, enums::AttemptStatus::Authorized); - let connector_payment_id = utils::get_connector_transaction_id(response).unwrap_or_default(); + let connector_payment_id = + utils::get_connector_transaction_id(response.response).unwrap_or_default(); let sync_response = connector .sync_payment( Some(types::PaymentsSyncData { @@ -228,7 +228,8 @@ async fn should_sync_auto_auth_payment() { .await .unwrap(); assert_eq!(response.status, enums::AttemptStatus::Pending); - let connector_payment_id = utils::get_connector_transaction_id(response).unwrap_or_default(); + let connector_payment_id = + utils::get_connector_transaction_id(response.response).unwrap_or_default(); let sync_response = connector .sync_payment( Some(types::PaymentsSyncData { @@ -260,7 +261,8 @@ async fn should_capture_authorized_payment() { .await .unwrap(); assert_eq!(response.status, enums::AttemptStatus::Authorized); - let connector_payment_id = utils::get_connector_transaction_id(response).unwrap_or_default(); + let connector_payment_id = + utils::get_connector_transaction_id(response.response).unwrap_or_default(); let capture_response = WorldlineTest {} .capture_payment(connector_payment_id, None, None) .await @@ -298,7 +300,8 @@ async fn should_cancel_unauthorized_payment() { .await .unwrap(); assert_eq!(response.status, enums::AttemptStatus::Authorized); - let connector_payment_id = utils::get_connector_transaction_id(response).unwrap_or_default(); + let connector_payment_id = + utils::get_connector_transaction_id(response.response).unwrap_or_default(); let cancel_response = connector .void_payment(connector_payment_id, None, None) .await @@ -321,7 +324,8 @@ async fn should_cancel_uncaptured_payment() { .await .unwrap(); assert_eq!(response.status, enums::AttemptStatus::Pending); - let connector_payment_id = utils::get_connector_transaction_id(response).unwrap_or_default(); + let connector_payment_id = + utils::get_connector_transaction_id(response.response).unwrap_or_default(); let cancel_response = connector .void_payment(connector_payment_id, None, None) .await @@ -356,7 +360,8 @@ async fn should_fail_refund_with_invalid_payment_status() { .await .unwrap(); assert_eq!(response.status, enums::AttemptStatus::Authorized); - let connector_payment_id = utils::get_connector_transaction_id(response).unwrap_or_default(); + let connector_payment_id = + utils::get_connector_transaction_id(response.response).unwrap_or_default(); let refund_response = connector .refund_payment(connector_payment_id, None, None) .await diff --git a/crates/router/tests/connectors/worldpay.rs b/crates/router/tests/connectors/worldpay.rs index a0c76a0ea8..17e27f1b4f 100644 --- a/crates/router/tests/connectors/worldpay.rs +++ b/crates/router/tests/connectors/worldpay.rs @@ -51,7 +51,7 @@ async fn should_authorize_card_payment() { let response = conn.authorize_payment(None, None).await.unwrap(); assert_eq!(response.status, enums::AttemptStatus::Authorized); assert_eq!( - utils::get_connector_transaction_id(response), + utils::get_connector_transaction_id(response.response), Some("123456".to_string()) ); } @@ -76,7 +76,7 @@ async fn should_authorize_gpay_payment() { .unwrap(); assert_eq!(response.status, enums::AttemptStatus::Authorized); assert_eq!( - utils::get_connector_transaction_id(response), + utils::get_connector_transaction_id(response.response), Some("123456".to_string()) ); } @@ -101,7 +101,7 @@ async fn should_authorize_applepay_payment() { .unwrap(); assert_eq!(response.status, enums::AttemptStatus::Authorized); assert_eq!( - utils::get_connector_transaction_id(response), + utils::get_connector_transaction_id(response.response), Some("123456".to_string()) ); } @@ -113,7 +113,7 @@ async fn should_capture_already_authorized_payment() { let _mock = connector.start_server(get_mock_config()).await; let authorize_response = connector.authorize_payment(None, None).await.unwrap(); assert_eq!(authorize_response.status, enums::AttemptStatus::Authorized); - let txn_id = utils::get_connector_transaction_id(authorize_response); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); let response: OptionFuture<_> = txn_id .map(|transaction_id| async move { connector @@ -154,7 +154,7 @@ async fn should_void_already_authorized_payment() { let _mock = connector.start_server(get_mock_config()).await; let authorize_response = connector.authorize_payment(None, None).await.unwrap(); assert_eq!(authorize_response.status, enums::AttemptStatus::Authorized); - let txn_id = utils::get_connector_transaction_id(authorize_response); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); let response: OptionFuture<_> = txn_id .map(|transaction_id| async move { connector @@ -195,7 +195,7 @@ async fn should_refund_succeeded_payment() { let response = connector.make_payment(None, None).await.unwrap(); //try refund for previous payment - let transaction_id = utils::get_connector_transaction_id(response).unwrap(); + let transaction_id = utils::get_connector_transaction_id(response.response).unwrap(); let response = connector .refund_payment(transaction_id, None, None) .await diff --git a/scripts/add_connector.sh b/scripts/add_connector.sh index 494cc4a87e..b4c4a82230 100644 --- a/scripts/add_connector.sh +++ b/scripts/add_connector.sh @@ -11,7 +11,7 @@ fi cd $SCRIPT/.. # remove template files if already created for this connector rm -rf $conn/$pg $conn/$pg.rs -git checkout $conn.rs $src/types/api.rs $src/configs/settings.rs config/Development.toml config/docker_compose.toml config/config.example.toml loadtest/config/Development.toml crates/router/src/configs/defaults.toml crates/api_models/src/enums.rs +git checkout $conn.rs $src/types/api.rs $src/configs/settings.rs config/Development.toml config/docker_compose.toml config/config.example.toml loadtest/config/Development.toml crates/api_models/src/enums.rs # add enum for this connector in required places sed -i'' -e "s/pub use self::{/pub mod ${pg};\n\npub use self::{/" $conn.rs sed -i'' -e "s/};/${pg}::${pgc},\n};/" $conn.rs @@ -25,10 +25,9 @@ sed -i'' -e "s/\[connectors.supported\]/[connectors.${pg}]\nbase_url = ""\n\n[co sed -r -i'' -e "s/cards = \[(.*)\]/cards = [\1, \"${pg}\"]/" config/config.example.toml sed -i'' -e "s/\[connectors.supported\]/[connectors.${pg}]\nbase_url = ""\n\n[connectors.supported]/" loadtest/config/Development.toml sed -r -i'' -e "s/cards = \[(.*)\]/cards = [\1, \"${pg}\"]/" loadtest/config/Development.toml -sed -i'' -e "s/\[connectors.supported\]/[connectors.${pg}]\nbase_url = ""\n\n[connectors.supported]/" crates/router/src/configs/defaults.toml sed -i'' -e "s/Dummy,/Dummy,\n\t${pgc},/" crates/api_models/src/enums.rs # remove temporary files created in above step -rm $conn.rs-e $src/types/api.rs-e $src/configs/settings.rs-e config/Development.toml-e config/docker_compose.toml-e config/config.example.toml-e loadtest/config/Development.toml-e crates/router/src/configs/defaults.toml-e crates/api_models/src/enums.rs-e +rm $conn.rs-e $src/types/api.rs-e $src/configs/settings.rs-e config/Development.toml-e config/docker_compose.toml-e config/config.example.toml-e loadtest/config/Development.toml-e crates/api_models/src/enums.rs-e cd $conn/ # generate template files for the connector cargo install cargo-generate