diff --git a/config/config.example.toml b/config/config.example.toml index 1ed850bcc3..c137bd270a 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -294,6 +294,7 @@ base_url = "" # Base url used when adding links that should redirect to self stripe = { long_lived_token = false, payment_method = "wallet", payment_method_type = { type = "disable_only", list = "google_pay" } } checkout = { long_lived_token = false, payment_method = "wallet" } mollie = {long_lived_token = false, payment_method = "card"} +stax = { long_lived_token = true, payment_method = "card" } [dummy_connector] payment_ttl = 172800 # Time to live for dummy connector payment in redis @@ -344,6 +345,14 @@ pix = { country = "BR", currency = "BRL" } red_compra = { country = "CL", currency = "CLP" } red_pagos = { country = "UY", currency = "UYU" } +[pm_filters.stax] +credit = { currency = "USD" } +debit = { currency = "USD" } +ach = { currency = "USD" } + +[connector_customer] +connector_list = "stax" + [bank_config.online_banking_fpx] adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,may_bank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" diff --git a/config/development.toml b/config/development.toml index a7807ad529..65b5946630 100644 --- a/config/development.toml +++ b/config/development.toml @@ -298,6 +298,11 @@ debit = { not_available_flows = { capture_method = "manual" } } credit = { not_available_flows = { capture_method = "manual" } } debit = { not_available_flows = { capture_method = "manual" } } +[pm_filters.stax] +credit = { currency = "USD" } +debit = { currency = "USD" } +ach = { currency = "USD" } + [pm_filters.trustpay] credit = { not_available_flows = { capture_method = "manual" } } debit = { not_available_flows = { capture_method = "manual" } } @@ -321,10 +326,11 @@ debit = { currency = "USD" } [tokenization] stripe = { long_lived_token = false, payment_method = "wallet", payment_method_type = { type = "disable_only", list = "google_pay" } } checkout = { long_lived_token = false, payment_method = "wallet" } +stax = { long_lived_token = true, payment_method = "card" } mollie = {long_lived_token = false, payment_method = "card"} [connector_customer] -connector_list = "bluesnap,stripe" +connector_list = "bluesnap,stax,stripe" payout_connector_list = "wise" [dummy_connector] diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 806d5c3d1e..730ac852b8 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -190,6 +190,7 @@ consumer_group = "SCHEDULER_GROUP" stripe = { long_lived_token = false, payment_method = "wallet", payment_method_type = { type = "disable_only", list = "google_pay" } } checkout = { long_lived_token = false, payment_method = "wallet" } mollie = {long_lived_token = false, payment_method = "card"} +stax = { long_lived_token = true, payment_method = "card" } [dummy_connector] payment_ttl = 172800 @@ -226,6 +227,11 @@ pix = { country = "BR", currency = "BRL" } red_compra = { country = "CL", currency = "CLP" } red_pagos = { country = "UY", currency = "UYU" } +[pm_filters.stax] +credit = { currency = "USD" } +debit = { currency = "USD" } +ach = { currency = "USD" } + [bank_config.online_banking_fpx] adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,may_bank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" @@ -239,3 +245,6 @@ wallet.apple_pay = {connector_list = "stripe,adyen"} wallet.paypal = {connector_list = "adyen"} card.credit = {connector_list = "stripe,adyen,authorizedotnet,globalpay,worldpay,multisafepay,nmi,nexinets,noon"} card.debit = {connector_list = "stripe,adyen,authorizedotnet,globalpay,worldpay,multisafepay,nmi,nexinets,noon"} + +[connector_customer] +connector_list = "stax" diff --git a/crates/router/src/connector/stax.rs b/crates/router/src/connector/stax.rs index 460539930b..002058bb8e 100644 --- a/crates/router/src/connector/stax.rs +++ b/crates/router/src/connector/stax.rs @@ -3,10 +3,13 @@ mod transformers; use std::fmt::Debug; use error_stack::{IntoReport, ResultExt}; +use masking::PeekInterface; use transformers as stax; +use super::utils::{to_connector_meta, RefundsRequestData}; use crate::{ configs::settings, + consts, core::errors::{self, CustomResult}, headers, services::{ @@ -38,16 +41,6 @@ impl api::RefundExecute for Stax {} impl api::RefundSync for Stax {} impl api::PaymentToken for Stax {} -impl - ConnectorIntegration< - api::PaymentMethodToken, - types::PaymentMethodTokenizationData, - types::PaymentsResponseData, - > for Stax -{ - // Not Implemented (R) -} - impl ConnectorCommonExt for Stax where Self: ConnectorIntegration, @@ -95,9 +88,10 @@ impl ConnectorCommon for Stax { ) -> CustomResult)>, errors::ConnectorError> { let auth = stax::StaxAuthType::try_from(auth_type) .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + Ok(vec![( headers::AUTHORIZATION.to_string(), - auth.api_key.into_masked(), + format!("Bearer {}", auth.api_key.peek()).into_masked(), )]) } @@ -105,20 +99,196 @@ impl ConnectorCommon for Stax { &self, res: Response, ) -> CustomResult { - let response: stax::StaxErrorResponse = res - .response - .parse_struct("StaxErrorResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - Ok(ErrorResponse { status_code: res.status_code, - code: response.code, - message: response.message, - reason: response.reason, + code: consts::NO_ERROR_CODE.to_string(), + message: consts::NO_ERROR_MESSAGE.to_string(), + reason: Some( + std::str::from_utf8(&res.response) + .into_report() + .change_context(errors::ConnectorError::ResponseDeserializationFailed)? + .to_owned(), + ), }) } } +impl api::ConnectorCustomer for Stax {} + +impl + ConnectorIntegration< + api::CreateConnectorCustomer, + types::ConnectorCustomerData, + types::PaymentsResponseData, + > for Stax +{ + fn get_headers( + &self, + req: &types::ConnectorCustomerRouterData, + 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::ConnectorCustomerRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}customer", self.base_url(connectors),)) + } + + fn get_request_body( + &self, + req: &types::ConnectorCustomerRouterData, + ) -> CustomResult, errors::ConnectorError> { + let connector_request = stax::StaxCustomerRequest::try_from(req)?; + + let stax_req = types::RequestBody::log_and_get_request_body( + &connector_request, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(stax_req)) + } + + fn build_request( + &self, + req: &types::ConnectorCustomerRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::ConnectorCustomerType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::ConnectorCustomerType::get_headers( + self, req, connectors, + )?) + .body(types::ConnectorCustomerType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::ConnectorCustomerRouterData, + res: Response, + ) -> CustomResult + where + types::PaymentsResponseData: Clone, + { + let response: stax::StaxCustomerResponse = res + .response + .parse_struct("StaxCustomerResponse") + .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< + api::PaymentMethodToken, + types::PaymentMethodTokenizationData, + types::PaymentsResponseData, + > for Stax +{ + fn get_headers( + &self, + req: &types::TokenizationRouterData, + 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::TokenizationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}payment-method/", self.base_url(connectors))) + } + + fn get_request_body( + &self, + req: &types::TokenizationRouterData, + ) -> CustomResult, errors::ConnectorError> { + let connector_request = stax::StaxTokenRequest::try_from(req)?; + + let stax_req = types::RequestBody::log_and_get_request_body( + &connector_request, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(stax_req)) + } + + fn build_request( + &self, + req: &types::TokenizationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::TokenizationType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::TokenizationType::get_headers(self, req, connectors)?) + .body(types::TokenizationType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::TokenizationRouterData, + res: Response, + ) -> CustomResult + where + types::PaymentsResponseData: Clone, + { + let response: stax::StaxTokenResponse = res + .response + .parse_struct("StaxTokenResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + impl ConnectorIntegration for Stax { @@ -153,9 +323,9 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!("{}charge", self.base_url(connectors),)) } fn get_request_body( @@ -163,6 +333,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = stax::StaxPaymentsRequest::try_from(req)?; + let stax_req = types::RequestBody::log_and_get_request_body( &req_obj, utils::Encode::::encode_to_string_of_json, @@ -198,7 +369,7 @@ impl ConnectorIntegration CustomResult { let response: stax::StaxPaymentsResponse = res .response - .parse_struct("Stax PaymentsAuthorizeResponse") + .parse_struct("StaxPaymentsAuthorizeResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, @@ -232,10 +403,19 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let connector_payment_id = req + .request + .connector_transaction_id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; + + Ok(format!( + "{}/transaction/{connector_payment_id}", + self.base_url(connectors), + )) } fn build_request( @@ -260,7 +440,7 @@ impl ConnectorIntegration CustomResult { let response: stax::StaxPaymentsResponse = res .response - .parse_struct("stax PaymentsSyncResponse") + .parse_struct("StaxPaymentsSyncResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, @@ -294,17 +474,27 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!( + "{}/transaction/{}/capture", + self.base_url(connectors), + req.request.connector_transaction_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 = stax::StaxCaptureRequest::try_from(req)?; + let stax_req = types::RequestBody::log_and_get_request_body( + &connector_req, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(stax_req)) } fn build_request( @@ -332,7 +522,7 @@ impl ConnectorIntegration CustomResult { let response: stax::StaxPaymentsResponse = res .response - .parse_struct("Stax PaymentsCaptureResponse") + .parse_struct("StaxPaymentsCaptureResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, @@ -352,6 +542,67 @@ impl ConnectorIntegration for Stax { + 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 { + Ok(format!( + "{}/transaction/{}/void-or-refund", + self.base_url(connectors), + req.request.connector_transaction_id, + )) + } + + fn build_request( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsCancelRouterData, + res: Response, + ) -> CustomResult { + let response: stax::StaxPaymentsResponse = res + .response + .parse_struct("StaxPaymentsVoidResponse") + .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 Stax { @@ -369,10 +620,22 @@ impl ConnectorIntegration, - _connectors: &settings::Connectors, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let connector_transaction_id = if req.request.connector_metadata.is_some() { + let stax_capture: stax::StaxMetaData = + to_connector_meta(req.request.connector_metadata.clone())?; + stax_capture.capture_id + } else { + req.request.connector_transaction_id.clone() + }; + + Ok(format!( + "{}/transaction/{}/refund", + self.base_url(connectors), + connector_transaction_id, + )) } fn get_request_body( @@ -412,7 +675,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let response: stax::RefundResponse = res .response - .parse_struct("stax RefundResponse") + .parse_struct("StaxRefundResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, @@ -444,10 +707,14 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!( + "{}/transaction/{}", + self.base_url(connectors), + req.request.get_connector_refund_id()?, + )) } fn build_request( @@ -461,7 +728,6 @@ impl ConnectorIntegration CustomResult { - let response: stax::RefundResponse = - res.response - .parse_struct("stax RefundSyncResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let response: stax::RefundResponse = res + .response + .parse_struct("StaxRefundSyncResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, data: data.clone(), diff --git a/crates/router/src/connector/stax/transformers.rs b/crates/router/src/connector/stax/transformers.rs index c7bec9cd27..83ddf72f39 100644 --- a/crates/router/src/connector/stax/transformers.rs +++ b/crates/router/src/connector/stax/transformers.rs @@ -1,45 +1,40 @@ -use masking::Secret; +use common_utils::pii::Email; +use error_stack::IntoReport; +use masking::{ExposeInterface, Secret}; use serde::{Deserialize, Serialize}; use crate::{ - connector::utils::PaymentsAuthorizeRequestData, + connector::utils::{CardData, PaymentsAuthorizeRequestData, RouterData}, core::errors, types::{self, api, storage::enums}, }; -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct StaxPaymentsRequest { - amount: i64, - card: StaxCard, +#[derive(Debug, Serialize)] +pub struct StaxPaymentsRequestMetaData { + tax: i64, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct StaxCard { - name: Secret, - number: cards::CardNumber, - expiry_month: Secret, - expiry_year: Secret, - cvc: Secret, - complete: bool, +#[derive(Debug, Serialize)] +pub struct StaxPaymentsRequest { + payment_method_id: Secret, + total: i64, + is_refundable: bool, + pre_auth: bool, + meta: StaxPaymentsRequestMetaData, } impl TryFrom<&types::PaymentsAuthorizeRouterData> for StaxPaymentsRequest { 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 = StaxCard { - 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()?, - }; + api::PaymentMethodData::Card(_) => { + let pre_auth = !item.request.is_auto_capture()?; Ok(Self { - amount: item.request.amount, - card, + meta: StaxPaymentsRequestMetaData { tax: 0 }, + total: item.request.amount, + is_refundable: true, + pre_auth, + payment_method_id: Secret::new(item.get_payment_method_token()?), }) } _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), @@ -47,7 +42,6 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for StaxPaymentsRequest { } } -//TODO: Fill the struct with respective fields // Auth Struct pub struct StaxAuthType { pub(super) api_key: Secret, @@ -64,34 +58,153 @@ impl TryFrom<&types::ConnectorAuthType> for StaxAuthType { } } } -// PaymentsResponse -//TODO: Append the remaining status flags -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum StaxPaymentStatus { - Succeeded, - Failed, - #[default] - Processing, + +#[derive(Debug, Serialize)] +pub struct StaxCustomerRequest { + #[serde(skip_serializing_if = "Option::is_none")] + email: Option, + #[serde(skip_serializing_if = "Option::is_none")] + firstname: Option, } -impl From for enums::AttemptStatus { - fn from(item: StaxPaymentStatus) -> Self { - match item { - StaxPaymentStatus::Succeeded => Self::Charged, - StaxPaymentStatus::Failed => Self::Failure, - StaxPaymentStatus::Processing => Self::Authorizing, +impl TryFrom<&types::ConnectorCustomerRouterData> for StaxCustomerRequest { + type Error = error_stack::Report; + fn try_from(item: &types::ConnectorCustomerRouterData) -> Result { + if item.request.email.is_none() && item.request.name.is_none() { + Err(errors::ConnectorError::MissingRequiredField { + field_name: "email or name", + }) + .into_report() + } else { + Ok(Self { + email: item.request.email.to_owned(), + firstname: item.request.name.to_owned(), + }) } } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct StaxPaymentsResponse { - status: StaxPaymentStatus, +#[derive(Debug, Deserialize)] +pub struct StaxCustomerResponse { + id: Secret, +} + +impl + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(types::PaymentsResponseData::ConnectorCustomerResponse { + connector_customer_id: item.response.id.expose(), + }), + ..item.data + }) + } +} + +#[derive(Debug, Serialize)] +pub struct StaxTokenizeData { + person_name: Secret, + card_number: cards::CardNumber, + card_exp: Secret, + card_cvv: Secret, + customer_id: Secret, +} + +#[derive(Debug, Serialize)] +#[serde(tag = "method")] +#[serde(rename_all = "lowercase")] +pub enum StaxTokenRequest { + Card(StaxTokenizeData), +} + +impl TryFrom<&types::TokenizationRouterData> for StaxTokenRequest { + type Error = error_stack::Report; + fn try_from(item: &types::TokenizationRouterData) -> Result { + let customer_id = item.get_connector_customer_id()?; + match item.request.payment_method_data.clone() { + api::PaymentMethodData::Card(card_data) => { + let stax_card_data = StaxTokenizeData { + card_exp: card_data + .get_card_expiry_month_year_2_digit_with_delimiter("".to_string()), + person_name: card_data.card_holder_name, + card_number: card_data.card_number, + card_cvv: card_data.card_cvc, + customer_id: Secret::new(customer_id), + }; + Ok(Self::Card(stax_card_data)) + } + api::PaymentMethodData::BankDebit(_) + | api::PaymentMethodData::Wallet(_) + | api::PaymentMethodData::PayLater(_) + | api::PaymentMethodData::BankRedirect(_) + | api::PaymentMethodData::BankTransfer(_) + | api::PaymentMethodData::Crypto(_) + | api::PaymentMethodData::MandatePayment + | api::PaymentMethodData::Reward(_) + | api::PaymentMethodData::Voucher(_) + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::Upi(_) => Err(errors::ConnectorError::NotImplemented( + "Payment Method".to_string(), + )) + .into_report(), + } + } +} + +#[derive(Debug, Deserialize)] +pub struct StaxTokenResponse { + id: Secret, +} + +impl TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(types::PaymentsResponseData::TokenizationResponse { + token: item.response.id.expose(), + }), + ..item.data + }) + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum StaxPaymentResponseTypes { + Charge, + PreAuth, +} + +#[derive(Debug, Deserialize)] +pub struct StaxChildCapture { id: String, } +#[derive(Debug, Deserialize)] +pub struct StaxPaymentsResponse { + success: bool, + id: String, + is_captured: i8, + is_voided: bool, + child_captures: Vec, + #[serde(rename = "type")] + payment_response_type: StaxPaymentResponseTypes, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct StaxMetaData { + pub capture_id: String, +} + impl TryFrom> for types::RouterData @@ -100,13 +213,36 @@ impl fn try_from( item: types::ResponseRouterData, ) -> Result { + let mut connector_metadata = None; + let mut status = match item.response.success { + true => match item.response.payment_response_type { + StaxPaymentResponseTypes::Charge => enums::AttemptStatus::Charged, + StaxPaymentResponseTypes::PreAuth => match item.response.is_captured { + 0 => enums::AttemptStatus::Authorized, + _ => { + connector_metadata = + item.response.child_captures.first().map(|child_captures| { + serde_json::json!(StaxMetaData { + capture_id: child_captures.id.clone() + }) + }); + enums::AttemptStatus::Charged + } + }, + }, + false => enums::AttemptStatus::Failure, + }; + if item.response.is_voided { + status = enums::AttemptStatus::Voided; + } + Ok(Self { - status: enums::AttemptStatus::from(item.response.status), + status, response: Ok(types::PaymentsResponseData::TransactionResponse { resource_id: types::ResponseId::ConnectorTransactionId(item.response.id), redirection_data: None, mandate_reference: None, - connector_metadata: None, + connector_metadata, network_txn_id: None, connector_response_reference_id: None, }), @@ -115,50 +251,48 @@ impl } } -//TODO: Fill the struct with respective fields +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct StaxCaptureRequest { + total: Option, +} + +impl TryFrom<&types::PaymentsCaptureRouterData> for StaxCaptureRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsCaptureRouterData) -> Result { + let total = item.request.amount_to_capture; + Ok(Self { total: Some(total) }) + } +} + // REFUND : // Type definition for RefundRequest -#[derive(Default, Debug, Serialize)] +#[derive(Debug, Serialize)] pub struct StaxRefundRequest { - pub amount: i64, + pub total: i64, } impl TryFrom<&types::RefundsRouterData> for StaxRefundRequest { type Error = error_stack::Report; fn try_from(item: &types::RefundsRouterData) -> Result { Ok(Self { - amount: item.request.refund_amount, + total: item.request.refund_amount, }) } } -// Type definition for Refund Response - -#[allow(dead_code)] -#[derive(Debug, Serialize, Default, Deserialize, Clone)] -pub enum RefundStatus { - Succeeded, - Failed, - #[default] - Processing, +#[derive(Debug, Deserialize)] +pub struct ChildTransactionsInResponse { + id: String, + success: bool, + created_at: String, + total: i64, } - -impl From for enums::RefundStatus { - fn from(item: RefundStatus) -> Self { - match item { - RefundStatus::Succeeded => Self::Success, - RefundStatus::Failed => Self::Failure, - RefundStatus::Processing => Self::Pending, - //TODO: Review mapping - } - } -} - -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Deserialize)] pub struct RefundResponse { id: String, - status: RefundStatus, + success: bool, + child_transactions: Vec, } impl TryFrom> @@ -168,10 +302,32 @@ impl TryFrom> fn try_from( item: types::RefundsResponseRouterData, ) -> Result { + let filtered_txn: Vec<&ChildTransactionsInResponse> = item + .response + .child_transactions + .iter() + .filter(|txn| txn.total == item.data.request.refund_amount) + .collect(); + + let mut refund_txn = filtered_txn + .first() + .ok_or(errors::ConnectorError::ResponseHandlingFailed)?; + + for child in filtered_txn.iter() { + if child.created_at > refund_txn.created_at { + refund_txn = child; + } + } + + let refund_status = match refund_txn.success { + true => enums::RefundStatus::Success, + false => enums::RefundStatus::Failure, + }; + 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: refund_txn.id.clone(), + refund_status, }), ..item.data }) @@ -185,21 +341,16 @@ impl TryFrom> fn try_from( item: types::RefundsResponseRouterData, ) -> Result { + let refund_status = match item.response.success { + true => enums::RefundStatus::Success, + false => enums::RefundStatus::Failure, + }; 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.id, + refund_status, }), ..item.data }) } } - -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct StaxErrorResponse { - pub status_code: u16, - pub code: String, - pub message: String, - pub reason: Option, -} diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 66f35fdc99..feaba9a708 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -137,7 +137,6 @@ impl } default_imp_for_complete_authorize!( - connector::Stax, connector::Aci, connector::Adyen, connector::Bitpay, @@ -164,6 +163,7 @@ default_imp_for_complete_authorize!( connector::Payme, connector::Payu, connector::Rapyd, + connector::Stax, connector::Stripe, connector::Trustpay, connector::Tsys, @@ -201,7 +201,6 @@ impl } default_imp_for_create_customer!( - connector::Stax, connector::Aci, connector::Adyen, connector::Airwallex, @@ -275,7 +274,6 @@ impl services::ConnectorRedirectResponse for connector::DummyConnec } default_imp_for_connector_redirect_response!( - connector::Stax, connector::Aci, connector::Adyen, connector::Bitpay, @@ -302,6 +300,7 @@ default_imp_for_connector_redirect_response!( connector::Powertranz, connector::Rapyd, connector::Shift4, + connector::Stax, connector::Tsys, connector::Wise, connector::Worldline, @@ -320,7 +319,6 @@ macro_rules! default_imp_for_connector_request_id { impl api::ConnectorTransactionId for connector::DummyConnector {} default_imp_for_connector_request_id!( - connector::Stax, connector::Aci, connector::Adyen, connector::Airwallex, @@ -355,6 +353,7 @@ default_imp_for_connector_request_id!( connector::Powertranz, connector::Rapyd, connector::Shift4, + connector::Stax, connector::Stripe, connector::Trustpay, connector::Tsys, @@ -395,7 +394,6 @@ impl } default_imp_for_accept_dispute!( - connector::Stax, connector::Aci, connector::Adyen, connector::Airwallex, @@ -430,6 +428,7 @@ default_imp_for_accept_dispute!( connector::Powertranz, connector::Rapyd, connector::Shift4, + connector::Stax, connector::Stripe, connector::Trustpay, connector::Tsys, @@ -490,7 +489,6 @@ impl } default_imp_for_file_upload!( - connector::Stax, connector::Aci, connector::Adyen, connector::Airwallex, @@ -525,6 +523,7 @@ default_imp_for_file_upload!( connector::Powertranz, connector::Rapyd, connector::Shift4, + connector::Stax, connector::Trustpay, connector::Tsys, connector::Opennode, @@ -562,7 +561,6 @@ impl } default_imp_for_submit_evidence!( - connector::Stax, connector::Aci, connector::Adyen, connector::Airwallex, @@ -597,6 +595,7 @@ default_imp_for_submit_evidence!( connector::Powertranz, connector::Rapyd, connector::Shift4, + connector::Stax, connector::Trustpay, connector::Tsys, connector::Opennode, @@ -634,7 +633,6 @@ impl } default_imp_for_defend_dispute!( - connector::Stax, connector::Aci, connector::Adyen, connector::Airwallex, @@ -668,8 +666,9 @@ default_imp_for_defend_dispute!( connector::Payu, connector::Powertranz, connector::Rapyd, - connector::Stripe, connector::Shift4, + connector::Stax, + connector::Stripe, connector::Trustpay, connector::Tsys, connector::Opennode, @@ -707,7 +706,6 @@ impl } default_imp_for_pre_processing_steps!( - connector::Stax, connector::Aci, connector::Adyen, connector::Airwallex, @@ -744,6 +742,7 @@ default_imp_for_pre_processing_steps!( connector::Powertranz, connector::Rapyd, connector::Shift4, + connector::Stax, connector::Tsys, connector::Wise, connector::Worldline, diff --git a/crates/router/tests/connectors/payme.rs b/crates/router/tests/connectors/payme.rs index 91e9e0b881..ad3b7d8cf7 100644 --- a/crates/router/tests/connectors/payme.rs +++ b/crates/router/tests/connectors/payme.rs @@ -61,6 +61,8 @@ fn get_default_payment_info() -> Option { access_token: None, connector_meta_data: None, return_url: None, + connector_customer: None, + payment_method_token: None, country: None, currency: None, payout_method_data: None, diff --git a/crates/router/tests/connectors/stax.rs b/crates/router/tests/connectors/stax.rs index e12c6cf737..ebf57d53bb 100644 --- a/crates/router/tests/connectors/stax.rs +++ b/crates/router/tests/connectors/stax.rs @@ -1,5 +1,7 @@ +use std::{str::FromStr, time::Duration}; + use masking::Secret; -use router::types::{self, api, storage::enums}; +use router::types::{self, api, storage::enums, PaymentsResponseData}; use test_utils::connector_auth; use crate::utils::{self, ConnectorActions}; @@ -32,12 +34,72 @@ impl utils::Connector for StaxTest { static CONNECTOR: StaxTest = StaxTest {}; -fn get_default_payment_info() -> Option { - None +fn get_default_payment_info( + connector_customer: Option, + payment_method_token: Option, +) -> Option { + Some(utils::PaymentInfo { + address: None, + auth_type: None, + access_token: None, + connector_meta_data: None, + return_url: None, + connector_customer, + payment_method_token, + payout_method_data: None, + currency: None, + country: None, + }) +} + +fn customer_details() -> Option { + Some(types::ConnectorCustomerData { + ..utils::CustomerType::default().0 + }) +} + +fn token_details() -> Option { + Some(types::PaymentMethodTokenizationData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_number: cards::CardNumber::from_str("4111111111111111").unwrap(), + card_exp_month: Secret::new("04".to_string()), + card_exp_year: Secret::new("2027".to_string()), + card_cvc: Secret::new("123".to_string()), + ..utils::CCardType::default().0 + }), + browser_info: None, + }) } fn payment_method_details() -> Option { - None + Some(types::PaymentsAuthorizeData { + ..utils::PaymentAuthorizeType::default().0 + }) +} + +async fn create_customer_and_get_token() -> Option { + let customer_response = CONNECTOR + .create_connector_customer(customer_details(), get_default_payment_info(None, None)) + .await + .expect("Authorize payment response"); + let connector_customer_id = match customer_response.response.unwrap() { + PaymentsResponseData::ConnectorCustomerResponse { + connector_customer_id, + } => Some(connector_customer_id), + _ => None, + }; + + let token_response = CONNECTOR + .create_connector_pm_token( + token_details(), + get_default_payment_info(connector_customer_id, None), + ) + .await + .expect("Authorize payment response"); + match token_response.response.unwrap() { + PaymentsResponseData::TokenizationResponse { token } => Some(token), + _ => None, + } } // Cards Positive Tests @@ -45,7 +107,10 @@ 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( + payment_method_details(), + get_default_payment_info(None, create_customer_and_get_token().await), + ) .await .expect("Authorize payment response"); assert_eq!(response.status, enums::AttemptStatus::Authorized); @@ -55,7 +120,11 @@ async fn should_only_authorize_payment() { #[actix_web::test] async fn should_capture_authorized_payment() { let response = CONNECTOR - .authorize_and_capture_payment(payment_method_details(), None, get_default_payment_info()) + .authorize_and_capture_payment( + payment_method_details(), + None, + get_default_payment_info(None, create_customer_and_get_token().await), + ) .await .expect("Capture payment response"); assert_eq!(response.status, enums::AttemptStatus::Charged); @@ -71,7 +140,7 @@ async fn should_partially_capture_authorized_payment() { amount_to_capture: 50, ..utils::PaymentCaptureType::default().0 }), - get_default_payment_info(), + get_default_payment_info(None, create_customer_and_get_token().await), ) .await .expect("Capture payment response"); @@ -82,7 +151,10 @@ async fn should_partially_capture_authorized_payment() { #[actix_web::test] async fn should_sync_authorized_payment() { let authorize_response = CONNECTOR - .authorize_payment(payment_method_details(), get_default_payment_info()) + .authorize_payment( + payment_method_details(), + get_default_payment_info(None, create_customer_and_get_token().await), + ) .await .expect("Authorize payment response"); let txn_id = utils::get_connector_transaction_id(authorize_response.response); @@ -95,7 +167,7 @@ async fn should_sync_authorized_payment() { ), ..Default::default() }), - get_default_payment_info(), + get_default_payment_info(None, None), ) .await .expect("PSync response"); @@ -113,7 +185,7 @@ async fn should_void_authorized_payment() { cancellation_reason: Some("requested_by_customer".to_string()), ..Default::default() }), - get_default_payment_info(), + get_default_payment_info(None, create_customer_and_get_token().await), ) .await .expect("Void payment response"); @@ -123,15 +195,33 @@ async fn should_void_authorized_payment() { // Refunds a payment using the manual capture flow (Non 3DS). #[actix_web::test] async fn should_refund_manually_captured_payment() { - let response = CONNECTOR - .capture_payment_and_refund( + let capture_response = CONNECTOR + .authorize_and_capture_payment( payment_method_details(), - None, - None, - get_default_payment_info(), + Some(types::PaymentsCaptureData { + ..utils::PaymentCaptureType::default().0 + }), + get_default_payment_info(None, create_customer_and_get_token().await), + ) + .await + .expect("Capture payment response"); + + let refund_txn_id = + utils::get_connector_transaction_id(capture_response.response.clone()).unwrap(); + let refund_connector_meta = utils::get_connector_metadata(capture_response.response); + + let response = CONNECTOR + .refund_payment( + refund_txn_id, + Some(types::RefundsData { + connector_metadata: refund_connector_meta, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(None, None), ) .await .unwrap(); + assert_eq!( response.response.unwrap().refund_status, enums::RefundStatus::Success, @@ -141,15 +231,30 @@ async fn should_refund_manually_captured_payment() { // Partially refunds a payment using the manual capture flow (Non 3DS). #[actix_web::test] async fn should_partially_refund_manually_captured_payment() { - let response = CONNECTOR - .capture_payment_and_refund( + let capture_response = CONNECTOR + .authorize_and_capture_payment( payment_method_details(), - None, + Some(types::PaymentsCaptureData { + ..utils::PaymentCaptureType::default().0 + }), + get_default_payment_info(None, create_customer_and_get_token().await), + ) + .await + .expect("Capture payment response"); + + let refund_txn_id = + utils::get_connector_transaction_id(capture_response.response.clone()).unwrap(); + let refund_connector_meta = utils::get_connector_metadata(capture_response.response); + + let response = CONNECTOR + .refund_payment( + refund_txn_id, Some(types::RefundsData { refund_amount: 50, + connector_metadata: refund_connector_meta, ..utils::PaymentRefundType::default().0 }), - get_default_payment_info(), + get_default_payment_info(None, None), ) .await .unwrap(); @@ -162,21 +267,39 @@ async fn should_partially_refund_manually_captured_payment() { // Synchronizes a refund using the manual capture flow (Non 3DS). #[actix_web::test] async fn should_sync_manually_captured_refund() { - let refund_response = CONNECTOR - .capture_payment_and_refund( + let capture_response = CONNECTOR + .authorize_and_capture_payment( payment_method_details(), - None, - None, - get_default_payment_info(), + Some(types::PaymentsCaptureData { + ..utils::PaymentCaptureType::default().0 + }), + get_default_payment_info(None, create_customer_and_get_token().await), + ) + .await + .expect("Capture payment response"); + + let refund_txn_id = + utils::get_connector_transaction_id(capture_response.response.clone()).unwrap(); + let refund_connector_meta = utils::get_connector_metadata(capture_response.response); + + let refund_response = CONNECTOR + .refund_payment( + refund_txn_id, + Some(types::RefundsData { + connector_metadata: refund_connector_meta, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(None, None), ) .await .unwrap(); + let response = CONNECTOR .rsync_retry_till_status_matches( enums::RefundStatus::Success, refund_response.response.unwrap().connector_refund_id, None, - get_default_payment_info(), + get_default_payment_info(None, None), ) .await .unwrap(); @@ -190,7 +313,10 @@ async fn should_sync_manually_captured_refund() { #[actix_web::test] async fn should_make_payment() { let authorize_response = CONNECTOR - .make_payment(payment_method_details(), get_default_payment_info()) + .make_payment( + payment_method_details(), + get_default_payment_info(None, create_customer_and_get_token().await), + ) .await .unwrap(); assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); @@ -200,7 +326,10 @@ async fn should_make_payment() { #[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( + payment_method_details(), + get_default_payment_info(None, create_customer_and_get_token().await), + ) .await .unwrap(); assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); @@ -216,7 +345,7 @@ async fn should_sync_auto_captured_payment() { capture_method: Some(enums::CaptureMethod::Automatic), ..Default::default() }), - get_default_payment_info(), + get_default_payment_info(None, None), ) .await .unwrap(); @@ -227,7 +356,11 @@ async fn should_sync_auto_captured_payment() { #[actix_web::test] async fn should_refund_auto_captured_payment() { let response = CONNECTOR - .make_payment_and_refund(payment_method_details(), None, get_default_payment_info()) + .make_payment_and_refund( + payment_method_details(), + None, + get_default_payment_info(None, create_customer_and_get_token().await), + ) .await .unwrap(); assert_eq!( @@ -246,7 +379,7 @@ async fn should_partially_refund_succeeded_payment() { refund_amount: 50, ..utils::PaymentRefundType::default().0 }), - get_default_payment_info(), + get_default_payment_info(None, create_customer_and_get_token().await), ) .await .unwrap(); @@ -259,23 +392,47 @@ async fn should_partially_refund_succeeded_payment() { // Creates multiple refunds against a payment using the automatic capture flow (Non 3DS). #[actix_web::test] async fn should_refund_succeeded_payment_multiple_times() { - CONNECTOR - .make_payment_and_multiple_refund( + let payment_method_token = create_customer_and_get_token().await; + + let response = CONNECTOR + .make_payment( payment_method_details(), - Some(types::RefundsData { - refund_amount: 50, - ..utils::PaymentRefundType::default().0 - }), - get_default_payment_info(), + get_default_payment_info(None, payment_method_token.clone()), ) - .await; + .await + .unwrap(); + + //try refund for previous payment + let transaction_id = utils::get_connector_transaction_id(response.response).unwrap(); + for _x in 0..2 { + tokio::time::sleep(Duration::from_secs(60)).await; // to avoid 404 error + let refund_response = CONNECTOR + .refund_payment( + transaction_id.clone(), + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(None, payment_method_token.clone()), + ) + .await + .unwrap(); + assert_eq!( + refund_response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); + } } // Synchronizes a refund using the automatic capture flow (Non 3DS). #[actix_web::test] async fn should_sync_refund() { let refund_response = CONNECTOR - .make_payment_and_refund(payment_method_details(), None, get_default_payment_info()) + .make_payment_and_refund( + payment_method_details(), + None, + get_default_payment_info(None, create_customer_and_get_token().await), + ) .await .unwrap(); let response = CONNECTOR @@ -283,7 +440,7 @@ async fn should_sync_refund() { enums::RefundStatus::Success, refund_response.response.unwrap().connector_refund_id, None, - get_default_payment_info(), + get_default_payment_info(None, None), ) .await .unwrap(); @@ -297,81 +454,127 @@ async fn should_sync_refund() { // Creates a payment with incorrect CVC. #[actix_web::test] async fn should_fail_payment_for_incorrect_cvc() { - let response = CONNECTOR - .make_payment( - Some(types::PaymentsAuthorizeData { + let customer_response = CONNECTOR + .create_connector_customer(customer_details(), get_default_payment_info(None, None)) + .await + .expect("Authorize payment response"); + let connector_customer_id = match customer_response.response.unwrap() { + PaymentsResponseData::ConnectorCustomerResponse { + connector_customer_id, + } => Some(connector_customer_id), + _ => None, + }; + + let token_response = CONNECTOR + .create_connector_pm_token( + Some(types::PaymentMethodTokenizationData { payment_method_data: types::api::PaymentMethodData::Card(api::Card { - card_cvc: Secret::new("12345".to_string()), + card_number: cards::CardNumber::from_str("4111111111111111").unwrap(), + card_exp_month: Secret::new("11".to_string()), + card_exp_year: Secret::new("2027".to_string()), + card_cvc: Secret::new("123456".to_string()), ..utils::CCardType::default().0 }), - ..utils::PaymentAuthorizeType::default().0 + browser_info: None, }), - get_default_payment_info(), + get_default_payment_info(connector_customer_id, None), ) .await - .unwrap(); + .expect("Authorize payment response"); assert_eq!( - response.response.unwrap_err().message, - "Your card's security code is invalid.".to_string(), + token_response.response.unwrap_err().reason, + Some(r#"{"card_cvv":["The card cvv may not be greater than 99999."]}"#.to_string()), ); } // Creates a payment with incorrect expiry month. #[actix_web::test] async fn should_fail_payment_for_invalid_exp_month() { - let response = CONNECTOR - .make_payment( - Some(types::PaymentsAuthorizeData { + let customer_response = CONNECTOR + .create_connector_customer(customer_details(), get_default_payment_info(None, None)) + .await + .expect("Authorize payment response"); + let connector_customer_id = match customer_response.response.unwrap() { + PaymentsResponseData::ConnectorCustomerResponse { + connector_customer_id, + } => Some(connector_customer_id), + _ => None, + }; + + let token_response = CONNECTOR + .create_connector_pm_token( + Some(types::PaymentMethodTokenizationData { payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_number: cards::CardNumber::from_str("4111111111111111").unwrap(), card_exp_month: Secret::new("20".to_string()), + card_exp_year: Secret::new("2027".to_string()), + card_cvc: Secret::new("123".to_string()), ..utils::CCardType::default().0 }), - ..utils::PaymentAuthorizeType::default().0 + browser_info: None, }), - get_default_payment_info(), + get_default_payment_info(connector_customer_id, None), ) .await - .unwrap(); + .expect("Authorize payment response"); assert_eq!( - response.response.unwrap_err().message, - "Your card's expiration month is invalid.".to_string(), + token_response.response.unwrap_err().reason, + Some(r#"{"validation":["Tokenization Validation Errors: Month is invalid"]}"#.to_string()), ); } // Creates a payment with incorrect expiry year. #[actix_web::test] async fn should_fail_payment_for_incorrect_expiry_year() { - let response = CONNECTOR - .make_payment( - Some(types::PaymentsAuthorizeData { + let customer_response = CONNECTOR + .create_connector_customer(customer_details(), get_default_payment_info(None, None)) + .await + .expect("Authorize payment response"); + let connector_customer_id = match customer_response.response.unwrap() { + PaymentsResponseData::ConnectorCustomerResponse { + connector_customer_id, + } => Some(connector_customer_id), + _ => None, + }; + + let token_response = CONNECTOR + .create_connector_pm_token( + Some(types::PaymentMethodTokenizationData { payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_number: cards::CardNumber::from_str("4111111111111111").unwrap(), + card_exp_month: Secret::new("04".to_string()), card_exp_year: Secret::new("2000".to_string()), + card_cvc: Secret::new("123".to_string()), ..utils::CCardType::default().0 }), - ..utils::PaymentAuthorizeType::default().0 + browser_info: None, }), - get_default_payment_info(), + get_default_payment_info(connector_customer_id, None), ) .await - .unwrap(); + .expect("Authorize payment response"); assert_eq!( - response.response.unwrap_err().message, - "Your card's expiration year is invalid.".to_string(), + token_response.response.unwrap_err().reason, + Some(r#"{"validation":["Tokenization Validation Errors: Year is invalid"]}"#.to_string()), ); } // Voids a payment using automatic capture flow (Non 3DS). #[actix_web::test] +#[ignore = "Connector Refunds the payment on Void call for Auto Captured Payment"] async fn should_fail_void_payment_for_auto_capture() { let authorize_response = CONNECTOR - .make_payment(payment_method_details(), get_default_payment_info()) + .make_payment( + payment_method_details(), + get_default_payment_info(None, create_customer_and_get_token().await), + ) .await .unwrap(); assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); let txn_id = utils::get_connector_transaction_id(authorize_response.response); assert_ne!(txn_id, None, "Empty connector transaction id"); let void_response = CONNECTOR - .void_payment(txn_id.unwrap(), None, get_default_payment_info()) + .void_payment(txn_id.unwrap(), None, get_default_payment_info(None, None)) .await .unwrap(); assert_eq!( @@ -384,12 +587,16 @@ async fn should_fail_void_payment_for_auto_capture() { #[actix_web::test] async fn should_fail_capture_for_invalid_payment() { let capture_response = CONNECTOR - .capture_payment("123456789".to_string(), None, get_default_payment_info()) + .capture_payment( + "123456789".to_string(), + None, + get_default_payment_info(None, create_customer_and_get_token().await), + ) .await .unwrap(); assert_eq!( - capture_response.response.unwrap_err().message, - String::from("No such payment_intent: '123456789'") + capture_response.response.unwrap_err().reason, + Some(r#"{"id":["The selected id is invalid."]}"#.to_string()), ); } @@ -403,13 +610,13 @@ async fn should_fail_for_refund_amount_higher_than_payment_amount() { refund_amount: 150, ..utils::PaymentRefundType::default().0 }), - get_default_payment_info(), + get_default_payment_info(None, create_customer_and_get_token().await), ) .await .unwrap(); assert_eq!( - response.response.unwrap_err().message, - "Refund amount (₹1.50) is greater than charge amount (₹1.00)", + response.response.unwrap_err().reason, + Some(r#"{"total":["The total may not be greater than 100."]}"#.to_string()), ); } diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 2f7eae9fd9..1bffcb36fa 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -1,6 +1,7 @@ use std::{fmt::Debug, marker::PhantomData, str::FromStr, time::Duration}; use async_trait::async_trait; +use common_utils::pii::Email; use error_stack::Report; use masking::Secret; use router::{ @@ -36,6 +37,8 @@ pub struct PaymentInfo { pub access_token: Option, pub connector_meta_data: Option, pub return_url: Option, + pub connector_customer: Option, + pub payment_method_token: Option, pub payout_method_data: Option, pub currency: Option, pub country: Option, @@ -70,6 +73,52 @@ pub trait ConnectorActions: Connector { call_connector(request, integration).await } + async fn create_connector_customer( + &self, + payment_data: Option, + payment_info: Option, + ) -> Result> { + let integration = self.get_data().connector.get_connector_integration(); + let mut request = self.generate_data( + types::ConnectorCustomerData { + ..(payment_data.unwrap_or(CustomerType::default().0)) + }, + payment_info, + ); + let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( + Settings::new().unwrap(), + StorageImpl::PostgresqlTest, + tx, + ) + .await; + integration.execute_pretasks(&mut request, &state).await?; + call_connector(request, integration).await + } + + async fn create_connector_pm_token( + &self, + payment_data: Option, + payment_info: Option, + ) -> Result> { + let integration = self.get_data().connector.get_connector_integration(); + let mut request = self.generate_data( + types::PaymentMethodTokenizationData { + ..(payment_data.unwrap_or(TokenType::default().0)) + }, + payment_info, + ); + let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( + Settings::new().unwrap(), + StorageImpl::PostgresqlTest, + tx, + ) + .await; + integration.execute_pretasks(&mut request, &state).await?; + call_connector(request, integration).await + } + /// For initiating payments when `CaptureMethod` is set to `Automatic` /// This does complete the transaction without user intervention to Capture the payment async fn make_payment( @@ -443,8 +492,8 @@ pub trait ConnectorActions: Connector { access_token: info.clone().and_then(|a| a.access_token), session_token: None, reference_id: None, - payment_method_token: None, - connector_customer: None, + payment_method_token: info.clone().and_then(|a| a.payment_method_token), + connector_customer: info.clone().and_then(|a| a.connector_customer), recurring_mandate_payment_data: None, preprocessing_id: None, connector_request_reference_id: uuid::Uuid::new_v4().to_string(), @@ -764,6 +813,8 @@ pub struct PaymentSyncType(pub types::PaymentsSyncData); pub struct PaymentRefundType(pub types::RefundsData); pub struct CCardType(pub api::Card); pub struct BrowserInfoType(pub types::BrowserInformation); +pub struct CustomerType(pub types::ConnectorCustomerData); +pub struct TokenType(pub types::PaymentMethodTokenizationData); impl Default for CCardType { fn default() -> Self { @@ -887,6 +938,29 @@ impl Default for PaymentRefundType { } } +impl Default for CustomerType { + fn default() -> Self { + let data = types::ConnectorCustomerData { + description: None, + email: Some(Email::from(Secret::new("test@juspay.in".to_string()))), + phone: None, + name: None, + preprocessing_id: None, + }; + Self(data) + } +} + +impl Default for TokenType { + fn default() -> Self { + let data = types::PaymentMethodTokenizationData { + payment_method_data: types::api::PaymentMethodData::Card(CCardType::default().0), + browser_info: None, + }; + Self(data) + } +} + pub fn get_connector_transaction_id( response: Result, ) -> Option {