diff --git a/.github/secrets/connector_auth.toml.gpg b/.github/secrets/connector_auth.toml.gpg index 8a129e2a89..a0627a8d25 100644 Binary files a/.github/secrets/connector_auth.toml.gpg and b/.github/secrets/connector_auth.toml.gpg differ diff --git a/config/development.toml b/config/development.toml index 7b5fc0b1f6..65cc1bc4d6 100644 --- a/config/development.toml +++ b/config/development.toml @@ -187,3 +187,4 @@ apple_pay = { country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,D [tokenization] stripe = { long_lived_token = false, payment_method = "wallet"} +checkout = { long_lived_token = false, payment_method = "wallet"} diff --git a/crates/router/src/connector/checkout.rs b/crates/router/src/connector/checkout.rs index 6c6ff98b69..b034f25bb4 100644 --- a/crates/router/src/connector/checkout.rs +++ b/crates/router/src/connector/checkout.rs @@ -4,10 +4,11 @@ mod transformers; use std::fmt::Debug; +use common_utils::{crypto, ext_traits::ByteSliceExt}; use error_stack::{IntoReport, ResultExt}; use self::transformers as checkout; -use super::utils::RefundsRequestData; +use super::utils::{self as conn_utils, RefundsRequestData}; use crate::{ configs::settings, consts, @@ -15,10 +16,12 @@ use crate::{ errors::{self, CustomResult}, payments, }, - headers, services, + db::StorageInterface, + headers, + services::{self, ConnectorIntegration}, types::{ self, - api::{self, ConnectorCommon}, + api::{self, ConnectorCommon, ConnectorCommonExt}, }, utils::{self, BytesExt}, }; @@ -26,6 +29,25 @@ use crate::{ #[derive(Debug, Clone)] pub struct Checkout; +impl ConnectorCommonExt for Checkout +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &types::RouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let mut header = vec![( + headers::CONTENT_TYPE.to_string(), + types::PaymentsAuthorizeType::get_content_type(self).to_string(), + )]; + let mut api_key = self.get_auth_header(&req.connector_auth_type)?; + header.append(&mut api_key); + Ok(header) + } +} + impl ConnectorCommon for Checkout { fn id(&self) -> &'static str { "checkout" @@ -44,13 +66,45 @@ impl ConnectorCommon for Checkout { .change_context(errors::ConnectorError::FailedToObtainAuthType)?; Ok(vec![( headers::AUTHORIZATION.to_string(), - format!("Bearer {}", auth.api_key), + format!("Bearer {}", auth.api_secret), )]) } fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { connectors.checkout.base_url.as_ref() } + fn build_error_response( + &self, + res: types::Response, + ) -> CustomResult { + let response: checkout::ErrorResponse = if res.response.is_empty() { + checkout::ErrorResponse { + request_id: None, + error_type: if res.status_code == 401 | 422 { + Some("Invalid Api Key".to_owned()) + } else { + None + }, + error_codes: None, + } + } else { + res.response + .parse_struct("ErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)? + }; + + Ok(types::ErrorResponse { + status_code: res.status_code, + code: response + .error_codes + .unwrap_or_else(|| vec![consts::NO_ERROR_CODE.to_string()]) + .join(" & "), + message: response + .error_type + .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), + reason: None, + }) + } } impl api::Payment for Checkout {} @@ -64,68 +118,130 @@ impl api::ConnectorAccessToken for Checkout {} impl api::PaymentToken for Checkout {} impl - services::ConnectorIntegration< + ConnectorIntegration< api::PaymentMethodToken, types::PaymentMethodTokenizationData, types::PaymentsResponseData, > for Checkout -{ - // Not Implemented (R) -} - -impl - services::ConnectorIntegration< - api::Session, - types::PaymentsSessionData, - types::PaymentsResponseData, - > for Checkout -{ - // Not Implemented (R) -} - -impl - services::ConnectorIntegration< - api::AccessTokenAuth, - types::AccessTokenRequestData, - types::AccessToken, - > for Checkout -{ - // Not Implemented (R) -} - -impl api::PreVerify for Checkout {} - -impl - services::ConnectorIntegration< - api::Verify, - types::VerifyRequestData, - types::PaymentsResponseData, - > for Checkout -{ - // Issue: #173 -} - -impl - services::ConnectorIntegration< - api::Capture, - types::PaymentsCaptureData, - types::PaymentsResponseData, - > for Checkout { fn get_headers( &self, - req: &types::PaymentsCaptureRouterData, + req: &types::TokenizationRouterData, _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let mut header = vec![( headers::CONTENT_TYPE.to_string(), self.common_get_content_type().to_string(), )]; - let mut api_key = self.get_auth_header(&req.connector_auth_type)?; - header.append(&mut api_key); + let api_key = checkout::CheckoutAuthType::try_from(&req.connector_auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let mut auth = vec![( + headers::AUTHORIZATION.to_string(), + format!("Bearer {}", api_key.api_key), + )]; + header.append(&mut auth); Ok(header) } + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::TokenizationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}tokens", self.base_url(connectors))) + } + + fn get_request_body( + &self, + req: &types::TokenizationRouterData, + ) -> CustomResult, errors::ConnectorError> { + let connector_req = checkout::TokenRequest::try_from(req)?; + let checkout_req = + utils::Encode::::encode_to_string_of_json(&connector_req) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(checkout_req)) + } + + fn build_request( + &self, + req: &types::TokenizationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::TokenizationType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::TokenizationType::get_headers(self, req, connectors)?) + .body(types::TokenizationType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::TokenizationRouterData, + res: types::Response, + ) -> CustomResult + where + types::PaymentsResponseData: Clone, + { + let response: checkout::CheckoutTokenResponse = res + .response + .parse_struct("CheckoutTokenResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: types::Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Checkout +{ + // Not Implemented (R) +} + +impl ConnectorIntegration + for Checkout +{ + // Not Implemented (R) +} + +impl api::PreVerify for Checkout {} + +impl ConnectorIntegration + for Checkout +{ + // Issue: #173 +} + +impl ConnectorIntegration + for Checkout +{ + fn get_headers( + &self, + req: &types::PaymentsCaptureRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + fn get_url( &self, req: &types::PaymentsCaptureRouterData, @@ -190,40 +306,19 @@ impl &self, res: types::Response, ) -> CustomResult { - let response: checkout::ErrorResponse = res - .response - .parse_struct("ErrorResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - Ok(types::ErrorResponse { - status_code: res.status_code, - code: response - .error_codes - .unwrap_or_else(|| vec![consts::NO_ERROR_CODE.to_string()]) - .join(" & "), - message: response - .error_type - .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), - reason: None, - }) + self.build_error_response(res) } } -impl - services::ConnectorIntegration +impl ConnectorIntegration for Checkout { fn get_headers( &self, req: &types::PaymentsSyncRouterData, - _connectors: &settings::Connectors, + connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - let mut header = vec![( - headers::CONTENT_TYPE.to_string(), - types::PaymentsAuthorizeType::get_content_type(self).to_string(), - )]; - let mut api_key = self.get_auth_header(&req.connector_auth_type)?; - header.append(&mut api_key); - Ok(header) + self.build_headers(req, connectors) } fn get_url( @@ -284,43 +379,19 @@ impl &self, res: types::Response, ) -> CustomResult { - let response: checkout::ErrorResponse = res - .response - .parse_struct("ErrorResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - Ok(types::ErrorResponse { - status_code: res.status_code, - code: response - .error_codes - .unwrap_or_else(|| vec![consts::NO_ERROR_CODE.to_string()]) - .join(" &"), - message: response - .error_type - .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), - reason: None, - }) + self.build_error_response(res) } } -impl - services::ConnectorIntegration< - api::Authorize, - types::PaymentsAuthorizeData, - types::PaymentsResponseData, - > for Checkout +impl ConnectorIntegration + for Checkout { fn get_headers( &self, req: &types::PaymentsAuthorizeRouterData, - _connectors: &settings::Connectors, + connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - let mut header = vec![( - headers::CONTENT_TYPE.to_string(), - types::PaymentsAuthorizeType::get_content_type(self).to_string(), - )]; - let mut api_key = self.get_auth_header(&req.connector_auth_type)?; - header.append(&mut api_key); - Ok(header) + self.build_headers(req, connectors) } fn get_url( @@ -386,55 +457,19 @@ impl &self, res: types::Response, ) -> CustomResult { - let response: checkout::ErrorResponse = if res.response.is_empty() { - checkout::ErrorResponse { - request_id: None, - error_type: if res.status_code == 401 { - Some("Invalid Api Key".to_owned()) - } else { - None - }, - error_codes: None, - } - } else { - res.response - .parse_struct("ErrorResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)? - }; - - Ok(types::ErrorResponse { - status_code: res.status_code, - code: response - .error_codes - .unwrap_or_else(|| vec![consts::NO_ERROR_CODE.to_string()]) - .join(" & "), - message: response - .error_type - .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), - reason: None, - }) + self.build_error_response(res) } } -impl - services::ConnectorIntegration< - api::Void, - types::PaymentsCancelData, - types::PaymentsResponseData, - > for Checkout +impl ConnectorIntegration + for Checkout { fn get_headers( &self, req: &types::PaymentsCancelRouterData, - _connectors: &settings::Connectors, + connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - let mut header = vec![( - headers::CONTENT_TYPE.to_string(), - types::PaymentsVoidType::get_content_type(self).to_string(), - )]; - let mut api_key = self.get_auth_header(&req.connector_auth_type)?; - header.append(&mut api_key); - Ok(header) + self.build_headers(req, connectors) } fn get_url( @@ -497,21 +532,7 @@ impl &self, res: types::Response, ) -> CustomResult { - let response: checkout::ErrorResponse = res - .response - .parse_struct("ErrorResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - Ok(types::ErrorResponse { - status_code: res.status_code, - code: response - .error_codes - .unwrap_or_else(|| vec![consts::NO_ERROR_CODE.to_string()]) - .join(" & "), - message: response - .error_type - .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), - reason: None, - }) + self.build_error_response(res) } } @@ -519,21 +540,15 @@ impl api::Refund for Checkout {} impl api::RefundExecute for Checkout {} impl api::RefundSync for Checkout {} -impl services::ConnectorIntegration +impl ConnectorIntegration for Checkout { fn get_headers( &self, req: &types::RefundsRouterData, - _connectors: &settings::Connectors, + connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - let mut header = vec![( - headers::CONTENT_TYPE.to_string(), - types::RefundExecuteType::get_content_type(self).to_string(), - )]; - let mut api_key = self.get_auth_header(&req.connector_auth_type)?; - header.append(&mut api_key); - Ok(header) + self.build_headers(req, connectors) } fn get_content_type(&self) -> &'static str { @@ -607,39 +622,17 @@ impl services::ConnectorIntegration CustomResult { - let response: checkout::ErrorResponse = res - .response - .parse_struct("ErrorResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - Ok(types::ErrorResponse { - status_code: res.status_code, - code: response - .error_codes - .unwrap_or_else(|| vec![consts::NO_ERROR_CODE.to_string()]) - .join(" & "), - message: response - .error_type - .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), - reason: None, - }) + self.build_error_response(res) } } -impl services::ConnectorIntegration - for Checkout -{ +impl ConnectorIntegration for Checkout { fn get_headers( &self, req: &types::RefundsRouterData, - _connectors: &settings::Connectors, + connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - let mut header = vec![( - headers::CONTENT_TYPE.to_string(), - types::RefundSyncType::get_content_type(self).to_string(), - )]; - let mut api_key = self.get_auth_header(&req.connector_auth_type)?; - header.append(&mut api_key); - Ok(header) + self.build_headers(req, connectors) } fn get_url( @@ -700,45 +693,126 @@ impl services::ConnectorIntegration CustomResult { - let response: checkout::ErrorResponse = res - .response - .parse_struct("ErrorResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - Ok(types::ErrorResponse { - status_code: res.status_code, - code: response - .error_codes - .unwrap_or_else(|| vec![consts::NO_ERROR_CODE.to_string()]) - .join(" & "), - message: response - .error_type - .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), - reason: None, - }) + self.build_error_response(res) } } #[async_trait::async_trait] impl api::IncomingWebhook for Checkout { - fn get_webhook_object_reference_id( + fn get_webhook_source_verification_algorithm( &self, _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + Ok(Box::new(crypto::HmacSha256)) + } + fn get_webhook_source_verification_signature( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + let signature = conn_utils::get_header_key_value("cko-signature", request.headers) + .change_context(errors::ConnectorError::WebhookSignatureNotFound)?; + hex::decode(signature) + .into_report() + .change_context(errors::ConnectorError::WebhookSignatureNotFound) + } + fn get_webhook_source_verification_message( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + _merchant_id: &str, + _secret: &[u8], + ) -> CustomResult, errors::ConnectorError> { + Ok(format!("{}", String::from_utf8_lossy(request.body)).into_bytes()) + } + async fn get_webhook_source_verification_merchant_secret( + &self, + db: &dyn StorageInterface, + merchant_id: &str, + ) -> CustomResult, errors::ConnectorError> { + let key = format!("whsec_verification_{}_{}", self.id(), merchant_id); + let secret = db + .find_config_by_key(&key) + .await + .change_context(errors::ConnectorError::WebhookVerificationSecretNotFound)?; + Ok(secret.config.into_bytes()) + } + fn get_webhook_object_reference_id( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + let details: checkout::CheckoutWebhookBody = request + .body + .parse_struct("CheckoutWebhookBody") + .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; + + if checkout::is_chargeback_event(&details.txn_type) { + return Ok(api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::ConnectorTransactionId( + details + .data + .payment_id + .ok_or(errors::ConnectorError::WebhookReferenceIdNotFound)?, + ), + )); + } + if checkout::is_refund_event(&details.txn_type) { + return Ok(api_models::webhooks::ObjectReferenceId::RefundId( + api_models::webhooks::RefundIdType::ConnectorRefundId( + details + .data + .action_id + .ok_or(errors::ConnectorError::WebhookReferenceIdNotFound)?, + ), + )); + } + Ok(api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::ConnectorTransactionId(details.data.id), + )) } fn get_webhook_event_type( &self, - _request: &api::IncomingWebhookRequestDetails<'_>, + request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + let details: checkout::CheckoutWebhookBody = request + .body + .parse_struct("CheckoutWebhookBody") + .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; + + Ok(api::IncomingWebhookEvent::from(details.txn_type)) } fn get_webhook_resource_object( &self, - _request: &api::IncomingWebhookRequestDetails<'_>, + request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + let details: checkout::CheckoutWebhookObjectResource = request + .body + .parse_struct("CheckoutWebhookObjectResource") + .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; + + Ok(details.data) + } + + fn get_dispute_details( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + let dispute_details: checkout::CheckoutWebhookBody = request + .body + .parse_struct("CheckoutWebhookBody") + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + Ok(api::disputes::DisputePayload { + amount: dispute_details.data.amount.to_string(), + currency: dispute_details.data.currency, + dispute_stage: api_models::enums::DisputeStage::from(dispute_details.txn_type.clone()), + connector_dispute_id: dispute_details.data.id, + connector_reason: None, + connector_reason_code: dispute_details.data.reason_code, + challenge_required_by: dispute_details.data.evidence_required_by, + connector_status: dispute_details.txn_type.to_string(), + created_at: dispute_details.created_on, + updated_at: dispute_details.data.date, + }) } } diff --git a/crates/router/src/connector/checkout/transformers.rs b/crates/router/src/connector/checkout/transformers.rs index a9d5eef4fa..1de1256d7e 100644 --- a/crates/router/src/connector/checkout/transformers.rs +++ b/crates/router/src/connector/checkout/transformers.rs @@ -1,30 +1,131 @@ +use error_stack::IntoReport; use serde::{Deserialize, Serialize}; use url::Url; use crate::{ + connector::utils::{RouterData, WalletData}, core::errors, pii, services, types::{self, api, storage::enums, transformers::ForeignFrom}, }; +#[derive(Debug, Serialize)] +#[serde(rename_all = "lowercase")] +#[serde(tag = "type", content = "token_data")] +pub enum TokenRequest { + Googlepay(CheckoutGooglePayData), + Applepay(CheckoutApplePayData), +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CheckoutGooglePayData { + protocol_version: pii::Secret, + signature: pii::Secret, + signed_message: pii::Secret, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct CheckoutApplePayData { + version: pii::Secret, + data: pii::Secret, + signature: pii::Secret, + header: CheckoutApplePayHeader, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CheckoutApplePayHeader { + ephemeral_public_key: pii::Secret, + public_key_hash: pii::Secret, + transaction_id: pii::Secret, +} + +impl TryFrom<&types::TokenizationRouterData> for TokenRequest { + type Error = error_stack::Report; + fn try_from(item: &types::TokenizationRouterData) -> Result { + match item.request.payment_method_data.clone() { + api::PaymentMethodData::Wallet(wallet_data) => match wallet_data.clone() { + api_models::payments::WalletData::GooglePay(_data) => { + let json_wallet_data: CheckoutGooglePayData = + wallet_data.get_wallet_token_as_json()?; + Ok(Self::Googlepay(json_wallet_data)) + } + api_models::payments::WalletData::ApplePay(_data) => { + let json_wallet_data: CheckoutApplePayData = + wallet_data.get_wallet_token_as_json()?; + Ok(Self::Applepay(json_wallet_data)) + } + _ => Err(errors::ConnectorError::NotImplemented( + "Payment Method".to_string(), + )) + .into_report(), + }, + _ => Err(errors::ConnectorError::NotImplemented( + "Payment Method".to_string(), + )) + .into_report(), + } + } +} + +#[derive(Debug, Eq, PartialEq, Deserialize)] +pub struct CheckoutTokenResponse { + token: String, +} + +impl + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(types::PaymentsResponseData::TokenizationResponse { + token: item.response.token, + }), + ..item.data + }) + } +} + #[derive(Debug, Serialize)] pub struct CardSource { #[serde(rename = "type")] - pub source_type: Option, - pub number: Option>, - pub expiry_month: Option>, - pub expiry_year: Option>, + pub source_type: CheckoutSourceTypes, + pub number: pii::Secret, + pub expiry_month: pii::Secret, + pub expiry_year: pii::Secret, + pub cvv: pii::Secret, +} + +#[derive(Debug, Serialize)] +pub struct WalletSource { + #[serde(rename = "type")] + pub source_type: CheckoutSourceTypes, + pub token: String, } #[derive(Debug, Serialize)] #[serde(untagged)] -pub enum Source { +pub enum PaymentSource { Card(CardSource), + Wallets(WalletSource), +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum CheckoutSourceTypes { + Card, + Token, } pub struct CheckoutAuthType { pub(super) api_key: String, pub(super) processing_channel_id: String, + pub(super) api_secret: String, } #[derive(Debug, Serialize)] @@ -35,7 +136,7 @@ pub struct ReturnUrl { #[derive(Debug, Serialize)] pub struct PaymentsRequest { - pub source: Source, + pub source: PaymentSource, pub amount: i64, pub currency: String, pub processing_channel_id: String, @@ -55,9 +156,15 @@ pub struct CheckoutThreeDS { impl TryFrom<&types::ConnectorAuthType> for CheckoutAuthType { type Error = error_stack::Report; fn try_from(auth_type: &types::ConnectorAuthType) -> Result { - if let types::ConnectorAuthType::BodyKey { api_key, key1 } = auth_type { + if let types::ConnectorAuthType::SignatureKey { + api_key, + api_secret, + key1, + } = auth_type + { Ok(Self { api_key: api_key.to_string(), + api_secret: api_secret.to_string(), processing_channel_id: key1.to_string(), }) } else { @@ -68,13 +175,33 @@ impl TryFrom<&types::ConnectorAuthType> for CheckoutAuthType { impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaymentsRequest { type Error = error_stack::Report; fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { - let ccard = match item.request.payment_method_data { - api::PaymentMethodData::Card(ref ccard) => Some(ccard), - api::PaymentMethodData::Wallet(_) - | api::PaymentMethodData::PayLater(_) - | api::PaymentMethodData::BankRedirect(_) - | api::PaymentMethodData::Crypto(_) => None, - }; + let source_var = match item.request.payment_method_data.clone() { + api::PaymentMethodData::Card(ccard) => { + let a = PaymentSource::Card(CardSource { + source_type: CheckoutSourceTypes::Card, + number: ccard.card_number.clone(), + expiry_month: ccard.card_exp_month.clone(), + expiry_year: ccard.card_exp_year.clone(), + cvv: ccard.card_cvc, + }); + Ok(a) + } + api::PaymentMethodData::Wallet(wallet_data) => match wallet_data { + api_models::payments::WalletData::GooglePay(_) + | api_models::payments::WalletData::ApplePay(_) => { + Ok(PaymentSource::Wallets(WalletSource { + source_type: CheckoutSourceTypes::Token, + token: item.get_payment_method_token()?, + })) + } + _ => Err(errors::ConnectorError::NotImplemented( + "Payment Method".to_string(), + )), + }, + _ => Err(errors::ConnectorError::NotImplemented( + "Payment Method".to_string(), + )), + }?; let three_ds = match item.auth_type { enums::AuthenticationType::ThreeDs => CheckoutThreeDS { @@ -105,12 +232,6 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaymentsRequest { Some(enums::CaptureMethod::Automatic) ); - let source_var = Source::Card(CardSource { - source_type: Some("card".to_owned()), - number: ccard.map(|x| x.card_number.clone()), - expiry_month: ccard.map(|x| x.card_exp_month.clone()), - expiry_year: ccard.map(|x| x.card_exp_year.clone()), - }); let connector_auth = &item.connector_auth_type; let auth_type: CheckoutAuthType = connector_auth.try_into()?; let processing_channel_id = auth_type.processing_channel_id; @@ -551,3 +672,106 @@ impl From for enums::AttemptStatus { } } } + +pub fn is_refund_event(event_code: &CheckoutTxnType) -> bool { + matches!( + event_code, + CheckoutTxnType::PaymentRefunded | CheckoutTxnType::PaymentRefundDeclined + ) +} + +pub fn is_chargeback_event(event_code: &CheckoutTxnType) -> bool { + matches!( + event_code, + CheckoutTxnType::DisputeReceived + | CheckoutTxnType::DisputeExpired + | CheckoutTxnType::DisputeAccepted + | CheckoutTxnType::DisputeCanceled + | CheckoutTxnType::DisputeEvidenceSubmitted + | CheckoutTxnType::DisputeEvidenceAcknowledgedByScheme + | CheckoutTxnType::DisputeEvidenceRequired + | CheckoutTxnType::DisputeArbitrationLost + | CheckoutTxnType::DisputeArbitrationWon + | CheckoutTxnType::DisputeWon + | CheckoutTxnType::DisputeLost + ) +} + +#[derive(Debug, Deserialize)] +pub struct CheckoutWebhookData { + pub id: String, + pub payment_id: Option, + pub action_id: Option, + pub amount: i32, + pub currency: String, + pub evidence_required_by: Option, + pub reason_code: Option, + pub date: Option, +} +#[derive(Debug, Deserialize)] +pub struct CheckoutWebhookBody { + #[serde(rename = "type")] + pub txn_type: CheckoutTxnType, + pub data: CheckoutWebhookData, + pub created_on: Option, +} +#[derive(Debug, Deserialize, strum::Display, Clone)] +#[serde(rename_all = "snake_case")] +pub enum CheckoutTxnType { + PaymentApproved, + PaymentDeclined, + PaymentRefunded, + PaymentRefundDeclined, + DisputeReceived, + DisputeExpired, + DisputeAccepted, + DisputeCanceled, + DisputeEvidenceSubmitted, + DisputeEvidenceAcknowledgedByScheme, + DisputeEvidenceRequired, + DisputeArbitrationLost, + DisputeArbitrationWon, + DisputeWon, + DisputeLost, +} + +impl From for api::IncomingWebhookEvent { + fn from(txn_type: CheckoutTxnType) -> Self { + match txn_type { + CheckoutTxnType::PaymentApproved => Self::PaymentIntentSuccess, + CheckoutTxnType::PaymentDeclined => Self::PaymentIntentSuccess, + CheckoutTxnType::PaymentRefunded => Self::RefundSuccess, + CheckoutTxnType::PaymentRefundDeclined => Self::RefundFailure, + CheckoutTxnType::DisputeReceived | CheckoutTxnType::DisputeEvidenceRequired => { + Self::DisputeOpened + } + CheckoutTxnType::DisputeExpired => Self::DisputeExpired, + CheckoutTxnType::DisputeAccepted => Self::DisputeAccepted, + CheckoutTxnType::DisputeCanceled => Self::DisputeCancelled, + CheckoutTxnType::DisputeEvidenceSubmitted + | CheckoutTxnType::DisputeEvidenceAcknowledgedByScheme => Self::DisputeChallenged, + CheckoutTxnType::DisputeWon | CheckoutTxnType::DisputeArbitrationWon => { + Self::DisputeWon + } + CheckoutTxnType::DisputeLost | CheckoutTxnType::DisputeArbitrationLost => { + Self::DisputeLost + } + } + } +} + +impl From for api_models::enums::DisputeStage { + fn from(code: CheckoutTxnType) -> Self { + match code { + CheckoutTxnType::DisputeArbitrationLost | CheckoutTxnType::DisputeArbitrationWon => { + Self::PreArbitration + } + _ => Self::Dispute, + } + } +} + +#[derive(Debug, Deserialize)] +pub struct CheckoutWebhookObjectResource { + pub data: serde_json::Value, +} diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 172c0de2ef..f0591e9b26 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -61,6 +61,7 @@ pub trait RouterData { where T: serde::de::DeserializeOwned; fn is_three_ds(&self) -> bool; + fn get_payment_method_token(&self) -> Result; } impl RouterData for types::RouterData { @@ -139,6 +140,11 @@ impl RouterData for types::RouterData Result { + self.payment_method_token + .clone() + .ok_or_else(missing_field_err("payment_method_token")) + } } pub trait PaymentsAuthorizeRequestData { diff --git a/crates/router/tests/connectors/checkout.rs b/crates/router/tests/connectors/checkout.rs index a1869f2ff5..70e537fc7d 100644 --- a/crates/router/tests/connectors/checkout.rs +++ b/crates/router/tests/connectors/checkout.rs @@ -1,317 +1,479 @@ -use std::marker::PhantomData; +use masking::Secret; +use router::types::{self, api, storage::enums}; -use router::{ - core::payments, - db::StorageImpl, - routes, - types::{self, api, storage::enums, PaymentAddress}, +use crate::{ + connector_auth, + utils::{self, ConnectorActions}, }; -use crate::connector_auth::ConnectorAuthentication; +#[derive(Clone, Copy)] +struct CheckoutTest; +impl ConnectorActions for CheckoutTest {} +impl utils::Connector for CheckoutTest { + fn get_data(&self) -> types::api::ConnectorData { + use router::connector::Checkout; + types::api::ConnectorData { + connector: Box::new(&Checkout), + connector_name: types::Connector::Checkout, + get_token: types::api::GetToken::Connector, + } + } -fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { - let auth = ConnectorAuthentication::new() - .checkout - .expect("Missing Checkout connector authentication configuration"); + fn get_auth_token(&self) -> types::ConnectorAuthType { + types::ConnectorAuthType::from( + connector_auth::ConnectorAuthentication::new() + .checkout + .expect("Missing connector authentication configuration"), + ) + } - types::RouterData { - flow: PhantomData, - merchant_id: "checkout".to_string(), - connector: "checkout".to_string(), - payment_id: uuid::Uuid::new_v4().to_string(), - attempt_id: uuid::Uuid::new_v4().to_string(), - status: enums::AttemptStatus::default(), - auth_type: enums::AuthenticationType::NoThreeDs, - payment_method: enums::PaymentMethod::Card, - connector_auth_type: auth.into(), - description: Some("This is a test".to_string()), - return_url: None, - request: types::PaymentsAuthorizeData { - amount: 100, - currency: enums::Currency::USD, - payment_method_data: types::api::PaymentMethodData::Card(api::Card { - card_number: "4242424242424242".to_string().into(), - card_exp_month: "10".to_string().into(), - card_exp_year: "35".to_string().into(), - card_holder_name: "John Doe".to_string().into(), - card_cvc: "123".to_string().into(), - card_issuer: None, - card_network: None, + fn get_name(&self) -> String { + "checkout".to_string() + } +} + +static CONNECTOR: CheckoutTest = CheckoutTest {}; + +fn get_default_payment_info() -> Option { + None +} + +fn payment_method_details() -> Option { + None +} + +// Cards Positive Tests +// Creates a payment using the manual capture flow (Non 3DS). +#[serial_test::serial] +#[actix_web::test] +async fn should_only_authorize_payment() { + let response = CONNECTOR + .authorize_payment(payment_method_details(), get_default_payment_info()) + .await + .expect("Authorize payment response"); + assert_eq!(response.status, enums::AttemptStatus::Authorized); +} + +// Captures a payment using the manual capture flow (Non 3DS). +#[serial_test::serial] +#[actix_web::test] +async fn should_capture_authorized_payment() { + let response = CONNECTOR + .authorize_and_capture_payment(payment_method_details(), None, get_default_payment_info()) + .await + .expect("Capture payment response"); + assert_eq!(response.status, enums::AttemptStatus::Charged); +} + +// Partially captures a payment using the manual capture flow (Non 3DS). +#[serial_test::serial] +#[actix_web::test] +async fn should_partially_capture_authorized_payment() { + let response = CONNECTOR + .authorize_and_capture_payment( + payment_method_details(), + Some(types::PaymentsCaptureData { + amount_to_capture: 50, + ..utils::PaymentCaptureType::default().0 }), - confirm: true, - statement_descriptor_suffix: None, - statement_descriptor: None, - setup_future_usage: None, - mandate_id: None, - off_session: None, - setup_mandate_details: None, - capture_method: None, - browser_info: None, - order_details: None, - email: None, - session_token: None, - enrolled_for_3ds: false, - related_transaction_id: None, - payment_experience: None, - payment_method_type: None, - router_return_url: None, - webhook_url: None, - complete_authorize_url: None, - }, - response: Err(types::ErrorResponse::default()), - payment_method_id: None, - address: PaymentAddress::default(), - connector_meta_data: None, - amount_captured: None, - access_token: None, - session_token: None, - reference_id: None, - payment_method_token: None, - } -} - -fn construct_refund_router_data() -> types::RefundsRouterData { - let auth = ConnectorAuthentication::new() - .checkout - .expect("Missing Checkout connector authentication configuration"); - - types::RouterData { - flow: PhantomData, - connector_meta_data: None, - merchant_id: "checkout".to_string(), - connector: "checkout".to_string(), - payment_id: uuid::Uuid::new_v4().to_string(), - attempt_id: uuid::Uuid::new_v4().to_string(), - status: enums::AttemptStatus::default(), - payment_method: enums::PaymentMethod::Card, - auth_type: enums::AuthenticationType::NoThreeDs, - connector_auth_type: auth.into(), - description: Some("This is a test".to_string()), - return_url: None, - request: types::RefundsData { - amount: 100, - currency: enums::Currency::USD, - refund_id: uuid::Uuid::new_v4().to_string(), - connector_transaction_id: String::new(), - refund_amount: 10, - connector_metadata: None, - reason: None, - connector_refund_id: None, - }, - response: Err(types::ErrorResponse::default()), - payment_method_id: None, - address: PaymentAddress::default(), - amount_captured: None, - access_token: None, - session_token: None, - reference_id: None, - payment_method_token: None, - } + get_default_payment_info(), + ) + .await + .expect("Capture payment response"); + assert_eq!(response.status, enums::AttemptStatus::Charged); } +// Synchronizes a payment using the manual capture flow (Non 3DS). +#[serial_test::serial] #[actix_web::test] -#[ignore] -async fn test_checkout_payment_success() { - use router::{configs::settings::Settings, connector::Checkout, services}; +async fn should_sync_authorized_payment() { + let authorize_response = CONNECTOR + .authorize_payment(payment_method_details(), get_default_payment_info()) + .await + .expect("Authorize payment response"); + 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(), + ), + ..Default::default() + }), + get_default_payment_info(), + ) + .await + .expect("PSync response"); + assert_eq!(response.status, enums::AttemptStatus::Authorized,); +} - let conf = Settings::new().unwrap(); - static CV: Checkout = Checkout; - let connector = types::api::ConnectorData { - connector: Box::new(&CV), - connector_name: types::Connector::Checkout, - get_token: types::api::GetToken::Connector, - }; - let state = routes::AppState::with_storage(conf, StorageImpl::PostgresqlTest).await; - let connector_integration: services::BoxedConnectorIntegration< - '_, - types::api::Authorize, - types::PaymentsAuthorizeData, - types::PaymentsResponseData, - > = connector.connector.get_connector_integration(); - let request = construct_payment_router_data(); +// Voids a payment using the manual capture flow (Non 3DS). +#[serial_test::serial] +#[actix_web::test] +async fn should_void_authorized_payment() { + let response = CONNECTOR + .authorize_and_void_payment( + payment_method_details(), + Some(types::PaymentsCancelData { + connector_transaction_id: String::from(""), + cancellation_reason: Some("requested_by_customer".to_string()), + ..Default::default() + }), + get_default_payment_info(), + ) + .await + .expect("Void payment response"); + assert_eq!(response.status, enums::AttemptStatus::Voided); +} - let response = services::api::execute_connector_processing_step( - &state, - connector_integration, - &request, - payments::CallConnectorAction::Trigger, - ) - .await - .unwrap(); - - println!("{response:?}"); - - assert!( - response.status == enums::AttemptStatus::Charged, - "The payment failed" +// Refunds a payment using the manual capture flow (Non 3DS). +#[serial_test::serial] +#[actix_web::test] +async fn should_refund_manually_captured_payment() { + let response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, ); } +// Partially refunds a payment using the manual capture flow (Non 3DS). +#[serial_test::serial] #[actix_web::test] -#[ignore] -async fn test_checkout_refund_success() { - // Successful payment - use router::{configs::settings::Settings, connector::Checkout, services}; - - let conf = Settings::new().expect("invalid settings"); - let state = routes::AppState::with_storage(conf, StorageImpl::PostgresqlTest).await; - static CV: Checkout = Checkout; - let connector = types::api::ConnectorData { - connector: Box::new(&CV), - connector_name: types::Connector::Checkout, - get_token: types::api::GetToken::Connector, - }; - let connector_integration: services::BoxedConnectorIntegration< - '_, - types::api::Authorize, - types::PaymentsAuthorizeData, - types::PaymentsResponseData, - > = connector.connector.get_connector_integration(); - let request = construct_payment_router_data(); - - let response = services::api::execute_connector_processing_step( - &state, - connector_integration, - &request, - payments::CallConnectorAction::Trigger, - ) - .await - .unwrap(); - - println!("{response:?}"); - - assert!( - response.status == enums::AttemptStatus::Charged, - "The payment failed" - ); - // Successful refund - let connector_integration: services::BoxedConnectorIntegration< - '_, - types::api::Execute, - types::RefundsData, - types::RefundsResponseData, - > = connector.connector.get_connector_integration(); - let mut refund_request = construct_refund_router_data(); - - refund_request.request.connector_transaction_id = match response.response.unwrap() { - types::PaymentsResponseData::TransactionResponse { resource_id, .. } => { - resource_id.get_connector_transaction_id().unwrap() - } - _ => panic!("Connector transaction id not found"), - }; - - let response = services::api::execute_connector_processing_step( - &state, - connector_integration, - &refund_request, - payments::CallConnectorAction::Trigger, - ) - .await; - - let response = response.unwrap(); - println!("{response:?}"); - - assert!( - response.response.unwrap().refund_status == enums::RefundStatus::Success, - "The refund failed" +async fn should_partially_refund_manually_captured_payment() { + let response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .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 test_checkout_payment_failure() { - use router::{configs::settings::Settings, connector::Checkout, services}; - - let conf = Settings::new().expect("invalid settings"); - let state = routes::AppState::with_storage(conf, StorageImpl::PostgresqlTest).await; - static CV: Checkout = Checkout; - let connector = types::api::ConnectorData { - connector: Box::new(&CV), - connector_name: types::Connector::Checkout, - get_token: types::api::GetToken::Connector, - }; - let connector_integration: services::BoxedConnectorIntegration< - '_, - types::api::Authorize, - types::PaymentsAuthorizeData, - types::PaymentsResponseData, - > = connector.connector.get_connector_integration(); - let mut request = construct_payment_router_data(); - request.connector_auth_type = types::ConnectorAuthType::BodyKey { - api_key: "".to_string(), - key1: "".to_string(), - }; - let response = services::api::execute_connector_processing_step( - &state, - connector_integration, - &request, - payments::CallConnectorAction::Trigger, - ) - .await; - assert!(response.is_err(), "The payment passed"); -} -#[actix_web::test] -#[ignore] -async fn test_checkout_refund_failure() { - use router::{configs::settings::Settings, connector::Checkout, services}; - - let conf = Settings::new().expect("invalid settings"); - let state = routes::AppState::with_storage(conf, StorageImpl::PostgresqlTest).await; - static CV: Checkout = Checkout; - let connector = types::api::ConnectorData { - connector: Box::new(&CV), - connector_name: types::Connector::Checkout, - get_token: types::api::GetToken::Connector, - }; - let connector_integration: services::BoxedConnectorIntegration< - '_, - types::api::Authorize, - types::PaymentsAuthorizeData, - types::PaymentsResponseData, - > = connector.connector.get_connector_integration(); - let request = construct_payment_router_data(); - - let response = services::api::execute_connector_processing_step( - &state, - connector_integration, - &request, - payments::CallConnectorAction::Trigger, - ) - .await - .unwrap(); - - assert!( - response.status == enums::AttemptStatus::Charged, - "The payment failed" +#[ignore = "Connector Error, needs to be looked into and fixed"] +async fn should_sync_manually_captured_refund() { + let refund_response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + let response = CONNECTOR + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + refund_response.response.unwrap().connector_refund_id, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, ); - // Unsuccessful refund - let connector_integration: services::BoxedConnectorIntegration< - '_, - types::api::Execute, - types::RefundsData, - types::RefundsResponseData, - > = connector.connector.get_connector_integration(); - let mut refund_request = construct_refund_router_data(); - refund_request.request.connector_transaction_id = match response.response.unwrap() { - types::PaymentsResponseData::TransactionResponse { resource_id, .. } => { - resource_id.get_connector_transaction_id().unwrap() - } - _ => panic!("Connector transaction id not found"), - }; - - // Higher amount than that of payment - refund_request.request.refund_amount = 696969; - let response = services::api::execute_connector_processing_step( - &state, - connector_integration, - &refund_request, - payments::CallConnectorAction::Trigger, - ) - .await; - - println!("{response:?}"); - let response = response.unwrap(); - assert!(response.response.is_err()); - - let code = response.response.unwrap_err().code; - assert_eq!(code, "refund_amount_exceeds_balance"); } + +// Creates a payment using the automatic capture flow (Non 3DS). +#[serial_test::serial] +#[actix_web::test] +async fn should_make_payment() { + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_default_payment_info()) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); +} + +// Synchronizes a payment using the automatic capture flow (Non 3DS). +#[serial_test::serial] +#[actix_web::test] +async fn should_sync_auto_captured_payment() { + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_default_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"); + let response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Charged, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + txn_id.unwrap(), + ), + capture_method: Some(enums::CaptureMethod::Automatic), + ..Default::default() + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!(response.status, enums::AttemptStatus::Charged,); +} + +// Refunds a payment using the automatic capture flow (Non 3DS). +#[serial_test::serial] +#[actix_web::test] +async fn should_refund_auto_captured_payment() { + let response = CONNECTOR + .make_payment_and_refund(payment_method_details(), None, get_default_payment_info()) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Partially refunds a payment using the automatic capture flow (Non 3DS). +#[serial_test::serial] +#[actix_web::test] +async fn should_partially_refund_succeeded_payment() { + let refund_response = CONNECTOR + .make_payment_and_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + refund_response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Creates multiple refunds against a payment using the automatic capture flow (Non 3DS). +#[serial_test::serial] +#[actix_web::test] +async fn should_refund_succeeded_payment_multiple_times() { + CONNECTOR + .make_payment_and_multiple_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await; +} + +// Synchronizes a refund using the automatic capture flow (Non 3DS). +#[actix_web::test] +#[ignore = "Connector Error, needs to be looked into and fixed"] +async fn should_sync_refund() { + let refund_response = CONNECTOR + .make_payment_and_refund(payment_method_details(), None, get_default_payment_info()) + .await + .unwrap(); + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + let response = CONNECTOR + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + refund_response.response.unwrap().connector_refund_id, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// 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: Secret::new("1234567891011".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().code, + "card_number_invalid".to_string(), + ); +} + +// Creates a payment with empty card number. +#[serial_test::serial] +#[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::PaymentMethodData::Card(api::Card { + card_number: Secret::new(String::from("")), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + let x = response.response.unwrap_err(); + assert_eq!(x.code, "card_number_required",); +} + +// Creates a payment with incorrect CVC. +#[serial_test::serial] +#[actix_web::test] +async fn should_fail_payment_for_incorrect_cvc() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_cvc: Secret::new("12345".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().code, + "cvv_invalid".to_string(), + ); +} + +// Creates a payment with incorrect expiry month. +#[serial_test::serial] +#[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::PaymentMethodData::Card(api::Card { + card_exp_month: Secret::new("20".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().code, + "card_expiry_month_invalid".to_string(), + ); +} + +// Creates a payment with incorrect expiry year. +#[serial_test::serial] +#[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::PaymentMethodData::Card(api::Card { + card_exp_year: Secret::new("2000".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().code, + "card_expired".to_string(), + ); +} + +// Voids a payment using automatic capture flow (Non 3DS). +#[serial_test::serial] +#[actix_web::test] +async fn should_fail_void_payment_for_auto_capture() { + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_default_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"); + let void_response = CONNECTOR + .void_payment(txn_id.unwrap(), None, get_default_payment_info()) + .await + .unwrap(); + assert_eq!(void_response.response.unwrap_err().status_code, 403); +} + +// Captures a payment using invalid connector payment id. +#[serial_test::serial] +#[actix_web::test] +async fn should_fail_capture_for_invalid_payment() { + let capture_response = CONNECTOR + .capture_payment("123456789".to_string(), None, get_default_payment_info()) + .await + .unwrap(); + assert_eq!(capture_response.response.unwrap_err().status_code, 404); +} + +// Refunds a payment with refund amount higher than payment amount. +#[serial_test::serial] +#[actix_web::test] +async fn should_fail_for_refund_amount_higher_than_payment_amount() { + let response = CONNECTOR + .make_payment_and_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 150, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().code, + "refund_amount_exceeds_balance", + ); +} + +// Connector dependent test cases goes here + +// [#478]: add unit tests for non 3DS, wallets & webhooks in connector tests diff --git a/crates/router/tests/connectors/connector_auth.rs b/crates/router/tests/connectors/connector_auth.rs index 32d235874e..dbfa514772 100644 --- a/crates/router/tests/connectors/connector_auth.rs +++ b/crates/router/tests/connectors/connector_auth.rs @@ -11,7 +11,7 @@ pub(crate) struct ConnectorAuthentication { pub authorizedotnet: Option, pub bambora: Option, pub bluesnap: Option, - pub checkout: Option, + pub checkout: Option, pub coinbase: Option, pub cybersource: Option, pub dlocal: Option, diff --git a/crates/router/tests/connectors/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index 4a43bde472..5892e409c5 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -14,7 +14,8 @@ api_key = "MyMerchantName" key1 = "MyTransactionKey" [checkout] -api_key = "Bearer MyApiKey" +api_key = "Bearer PublicKey" +api_secret = "Bearer SecretKey" key1 = "MyProcessingChannelId" [cybersource]