diff --git a/config/development.toml b/config/development.toml index 00c5a1228d..e5b4ca0090 100644 --- a/config/development.toml +++ b/config/development.toml @@ -217,6 +217,10 @@ apple_pay = { country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,D bucket_name = "" region = "" +[pm_filters.forte] +credit = {currency = "USD"} +debit = {currency = "USD"} + [tokenization] stripe = { long_lived_token = false, payment_method = "wallet" } checkout = { long_lived_token = false, payment_method = "wallet" } diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index 5ab3ac128c..fbee4ea058 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -596,7 +596,7 @@ pub enum Connector { Bambora, Dlocal, Fiserv, - //Forte, + Forte, Globalpay, Klarna, Mollie, @@ -661,7 +661,7 @@ pub enum RoutableConnectors { Cybersource, Dlocal, Fiserv, - //Forte, + Forte, Globalpay, Klarna, Mollie, diff --git a/crates/router/src/connector/aci/transformers.rs b/crates/router/src/connector/aci/transformers.rs index 7ac9e43031..6ecd6974c0 100644 --- a/crates/router/src/connector/aci/transformers.rs +++ b/crates/router/src/connector/aci/transformers.rs @@ -186,7 +186,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for AciPaymentsRequest { } api::PaymentMethodData::Crypto(_) | api::PaymentMethodData::BankDebit(_) => { Err(errors::ConnectorError::NotSupported { - payment_method: format!("{:?}", item.payment_method), + message: format!("{:?}", item.payment_method), connector: "Aci", payment_experience: api_models::enums::PaymentExperience::RedirectToUrl .to_string(), diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index 72ae3d0e5d..ed8f5fadf8 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -348,7 +348,7 @@ impl TryFrom<&api_enums::BankNames> for OnlineBankingCzechRepublicBanks { api::enums::BankNames::CeskaSporitelna => Ok(Self::CS), api::enums::BankNames::PlatnoscOnlineKartaPlatnicza => Ok(Self::C), _ => Err(errors::ConnectorError::NotSupported { - payment_method: String::from("BankRedirect"), + message: String::from("BankRedirect"), connector: "Adyen", payment_experience: api_enums::PaymentExperience::RedirectToUrl.to_string(), })?, @@ -429,7 +429,7 @@ impl TryFrom<&api_enums::BankNames> for OnlineBankingPolandBanks { api_models::enums::BankNames::VeloBank => Ok(Self::VeloBank), api_models::enums::BankNames::ETransferPocztowy24 => Ok(Self::ETransferPocztowy24), _ => Err(errors::ConnectorError::NotSupported { - payment_method: String::from("BankRedirect"), + message: String::from("BankRedirect"), connector: "Adyen", payment_experience: api_enums::PaymentExperience::RedirectToUrl.to_string(), })?, @@ -465,7 +465,7 @@ impl TryFrom<&api_enums::BankNames> for OnlineBankingSlovakiaBanks { api::enums::BankNames::TatraPay => Ok(Self::Tatra), api::enums::BankNames::Viamo => Ok(Self::Viamo), _ => Err(errors::ConnectorError::NotSupported { - payment_method: String::from("BankRedirect"), + message: String::from("BankRedirect"), connector: "Adyen", payment_experience: api_enums::PaymentExperience::RedirectToUrl.to_string(), })?, @@ -697,7 +697,7 @@ impl<'a> TryFrom<&api_enums::BankNames> for AdyenTestBankNames<'a> { Self("4a0a975b-0594-4b40-9068-39f77b3a91f9") } _ => Err(errors::ConnectorError::NotSupported { - payment_method: String::from("BankRedirect"), + message: String::from("BankRedirect"), connector: "Adyen", payment_experience: api_enums::PaymentExperience::RedirectToUrl.to_string(), })?, @@ -743,7 +743,7 @@ impl<'a> TryFrom<&types::PaymentsAuthorizeRouterData> for AdyenPaymentRequest<'a AdyenPaymentRequest::try_from((item, bank_redirect)) } _ => Err(errors::ConnectorError::NotSupported { - payment_method: format!("{:?}", item.request.payment_method_type), + message: format!("{:?}", item.request.payment_method_type), connector: "Adyen", payment_experience: api_models::enums::PaymentExperience::RedirectToUrl .to_string(), @@ -1130,7 +1130,7 @@ impl<'a> TryFrom<(&types::PaymentsAuthorizeRouterData, MandateReferenceId)> Ok(AdyenPaymentMethod::AdyenCard(Box::new(adyen_card))) } _ => Err(errors::ConnectorError::NotSupported { - payment_method: format!("mandate_{:?}", item.payment_method), + message: format!("mandate_{:?}", item.payment_method), connector: "Adyen", payment_experience: api_models::enums::PaymentExperience::RedirectToUrl .to_string(), diff --git a/crates/router/src/connector/authorizedotnet/transformers.rs b/crates/router/src/connector/authorizedotnet/transformers.rs index 8f26534058..55500b281a 100644 --- a/crates/router/src/connector/authorizedotnet/transformers.rs +++ b/crates/router/src/connector/authorizedotnet/transformers.rs @@ -104,7 +104,7 @@ fn get_pm_and_subsequent_auth_detail( Ok((payment_details, processing_options, subseuent_auth_info)) } _ => Err(errors::ConnectorError::NotSupported { - payment_method: format!("{:?}", item.request.payment_method_data), + message: format!("{:?}", item.request.payment_method_data), connector: "AuthorizeDotNet", payment_experience: api_models::enums::PaymentExperience::RedirectToUrl .to_string(), @@ -131,7 +131,7 @@ fn get_pm_and_subsequent_auth_detail( } api::PaymentMethodData::Crypto(_) | api::PaymentMethodData::BankDebit(_) => { Err(errors::ConnectorError::NotSupported { - payment_method: format!("{:?}", item.request.payment_method_data), + message: format!("{:?}", item.request.payment_method_data), connector: "AuthorizeDotNet", payment_experience: api_models::enums::PaymentExperience::RedirectToUrl .to_string(), diff --git a/crates/router/src/connector/forte.rs b/crates/router/src/connector/forte.rs index 3e834c2ed7..c42044d543 100644 --- a/crates/router/src/connector/forte.rs +++ b/crates/router/src/connector/forte.rs @@ -2,11 +2,14 @@ mod transformers; use std::fmt::Debug; +use base64::Engine; use error_stack::{IntoReport, ResultExt}; use transformers as forte; use crate::{ configs::settings, + connector::utils::{PaymentsSyncRequestData, RefundsRequestData}, + consts, core::errors::{self, CustomResult}, headers, services::{self, ConnectorIntegration}, @@ -42,6 +45,7 @@ impl > for Forte { } +pub const AUTH_ORG_ID_HEADER: &str = "X-Forte-Auth-Organization-Id"; impl ConnectorCommonExt for Forte where @@ -52,13 +56,10 @@ where 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) + let content_type = ConnectorCommon::common_get_content_type(self); + let mut common_headers = self.get_auth_header(&req.connector_auth_type)?; + common_headers.push((headers::CONTENT_TYPE.to_string(), content_type.to_string())); + Ok(common_headers) } } @@ -79,25 +80,34 @@ impl ConnectorCommon for Forte { &self, auth_type: &types::ConnectorAuthType, ) -> CustomResult, errors::ConnectorError> { - let auth = forte::ForteAuthType::try_from(auth_type) + let auth: forte::ForteAuthType = auth_type + .try_into() .change_context(errors::ConnectorError::FailedToObtainAuthType)?; - Ok(vec![(headers::AUTHORIZATION.to_string(), auth.api_key)]) + let raw_basic_token = format!("{}:{}", auth.api_access_id, auth.api_secret_key); + let basic_token = format!("Basic {}", consts::BASE64_ENGINE.encode(raw_basic_token)); + Ok(vec![ + (headers::AUTHORIZATION.to_string(), basic_token), + (AUTH_ORG_ID_HEADER.to_string(), auth.organization_id), + ]) } - fn build_error_response( &self, res: Response, ) -> CustomResult { - let response: forte::ForteErrorResponse = - res.response - .parse_struct("ForteErrorResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - + let response: forte::ForteErrorResponse = res + .response + .parse_struct("Forte ErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let message = response.response.response_desc; + let code = response + .response + .response_code + .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()); Ok(ErrorResponse { status_code: res.status_code, - code: response.code, - message: response.message, - reason: response.reason, + code, + message, + reason: None, }) } } @@ -134,19 +144,25 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let auth: forte::ForteAuthType = forte::ForteAuthType::try_from(&req.connector_auth_type)?; + Ok(format!( + "{}/organizations/{}/locations/{}/transactions", + self.base_url(connectors), + auth.organization_id, + auth.location_id + )) } fn get_request_body( &self, req: &types::PaymentsAuthorizeRouterData, ) -> CustomResult, errors::ConnectorError> { - let req_obj = forte::FortePaymentsRequest::try_from(req)?; + let connector_req = forte::FortePaymentsRequest::try_from(req)?; let forte_req = - utils::Encode::::encode_to_string_of_json(&req_obj) + utils::Encode::::encode_to_string_of_json(&connector_req) .change_context(errors::ConnectorError::RequestEncodingFailed)?; Ok(Some(forte_req)) } @@ -178,14 +194,13 @@ impl ConnectorIntegration CustomResult { let response: forte::FortePaymentsResponse = res .response - .parse_struct("Forte PaymentsAuthorizeResponse") + .parse_struct("Forte AuthorizeResponse") .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( @@ -213,10 +228,19 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let auth: forte::ForteAuthType = forte::ForteAuthType::try_from(&req.connector_auth_type)?; + let txn_id = PaymentsSyncRequestData::get_connector_transaction_id(&req.request) + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; + Ok(format!( + "{}/organizations/{}/locations/{}/transactions/{}", + self.base_url(connectors), + auth.organization_id, + auth.location_id, + txn_id + )) } fn build_request( @@ -233,13 +257,12 @@ impl ConnectorIntegration CustomResult { - let response: forte::FortePaymentsResponse = res + let response: forte::FortePaymentsSyncResponse = res .response .parse_struct("forte PaymentsSyncResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -248,7 +271,6 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let auth: forte::ForteAuthType = forte::ForteAuthType::try_from(&req.connector_auth_type)?; + Ok(format!( + "{}/organizations/{}/locations/{}/transactions", + self.base_url(connectors), + auth.organization_id, + auth.location_id + )) } fn get_request_body( &self, - _req: &types::PaymentsCaptureRouterData, + req: &types::PaymentsCaptureRouterData, ) -> CustomResult, errors::ConnectorError> { - Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) + let connector_req = forte::ForteCaptureRequest::try_from(req)?; + let forte_req = + utils::Encode::::encode_to_string_of_json(&connector_req) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(forte_req)) } fn build_request( @@ -296,12 +328,13 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { Ok(Some( services::RequestBuilder::new() - .method(services::Method::Post) + .method(services::Method::Put) .url(&types::PaymentsCaptureType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsCaptureType::get_headers( self, req, connectors, )?) + .body(types::PaymentsCaptureType::get_request_body(self, req)?) .build(), )) } @@ -311,7 +344,7 @@ impl ConnectorIntegration CustomResult { - let response: forte::FortePaymentsResponse = res + let response: forte::ForteCaptureResponse = res .response .parse_struct("Forte PaymentsCaptureResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -320,7 +353,6 @@ impl ConnectorIntegration for Forte { + 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 { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let auth: forte::ForteAuthType = forte::ForteAuthType::try_from(&req.connector_auth_type)?; + Ok(format!( + "{}/organizations/{}/locations/{}/transactions/{}", + self.base_url(connectors), + auth.organization_id, + auth.location_id, + req.request.connector_transaction_id + )) + } + fn get_request_body( + &self, + req: &types::PaymentsCancelRouterData, + ) -> CustomResult, errors::ConnectorError> { + let connector_req = forte::ForteCancelRequest::try_from(req)?; + let forte_req = + utils::Encode::::encode_to_string_of_json(&connector_req) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(forte_req)) + } + + fn build_request( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Put) + .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) + .body(types::PaymentsVoidType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsCancelRouterData, + res: Response, + ) -> CustomResult { + let response: forte::ForteCancelResponse = res + .response + .parse_struct("forte CancelResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } } impl ConnectorIntegration for Forte { @@ -351,19 +457,25 @@ impl ConnectorIntegration, - _connectors: &settings::Connectors, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let auth: forte::ForteAuthType = forte::ForteAuthType::try_from(&req.connector_auth_type)?; + Ok(format!( + "{}/organizations/{}/locations/{}/transactions", + self.base_url(connectors), + auth.organization_id, + auth.location_id + )) } fn get_request_body( &self, req: &types::RefundsRouterData, ) -> CustomResult, errors::ConnectorError> { - let req_obj = forte::ForteRefundRequest::try_from(req)?; + let connector_req = forte::ForteRefundRequest::try_from(req)?; let forte_req = - utils::Encode::::encode_to_string_of_json(&req_obj) + utils::Encode::::encode_to_string_of_json(&connector_req) .change_context(errors::ConnectorError::RequestEncodingFailed)?; Ok(Some(forte_req)) } @@ -399,7 +511,6 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let auth: forte::ForteAuthType = forte::ForteAuthType::try_from(&req.connector_auth_type)?; + Ok(format!( + "{}/organizations/{}/locations/{}/transactions/{}", + self.base_url(connectors), + auth.organization_id, + auth.location_id, + req.request.get_connector_refund_id()? + )) } fn build_request( @@ -442,7 +560,6 @@ impl ConnectorIntegration CustomResult { - let response: forte::RefundResponse = res + let response: forte::RefundSyncResponse = res .response .parse_struct("forte RefundSyncResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -461,7 +578,6 @@ impl ConnectorIntegration, + last_name: Secret, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct ForteCard { - name: Secret, - number: Secret, - expiry_month: Secret, - expiry_year: Secret, - cvc: Secret, - complete: bool, +#[derive(Debug, Serialize)] +pub struct Card { + card_type: ForteCardType, + name_on_card: Secret, + account_number: Secret, + expire_month: Secret, + expire_year: Secret, + card_verification_value: Secret, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ForteCardType { + Visa, + MasterCard, + Amex, + Discover, + DinersClub, + Jcb, +} + +impl TryFrom for ForteCardType { + type Error = error_stack::Report; + fn try_from(issuer: utils::CardIssuer) -> Result { + match issuer { + utils::CardIssuer::AmericanExpress => Ok(Self::Amex), + utils::CardIssuer::Master => Ok(Self::MasterCard), + utils::CardIssuer::Discover => Ok(Self::Discover), + utils::CardIssuer::Visa => Ok(Self::Visa), + utils::CardIssuer::DinersClub => Ok(Self::DinersClub), + utils::CardIssuer::JCB => Ok(Self::Jcb), + _ => Err(errors::ConnectorError::NotSupported { + message: issuer.to_string(), + connector: "Forte", + payment_experience: api::enums::PaymentExperience::RedirectToUrl.to_string(), + } + .into()), + } + } } impl TryFrom<&types::PaymentsAuthorizeRouterData> for FortePaymentsRequest { type Error = error_stack::Report; fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { - match item.request.payment_method_data.clone() { - api::PaymentMethodData::Card(req_card) => { - let card = ForteCard { - name: req_card.card_holder_name, - number: req_card.card_number, - expiry_month: req_card.card_exp_month, - expiry_year: req_card.card_exp_year, - cvc: req_card.card_cvc, - complete: item.request.is_auto_capture()?, + if item.request.currency != enums::Currency::USD { + Err(errors::ConnectorError::NotSupported { + message: item.request.currency.to_string(), + connector: "Forte", + payment_experience: api::enums::PaymentExperience::RedirectToUrl.to_string(), + })? + } + match item.request.payment_method_data { + api_models::payments::PaymentMethodData::Card(ref ccard) => { + let action = match item.request.is_auto_capture()? { + true => ForteAction::Sale, + false => ForteAction::Authorize, }; + let card_type = ForteCardType::try_from(ccard.get_card_issuer()?)?; + let address = item.get_billing_address()?; + let card = Card { + card_type, + name_on_card: ccard.card_holder_name.clone(), + account_number: ccard.card_number.clone(), + expire_month: ccard.card_exp_month.clone(), + expire_year: ccard.card_exp_year.clone(), + card_verification_value: ccard.card_cvc.clone(), + }; + let billing_address = BillingAddress { + first_name: address.get_first_name()?.to_owned(), + last_name: address.get_last_name()?.to_owned(), + }; + let authorization_amount = + utils::to_currency_base_unit_asf64(item.request.amount, item.request.currency)?; Ok(Self { - amount: item.request.amount, + action, + authorization_amount, + billing_address, card, }) } - _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), + _ => Err(errors::ConnectorError::NotImplemented( + "Payment Method".to_string(), + ))?, } } } // Auth Struct pub struct ForteAuthType { - pub(super) api_key: String, + pub(super) api_access_id: String, + pub(super) organization_id: String, + pub(super) location_id: String, + pub(super) api_secret_key: String, } impl TryFrom<&types::ConnectorAuthType> for ForteAuthType { type Error = error_stack::Report; fn try_from(auth_type: &types::ConnectorAuthType) -> Result { match auth_type { - types::ConnectorAuthType::HeaderKey { api_key } => Ok(Self { - api_key: api_key.to_string(), + types::ConnectorAuthType::MultiAuthKey { + api_key, + key1, + api_secret, + key2, + } => Ok(Self { + api_access_id: api_key.to_string(), + organization_id: format!("org_{}", key1), + location_id: format!("loc_{}", key2), + api_secret_key: api_secret.to_string(), }), - _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), + _ => Err(errors::ConnectorError::FailedToObtainAuthType)?, } } } // PaymentsResponse -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Deserialize)] #[serde(rename_all = "lowercase")] pub enum FortePaymentStatus { - Succeeded, + Complete, Failed, - #[default] - Processing, + Authorized, + Ready, + Voided, + Settled, } impl From for enums::AttemptStatus { fn from(item: FortePaymentStatus) -> Self { match item { - FortePaymentStatus::Succeeded => Self::Charged, + FortePaymentStatus::Complete | FortePaymentStatus::Settled => Self::Charged, FortePaymentStatus::Failed => Self::Failure, - FortePaymentStatus::Processing => Self::Authorizing, + FortePaymentStatus::Ready => Self::Pending, + FortePaymentStatus::Authorized => Self::Authorized, + FortePaymentStatus::Voided => Self::Voided, } } } -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +impl ForeignFrom<(ForteResponseCode, ForteAction)> for enums::AttemptStatus { + fn foreign_from((response_code, action): (ForteResponseCode, ForteAction)) -> Self { + match response_code { + ForteResponseCode::A01 => match action { + ForteAction::Authorize => Self::Authorized, + ForteAction::Sale => Self::Pending, + }, + ForteResponseCode::A05 | ForteResponseCode::A06 => Self::Authorizing, + _ => Self::Failure, + } + } +} + +#[derive(Debug, Deserialize)] +pub struct CardResponse { + pub name_on_card: Secret, + pub last_4_account_number: String, + pub masked_account_number: String, + pub card_type: String, +} + +#[derive(Debug, Deserialize)] +pub enum ForteResponseCode { + A01, + A05, + A06, + U13, + U14, + U18, + U20, +} + +impl From for enums::AttemptStatus { + fn from(item: ForteResponseCode) -> Self { + match item { + ForteResponseCode::A01 | ForteResponseCode::A05 | ForteResponseCode::A06 => { + Self::Pending + } + _ => Self::Failure, + } + } +} + +#[derive(Debug, Deserialize)] +pub struct ResponseStatus { + pub environment: String, + pub response_type: String, + pub response_code: ForteResponseCode, + pub response_desc: String, + pub authorization_code: String, + pub avs_result: Option, + pub cvv_result: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ForteAction { + Sale, + Authorize, +} + +#[derive(Debug, Deserialize)] pub struct FortePaymentsResponse { - status: FortePaymentStatus, - id: String, + pub transaction_id: String, + pub location_id: String, + pub action: ForteAction, + pub authorization_amount: Option, + pub authorization_code: String, + pub entered_by: String, + pub billing_address: Option, + pub card: Option, + pub response: ResponseStatus, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ForteMeta { + pub auth_id: String, } impl @@ -96,13 +248,197 @@ impl fn try_from( item: types::ResponseRouterData, ) -> Result { + let response_code = item.response.response.response_code; + let action = item.response.action; + let transaction_id = &item.response.transaction_id; + Ok(Self { + status: enums::AttemptStatus::foreign_from((response_code, action)), + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(transaction_id.to_string()), + redirection_data: None, + mandate_reference: None, + connector_metadata: Some(serde_json::json!(ForteMeta { + auth_id: item.response.authorization_code, + })), + network_txn_id: None, + }), + ..item.data + }) + } +} + +//PsyncResponse + +#[derive(Debug, Deserialize)] +pub struct FortePaymentsSyncResponse { + pub transaction_id: String, + pub location_id: String, + pub status: FortePaymentStatus, + pub action: ForteAction, + pub authorization_amount: Option, + pub authorization_code: String, + pub entered_by: String, + pub billing_address: Option, + pub card: Option, + pub response: ResponseStatus, +} + +impl + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + FortePaymentsSyncResponse, + T, + types::PaymentsResponseData, + >, + ) -> Result { + let transaction_id = &item.response.transaction_id; Ok(Self { status: enums::AttemptStatus::from(item.response.status), response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(item.response.id), + resource_id: types::ResponseId::ConnectorTransactionId(transaction_id.to_string()), redirection_data: None, mandate_reference: None, - connector_metadata: None, + connector_metadata: Some(serde_json::json!(ForteMeta { + auth_id: item.response.authorization_code, + })), + network_txn_id: None, + }), + ..item.data + }) + } +} + +// Capture + +#[derive(Debug, Serialize)] +pub struct ForteCaptureRequest { + action: String, + transaction_id: String, + authorization_code: String, +} + +impl TryFrom<&types::PaymentsCaptureRouterData> for ForteCaptureRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsCaptureRouterData) -> Result { + let trn_id = item.request.connector_transaction_id.clone(); + let connector_auth_id: ForteMeta = + utils::to_connector_meta(item.request.connector_meta.clone())?; + let auth_code = connector_auth_id.auth_id; + Ok(Self { + action: "capture".to_string(), + transaction_id: trn_id, + authorization_code: auth_code, + }) + } +} + +#[derive(Debug, Deserialize)] +pub struct CaptureResponseStatus { + pub environment: String, + pub response_type: String, + pub response_code: ForteResponseCode, + pub response_desc: String, + pub authorization_code: String, +} +// Capture Response +#[derive(Debug, Deserialize)] +pub struct ForteCaptureResponse { + pub transaction_id: String, + pub original_transaction_id: String, + pub entered_by: String, + pub authorization_code: String, + pub response: CaptureResponseStatus, +} + +impl TryFrom> + for types::PaymentsCaptureRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::PaymentsCaptureResponseRouterData, + ) -> Result { + Ok(Self { + status: enums::AttemptStatus::from(item.response.response.response_code), + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + item.response.transaction_id, + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: Some(serde_json::json!(ForteMeta { + auth_id: item.response.authorization_code, + })), + network_txn_id: None, + }), + amount_captured: None, + ..item.data + }) + } +} + +//Cancel + +#[derive(Debug, Serialize)] +pub struct ForteCancelRequest { + action: String, + authorization_code: String, +} + +impl TryFrom<&types::PaymentsCancelRouterData> for ForteCancelRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsCancelRouterData) -> Result { + let action = "void".to_string(); + let connector_auth_id: ForteMeta = + utils::to_connector_meta(item.request.connector_meta.clone())?; + let authorization_code = connector_auth_id.auth_id; + Ok(Self { + action, + authorization_code, + }) + } +} + +#[derive(Debug, Deserialize)] +pub struct CancelResponseStatus { + pub response_type: String, + pub response_code: ForteResponseCode, + pub response_desc: String, + pub authorization_code: String, +} + +#[derive(Debug, Deserialize)] +pub struct ForteCancelResponse { + pub transaction_id: String, + pub location_id: String, + pub action: String, + pub authorization_code: String, + pub entered_by: String, + pub response: CancelResponseStatus, +} + +impl + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData, + ) -> Result { + let transaction_id = &item.response.transaction_id; + Ok(Self { + status: enums::AttemptStatus::from(item.response.response.response_code), + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(transaction_id.to_string()), + redirection_data: None, + mandate_reference: None, + connector_metadata: Some(serde_json::json!(ForteMeta { + auth_id: item.response.authorization_code, + })), network_txn_id: None, }), ..item.data @@ -111,46 +447,68 @@ impl } // REFUND : -// Type definition for RefundRequest #[derive(Default, Debug, Serialize)] pub struct ForteRefundRequest { - pub amount: i64, + action: String, + authorization_amount: f64, + original_transaction_id: String, + authorization_code: String, } impl TryFrom<&types::RefundsRouterData> for ForteRefundRequest { type Error = error_stack::Report; fn try_from(item: &types::RefundsRouterData) -> Result { + let trn_id = item.request.connector_transaction_id.clone(); + let connector_auth_id: ForteMeta = + utils::to_connector_meta(item.request.connector_metadata.clone())?; + let auth_code = connector_auth_id.auth_id; + let authorization_amount = + utils::to_currency_base_unit_asf64(item.request.amount, item.request.currency)?; Ok(Self { - amount: item.request.amount, + action: "reverse".to_string(), + authorization_amount, + original_transaction_id: trn_id, + authorization_code: auth_code, }) } } -// Type definition for Refund Response - -#[allow(dead_code)] -#[derive(Debug, Serialize, Default, Deserialize, Clone)] +#[derive(Debug, Deserialize)] +#[serde(rename_all = "lowercase")] pub enum RefundStatus { - Succeeded, + Complete, + Ready, Failed, - #[default] - Processing, } impl From for enums::RefundStatus { fn from(item: RefundStatus) -> Self { match item { - RefundStatus::Succeeded => Self::Success, + RefundStatus::Complete => Self::Success, + RefundStatus::Ready => Self::Pending, RefundStatus::Failed => Self::Failure, - RefundStatus::Processing => Self::Pending, + } + } +} +impl From for enums::RefundStatus { + fn from(item: ForteResponseCode) -> Self { + match item { + ForteResponseCode::A01 | ForteResponseCode::A05 | ForteResponseCode::A06 => { + Self::Pending + } + _ => Self::Failure, } } } -#[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Deserialize)] pub struct RefundResponse { - id: String, - status: RefundStatus, + pub transaction_id: String, + pub original_transaction_id: String, + pub action: String, + pub authorization_amount: Option, + pub authorization_code: String, + pub response: ResponseStatus, } impl TryFrom> @@ -162,24 +520,30 @@ impl TryFrom> ) -> Result { Ok(Self { response: Ok(types::RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), + connector_refund_id: item.response.transaction_id, + refund_status: enums::RefundStatus::from(item.response.response.response_code), }), ..item.data }) } } -impl TryFrom> +#[derive(Debug, Deserialize)] +pub struct RefundSyncResponse { + status: RefundStatus, + transaction_id: String, +} + +impl TryFrom> for types::RefundsRouterData { type Error = error_stack::Report; fn try_from( - item: types::RefundsResponseRouterData, + item: types::RefundsResponseRouterData, ) -> Result { Ok(Self { response: Ok(types::RefundsResponseData { - connector_refund_id: item.response.id.to_string(), + connector_refund_id: item.response.transaction_id, refund_status: enums::RefundStatus::from(item.response.status), }), ..item.data @@ -187,10 +551,15 @@ impl TryFrom> } } -#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct ForteErrorResponse { - pub status_code: u16, - pub code: String, - pub message: String, - pub reason: Option, +#[derive(Debug, Deserialize)] +pub struct ErrorResponseStatus { + pub environment: String, + pub response_type: Option, + pub response_code: Option, + pub response_desc: String, +} + +#[derive(Debug, Deserialize)] +pub struct ForteErrorResponse { + pub response: ErrorResponseStatus, } diff --git a/crates/router/src/connector/klarna.rs b/crates/router/src/connector/klarna.rs index f2be4b2afb..4ed92d73d0 100644 --- a/crates/router/src/connector/klarna.rs +++ b/crates/router/src/connector/klarna.rs @@ -261,7 +261,7 @@ impl token )), _ => Err(error_stack::report!(errors::ConnectorError::NotSupported { - payment_method: payment_method_type.to_string(), + message: payment_method_type.to_string(), connector: "klarna", payment_experience: payment_experience.to_string() })), diff --git a/crates/router/src/connector/multisafepay/transformers.rs b/crates/router/src/connector/multisafepay/transformers.rs index 3466392473..afc05858c6 100644 --- a/crates/router/src/connector/multisafepay/transformers.rs +++ b/crates/router/src/connector/multisafepay/transformers.rs @@ -194,14 +194,21 @@ pub struct MultisafepayPaymentsRequest { pub var3: Option, } -impl From for Gateway { - fn from(issuer: utils::CardIssuer) -> Self { +impl TryFrom for Gateway { + type Error = error_stack::Report; + fn try_from(issuer: utils::CardIssuer) -> Result { match issuer { - utils::CardIssuer::AmericanExpress => Self::Amex, - utils::CardIssuer::Master => Self::MasterCard, - utils::CardIssuer::Maestro => Self::Maestro, - utils::CardIssuer::Visa => Self::Visa, - utils::CardIssuer::Discover => Self::Discover, + utils::CardIssuer::AmericanExpress => Ok(Self::Amex), + utils::CardIssuer::Master => Ok(Self::MasterCard), + utils::CardIssuer::Maestro => Ok(Self::Maestro), + utils::CardIssuer::Discover => Ok(Self::Discover), + utils::CardIssuer::Visa => Ok(Self::Visa), + _ => Err(errors::ConnectorError::NotSupported { + message: issuer.to_string(), + connector: "Multisafe pay", + payment_experience: api::enums::PaymentExperience::RedirectToUrl.to_string(), + } + .into()), } } } @@ -216,7 +223,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for MultisafepayPaymentsReques }; let gateway = match item.request.payment_method_data { - api::PaymentMethodData::Card(ref ccard) => Gateway::from(ccard.get_card_issuer()?), + api::PaymentMethodData::Card(ref ccard) => Gateway::try_from(ccard.get_card_issuer()?)?, api::PaymentMethodData::PayLater( api_models::payments::PayLaterData::KlarnaRedirect { billing_email: _, diff --git a/crates/router/src/connector/nuvei/transformers.rs b/crates/router/src/connector/nuvei/transformers.rs index b80c599db2..15e15d5035 100644 --- a/crates/router/src/connector/nuvei/transformers.rs +++ b/crates/router/src/connector/nuvei/transformers.rs @@ -539,7 +539,7 @@ impl ) } _ => Err(errors::ConnectorError::NotSupported { - payment_method: "Bank Redirect".to_string(), + message: "Bank Redirect".to_string(), connector: "Nuvei", payment_experience: "Redirection".to_string(), })?, @@ -583,7 +583,7 @@ impl item, )), _ => Err(errors::ConnectorError::NotSupported { - payment_method: "Wallet".to_string(), + message: "Wallet".to_string(), connector: "Nuvei", payment_experience: "RedirectToUrl".to_string(), } @@ -611,7 +611,7 @@ impl item, )), _ => Err(errors::ConnectorError::NotSupported { - payment_method: "Bank Redirect".to_string(), + message: "Bank Redirect".to_string(), connector: "Nuvei", payment_experience: "RedirectToUrl".to_string(), } diff --git a/crates/router/src/connector/payeezy/transformers.rs b/crates/router/src/connector/payeezy/transformers.rs index 428cc12bba..b0cd6beaca 100644 --- a/crates/router/src/connector/payeezy/transformers.rs +++ b/crates/router/src/connector/payeezy/transformers.rs @@ -38,7 +38,7 @@ impl TryFrom for PayeezyCardType { utils::CardIssuer::Discover => Ok(Self::Discover), utils::CardIssuer::Visa => Ok(Self::Visa), _ => Err(errors::ConnectorError::NotSupported { - payment_method: api::enums::PaymentMethod::Card.to_string(), + message: issuer.to_string(), connector: "Payeezy", payment_experience: api::enums::PaymentExperience::RedirectToUrl.to_string(), } diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index 6d203610ac..d0bfd1d4a4 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -454,7 +454,7 @@ impl TryFrom<&api_models::enums::BankNames> for StripeBankNames { api_models::enums::BankNames::VolkskreditbankAg => Self::VolkskreditbankAg, api_models::enums::BankNames::VrBankBraunau => Self::VrBankBraunau, _ => Err(errors::ConnectorError::NotSupported { - payment_method: api_enums::PaymentMethod::BankRedirect.to_string(), + message: api_enums::PaymentMethod::BankRedirect.to_string(), connector: "Stripe", payment_experience: api_enums::PaymentExperience::RedirectToUrl.to_string(), })?, @@ -497,14 +497,14 @@ fn infer_stripe_pay_later_type( Ok(StripePaymentMethodType::AfterpayClearpay) } _ => Err(errors::ConnectorError::NotSupported { - payment_method: pm_type.to_string(), + message: pm_type.to_string(), connector: "stripe", payment_experience: experience.to_string(), }), } } else { Err(errors::ConnectorError::NotSupported { - payment_method: pm_type.to_string(), + message: pm_type.to_string(), connector: "stripe", payment_experience: experience.to_string(), }) @@ -1865,7 +1865,7 @@ impl })) } api::PaymentMethodData::Crypto(_) => Err(errors::ConnectorError::NotSupported { - payment_method: format!("{pm_type:?}"), + message: format!("{pm_type:?}"), connector: "Stripe", payment_experience: api_models::enums::PaymentExperience::RedirectToUrl.to_string(), })?, diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index c2c05df8b7..d9ced8e1e0 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -325,6 +325,14 @@ static CARD_REGEX: Lazy>> = Lazy CardIssuer::Maestro, Regex::new(r"^(5018|5020|5038|5893|6304|6759|6761|6762|6763)[0-9]{8,15}$"), ); + map.insert( + CardIssuer::DinersClub, + Regex::new(r"^3(?:0[0-5]|[68][0-9])[0-9]{11}$"), + ); + map.insert( + CardIssuer::JCB, + Regex::new(r"^(3(?:088|096|112|158|337|5(?:2[89]|[3-8][0-9]))\d{12})$"), + ); map }); @@ -335,6 +343,8 @@ pub enum CardIssuer { Maestro, Visa, Discover, + DinersClub, + JCB, } pub trait CardData { @@ -630,6 +640,14 @@ pub fn to_currency_base_unit( amount: i64, currency: storage_models::enums::Currency, ) -> Result> { + let amount_f64 = to_currency_base_unit_asf64(amount, currency)?; + Ok(format!("{amount_f64:.2}")) +} + +pub fn to_currency_base_unit_asf64( + amount: i64, + currency: storage_models::enums::Currency, +) -> Result> { let amount_u32 = u32::try_from(amount) .into_report() .change_context(errors::ConnectorError::RequestEncodingFailed)?; @@ -642,7 +660,7 @@ pub fn to_currency_base_unit( | storage_models::enums::Currency::OMR => amount_f64 / 1000.00, _ => amount_f64 / 100.00, }; - Ok(format!("{amount:.2}")) + Ok(amount) } pub fn str_to_f32(value: &str, serializer: S) -> Result diff --git a/crates/router/src/connector/worldline/transformers.rs b/crates/router/src/connector/worldline/transformers.rs index 4b34d05431..c0676e4d55 100644 --- a/crates/router/src/connector/worldline/transformers.rs +++ b/crates/router/src/connector/worldline/transformers.rs @@ -130,7 +130,7 @@ impl TryFrom for Gateway { utils::CardIssuer::Discover => Ok(Self::Discover), utils::CardIssuer::Visa => Ok(Self::Visa), _ => Err(errors::ConnectorError::NotSupported { - payment_method: api_enums::PaymentMethod::Card.to_string(), + message: issuer.to_string(), connector: "worldline", payment_experience: api_enums::PaymentExperience::RedirectToUrl.to_string(), } diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index b6c653f95a..ce71849c09 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -253,9 +253,9 @@ pub enum ConnectorError { FailedToObtainCertificateKey, #[error("This step has not been implemented for: {0}")] NotImplemented(String), - #[error("{payment_method} is not supported by {connector}")] + #[error("{message} is not supported by {connector}")] NotSupported { - payment_method: String, + message: String, connector: &'static str, payment_experience: String, }, diff --git a/crates/router/src/core/errors/utils.rs b/crates/router/src/core/errors/utils.rs index 62f77ae2d3..0df489edf2 100644 --- a/crates/router/src/core/errors/utils.rs +++ b/crates/router/src/core/errors/utils.rs @@ -107,8 +107,8 @@ impl ConnectorErrorExt for error_stack::Report { "payment_method_data, payment_method_type and payment_experience does not match", } }, - errors::ConnectorError::NotSupported { payment_method, connector, payment_experience } => { - errors::ApiErrorResponse::NotSupported { message: format!("Payment method type {payment_method} is not supported by {connector} through payment experience {payment_experience}") } + errors::ConnectorError::NotSupported { message, connector, payment_experience } => { + errors::ApiErrorResponse::NotSupported { message: format!("{message} is not supported by {connector} through payment experience {payment_experience}") } }, errors::ConnectorError::FlowNotSupported{ flow, connector } => { errors::ApiErrorResponse::FlowNotSupported { flow: flow.to_owned(), connector: connector.to_owned() } diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index a799c9245b..8bcbf2d580 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -537,6 +537,12 @@ pub enum ConnectorAuthType { key1: String, api_secret: String, }, + MultiAuthKey { + api_key: String, + key1: String, + api_secret: String, + key2: String, + }, #[default] NoKey, } diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 1a60dd3756..f84436112d 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -204,7 +204,7 @@ impl ConnectorData { "cybersource" => Ok(Box::new(&connector::Cybersource)), "dlocal" => Ok(Box::new(&connector::Dlocal)), "fiserv" => Ok(Box::new(&connector::Fiserv)), - // "forte" => Ok(Box::new(&connector::Forte)), + "forte" => Ok(Box::new(&connector::Forte)), "globalpay" => Ok(Box::new(&connector::Globalpay)), "klarna" => Ok(Box::new(&connector::Klarna)), "mollie" => Ok(Box::new(&connector::Mollie)), diff --git a/crates/router/tests/connectors/connector_auth.rs b/crates/router/tests/connectors/connector_auth.rs index d178c00a0d..3e83c00b9c 100644 --- a/crates/router/tests/connectors/connector_auth.rs +++ b/crates/router/tests/connectors/connector_auth.rs @@ -16,7 +16,7 @@ pub(crate) struct ConnectorAuthentication { pub cybersource: Option, pub dlocal: Option, pub fiserv: Option, - pub forte: Option, + pub forte: Option, pub globalpay: Option, pub mollie: Option, pub multisafepay: Option, @@ -91,3 +91,22 @@ impl From for ConnectorAuthType { } } } + +#[derive(Debug, Deserialize, Clone)] +pub(crate) struct MultiAuthKey { + pub api_key: String, + pub key1: String, + pub api_secret: String, + pub key2: String, +} + +impl From for ConnectorAuthType { + fn from(key: MultiAuthKey) -> Self { + Self::MultiAuthKey { + api_key: key.api_key, + key1: key.key1, + api_secret: key.api_secret, + key2: key.key2, + } + } +} diff --git a/crates/router/tests/connectors/forte.rs b/crates/router/tests/connectors/forte.rs index 5b5976acc6..57fa017500 100644 --- a/crates/router/tests/connectors/forte.rs +++ b/crates/router/tests/connectors/forte.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use masking::Secret; use router::types::{self, api, storage::enums}; @@ -14,7 +16,7 @@ impl utils::Connector for ForteTest { use router::connector::Forte; types::api::ConnectorData { connector: Box::new(&Forte), - connector_name: types::Connector::Dummy, + connector_name: types::Connector::Forte, get_token: types::api::GetToken::Connector, } } @@ -34,12 +36,39 @@ impl utils::Connector for ForteTest { static CONNECTOR: ForteTest = ForteTest {}; -fn get_default_payment_info() -> Option { - None +fn get_payment_data() -> Option { + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_number: Secret::new(String::from("4111111111111111")), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }) } -fn payment_method_details() -> Option { - None +fn get_default_payment_info() -> Option { + Some(utils::PaymentInfo { + address: Some(types::PaymentAddress { + billing: Some(api::Address { + address: Some(api::AddressDetails { + first_name: Some(Secret::new("first".to_string())), + last_name: Some(Secret::new("last".to_string())), + line1: Some(Secret::new("line1".to_string())), + line2: Some(Secret::new("line2".to_string())), + city: Some("city".to_string()), + zip: Some(Secret::new("zip".to_string())), + country: Some(api_models::enums::CountryAlpha2::IN), + ..Default::default() + }), + phone: Some(api::PhoneDetails { + number: Some(Secret::new("1234567890".to_string())), + country_code: Some("+91".to_string()), + }), + }), + ..Default::default() + }), + ..Default::default() + }) } // Cards Positive Tests @@ -47,7 +76,7 @@ fn payment_method_details() -> Option { #[actix_web::test] async fn should_only_authorize_payment() { let response = CONNECTOR - .authorize_payment(payment_method_details(), get_default_payment_info()) + .authorize_payment(get_payment_data(), get_default_payment_info()) .await .expect("Authorize payment response"); assert_eq!(response.status, enums::AttemptStatus::Authorized); @@ -56,20 +85,41 @@ async fn should_only_authorize_payment() { // Captures a payment using the manual capture flow (Non 3DS). #[actix_web::test] async fn should_capture_authorized_payment() { + let authorize_response = CONNECTOR + .authorize_payment(get_payment_data(), get_default_payment_info()) + .await + .expect("Authorize payment response"); + let txn_id = utils::get_connector_transaction_id(authorize_response.response.clone()).unwrap(); + let connector_meta = utils::get_connector_metadata(authorize_response.response); let response = CONNECTOR - .authorize_and_capture_payment(payment_method_details(), None, get_default_payment_info()) + .capture_payment( + txn_id, + Some(types::PaymentsCaptureData { + connector_meta, + ..utils::PaymentCaptureType::default().0 + }), + get_default_payment_info(), + ) .await .expect("Capture payment response"); - assert_eq!(response.status, enums::AttemptStatus::Charged); + //Status of the Payments is always in Pending State, Forte has to settle the sandbox transaction manually + assert_eq!(response.status, enums::AttemptStatus::Pending); } // Partially captures a payment using the manual capture flow (Non 3DS). #[actix_web::test] async fn should_partially_capture_authorized_payment() { + let authorize_response = CONNECTOR + .authorize_payment(get_payment_data(), get_default_payment_info()) + .await + .expect("Authorize payment response"); + let txn_id = utils::get_connector_transaction_id(authorize_response.response.clone()).unwrap(); + let connector_meta = utils::get_connector_metadata(authorize_response.response); let response = CONNECTOR - .authorize_and_capture_payment( - payment_method_details(), + .capture_payment( + txn_id, Some(types::PaymentsCaptureData { + connector_meta, amount_to_capture: 50, ..utils::PaymentCaptureType::default().0 }), @@ -77,14 +127,15 @@ async fn should_partially_capture_authorized_payment() { ) .await .expect("Capture payment response"); - assert_eq!(response.status, enums::AttemptStatus::Charged); + //Status of the Payments is always in Pending State, Forte has to settle the sandbox transactions manually + assert_eq!(response.status, enums::AttemptStatus::Pending); } // Synchronizes a payment using the manual capture flow (Non 3DS). #[actix_web::test] async fn should_sync_authorized_payment() { let authorize_response = CONNECTOR - .authorize_payment(payment_method_details(), get_default_payment_info()) + .authorize_payment(get_payment_data(), get_default_payment_info()) .await .expect("Authorize payment response"); let txn_id = utils::get_connector_transaction_id(authorize_response.response); @@ -95,7 +146,9 @@ async fn should_sync_authorized_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( txn_id.unwrap(), ), - ..Default::default() + encoded_data: None, + capture_method: None, + connector_meta: None, }), get_default_payment_info(), ) @@ -107,30 +160,59 @@ async fn should_sync_authorized_payment() { // Voids a payment using the manual capture flow (Non 3DS). #[actix_web::test] async fn should_void_authorized_payment() { + let authorize_response = CONNECTOR + .authorize_payment(get_payment_data(), get_default_payment_info()) + .await + .expect("Authorize payment response"); + let txn_id = utils::get_connector_transaction_id(authorize_response.response.clone()).unwrap(); + let connector_meta = utils::get_connector_metadata(authorize_response.response); let response = CONNECTOR - .authorize_and_void_payment( - payment_method_details(), + .void_payment( + txn_id, Some(types::PaymentsCancelData { connector_transaction_id: String::from(""), cancellation_reason: Some("requested_by_customer".to_string()), + connector_meta, ..Default::default() }), - get_default_payment_info(), + None, ) .await .expect("Void payment response"); - assert_eq!(response.status, enums::AttemptStatus::Voided); + //Forte doesnot send status in response, so setting it to pending so later it will be synced + assert_eq!(response.status, enums::AttemptStatus::Pending); } // Refunds a payment using the manual capture flow (Non 3DS). #[actix_web::test] +#[ignore = "Since Payment status is always in pending, cannot refund"] async fn should_refund_manually_captured_payment() { + let authorize_response = CONNECTOR + .authorize_payment(get_payment_data(), None) + .await + .expect("Authorize payment response"); + let txn_id = utils::get_connector_transaction_id(authorize_response.response.clone()).unwrap(); + let capture_connector_meta = utils::get_connector_metadata(authorize_response.response); + let capture_response = CONNECTOR + .capture_payment( + txn_id.clone(), + Some(types::PaymentsCaptureData { + connector_meta: capture_connector_meta, + ..utils::PaymentCaptureType::default().0 + }), + None, + ) + .await + .expect("Capture payment response"); + let refund_connector_metadata = utils::get_connector_metadata(capture_response.response); let response = CONNECTOR - .capture_payment_and_refund( - payment_method_details(), + .refund_payment( + txn_id, + Some(types::RefundsData { + connector_metadata: refund_connector_metadata, + ..utils::PaymentRefundType::default().0 + }), None, - None, - get_default_payment_info(), ) .await .unwrap(); @@ -142,16 +224,35 @@ async fn should_refund_manually_captured_payment() { // Partially refunds a payment using the manual capture flow (Non 3DS). #[actix_web::test] +#[ignore = "Since Payment status is always in pending, cannot refund"] async fn should_partially_refund_manually_captured_payment() { - let response = CONNECTOR - .capture_payment_and_refund( - payment_method_details(), + let authorize_response = CONNECTOR + .authorize_payment(get_payment_data(), None) + .await + .expect("Authorize payment response"); + let txn_id = utils::get_connector_transaction_id(authorize_response.response.clone()).unwrap(); + let capture_connector_meta = utils::get_connector_metadata(authorize_response.response); + let capture_response = CONNECTOR + .capture_payment( + txn_id.clone(), + Some(types::PaymentsCaptureData { + connector_meta: capture_connector_meta, + ..utils::PaymentCaptureType::default().0 + }), None, + ) + .await + .expect("Capture payment response"); + let refund_connector_metadata = utils::get_connector_metadata(capture_response.response); + let response = CONNECTOR + .refund_payment( + txn_id, Some(types::RefundsData { + connector_metadata: refund_connector_metadata, refund_amount: 50, ..utils::PaymentRefundType::default().0 }), - get_default_payment_info(), + None, ) .await .unwrap(); @@ -163,14 +264,10 @@ async fn should_partially_refund_manually_captured_payment() { // Synchronizes a refund using the manual capture flow (Non 3DS). #[actix_web::test] +#[ignore = "Since Payment status is always in pending, cannot refund"] async fn should_sync_manually_captured_refund() { let refund_response = CONNECTOR - .capture_payment_and_refund( - payment_method_details(), - None, - None, - get_default_payment_info(), - ) + .capture_payment_and_refund(None, None, None, get_default_payment_info()) .await .unwrap(); let response = CONNECTOR @@ -191,21 +288,22 @@ async fn should_sync_manually_captured_refund() { // Creates a payment using the automatic capture flow (Non 3DS). #[actix_web::test] async fn should_make_payment() { - let authorize_response = CONNECTOR - .make_payment(payment_method_details(), get_default_payment_info()) + let response = CONNECTOR + .make_payment(get_payment_data(), get_default_payment_info()) .await .unwrap(); - assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); + //Status of the Payments is always in Pending State, Forte has to settle the sandbox transaction manually + assert_eq!(response.status, enums::AttemptStatus::Pending); } // Synchronizes a payment using the automatic capture flow (Non 3DS). #[actix_web::test] async fn should_sync_auto_captured_payment() { let authorize_response = CONNECTOR - .make_payment(payment_method_details(), get_default_payment_info()) + .make_payment(get_payment_data(), get_default_payment_info()) .await .unwrap(); - assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); + assert_eq!(authorize_response.status, enums::AttemptStatus::Pending); let txn_id = utils::get_connector_transaction_id(authorize_response.response); assert_ne!(txn_id, None, "Empty connector transaction id"); let response = CONNECTOR @@ -215,37 +313,63 @@ async fn should_sync_auto_captured_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( txn_id.unwrap(), ), - capture_method: Some(enums::CaptureMethod::Automatic), + encoded_data: None, + capture_method: None, ..Default::default() }), get_default_payment_info(), ) .await .unwrap(); - assert_eq!(response.status, enums::AttemptStatus::Charged,); + //Status of the Payments is always in Pending State, Forte has to settle the sandbox transaction manually + assert_eq!(response.status, enums::AttemptStatus::Pending,); } // Refunds a payment using the automatic capture flow (Non 3DS). #[actix_web::test] +#[ignore = "Since Payment status is always in pending, cannot refund"] async fn should_refund_auto_captured_payment() { + let authorize_response = CONNECTOR + .make_payment(get_payment_data(), get_default_payment_info()) + .await + .unwrap(); + let txn_id = utils::get_connector_transaction_id(authorize_response.response.clone()).unwrap(); + tokio::time::sleep(Duration::from_secs(10)).await; + let refund_connector_metadata = utils::get_connector_metadata(authorize_response.response); let response = CONNECTOR - .make_payment_and_refund(payment_method_details(), None, get_default_payment_info()) + .refund_payment( + txn_id, + Some(types::RefundsData { + connector_metadata: refund_connector_metadata, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) .await .unwrap(); assert_eq!( response.response.unwrap().refund_status, - enums::RefundStatus::Success, + enums::RefundStatus::Pending, ); } // Partially refunds a payment using the automatic capture flow (Non 3DS). #[actix_web::test] +#[ignore = "Since Payment status is always in pending, cannot refund"] async fn should_partially_refund_succeeded_payment() { - let refund_response = CONNECTOR - .make_payment_and_refund( - payment_method_details(), + let authorize_response = CONNECTOR + .make_payment(get_payment_data(), get_default_payment_info()) + .await + .unwrap(); + let txn_id = utils::get_connector_transaction_id(authorize_response.response.clone()).unwrap(); + tokio::time::sleep(Duration::from_secs(10)).await; + let refund_connector_metadata = utils::get_connector_metadata(authorize_response.response); + let response = CONNECTOR + .refund_payment( + txn_id, Some(types::RefundsData { refund_amount: 50, + connector_metadata: refund_connector_metadata, ..utils::PaymentRefundType::default().0 }), get_default_payment_info(), @@ -253,31 +377,63 @@ async fn should_partially_refund_succeeded_payment() { .await .unwrap(); assert_eq!( - refund_response.response.unwrap().refund_status, - enums::RefundStatus::Success, + response.response.unwrap().refund_status, + enums::RefundStatus::Pending, ); } // Creates multiple refunds against a payment using the automatic capture flow (Non 3DS). #[actix_web::test] +#[ignore = "Since Payment status is always in pending, cannot refund"] 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; + let authorize_response = CONNECTOR + .make_payment(get_payment_data(), get_default_payment_info()) + .await + .unwrap(); + let txn_id = utils::get_connector_transaction_id(authorize_response.response.clone()).unwrap(); + tokio::time::sleep(Duration::from_secs(10)).await; + + let refund_connector_metadata = utils::get_connector_metadata(authorize_response.response); + for _x in 0..2 { + let refund_response = CONNECTOR + .refund_payment( + txn_id.clone(), + Some(types::RefundsData { + connector_metadata: refund_connector_metadata.clone(), + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + refund_response.response.unwrap().refund_status, + enums::RefundStatus::Pending, + ); + } } // Synchronizes a refund using the automatic capture flow (Non 3DS). #[actix_web::test] +#[ignore = "Since Payment status is always in pending, cannot refund"] async fn should_sync_refund() { + let authorize_response = CONNECTOR + .make_payment(get_payment_data(), get_default_payment_info()) + .await + .unwrap(); + let txn_id = utils::get_connector_transaction_id(authorize_response.response.clone()).unwrap(); + tokio::time::sleep(Duration::from_secs(10)).await; + let refund_connector_metadata = utils::get_connector_metadata(authorize_response.response); let refund_response = CONNECTOR - .make_payment_and_refund(payment_method_details(), None, get_default_payment_info()) + .refund_payment( + txn_id, + Some(types::RefundsData { + connector_metadata: refund_connector_metadata, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) .await .unwrap(); let response = CONNECTOR @@ -291,7 +447,7 @@ async fn should_sync_refund() { .unwrap(); assert_eq!( response.response.unwrap().refund_status, - enums::RefundStatus::Success, + enums::RefundStatus::Pending, ); } @@ -303,7 +459,7 @@ async fn should_fail_payment_for_incorrect_card_number() { .make_payment( Some(types::PaymentsAuthorizeData { payment_method_data: types::api::PaymentMethodData::Card(api::Card { - card_number: Secret::new("1234567891011".to_string()), + card_number: Secret::new("4111111111111100".to_string()), ..utils::CCardType::default().0 }), ..utils::PaymentAuthorizeType::default().0 @@ -314,7 +470,7 @@ async fn should_fail_payment_for_incorrect_card_number() { .unwrap(); assert_eq!( response.response.unwrap_err().message, - "Your card number is incorrect.".to_string(), + "INVALID CREDIT CARD NUMBER".to_string(), ); } @@ -332,13 +488,11 @@ async fn should_fail_payment_for_empty_card_number() { }), get_default_payment_info(), ) - .await - .unwrap(); - let x = response.response.unwrap_err(); + .await; assert_eq!( - x.message, - "You passed an empty string for 'payment_method_data[card][number]'.", - ); + *response.unwrap_err().current_context(), + router::core::errors::ConnectorError::NotImplemented("Card Type".into()) + ) } // Creates a payment with incorrect CVC. @@ -359,7 +513,7 @@ async fn should_fail_payment_for_incorrect_cvc() { .unwrap(); assert_eq!( response.response.unwrap_err().message, - "Your card's security code is invalid.".to_string(), + "INVALID CVV DATA".to_string(), ); } @@ -381,12 +535,13 @@ async fn should_fail_payment_for_invalid_exp_month() { .unwrap(); assert_eq!( response.response.unwrap_err().message, - "Your card's expiration month is invalid.".to_string(), + "INVALID EXPIRATION DATE".to_string(), ); } // Creates a payment with incorrect expiry year. #[actix_web::test] +#[ignore] async fn should_fail_payment_for_incorrect_expiry_year() { let response = CONNECTOR .make_payment( @@ -397,7 +552,7 @@ async fn should_fail_payment_for_incorrect_expiry_year() { }), ..utils::PaymentAuthorizeType::default().0 }), - get_default_payment_info(), + None, ) .await .unwrap(); @@ -411,46 +566,85 @@ async fn should_fail_payment_for_incorrect_expiry_year() { #[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()) + .authorize_payment(get_payment_data(), 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"); + .expect("Authorize payment response"); + let txn_id = utils::get_connector_transaction_id(authorize_response.response.clone()).unwrap(); + let capture_connector_meta = utils::get_connector_metadata(authorize_response.response); + let capture_response = CONNECTOR + .capture_payment( + txn_id, + Some(types::PaymentsCaptureData { + connector_meta: capture_connector_meta, + ..utils::PaymentCaptureType::default().0 + }), + get_default_payment_info(), + ) + .await + .expect("Capture payment response"); + let txn_id = utils::get_connector_transaction_id(capture_response.clone().response).unwrap(); + let connector_meta = utils::get_connector_metadata(capture_response.response); let void_response = CONNECTOR - .void_payment(txn_id.unwrap(), None, get_default_payment_info()) + .void_payment( + txn_id, + Some(types::PaymentsCancelData { + cancellation_reason: Some("requested_by_customer".to_string()), + connector_meta, + ..Default::default() + }), + get_default_payment_info(), + ) .await - .unwrap(); + .expect("Void payment response"); assert_eq!( void_response.response.unwrap_err().message, - "You cannot cancel this PaymentIntent because it has a status of succeeded." + "ORIG TRANS NOT FOUND" ); } // Captures a payment using invalid connector payment id. #[actix_web::test] async fn should_fail_capture_for_invalid_payment() { + let connector_meta = Some(serde_json::json!({ + "auth_id": "56YH8TZ", + })); let capture_response = CONNECTOR - .capture_payment("123456789".to_string(), None, get_default_payment_info()) + .capture_payment( + "123456789".to_string(), + Some(types::PaymentsCaptureData { + connector_meta, + ..utils::PaymentCaptureType::default().0 + }), + None, + ) .await .unwrap(); assert_eq!( capture_response.response.unwrap_err().message, - String::from("No such payment_intent: '123456789'") + "Error[1]: The value for field transaction_id is invalid. Check for possible formatting issues. Error[2]: The value for field transaction_id is invalid. Check for possible formatting issues.", ); } // Refunds a payment with refund amount higher than payment amount. #[actix_web::test] +#[ignore] async fn should_fail_for_refund_amount_higher_than_payment_amount() { + let authorize_response = CONNECTOR + .make_payment(get_payment_data(), None) + .await + .unwrap(); + let txn_id = utils::get_connector_transaction_id(authorize_response.response.clone()).unwrap(); + tokio::time::sleep(Duration::from_secs(10)).await; + let refund_connector_metadata = utils::get_connector_metadata(authorize_response.response); let response = CONNECTOR - .make_payment_and_refund( - payment_method_details(), + .refund_payment( + txn_id, Some(types::RefundsData { - refund_amount: 150, + refund_amount: 1500, + connector_metadata: refund_connector_metadata, ..utils::PaymentRefundType::default().0 }), - get_default_payment_info(), + None, ) .await .unwrap(); @@ -463,3 +657,27 @@ async fn should_fail_for_refund_amount_higher_than_payment_amount() { // Connector dependent test cases goes here // [#478]: add unit tests for non 3DS, wallets & webhooks in connector tests + +// Cards Negative scenerios +// Creates a payment with incorrect card issuer. + +#[actix_web::test] +async fn should_throw_not_implemented_for_unsupported_issuer() { + let authorize_data = Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_number: Secret::new(String::from("6759649826438453")), + ..utils::CCardType::default().0 + }), + capture_method: Some(enums::CaptureMethod::Automatic), + ..utils::PaymentAuthorizeType::default().0 + }); + let response = CONNECTOR.make_payment(authorize_data, None).await; + assert_eq!( + *response.unwrap_err().current_context(), + router::core::errors::ConnectorError::NotSupported { + message: "Maestro".to_string(), + connector: "Forte", + payment_experience: api::enums::PaymentExperience::RedirectToUrl.to_string(), + } + ) +} diff --git a/crates/router/tests/connectors/payeezy.rs b/crates/router/tests/connectors/payeezy.rs index 0f2ff7bb23..166457fe3c 100644 --- a/crates/router/tests/connectors/payeezy.rs +++ b/crates/router/tests/connectors/payeezy.rs @@ -376,7 +376,7 @@ async fn should_throw_not_implemented_for_unsupported_issuer() { assert_eq!( *response.unwrap_err().current_context(), errors::ConnectorError::NotSupported { - payment_method: "card".to_string(), + message: "card".to_string(), connector: "Payeezy", payment_experience: "RedirectToUrl".to_string(), } diff --git a/crates/router/tests/connectors/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index 5892e409c5..73311e860c 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -76,7 +76,10 @@ key1 = "key1" api_key = "API Key" [forte] -api_key="API Key" +api_key = "api_key" +key1 = "key1" +key2 = "key2" +api_secret = "api_secret" [coinbase] diff --git a/crates/router/tests/connectors/worldline.rs b/crates/router/tests/connectors/worldline.rs index a9c28b4502..305e17c40e 100644 --- a/crates/router/tests/connectors/worldline.rs +++ b/crates/router/tests/connectors/worldline.rs @@ -141,7 +141,7 @@ async fn should_throw_not_implemented_for_unsupported_issuer() { assert_eq!( *response.unwrap_err().current_context(), errors::ConnectorError::NotSupported { - payment_method: "card".to_string(), + message: "card".to_string(), connector: "worldline", payment_experience: "RedirectToUrl".to_string(), }