diff --git a/crates/router/src/connector/airwallex/transformers.rs b/crates/router/src/connector/airwallex/transformers.rs index b33a8383dd..04d976e897 100644 --- a/crates/router/src/connector/airwallex/transformers.rs +++ b/crates/router/src/connector/airwallex/transformers.rs @@ -155,10 +155,7 @@ impl TryFrom<&types::PaymentsCaptureRouterData> for AirwallexPaymentsCaptureRequ Ok(Self { request_id: Uuid::new_v4().to_string(), amount: match item.request.amount_to_capture { - Some(_a) => Some(utils::to_currency_base_unit( - item.request.amount, - item.request.currency, - )?), + Some(a) => Some(utils::to_currency_base_unit(a, item.request.currency)?), _ => None, }, }) diff --git a/crates/router/src/connector/fiserv.rs b/crates/router/src/connector/fiserv.rs index a1ca56b04a..54a4919fb7 100644 --- a/crates/router/src/connector/fiserv.rs +++ b/crates/router/src/connector/fiserv.rs @@ -16,10 +16,11 @@ use crate::{ errors::{self, CustomResult}, payments, }, - headers, services, + headers, logger, + services::{self, api::ConnectorIntegration}, types::{ self, - api::{self}, + api::{self, ConnectorCommon, ConnectorCommonExt}, }, utils::{self, BytesExt}, }; @@ -49,7 +50,44 @@ impl Fiserv { } } -impl api::ConnectorCommon for Fiserv { +impl ConnectorCommonExt for Fiserv +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &types::RouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let timestamp = OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000_000; + let auth: fiserv::FiservAuthType = + fiserv::FiservAuthType::try_from(&req.connector_auth_type)?; + let mut auth_header = self.get_auth_header(&req.connector_auth_type)?; + + let fiserv_req = self + .get_request_body(req)? + .ok_or(errors::ConnectorError::RequestEncodingFailed)?; + + let client_request_id = Uuid::new_v4().to_string(); + let hmac = self + .generate_authorization_signature(auth, &client_request_id, &fiserv_req, timestamp) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let mut headers = vec![ + ( + headers::CONTENT_TYPE.to_string(), + types::PaymentsAuthorizeType::get_content_type(self).to_string(), + ), + ("Client-Request-Id".to_string(), client_request_id), + ("Auth-Token-Type".to_string(), "HMAC".to_string()), + (headers::TIMESTAMP.to_string(), timestamp.to_string()), + (headers::AUTHORIZATION.to_string(), hmac), + ]; + headers.append(&mut auth_header); + Ok(headers) + } +} + +impl ConnectorCommon for Fiserv { fn id(&self) -> &'static str { "fiserv" } @@ -61,16 +99,54 @@ impl api::ConnectorCommon for Fiserv { fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { connectors.fiserv.base_url.as_ref() } + fn get_auth_header( + &self, + auth_type: &types::ConnectorAuthType, + ) -> CustomResult, errors::ConnectorError> { + let auth: fiserv::FiservAuthType = auth_type + .try_into() + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + Ok(vec![(headers::API_KEY.to_string(), auth.api_key)]) + } + fn build_error_response( + &self, + res: types::Response, + ) -> CustomResult { + let response: fiserv::ErrorResponse = res + .response + .parse_struct("Fiserv ErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + let fiserv::ErrorResponse { error, details } = response; + + Ok(error + .or(details) + .and_then(|error_details| { + error_details + .first() + .map(|first_error| types::ErrorResponse { + code: first_error + .code + .to_owned() + .unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: first_error.message.to_owned(), + reason: first_error.field.to_owned(), + status_code: res.status_code, + }) + }) + .unwrap_or(types::ErrorResponse { + code: consts::NO_ERROR_CODE.to_string(), + message: consts::NO_ERROR_MESSAGE.to_string(), + reason: None, + status_code: res.status_code, + })) + } } impl api::ConnectorAccessToken for Fiserv {} -impl - services::ConnectorIntegration< - api::AccessTokenAuth, - types::AccessTokenRequestData, - types::AccessToken, - > for Fiserv +impl ConnectorIntegration + for Fiserv { // Not Implemented (R) } @@ -80,73 +156,188 @@ impl api::Payment for Fiserv {} impl api::PreVerify for Fiserv {} #[allow(dead_code)] -impl - services::ConnectorIntegration< - api::Verify, - types::VerifyRequestData, - types::PaymentsResponseData, - > for Fiserv +impl ConnectorIntegration + for Fiserv { } impl api::PaymentVoid for Fiserv {} #[allow(dead_code)] -impl - services::ConnectorIntegration< - api::Void, - types::PaymentsCancelData, - types::PaymentsResponseData, - > for Fiserv +impl ConnectorIntegration + for Fiserv { + fn get_headers( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + "application/json" + } + + fn get_url( + &self, + _req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + //The docs has this url wrong, cancels is the working endpoint + "{}ch/payments/v1/cancels", + connectors.fiserv.base_url + )) + } + + fn get_request_body( + &self, + req: &types::PaymentsCancelRouterData, + ) -> CustomResult, errors::ConnectorError> { + let connector_req = fiserv::FiservCancelRequest::try_from(req)?; + let fiserv_req = + utils::Encode::::encode_to_string_of_json(&connector_req) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(fiserv_req)) + } + + fn build_request( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) + .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) + .body(types::PaymentsVoidType::get_request_body(self, req)?) + .build(), + ); + + Ok(request) + } + + fn handle_response( + &self, + data: &types::PaymentsCancelRouterData, + res: types::Response, + ) -> CustomResult { + let response: fiserv::FiservPaymentsResponse = res + .response + .parse_struct("Fiserv PaymentResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + } + .try_into() + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: types::Response, + ) -> CustomResult { + self.build_error_response(res) + } } impl api::PaymentSync for Fiserv {} #[allow(dead_code)] -impl - services::ConnectorIntegration +impl ConnectorIntegration for Fiserv { + fn get_headers( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + "application/json" + } + + fn get_url( + &self, + _req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}ch/payments/v1/transaction-inquiry", + connectors.fiserv.base_url + )) + } + + fn get_request_body( + &self, + req: &types::PaymentsSyncRouterData, + ) -> CustomResult, errors::ConnectorError> { + let connector_req = fiserv::FiservSyncRequest::try_from(req)?; + let fiserv_req = + utils::Encode::::encode_to_string_of_json(&connector_req) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(fiserv_req)) + } + + fn build_request( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) + .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) + .body(types::PaymentsSyncType::get_request_body(self, req)?) + .build(), + ); + Ok(request) + } + + fn handle_response( + &self, + data: &types::PaymentsSyncRouterData, + res: types::Response, + ) -> CustomResult { + let response: fiserv::FiservSyncResponse = res + .response + .parse_struct("Fiserv PaymentSyncResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + } + .try_into() + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: types::Response, + ) -> CustomResult { + self.build_error_response(res) + } } impl api::PaymentCapture for Fiserv {} -impl - services::ConnectorIntegration< - api::Capture, - types::PaymentsCaptureData, - types::PaymentsResponseData, - > for Fiserv +impl ConnectorIntegration + for Fiserv { fn get_headers( &self, req: &types::PaymentsCaptureRouterData, - _connectors: &settings::Connectors, + connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - let timestamp = OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000_000; - let auth: fiserv::FiservAuthType = - fiserv::FiservAuthType::try_from(&req.connector_auth_type)?; - let api_key = auth.api_key.clone(); - - let fiserv_req = self - .get_request_body(req)? - .ok_or(errors::ConnectorError::RequestEncodingFailed)?; - let client_request_id = Uuid::new_v4().to_string(); - let hmac = self - .generate_authorization_signature(auth, &client_request_id, &fiserv_req, timestamp) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - let headers = vec![ - ( - headers::CONTENT_TYPE.to_string(), - types::PaymentsAuthorizeType::get_content_type(self).to_string(), - ), - ("Client-Request-Id".to_string(), client_request_id), - ("Auth-Token-Type".to_string(), "HMAC".to_string()), - ("Api-Key".to_string(), api_key), - ("Timestamp".to_string(), timestamp.to_string()), - ("Authorization".to_string(), hmac), - ]; - Ok(headers) + self.build_headers(req, connectors) } fn get_content_type(&self) -> &'static str { @@ -215,86 +406,29 @@ impl &self, res: types::Response, ) -> CustomResult { - let response: fiserv::ErrorResponse = res - .response - .parse_struct("Fiserv ErrorResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - - let fiserv::ErrorResponse { error, details } = response; - - let message = match (error, details) { - (Some(err), _) => err - .iter() - .map(|v| v.message.clone()) - .collect::>() - .join(""), - (None, Some(err_details)) => err_details - .iter() - .map(|v| v.message.clone()) - .collect::>() - .join(""), - (None, None) => consts::NO_ERROR_MESSAGE.to_string(), - }; - - Ok(types::ErrorResponse { - status_code: res.status_code, - code: consts::NO_ERROR_CODE.to_string(), - message, - reason: None, - }) + self.build_error_response(res) } } impl api::PaymentSession for Fiserv {} #[allow(dead_code)] -impl - services::ConnectorIntegration< - api::Session, - types::PaymentsSessionData, - types::PaymentsResponseData, - > for Fiserv +impl ConnectorIntegration + for Fiserv { } impl api::PaymentAuthorize for Fiserv {} -impl - services::ConnectorIntegration< - api::Authorize, - types::PaymentsAuthorizeData, - types::PaymentsResponseData, - > for Fiserv +impl ConnectorIntegration + for Fiserv { fn get_headers( &self, req: &types::PaymentsAuthorizeRouterData, - _connectors: &settings::Connectors, + connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - let timestamp = OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000_000; - let auth: fiserv::FiservAuthType = - fiserv::FiservAuthType::try_from(&req.connector_auth_type)?; - let api_key = auth.api_key.clone(); - - let fiserv_req = self - .get_request_body(req)? - .ok_or(errors::ConnectorError::RequestEncodingFailed)?; - let client_request_id = Uuid::new_v4().to_string(); - let hmac = self - .generate_authorization_signature(auth, &client_request_id, &fiserv_req, timestamp) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - let headers = vec![ - ( - headers::CONTENT_TYPE.to_string(), - types::PaymentsAuthorizeType::get_content_type(self).to_string(), - ), - ("Client-Request-Id".to_string(), client_request_id), - ("Auth-Token-Type".to_string(), "HMAC".to_string()), - ("Api-Key".to_string(), api_key), - ("Timestamp".to_string(), timestamp.to_string()), - ("Authorization".to_string(), hmac), - ]; - Ok(headers) + self.build_headers(req, connectors) } fn get_content_type(&self) -> &'static str { @@ -367,32 +501,7 @@ impl &self, res: types::Response, ) -> CustomResult { - let response: fiserv::ErrorResponse = res - .response - .parse_struct("Fiserv ErrorResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - - let fiserv::ErrorResponse { error, details } = response; - - let message = match (error, details) { - (Some(err), _) => err - .iter() - .map(|v| v.message.clone()) - .collect::>() - .join(""), - (None, Some(err_details)) => err_details - .iter() - .map(|v| v.message.clone()) - .collect::>() - .join(""), - (None, None) => consts::NO_ERROR_MESSAGE.to_string(), - }; - Ok(types::ErrorResponse { - status_code: res.status_code, - code: consts::NO_ERROR_CODE.to_string(), - message, - reason: None, - }) + self.build_error_response(res) } } @@ -401,15 +510,157 @@ impl api::RefundExecute for Fiserv {} impl api::RefundSync for Fiserv {} #[allow(dead_code)] -impl services::ConnectorIntegration - for Fiserv -{ +impl ConnectorIntegration for Fiserv { + fn get_headers( + &self, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + fn get_content_type(&self) -> &'static str { + "application/json" + } + fn get_url( + &self, + _req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}ch/payments/v1/refunds", + connectors.fiserv.base_url + )) + } + fn get_request_body( + &self, + req: &types::RefundsRouterData, + ) -> CustomResult, errors::ConnectorError> { + let connector_req = fiserv::FiservRefundRequest::try_from(req)?; + let fiserv_req = + utils::Encode::::encode_to_string_of_json(&connector_req) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(fiserv_req)) + } + fn build_request( + &self, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::RefundExecuteType::get_url(self, req, connectors)?) + .headers(types::RefundExecuteType::get_headers( + self, req, connectors, + )?) + .body(types::RefundExecuteType::get_request_body(self, req)?) + .build(); + Ok(Some(request)) + } + + fn handle_response( + &self, + data: &types::RefundsRouterData, + res: types::Response, + ) -> CustomResult, errors::ConnectorError> { + logger::debug!(target: "router::connector::fiserv", response=?res); + let response: fiserv::RefundResponse = + res.response + .parse_struct("fiserv RefundResponse") + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + } + .try_into() + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + fn get_error_response( + &self, + res: types::Response, + ) -> CustomResult { + self.build_error_response(res) + } } #[allow(dead_code)] -impl services::ConnectorIntegration - for Fiserv -{ +impl ConnectorIntegration for Fiserv { + fn get_headers( + &self, + req: &types::RefundSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + "application/json" + } + + fn get_url( + &self, + _req: &types::RefundSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}ch/payments/v1/transaction-inquiry", + connectors.fiserv.base_url + )) + } + + fn get_request_body( + &self, + req: &types::RefundSyncRouterData, + ) -> CustomResult, errors::ConnectorError> { + let connector_req = fiserv::FiservSyncRequest::try_from(req)?; + let fiserv_req = + utils::Encode::::encode_to_string_of_json(&connector_req) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(fiserv_req)) + } + + fn build_request( + &self, + req: &types::RefundSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::RefundSyncType::get_url(self, req, connectors)?) + .headers(types::RefundSyncType::get_headers(self, req, connectors)?) + .body(types::RefundSyncType::get_request_body(self, req)?) + .build(), + ); + Ok(request) + } + + fn handle_response( + &self, + data: &types::RefundSyncRouterData, + res: types::Response, + ) -> CustomResult { + logger::debug!(target: "router::connector::fiserv", response=?res); + + let response: fiserv::FiservSyncResponse = res + .response + .parse_struct("Fiserv Refund Response") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + } + .try_into() + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: types::Response, + ) -> CustomResult { + self.build_error_response(res) + } } #[async_trait::async_trait] diff --git a/crates/router/src/connector/fiserv/transformers.rs b/crates/router/src/connector/fiserv/transformers.rs index 6f2adf9044..f22e9d3a6c 100644 --- a/crates/router/src/connector/fiserv/transformers.rs +++ b/crates/router/src/connector/fiserv/transformers.rs @@ -3,12 +3,13 @@ use error_stack::ResultExt; use serde::{Deserialize, Serialize}; use crate::{ + connector::utils::{self, PaymentsCancelRequestData, PaymentsSyncRequestData, RouterData}, core::errors, pii::{self, Secret}, types::{self, api, storage::enums}, }; -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct FiservPaymentsRequest { amount: Amount, @@ -18,11 +19,18 @@ pub struct FiservPaymentsRequest { transaction_interaction: TransactionInteraction, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct Source { - source_type: String, - card: CardData, +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(tag = "sourceType")] +pub enum Source { + PaymentCard { + card: CardData, + }, + #[allow(dead_code)] + GooglePay { + data: String, + signature: String, + version: String, + }, } #[derive(Default, Debug, Serialize, Eq, PartialEq)] @@ -34,90 +42,119 @@ pub struct CardData { security_code: Secret, } +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct GooglePayToken { + signature: String, + signed_message: String, + protocol_version: String, +} + #[derive(Default, Debug, Serialize, Eq, PartialEq)] pub struct Amount { - total: i64, + #[serde(serialize_with = "utils::str_to_f32")] + total: String, currency: String, } #[derive(Default, Debug, Serialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct TransactionDetails { - capture_flag: bool, + capture_flag: Option, + reversal_reason_code: Option, } #[derive(Default, Debug, Serialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct MerchantDetails { merchant_id: String, - terminal_id: String, + terminal_id: Option, } #[derive(Default, Debug, Serialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct TransactionInteraction { - origin: String, - eci_indicator: String, - pos_condition_code: String, + origin: TransactionInteractionOrigin, + eci_indicator: TransactionInteractionEciIndicator, + pos_condition_code: TransactionInteractionPosConditionCode, +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "UPPERCASE")] +pub enum TransactionInteractionOrigin { + #[default] + Ecom, +} +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum TransactionInteractionEciIndicator { + #[default] + ChannelEncrypted, +} +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum TransactionInteractionPosConditionCode { + #[default] + CardNotPresentEcom, } impl TryFrom<&types::PaymentsAuthorizeRouterData> for FiservPaymentsRequest { type Error = error_stack::Report; fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { - match item.request.payment_method_data { - api::PaymentMethodData::Card(ref ccard) => { - let auth: FiservAuthType = FiservAuthType::try_from(&item.connector_auth_type)?; - let amount = Amount { - total: item.request.amount, - currency: item.request.currency.to_string(), - }; + let auth: FiservAuthType = FiservAuthType::try_from(&item.connector_auth_type)?; + let amount = Amount { + total: utils::to_currency_base_unit(item.request.amount, item.request.currency)?, + currency: item.request.currency.to_string(), + }; + let transaction_details = TransactionDetails { + capture_flag: Some(matches!( + item.request.capture_method, + Some(enums::CaptureMethod::Automatic) | None + )), + reversal_reason_code: None, + }; + let metadata = item.get_connector_meta()?; + let session: SessionObject = metadata + .parse_value("SessionObject") + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let merchant_details = MerchantDetails { + merchant_id: auth.merchant_account, + terminal_id: Some(session.terminal_id), + }; + + let transaction_interaction = TransactionInteraction { + //Payment is being made in online mode, card not present + origin: TransactionInteractionOrigin::Ecom, + // transaction encryption such as SSL/TLS, but authentication was not performed + eci_indicator: TransactionInteractionEciIndicator::ChannelEncrypted, + //card not present in online transaction + pos_condition_code: TransactionInteractionPosConditionCode::CardNotPresentEcom, + }; + let source = match item.request.payment_method_data.clone() { + api::PaymentMethodData::Card(ref ccard) => { let card = CardData { - card_data: ccard.card_number.clone(), + card_data: ccard + .card_number + .clone() + .map(|card| card.split_whitespace().collect()), expiration_month: ccard.card_exp_month.clone(), expiration_year: ccard.card_exp_year.clone(), security_code: ccard.card_cvc.clone(), }; - let source = Source { - source_type: "PaymentCard".to_string(), - card, - }; - let transaction_details = TransactionDetails { - capture_flag: matches!( - item.request.capture_method, - Some(enums::CaptureMethod::Automatic) | None - ), - }; - let metadata = item - .connector_meta_data - .clone() - .ok_or(errors::ConnectorError::RequestEncodingFailed)?; - let session: SessionObject = metadata - .parse_value("SessionObject") - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - - let merchant_details = MerchantDetails { - merchant_id: auth.merchant_account, - terminal_id: session.terminal_id, - }; - - let transaction_interaction = TransactionInteraction { - origin: "ECOM".to_string(), //Payment is being made in online mode, card not present - eci_indicator: "CHANNEL_ENCRYPTED".to_string(), // transaction encryption such as SSL/TLS, but authentication was not performed - pos_condition_code: "CARD_NOT_PRESENT_ECOM".to_string(), //card not present in online transaction - }; - Ok(Self { - amount, - source, - transaction_details, - merchant_details, - transaction_interaction, - }) + Source::PaymentCard { card } } _ => Err(errors::ConnectorError::NotImplemented( "Payment Methods".to_string(), ))?, - } + }; + Ok(Self { + amount, + source, + transaction_details, + merchant_details, + transaction_interaction, + }) } } @@ -147,6 +184,38 @@ impl TryFrom<&types::ConnectorAuthType> for FiservAuthType { } } +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct FiservCancelRequest { + transaction_details: TransactionDetails, + merchant_details: MerchantDetails, + reference_transaction_details: ReferenceTransactionDetails, +} + +impl TryFrom<&types::PaymentsCancelRouterData> for FiservCancelRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsCancelRouterData) -> Result { + let auth: FiservAuthType = FiservAuthType::try_from(&item.connector_auth_type)?; + let metadata = item.get_connector_meta()?; + let session: SessionObject = metadata + .parse_value("SessionObject") + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Self { + merchant_details: MerchantDetails { + merchant_id: auth.merchant_account, + terminal_id: Some(session.terminal_id), + }, + reference_transaction_details: ReferenceTransactionDetails { + reference_transaction_id: item.request.connector_transaction_id.to_string(), + }, + transaction_details: TransactionDetails { + capture_flag: None, + reversal_reason_code: Some(item.request.get_cancellation_reason()?), + }, + }) + } +} + #[derive(Debug, Default, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ErrorResponse { @@ -161,6 +230,7 @@ pub struct ErrorDetails { pub error_type: String, pub code: Option, pub message: String, + pub field: Option, } #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] @@ -188,12 +258,31 @@ impl From for enums::AttemptStatus { } } +impl From for enums::RefundStatus { + fn from(item: FiservPaymentStatus) -> Self { + match item { + FiservPaymentStatus::Succeeded + | FiservPaymentStatus::Authorized + | FiservPaymentStatus::Captured => Self::Success, + FiservPaymentStatus::Declined | FiservPaymentStatus::Failed => Self::Failure, + _ => Self::Pending, + } + } +} + #[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct FiservPaymentsResponse { gateway_response: GatewayResponse, } +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +#[serde(transparent)] +pub struct FiservSyncResponse { + sync_responses: Vec, +} + #[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct GatewayResponse { @@ -220,7 +309,7 @@ impl let gateway_resp = item.response.gateway_response; Ok(Self { - status: gateway_resp.transaction_state.into(), + status: enums::AttemptStatus::from(gateway_resp.transaction_state), response: Ok(types::PaymentsResponseData::TransactionResponse { resource_id: types::ResponseId::ConnectorTransactionId( gateway_resp.transaction_processing_details.transaction_id, @@ -234,6 +323,39 @@ impl } } +impl TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData, + ) -> Result { + let gateway_resp = match item.response.sync_responses.first() { + Some(gateway_response) => gateway_response, + _ => Err(errors::ConnectorError::ResponseHandlingFailed)?, + }; + + Ok(Self { + status: enums::AttemptStatus::from( + gateway_resp.gateway_response.transaction_state.clone(), + ), + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + gateway_resp + .gateway_response + .transaction_processing_details + .transaction_id + .clone(), + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + }), + ..item.data + }) + } +} + #[derive(Default, Debug, Serialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct FiservCaptureRequest { @@ -266,19 +388,22 @@ impl TryFrom<&types::PaymentsCaptureRouterData> for FiservCaptureRequest { let session: SessionObject = metadata .parse_value("SessionObject") .change_context(errors::ConnectorError::RequestEncodingFailed)?; - let amount = item - .request - .amount_to_capture - .ok_or(errors::ConnectorError::RequestEncodingFailed)?; + let amount = match item.request.amount_to_capture { + Some(a) => utils::to_currency_base_unit(a, item.request.currency)?, + _ => utils::to_currency_base_unit(item.request.amount, item.request.currency)?, + }; Ok(Self { amount: Amount { total: amount, currency: item.request.currency.to_string(), }, - transaction_details: TransactionDetails { capture_flag: true }, + transaction_details: TransactionDetails { + capture_flag: Some(true), + reversal_reason_code: None, + }, merchant_details: MerchantDetails { merchant_id: auth.merchant_account, - terminal_id: session.terminal_id, + terminal_id: Some(session.terminal_id), }, reference_transaction_details: ReferenceTransactionDetails { reference_transaction_id: item.request.connector_transaction_id.to_string(), @@ -287,56 +412,143 @@ impl TryFrom<&types::PaymentsCaptureRouterData> for FiservCaptureRequest { } } +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct FiservSyncRequest { + merchant_details: MerchantDetails, + reference_transaction_details: ReferenceTransactionDetails, +} + +impl TryFrom<&types::PaymentsSyncRouterData> for FiservSyncRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsSyncRouterData) -> Result { + let auth: FiservAuthType = FiservAuthType::try_from(&item.connector_auth_type)?; + Ok(Self { + merchant_details: MerchantDetails { + merchant_id: auth.merchant_account, + terminal_id: None, + }, + reference_transaction_details: ReferenceTransactionDetails { + reference_transaction_id: item + .request + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?, + }, + }) + } +} + +impl TryFrom<&types::RefundSyncRouterData> for FiservSyncRequest { + type Error = error_stack::Report; + fn try_from(item: &types::RefundSyncRouterData) -> Result { + let auth: FiservAuthType = FiservAuthType::try_from(&item.connector_auth_type)?; + Ok(Self { + merchant_details: MerchantDetails { + merchant_id: auth.merchant_account, + terminal_id: None, + }, + reference_transaction_details: ReferenceTransactionDetails { + reference_transaction_id: item + .request + .connector_refund_id + .clone() + .ok_or(errors::ConnectorError::RequestEncodingFailed)?, + }, + }) + } +} + #[derive(Default, Debug, Serialize)] -pub struct FiservRefundRequest {} +#[serde(rename_all = "camelCase")] +pub struct FiservRefundRequest { + amount: Amount, + merchant_details: MerchantDetails, + reference_transaction_details: ReferenceTransactionDetails, +} impl TryFrom<&types::RefundsRouterData> for FiservRefundRequest { type Error = error_stack::Report; - fn try_from(_item: &types::RefundsRouterData) -> Result { - Err(errors::ConnectorError::NotImplemented("fiserv".to_string()).into()) + fn try_from(item: &types::RefundsRouterData) -> Result { + let auth: FiservAuthType = FiservAuthType::try_from(&item.connector_auth_type)?; + let metadata = item + .connector_meta_data + .clone() + .ok_or(errors::ConnectorError::RequestEncodingFailed)?; + let session: SessionObject = metadata + .parse_value("SessionObject") + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Self { + amount: Amount { + total: utils::to_currency_base_unit( + item.request.refund_amount, + item.request.currency, + )?, + currency: item.request.currency.to_string(), + }, + merchant_details: MerchantDetails { + merchant_id: auth.merchant_account, + terminal_id: Some(session.terminal_id), + }, + reference_transaction_details: ReferenceTransactionDetails { + reference_transaction_id: item.request.connector_transaction_id.to_string(), + }, + }) } } -#[allow(dead_code)] -#[derive(Debug, Serialize, Default, Deserialize, Clone)] -pub enum RefundStatus { - Succeeded, - Failed, - #[default] - Processing, +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct RefundResponse { + gateway_response: GatewayResponse, } -impl From for enums::RefundStatus { - fn from(item: RefundStatus) -> Self { - match item { - RefundStatus::Succeeded => Self::Success, - RefundStatus::Failed => Self::Failure, - RefundStatus::Processing => Self::Pending, - } - } -} - -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct RefundResponse {} - impl TryFrom> for types::RefundsRouterData { type Error = error_stack::Report; fn try_from( - _item: types::RefundsResponseRouterData, + item: types::RefundsResponseRouterData, ) -> Result { - Err(errors::ConnectorError::NotImplemented("fiserv".to_string()).into()) + Ok(Self { + response: Ok(types::RefundsResponseData { + connector_refund_id: item + .response + .gateway_response + .transaction_processing_details + .transaction_id, + refund_status: enums::RefundStatus::from( + item.response.gateway_response.transaction_state, + ), + }), + ..item.data + }) } } -impl TryFrom> +impl TryFrom> for types::RefundsRouterData { type Error = error_stack::Report; fn try_from( - _item: types::RefundsResponseRouterData, + item: types::RefundsResponseRouterData, ) -> Result { - Err(errors::ConnectorError::NotImplemented("fiserv".to_string()).into()) + let gateway_resp = item + .response + .sync_responses + .first() + .ok_or(errors::ConnectorError::ResponseHandlingFailed)?; + Ok(Self { + response: Ok(types::RefundsResponseData { + connector_refund_id: gateway_resp + .gateway_response + .transaction_processing_details + .transaction_id + .clone(), + refund_status: enums::RefundStatus::from( + gateway_resp.gateway_response.transaction_state.clone(), + ), + }), + ..item.data + }) } } diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 5768160554..83fdfb9be7 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -5,11 +5,12 @@ use error_stack::{report, IntoReport, ResultExt}; use masking::Secret; use once_cell::sync::Lazy; use regex::Regex; +use serde::Serializer; use crate::{ core::errors::{self, CustomResult}, pii::PeekInterface, - types::{self, api, PaymentsCancelData}, + types::{self, api, PaymentsCancelData, ResponseId}, utils::OptionExt, }; @@ -142,17 +143,29 @@ impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData { pub trait PaymentsSyncRequestData { fn is_auto_capture(&self) -> bool; + fn get_connector_transaction_id(&self) -> CustomResult; } impl PaymentsSyncRequestData for types::PaymentsSyncData { fn is_auto_capture(&self) -> bool { self.capture_method == Some(storage_models::enums::CaptureMethod::Automatic) } + fn get_connector_transaction_id(&self) -> CustomResult { + match self.connector_transaction_id.clone() { + ResponseId::ConnectorTransactionId(txn_id) => Ok(txn_id), + _ => Err(errors::ValidationError::IncorrectValueProvided { + field_name: "connector_transaction_id", + }) + .into_report() + .attach_printable("Expected connector transaction ID not found"), + } + } } pub trait PaymentsCancelRequestData { fn get_amount(&self) -> Result; fn get_currency(&self) -> Result; + fn get_cancellation_reason(&self) -> Result; } impl PaymentsCancelRequestData for PaymentsCancelData { @@ -162,6 +175,11 @@ impl PaymentsCancelRequestData for PaymentsCancelData { fn get_currency(&self) -> Result { self.currency.ok_or_else(missing_field_err("currency")) } + fn get_cancellation_reason(&self) -> Result { + self.cancellation_reason + .clone() + .ok_or_else(missing_field_err("cancellation_reason")) + } } pub trait RefundsRequestData { @@ -355,3 +373,13 @@ pub fn to_currency_base_unit( _ => Ok((f64::from(amount_u32) / 100.0).to_string()), } } + +pub fn str_to_f32(value: &str, serializer: S) -> Result +where + S: Serializer, +{ + let float_value = value.parse::().map_err(|_| { + serde::ser::Error::custom("Invalid string, cannot be converted to float value") + })?; + serializer.serialize_f64(float_value) +} diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index 7c9bfa4230..c4329725d7 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -278,6 +278,8 @@ pub enum ConnectorError { WebhookResourceObjectNotFound, #[error("Invalid Date/time format")] InvalidDateFormat, + #[error("Invalid Data format")] + InvalidDataFormat { field_name: &'static str }, #[error("Payment Method data / Payment Method Type / Payment Experience Mismatch ")] MismatchedPaymentData, } diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index fb072c80bf..ff5b59e3f9 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -64,7 +64,7 @@ pub async fn construct_refund_router_data<'a, F>( // Does refund need shipping/billing address ? address: PaymentAddress::default(), auth_type: payment_attempt.authentication_type.unwrap_or_default(), - connector_meta_data: None, + connector_meta_data: merchant_connector_account.metadata, amount_captured: payment_intent.amount_captured, request: types::RefundsData { refund_id: refund.refund_id.clone(), diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 120e1566cc..06f0d3610a 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -42,14 +42,16 @@ static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; /// Header Constants pub mod headers { - pub const X_API_KEY: &str = "X-API-KEY"; - pub const CONTENT_TYPE: &str = "Content-Type"; - pub const X_ROUTER: &str = "X-router"; - pub const AUTHORIZATION: &str = "Authorization"; pub const ACCEPT: &str = "Accept"; - pub const X_API_VERSION: &str = "X-ApiVersion"; + pub const API_KEY: &str = "API-KEY"; + pub const AUTHORIZATION: &str = "Authorization"; + pub const CONTENT_TYPE: &str = "Content-Type"; pub const DATE: &str = "Date"; + pub const TIMESTAMP: &str = "Timestamp"; + pub const X_API_KEY: &str = "X-API-KEY"; + pub const X_API_VERSION: &str = "X-ApiVersion"; pub const X_MERCHANT_ID: &str = "X-Merchant-Id"; + pub const X_ROUTER: &str = "X-router"; pub const X_LOGIN: &str = "X-Login"; pub const X_TRANS_KEY: &str = "X-Trans-Key"; pub const X_VERSION: &str = "X-Version"; diff --git a/crates/router/tests/connectors/fiserv.rs b/crates/router/tests/connectors/fiserv.rs index 7bccbb2137..6d5098517c 100644 --- a/crates/router/tests/connectors/fiserv.rs +++ b/crates/router/tests/connectors/fiserv.rs @@ -1,4 +1,3 @@ -use futures::future::OptionFuture; use masking::Secret; use router::types::{self, api, storage::enums}; use serde_json::json; @@ -8,10 +7,10 @@ use crate::{ utils::{self, ConnectorActions}, }; -struct Fiserv; -impl ConnectorActions for Fiserv {} - -impl utils::Connector for Fiserv { +#[derive(Clone, Copy)] +struct FiservTest; +impl ConnectorActions for FiservTest {} +impl utils::Connector for FiservTest { fn get_data(&self) -> types::api::ConnectorData { use router::connector::Fiserv; types::api::ConnectorData { @@ -32,134 +31,488 @@ impl utils::Connector for Fiserv { fn get_name(&self) -> String { "fiserv".to_string() } - fn get_connector_meta(&self) -> Option { Some(json!({"terminalId": "10000001"})) } } +fn payment_method_details() -> Option { + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_number: Secret::new("4005550000000019".to_string()), + card_exp_month: Secret::new("02".to_string()), + card_exp_year: Secret::new("2035".to_string()), + card_holder_name: Secret::new("John Doe".to_string()), + card_cvc: Secret::new("123".to_string()), + card_issuer: None, + card_network: None, + }), + capture_method: Some(storage_models::enums::CaptureMethod::Manual), + ..utils::PaymentAuthorizeType::default().0 + }) +} + +fn get_default_payment_info() -> Option { + Some(utils::PaymentInfo { + connector_meta_data: Some(json!({"terminalId": "10000001"})), + ..Default::default() + }) +} + +static CONNECTOR: FiservTest = FiservTest {}; + +// Cards Positive Tests +// Creates a payment using the manual capture flow (Non 3DS). #[actix_web::test] +#[serial_test::serial] async fn should_only_authorize_payment() { - let response = Fiserv {} - .authorize_payment( - Some(types::PaymentsAuthorizeData { - payment_method_data: types::api::PaymentMethodData::Card(api::Card { - card_number: Secret::new("4005550000000019".to_string()), - card_exp_month: Secret::new("02".to_string()), - card_exp_year: Secret::new("2035".to_string()), - card_holder_name: Secret::new("John Doe".to_string()), - card_cvc: Secret::new("123".to_string()), - card_issuer: None, - card_network: None, - }), - capture_method: Some(storage_models::enums::CaptureMethod::Manual), - ..utils::PaymentAuthorizeType::default().0 - }), - None, - ) + let response = CONNECTOR + .authorize_payment(payment_method_details(), get_default_payment_info()) .await - .unwrap(); + .expect("Authorize payment response"); assert_eq!(response.status, enums::AttemptStatus::Authorized); } +// Captures a payment using the manual capture flow (Non 3DS). #[actix_web::test] -async fn should_authorize_and_capture_payment() { - let response = Fiserv {} - .make_payment( - Some(types::PaymentsAuthorizeData { - payment_method_data: types::api::PaymentMethodData::Card(api::Card { - card_number: Secret::new("4005550000000019".to_string()), - card_exp_month: Secret::new("02".to_string()), - card_exp_year: Secret::new("2035".to_string()), - card_holder_name: Secret::new("John Doe".to_string()), - card_cvc: Secret::new("123".to_string()), - card_issuer: None, - card_network: None, - }), - ..utils::PaymentAuthorizeType::default().0 - }), - None, - ) - .await; - assert_eq!(response.unwrap().status, enums::AttemptStatus::Charged); +#[serial_test::serial] +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). #[actix_web::test] -async fn should_capture_already_authorized_payment() { - let connector = Fiserv {}; - let authorize_response = connector - .authorize_payment( - Some(types::PaymentsAuthorizeData { - payment_method_data: types::api::PaymentMethodData::Card(api::Card { - card_number: Secret::new("4005550000000019".to_string()), - card_exp_month: Secret::new("02".to_string()), - card_exp_year: Secret::new("2035".to_string()), - card_holder_name: Secret::new("John Doe".to_string()), - card_cvc: Secret::new("123".to_string()), - card_issuer: None, - card_network: None, - }), - capture_method: Some(storage_models::enums::CaptureMethod::Manual), - ..utils::PaymentAuthorizeType::default().0 +#[serial_test::serial] +async fn should_partially_capture_authorized_payment() { + let response = CONNECTOR + .authorize_and_capture_payment( + payment_method_details(), + Some(types::PaymentsCaptureData { + amount_to_capture: Some(50), + ..utils::PaymentCaptureType::default().0 }), + 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). +#[actix_web::test] +#[serial_test::serial] +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); + tokio::time::sleep(std::time::Duration::from_secs(10)).await; + 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, + }), + get_default_payment_info(), + ) + .await + .expect("PSync response"); + assert_eq!(response.status, enums::AttemptStatus::Authorized); +} + +// Voids a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +#[serial_test::serial] +#[ignore] +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("TIMEOUT".to_string()), + ..Default::default() + }), + get_default_payment_info(), + ) + .await + .expect("Void payment response"); + assert_eq!(response.status, enums::AttemptStatus::Voided); +} + +// Refunds a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +#[serial_test::serial] +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!(authorize_response.status, enums::AttemptStatus::Authorized); - let txn_id = utils::get_connector_transaction_id(authorize_response.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)); + assert_eq!( + 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_fail_payment_for_missing_cvc() { - let response = Fiserv {} +#[serial_test::serial] +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] +#[serial_test::serial] +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(); + tokio::time::sleep(std::time::Duration::from_secs(7)).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, + ); +} + +// Creates a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +#[serial_test::serial] +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). +#[actix_web::test] +#[serial_test::serial] +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"); + tokio::time::sleep(std::time::Duration::from_secs(7)).await; + 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, + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!(response.status, enums::AttemptStatus::Charged); +} + +// Refunds a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +#[serial_test::serial] +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). +#[actix_web::test] +#[serial_test::serial] +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). +#[actix_web::test] +#[serial_test::serial] +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] +#[serial_test::serial] +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(7)).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. +#[actix_web::test] +#[serial_test::serial] +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("4005550000000019".to_string()), - card_exp_month: Secret::new("02".to_string()), - card_exp_year: Secret::new("2035".to_string()), - card_holder_name: Secret::new("John Doe".to_string()), - card_cvc: Secret::new("".to_string()), - card_issuer: None, - card_network: None, + card_number: Secret::new("1234567891011".to_string()), + ..utils::CCardType::default().0 }), ..utils::PaymentAuthorizeType::default().0 }), - None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Unable to assign card to brand: Invalid.".to_string(), + ); +} + +// Creates a payment with empty card number. +#[actix_web::test] +#[serial_test::serial] +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.message, "Invalid or Missing Field Data".to_string(),); + assert_eq!(x.message, "Invalid or Missing Field Data",); } -#[ignore] +// Creates a payment with incorrect CVC. #[actix_web::test] -async fn should_refund_succeeded_payment() { - let connector = Fiserv {}; - //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.response) { - let response = connector - .refund_payment(transaction_id, None, None) - .await - .unwrap(); - assert_eq!( - response.response.unwrap().refund_status, - enums::RefundStatus::Success, - ); - } +#[serial_test::serial] +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().message, + "Invalid or Missing Field Data".to_string(), + ); } + +// Creates a payment with incorrect expiry month. +#[actix_web::test] +#[serial_test::serial] +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().message, + "Invalid or Missing Field Data".to_string(), + ); +} + +// Creates a payment with incorrect expiry year. +#[actix_web::test] +#[serial_test::serial] +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().message, + "Unable to assign card to brand: Invalid.".to_string(), + ); +} + +// Voids a payment using automatic capture flow (Non 3DS). +#[actix_web::test] +#[serial_test::serial] +#[ignore] +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(), + Some(types::PaymentsCancelData { + connector_transaction_id: String::from(""), + cancellation_reason: Some("TIMEOUT".to_string()), + ..Default::default() + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + void_response.response.unwrap_err().message, + "You cannot cancel this PaymentIntent because it has a status of succeeded." + ); +} + +// Captures a payment using invalid connector payment id. +#[actix_web::test] +#[serial_test::serial] +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().message, + String::from("Referenced transaction is invalid or not found") + ); +} + +// Refunds a payment with refund amount higher than payment amount. +#[actix_web::test] +#[serial_test::serial] +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().message, + "Unable to Refund: Amount is greater than original transaction", + ); +} + +// Connector dependent test cases goes here + +// [#478]: add unit tests for non 3DS, wallets & webhooks in connector tests