From ec2b1b18fd1851012e9f877a08f847c194cc52bf Mon Sep 17 00:00:00 2001 From: Jagan Date: Wed, 15 Mar 2023 14:18:17 +0530 Subject: [PATCH] feat(connector): Add support for complete authorize payment after 3DS redirection (#741) --- config/Development.toml | 2 +- config/docker_compose.toml | 2 +- connector-template/mod.rs | 20 +- connector-template/test.rs | 3 +- crates/common_utils/src/lib.rs | 22 +- crates/router/src/connector/aci.rs | 2 - crates/router/src/connector/adyen.rs | 17 +- crates/router/src/connector/airwallex.rs | 14 +- crates/router/src/connector/applepay.rs | 2 - .../router/src/connector/authorizedotnet.rs | 2 - crates/router/src/connector/bambora.rs | 14 +- crates/router/src/connector/bluesnap.rs | 14 +- crates/router/src/connector/braintree.rs | 14 +- crates/router/src/connector/checkout.rs | 7 +- crates/router/src/connector/cybersource.rs | 2 - crates/router/src/connector/dlocal.rs | 14 +- crates/router/src/connector/fiserv.rs | 14 +- crates/router/src/connector/globalpay.rs | 14 +- .../src/connector/globalpay/transformers.rs | 7 +- crates/router/src/connector/klarna.rs | 2 - crates/router/src/connector/multisafepay.rs | 14 +- crates/router/src/connector/nuvei.rs | 408 ++++++++--- .../src/connector/nuvei/transformers.rs | 637 +++++++++++++----- crates/router/src/connector/payu.rs | 14 +- crates/router/src/connector/rapyd.rs | 14 +- crates/router/src/connector/shift4.rs | 14 +- crates/router/src/connector/stripe.rs | 2 + crates/router/src/connector/trustpay.rs | 2 + .../src/connector/trustpay/transformers.rs | 4 +- crates/router/src/connector/utils.rs | 102 ++- crates/router/src/connector/worldline.rs | 2 - crates/router/src/connector/worldpay.rs | 14 +- crates/router/src/core/errors.rs | 2 + crates/router/src/core/payments.rs | 197 ++++-- .../router/src/core/payments/access_token.rs | 1 + crates/router/src/core/payments/flows.rs | 86 ++- .../src/core/payments/flows/authorize_flow.rs | 9 + .../payments/flows/complete_authorize_flow.rs | 104 +++ crates/router/src/core/payments/helpers.rs | 11 + crates/router/src/core/payments/operations.rs | 1 + .../operations/payment_complete_authorize.rs | 342 ++++++++++ .../payments/operations/payment_response.rs | 22 +- .../router/src/core/payments/transformers.rs | 41 ++ crates/router/src/core/utils.rs | 1 + crates/router/src/routes/app.rs | 5 + crates/router/src/routes/payments.rs | 44 +- crates/router/src/services/api.rs | 8 + crates/router/src/types.rs | 63 +- crates/router/src/types/api/payments.rs | 15 + crates/router/tests/connectors/aci.rs | 5 + crates/router/tests/connectors/adyen.rs | 3 + crates/router/tests/connectors/airwallex.rs | 6 +- .../tests/connectors/authorizedotnet.rs | 5 + crates/router/tests/connectors/bambora.rs | 2 + crates/router/tests/connectors/bluesnap.rs | 6 +- crates/router/tests/connectors/checkout.rs | 5 + crates/router/tests/connectors/cybersource.rs | 3 +- crates/router/tests/connectors/dlocal.rs | 6 +- crates/router/tests/connectors/fiserv.rs | 6 +- crates/router/tests/connectors/globalpay.rs | 3 +- .../router/tests/connectors/multisafepay.rs | 6 +- crates/router/tests/connectors/nuvei.rs | 16 +- crates/router/tests/connectors/payu.rs | 24 +- crates/router/tests/connectors/shift4.rs | 6 +- crates/router/tests/connectors/stripe.rs | 6 +- crates/router/tests/connectors/trustpay.rs | 3 +- crates/router/tests/connectors/utils.rs | 5 + crates/router/tests/connectors/worldline.rs | 7 +- crates/router/tests/connectors/worldpay.rs | 3 +- crates/router_derive/src/macros/operation.rs | 6 + loadtest/config/Development.toml | 2 +- 71 files changed, 1870 insertions(+), 631 deletions(-) create mode 100644 crates/router/src/core/payments/flows/complete_authorize_flow.rs create mode 100644 crates/router/src/core/payments/operations/payment_complete_authorize.rs diff --git a/config/Development.toml b/config/Development.toml index 406dedb0b3..75716e7126 100644 --- a/config/Development.toml +++ b/config/Development.toml @@ -66,9 +66,9 @@ cards = [ "payu", "shift4", "stripe", + "trustpay", "worldline", "worldpay", - "trustpay", ] [refund] diff --git a/config/docker_compose.toml b/config/docker_compose.toml index e91423bc27..e013b6431a 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -114,9 +114,9 @@ cards = [ "payu", "shift4", "stripe", + "trustpay", "worldline", "worldpay", - "trustpay", ] diff --git a/connector-template/mod.rs b/connector-template/mod.rs index bbb2d4a672..2c5c3e63c4 100644 --- a/connector-template/mod.rs +++ b/connector-template/mod.rs @@ -62,8 +62,7 @@ impl ConnectorCommon for {{project-name | downcase | pascal_case}} { } fn get_auth_header(&self, auth_type:&types::ConnectorAuthType)-> CustomResult,errors::ConnectorError> { - let auth: {{project-name | downcase}}::{{project-name | downcase | pascal_case}}AuthType = auth_type - .try_into() + let auth = {{project-name | downcase}}::{{project-name | downcase | pascal_case}}AuthType::try_from(auth_type) .change_context(errors::ConnectorError::FailedToObtainAuthType)?; Ok(vec![(headers::AUTHORIZATION.to_string(), auth.api_key)]) } @@ -146,12 +145,11 @@ impl res: Response, ) -> CustomResult { let response: {{project-name | downcase}}::{{project-name | downcase | pascal_case}}PaymentsResponse = res.response.parse_struct("{{project-name | downcase | pascal_case}} PaymentsAuthorizeResponse").change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - types::ResponseRouterData { + types::RouterData::try_from(types::ResponseRouterData { response, data: data.clone(), http_code: res.status_code, - } - .try_into() + }) .change_context(errors::ConnectorError::ResponseHandlingFailed) } @@ -346,12 +344,11 @@ impl res: Response, ) -> CustomResult,errors::ConnectorError> { let response: {{project-name| downcase}}::RefundResponse = res.response.parse_struct("{{project-name | downcase}} RefundResponse").change_context(errors::ConnectorError::RequestEncodingFailed)?; - types::ResponseRouterData { + types::RouterData::try_from(types::ResponseRouterData { response, data: data.clone(), http_code: res.status_code, - } - .try_into() + }) .change_context(errors::ConnectorError::ResponseHandlingFailed) } @@ -395,12 +392,11 @@ impl res: Response, ) -> CustomResult { let response: {{project-name | downcase}}::RefundResponse = res.response.parse_struct("{{project-name | downcase}} RefundSyncResponse").change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - types::ResponseRouterData { + types::RouterData::try_from(types::ResponseRouterData { response, data: data.clone(), http_code: res.status_code, - } - .try_into() + }) .change_context(errors::ConnectorError::ResponseHandlingFailed) } @@ -437,6 +433,8 @@ impl services::ConnectorRedirectResponse for {{project-name | downcase | pascal_ fn get_flow_type( &self, _query_params: &str, + _json_payload: Option, + _action: services::PaymentAction ) -> CustomResult { Ok(payments::CallConnectorAction::Trigger) } diff --git a/connector-template/test.rs b/connector-template/test.rs index bd65d9433a..3c2a7b239e 100644 --- a/connector-template/test.rs +++ b/connector-template/test.rs @@ -87,8 +87,7 @@ async fn should_sync_authorized_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( txn_id.unwrap(), ), - encoded_data: None, - capture_method: None, + ..Default::default() }), None, ) diff --git a/crates/common_utils/src/lib.rs b/crates/common_utils/src/lib.rs index 6ec1856e96..9c41b40c11 100644 --- a/crates/common_utils/src/lib.rs +++ b/crates/common_utils/src/lib.rs @@ -49,26 +49,10 @@ pub mod date_time { (result, start.elapsed().as_seconds_f64() * 1000f64) } - /// Prefix the date field with zero if it has single digit Eg: 1 -> 01 - fn prefix_zero(input: u8) -> String { - if input < 10 { - return "0".to_owned() + input.to_string().as_str(); - } - input.to_string() - } - /// Return the current date and time in UTC with the format YYYYMMDDHHmmss Eg: 20191105081132 - pub fn date_as_yyyymmddhhmmss() -> String { - let now = OffsetDateTime::now_utc(); - format!( - "{}{}{}{}{}{}", - now.year(), - prefix_zero(u8::from(now.month())), - prefix_zero(now.day()), - prefix_zero(now.hour()), - prefix_zero(now.minute()), - prefix_zero(now.second()) - ) + pub fn date_as_yyyymmddhhmmss() -> Result { + let format = time::macros::format_description!("[year repr:full][month padding:zero repr:numerical][day padding:zero][hour padding:zero repr:24][minute padding:zero][second padding:zero]"); + now().format(&format) } /// Return the current date and time in UTC with the format [year]-[month]-[day]T[hour]:[minute]:[second].mmmZ Eg: 2023-02-15T13:33:18.898Z diff --git a/crates/router/src/connector/aci.rs b/crates/router/src/connector/aci.rs index 070e2b8f06..5c6d704480 100644 --- a/crates/router/src/connector/aci.rs +++ b/crates/router/src/connector/aci.rs @@ -545,5 +545,3 @@ impl api::IncomingWebhook for Aci { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } - -impl services::ConnectorRedirectResponse for Aci {} diff --git a/crates/router/src/connector/adyen.rs b/crates/router/src/connector/adyen.rs index be8f01c0d1..1741417633 100644 --- a/crates/router/src/connector/adyen.rs +++ b/crates/router/src/connector/adyen.rs @@ -11,10 +11,7 @@ use self::transformers as adyen; use crate::{ configs::settings, consts, - core::{ - errors::{self, CustomResult}, - payments, - }, + core::errors::{self, CustomResult}, db::StorageInterface, headers, logger, services, types::{ @@ -36,8 +33,7 @@ impl ConnectorCommon for Adyen { &self, auth_type: &types::ConnectorAuthType, ) -> CustomResult, errors::ConnectorError> { - let auth: adyen::AdyenAuthType = auth_type - .try_into() + let auth = adyen::AdyenAuthType::try_from(auth_type) .change_context(errors::ConnectorError::FailedToObtainAuthType)?; Ok(vec![(headers::X_API_KEY.to_string(), auth.api_key)]) } @@ -762,12 +758,3 @@ impl api::IncomingWebhook for Adyen { )) } } - -impl services::ConnectorRedirectResponse for Adyen { - fn get_flow_type( - &self, - _query_params: &str, - ) -> CustomResult { - Ok(payments::CallConnectorAction::Trigger) - } -} diff --git a/crates/router/src/connector/airwallex.rs b/crates/router/src/connector/airwallex.rs index fdd6601348..f3c1da9e40 100644 --- a/crates/router/src/connector/airwallex.rs +++ b/crates/router/src/connector/airwallex.rs @@ -296,7 +296,10 @@ impl ConnectorIntegration = Box::new(&Self); - let authorize_data = &types::PaymentsAuthorizeSessionTokenRouterData::from(&router_data); + let authorize_data = &types::PaymentsAuthorizeSessionTokenRouterData::from(( + &router_data, + types::AuthorizeSessionTokenData::from(&router_data), + )); let resp = services::execute_connector_processing_step( app_state, integ, @@ -905,12 +908,3 @@ impl api::IncomingWebhook for Airwallex { Ok(details.data.object) } } - -impl services::ConnectorRedirectResponse for Airwallex { - fn get_flow_type( - &self, - _query_params: &str, - ) -> CustomResult { - Ok(payments::CallConnectorAction::Trigger) - } -} diff --git a/crates/router/src/connector/applepay.rs b/crates/router/src/connector/applepay.rs index a4570c5d13..c4e08ff675 100644 --- a/crates/router/src/connector/applepay.rs +++ b/crates/router/src/connector/applepay.rs @@ -236,8 +236,6 @@ impl services::ConnectorIntegration CustomResult { - Ok(payments::CallConnectorAction::Trigger) - } -} - pub fn get_payment_flow(is_auto_capture: bool) -> bambora::PaymentFlow { if is_auto_capture { bambora::PaymentFlow::Capture diff --git a/crates/router/src/connector/bluesnap.rs b/crates/router/src/connector/bluesnap.rs index 7731617916..c3335307f0 100644 --- a/crates/router/src/connector/bluesnap.rs +++ b/crates/router/src/connector/bluesnap.rs @@ -11,10 +11,7 @@ use super::utils::RefundsRequestData; use crate::{ configs::settings, consts, - core::{ - errors::{self, CustomResult}, - payments, - }, + core::errors::{self, CustomResult}, db::StorageInterface, headers, logger, services::{self, ConnectorIntegration}, @@ -741,12 +738,3 @@ impl api::IncomingWebhook for Bluesnap { Ok(res_json) } } - -impl services::ConnectorRedirectResponse for Bluesnap { - fn get_flow_type( - &self, - _query_params: &str, - ) -> CustomResult { - Ok(payments::CallConnectorAction::Trigger) - } -} diff --git a/crates/router/src/connector/braintree.rs b/crates/router/src/connector/braintree.rs index 2c56b60511..581f9bc4ce 100644 --- a/crates/router/src/connector/braintree.rs +++ b/crates/router/src/connector/braintree.rs @@ -8,10 +8,7 @@ use self::transformers as braintree; use crate::{ configs::settings, consts, - core::{ - errors::{self, CustomResult}, - payments, - }, + core::errors::{self, CustomResult}, headers, services, types::{ self, @@ -696,12 +693,3 @@ impl api::IncomingWebhook for Braintree { Err(errors::ConnectorError::NotImplemented("braintree".to_string()).into()) } } - -impl services::ConnectorRedirectResponse for Braintree { - fn get_flow_type( - &self, - _query_params: &str, - ) -> CustomResult { - Ok(payments::CallConnectorAction::Trigger) - } -} diff --git a/crates/router/src/connector/checkout.rs b/crates/router/src/connector/checkout.rs index f262b7d9ec..66e07c34e8 100644 --- a/crates/router/src/connector/checkout.rs +++ b/crates/router/src/connector/checkout.rs @@ -749,16 +749,19 @@ impl services::ConnectorRedirectResponse for Checkout { fn get_flow_type( &self, query_params: &str, + _json_payload: Option, + _action: services::PaymentAction, ) -> CustomResult { let query = serde_urlencoded::from_str::(query_params) .into_report() .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - Ok(query + let connector_action = query .status .map(|checkout_status| { payments::CallConnectorAction::StatusUpdate(checkout_status.into()) }) - .unwrap_or(payments::CallConnectorAction::Trigger)) + .unwrap_or(payments::CallConnectorAction::Trigger); + Ok(connector_action) } } diff --git a/crates/router/src/connector/cybersource.rs b/crates/router/src/connector/cybersource.rs index 69118abfd5..35d6061d8b 100644 --- a/crates/router/src/connector/cybersource.rs +++ b/crates/router/src/connector/cybersource.rs @@ -709,5 +709,3 @@ impl api::IncomingWebhook for Cybersource { Err(errors::ConnectorError::NotImplemented("cybersource".to_string()).into()) } } - -impl services::ConnectorRedirectResponse for Cybersource {} diff --git a/crates/router/src/connector/dlocal.rs b/crates/router/src/connector/dlocal.rs index 470681701e..16bbbe6928 100644 --- a/crates/router/src/connector/dlocal.rs +++ b/crates/router/src/connector/dlocal.rs @@ -12,10 +12,7 @@ use transformers as dlocal; use crate::{ configs::settings, - core::{ - errors::{self, CustomResult}, - payments, - }, + core::errors::{self, CustomResult}, headers, logger, services::{self, ConnectorIntegration}, types::{ @@ -586,12 +583,3 @@ impl api::IncomingWebhook for Dlocal { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } - -impl services::ConnectorRedirectResponse for Dlocal { - fn get_flow_type( - &self, - _query_params: &str, - ) -> CustomResult { - Ok(payments::CallConnectorAction::Trigger) - } -} diff --git a/crates/router/src/connector/fiserv.rs b/crates/router/src/connector/fiserv.rs index 54a4919fb7..92f4cd7500 100644 --- a/crates/router/src/connector/fiserv.rs +++ b/crates/router/src/connector/fiserv.rs @@ -12,10 +12,7 @@ use uuid::Uuid; use crate::{ configs::settings, consts, - core::{ - errors::{self, CustomResult}, - payments, - }, + core::errors::{self, CustomResult}, headers, logger, services::{self, api::ConnectorIntegration}, types::{ @@ -686,12 +683,3 @@ impl api::IncomingWebhook for Fiserv { Err(errors::ConnectorError::NotImplemented("fiserv".to_string()).into()) } } - -impl services::ConnectorRedirectResponse for Fiserv { - fn get_flow_type( - &self, - _query_params: &str, - ) -> CustomResult { - Ok(payments::CallConnectorAction::Trigger) - } -} diff --git a/crates/router/src/connector/globalpay.rs b/crates/router/src/connector/globalpay.rs index fbbdb13942..125982a7c2 100644 --- a/crates/router/src/connector/globalpay.rs +++ b/crates/router/src/connector/globalpay.rs @@ -15,10 +15,7 @@ use self::{ }; use crate::{ configs::settings, - core::{ - errors::{self, CustomResult}, - payments, - }, + core::errors::{self, CustomResult}, headers, services::{self, ConnectorIntegration}, types::{ @@ -704,12 +701,3 @@ impl api::IncomingWebhook for Globalpay { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } - -impl services::ConnectorRedirectResponse for Globalpay { - fn get_flow_type( - &self, - _query_params: &str, - ) -> CustomResult { - Ok(payments::CallConnectorAction::Trigger) - } -} diff --git a/crates/router/src/connector/globalpay/transformers.rs b/crates/router/src/connector/globalpay/transformers.rs index 6973906f9e..c2b09faa54 100644 --- a/crates/router/src/connector/globalpay/transformers.rs +++ b/crates/router/src/connector/globalpay/transformers.rs @@ -8,7 +8,7 @@ use super::{ response::{GlobalpayPaymentStatus, GlobalpayPaymentsResponse, GlobalpayRefreshTokenResponse}, }; use crate::{ - connector::utils::{CardData, PaymentsRequestData, RouterData}, + connector::utils::{self, CardData, PaymentsAuthorizeRequestData, RouterData}, consts, core::errors, types::{self, api, storage::enums, ErrorResponse}, @@ -22,8 +22,9 @@ pub struct GlobalPayMeta { impl TryFrom<&types::PaymentsAuthorizeRouterData> for GlobalpayPaymentsRequest { type Error = error_stack::Report; fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { - let metadata: GlobalPayMeta = item.to_connector_meta()?; - let card = item.get_card()?; + let metadata: GlobalPayMeta = + utils::to_connector_meta_from_secret(item.connector_meta_data.clone())?; + let card = item.request.get_card()?; let expiry_year = card.get_card_expiry_year_2_digit(); Ok(Self { account_name: metadata.account_name, diff --git a/crates/router/src/connector/klarna.rs b/crates/router/src/connector/klarna.rs index 56d23b1652..61a7a2debf 100644 --- a/crates/router/src/connector/klarna.rs +++ b/crates/router/src/connector/klarna.rs @@ -377,5 +377,3 @@ impl api::IncomingWebhook for Klarna { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } - -impl services::ConnectorRedirectResponse for Klarna {} diff --git a/crates/router/src/connector/multisafepay.rs b/crates/router/src/connector/multisafepay.rs index ae581fccb9..f0a3eb57c2 100644 --- a/crates/router/src/connector/multisafepay.rs +++ b/crates/router/src/connector/multisafepay.rs @@ -7,10 +7,7 @@ use transformers as multisafepay; use crate::{ configs::settings, - core::{ - errors::{self, CustomResult}, - payments, - }, + core::errors::{self, CustomResult}, headers, services::{self, ConnectorIntegration}, types::{ @@ -448,12 +445,3 @@ impl api::IncomingWebhook for Multisafepay { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } - -impl services::ConnectorRedirectResponse for Multisafepay { - fn get_flow_type( - &self, - _query_params: &str, - ) -> CustomResult { - Ok(payments::CallConnectorAction::Trigger) - } -} diff --git a/crates/router/src/connector/nuvei.rs b/crates/router/src/connector/nuvei.rs index 9aff6524cf..88396c4294 100644 --- a/crates/router/src/connector/nuvei.rs +++ b/crates/router/src/connector/nuvei.rs @@ -2,23 +2,29 @@ mod transformers; use std::fmt::Debug; +use ::common_utils::{ + errors::ReportSwitchExt, + ext_traits::{BytesExt, StringExt, ValueExt}, +}; use error_stack::{IntoReport, ResultExt}; use transformers as nuvei; +use super::utils::{self, to_boolean, RouterData}; use crate::{ configs::settings, core::{ errors::{self, CustomResult}, payments, }, - headers, logger, + headers, services::{self, ConnectorIntegration}, types::{ self, - api::{self, ConnectorCommon, ConnectorCommonExt}, - ErrorResponse, Response, RouterData, + api::{self, ConnectorCommon, ConnectorCommonExt, InitPayment}, + storage::enums, + ErrorResponse, Response, }, - utils::{self, BytesExt}, + utils::{self as common_utils}, }; #[derive(Debug, Clone)] @@ -30,7 +36,7 @@ where { fn build_headers( &self, - _req: &RouterData, + _req: &types::RouterData, _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let headers = vec![( @@ -63,14 +69,106 @@ impl ConnectorCommon for Nuvei { } impl api::Payment for Nuvei {} - impl api::PreVerify for Nuvei {} +impl api::PaymentVoid for Nuvei {} +impl api::PaymentSync for Nuvei {} +impl api::PaymentCapture for Nuvei {} +impl api::PaymentSession for Nuvei {} +impl api::PaymentAuthorize for Nuvei {} +impl api::Refund for Nuvei {} +impl api::RefundExecute for Nuvei {} +impl api::RefundSync for Nuvei {} +impl api::PaymentsCompleteAuthorize for Nuvei {} +impl api::ConnectorAccessToken for Nuvei {} + impl ConnectorIntegration for Nuvei { } -impl api::PaymentVoid for Nuvei {} +impl + ConnectorIntegration< + api::CompleteAuthorize, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + > for Nuvei +{ + fn get_headers( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + 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::PaymentsCompleteAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}ppp/api/v1/payment.do", + api::ConnectorCommon::base_url(self, connectors) + )) + } + fn get_request_body( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + ) -> CustomResult, errors::ConnectorError> { + let meta: nuvei::NuveiMeta = utils::to_connector_meta(req.request.connector_meta.clone())?; + let req_obj = nuvei::NuveiPaymentsRequest::try_from((req, meta.session_token))?; + let req = + common_utils::Encode::::encode_to_string_of_json(&req_obj) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + + Ok(Some(req)) + } + fn build_request( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsComeplteAuthorizeType::get_url( + self, req, connectors, + )?) + .headers(types::PaymentsComeplteAuthorizeType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsComeplteAuthorizeType::get_request_body( + self, req, + )?) + .build(), + )) + } + fn handle_response( + &self, + data: &types::PaymentsCompleteAuthorizeRouterData, + res: Response, + ) -> CustomResult { + let response: nuvei::NuveiPaymentsResponse = res + .response + .parse_struct("NuveiPaymentsResponse") + .switch()?; + 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 Nuvei @@ -103,9 +201,10 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = nuvei::NuveiPaymentFlowRequest::try_from(req)?; - let req = - utils::Encode::::encode_to_string_of_json(&req_obj) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let req = common_utils::Encode::::encode_to_string_of_json( + &req_obj, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; Ok(Some(req)) } @@ -128,11 +227,10 @@ impl ConnectorIntegration CustomResult { - logger::debug!(target: "router::connector::nuvei", response=?res); let response: nuvei::NuveiPaymentsResponse = res .response - .parse_struct("nuvei NuveiPaymentsResponse") - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + .parse_struct("NuveiPaymentsResponse") + .switch()?; types::PaymentsCancelRouterData::try_from(types::ResponseRouterData { response, data: data.clone(), @@ -149,14 +247,11 @@ impl ConnectorIntegration for Nuvei { } -impl api::PaymentSync for Nuvei {} impl ConnectorIntegration for Nuvei { @@ -188,9 +283,10 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = nuvei::NuveiPaymentSyncRequest::try_from(req)?; - let req = - utils::Encode::::encode_to_string_of_json(&req_obj) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let req = common_utils::Encode::::encode_to_string_of_json( + &req_obj, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; Ok(Some(req)) } fn build_request( @@ -220,11 +316,10 @@ impl ConnectorIntegration CustomResult { - logger::debug!(payment_sync_response=?res); let response: nuvei::NuveiPaymentsResponse = res .response - .parse_struct("nuvei PaymentsResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + .parse_struct("NuveiPaymentsResponse") + .switch()?; types::RouterData::try_from(types::ResponseRouterData { response, data: data.clone(), @@ -234,7 +329,6 @@ impl ConnectorIntegration for Nuvei { @@ -266,9 +360,10 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = nuvei::NuveiPaymentFlowRequest::try_from(req)?; - let req = - utils::Encode::::encode_to_string_of_json(&req_obj) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let req = common_utils::Encode::::encode_to_string_of_json( + &req_obj, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; Ok(Some(req)) } @@ -296,9 +391,8 @@ impl ConnectorIntegration CustomResult { let response: nuvei::NuveiPaymentsResponse = res .response - .parse_struct("Nuvei PaymentsResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - logger::debug!(nuveipayments_create_response=?response); + .parse_struct("NuveiPaymentsResponse") + .switch()?; types::RouterData::try_from(types::ResponseRouterData { response, data: data.clone(), @@ -315,15 +409,11 @@ impl ConnectorIntegration for Nuvei { } -impl api::PaymentAuthorize for Nuvei {} - #[async_trait::async_trait] impl ConnectorIntegration for Nuvei @@ -365,7 +455,10 @@ impl ConnectorIntegration = Box::new(&Self); - let authorize_data = &types::PaymentsAuthorizeSessionTokenRouterData::from(&router_data); + let authorize_data = &types::PaymentsAuthorizeSessionTokenRouterData::from(( + &router_data, + types::AuthorizeSessionTokenData::from(&router_data), + )); let resp = services::execute_connector_processing_step( app_state, integ, @@ -374,15 +467,49 @@ impl ConnectorIntegration { + let integ: Box< + &(dyn ConnectorIntegration< + InitPayment, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + > + Send + + Sync + + 'static), + > = Box::new(&Self); + let init_data = &types::PaymentsInitRouterData::from(( + &router_data, + router_data.request.clone(), + )); + let init_resp = services::execute_connector_processing_step( + app_state, + integ, + init_data, + payments::CallConnectorAction::Trigger, + ) + .await?; + ( + init_resp.request.enrolled_for_3ds, + init_resp.request.related_transaction_id, + ) + } + storage_models::enums::AuthenticationType::NoThreeDs => (false, None), + }; + + router_data.request.enrolled_for_3ds = enrolled_for_3ds; + router_data.request.related_transaction_id = related_transaction_id; Ok(()) } fn get_request_body( &self, req: &types::PaymentsAuthorizeRouterData, ) -> CustomResult, errors::ConnectorError> { - let req_obj = nuvei::NuveiPaymentsRequest::try_from(req)?; - let req = utils::Encode::::encode_to_string_of_json(&req_obj) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let req_obj = nuvei::NuveiPaymentsRequest::try_from((req, req.get_session_token()?))?; + let req = + common_utils::Encode::::encode_to_string_of_json(&req_obj) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(req)) } @@ -412,9 +539,8 @@ impl ConnectorIntegration CustomResult { let response: nuvei::NuveiPaymentsResponse = res .response - .parse_struct("nuvei NuveiPaymentsResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - logger::debug!(nuveipayments_create_response=?response); + .parse_struct("NuveiPaymentsResponse") + .switch()?; types::RouterData::try_from(types::ResponseRouterData { response, data: data.clone(), @@ -440,11 +566,7 @@ impl { fn get_headers( &self, - req: &RouterData< - types::api::payments::AuthorizeSessionToken, - types::AuthorizeSessionTokenData, - types::PaymentsResponseData, - >, + req: &types::PaymentsAuthorizeSessionTokenRouterData, connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { self.build_headers(req, connectors) @@ -456,11 +578,7 @@ impl fn get_url( &self, - _req: &RouterData< - types::api::payments::AuthorizeSessionToken, - types::AuthorizeSessionTokenData, - types::PaymentsResponseData, - >, + _req: &types::PaymentsAuthorizeSessionTokenRouterData, connectors: &settings::Connectors, ) -> CustomResult { Ok(format!( @@ -471,25 +589,18 @@ impl fn get_request_body( &self, - req: &RouterData< - types::api::payments::AuthorizeSessionToken, - types::AuthorizeSessionTokenData, - types::PaymentsResponseData, - >, + req: &types::PaymentsAuthorizeSessionTokenRouterData, ) -> CustomResult, errors::ConnectorError> { let req_obj = nuvei::NuveiSessionRequest::try_from(req)?; - let req = utils::Encode::::encode_to_string_of_json(&req_obj) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let req = + common_utils::Encode::::encode_to_string_of_json(&req_obj) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; Ok(Some(req)) } fn build_request( &self, - req: &RouterData< - types::api::payments::AuthorizeSessionToken, - types::AuthorizeSessionTokenData, - types::PaymentsResponseData, - >, + req: &types::PaymentsAuthorizeSessionTokenRouterData, connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { Ok(Some( @@ -510,38 +621,16 @@ impl fn handle_response( &self, - data: &RouterData< - api::AuthorizeSessionToken, - types::AuthorizeSessionTokenData, - types::PaymentsResponseData, - >, + data: &types::PaymentsAuthorizeSessionTokenRouterData, res: Response, - ) -> CustomResult< - RouterData< - api::AuthorizeSessionToken, - types::AuthorizeSessionTokenData, - types::PaymentsResponseData, - >, - errors::ConnectorError, - > - where - api::AuthorizeSessionToken: Clone, - types::AuthorizeSessionTokenData: Clone, - types::PaymentsResponseData: Clone, - { - let response: nuvei::NuveiSessionResponse = res - .response - .parse_struct("nuvei NuveiSessionResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - logger::debug!(nuvei_session_response=?response); - - types::ResponseRouterData { + ) -> CustomResult { + let response: nuvei::NuveiSessionResponse = + res.response.parse_struct("NuveiSessionResponse").switch()?; + types::RouterData::try_from(types::ResponseRouterData { response, data: data.clone(), http_code: res.status_code, - } - .try_into() - .change_context(errors::ConnectorError::ResponseHandlingFailed) + }) } fn get_error_response( @@ -552,9 +641,99 @@ impl } } -impl api::Refund for Nuvei {} -impl api::RefundExecute for Nuvei {} -impl api::RefundSync for Nuvei {} +impl ConnectorIntegration + for Nuvei +{ + fn get_headers( + &self, + req: &types::PaymentsInitRouterData, + 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::PaymentsInitRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}ppp/api/v1/initPayment.do", + api::ConnectorCommon::base_url(self, connectors) + )) + } + + fn get_request_body( + &self, + req: &types::PaymentsInitRouterData, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = nuvei::NuveiPaymentsRequest::try_from((req, req.get_session_token()?))?; + let req = + common_utils::Encode::::encode_to_string_of_json(&req_obj) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + + Ok(Some(req)) + } + + fn build_request( + &self, + req: &types::PaymentsInitRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsInitType::get_url(self, req, connectors)?) + .headers(types::PaymentsInitType::get_headers(self, req, connectors)?) + .body(types::PaymentsInitType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsInitRouterData, + res: Response, + ) -> CustomResult { + let response: nuvei::NuveiPaymentsResponse = res + .response + .parse_struct("NuveiPaymentsResponse") + .switch()?; + let response_data = types::RouterData::try_from(types::ResponseRouterData { + response: response.clone(), + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed)?; + let is_enrolled_for_3ds = response + .clone() + .payment_option + .and_then(|po| po.card) + .and_then(|c| c.three_d) + .and_then(|t| t.v2supported) + .map(to_boolean) + .unwrap_or_default(); + Ok(types::RouterData { + request: types::PaymentsAuthorizeData { + enrolled_for_3ds: is_enrolled_for_3ds, + related_transaction_id: response.transaction_id, + ..response_data.request + }, + ..response_data + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} impl ConnectorIntegration for Nuvei { fn get_headers( @@ -585,9 +764,10 @@ impl ConnectorIntegration, ) -> CustomResult, errors::ConnectorError> { let req_obj = nuvei::NuveiPaymentFlowRequest::try_from(req)?; - let req = - utils::Encode::::encode_to_string_of_json(&req_obj) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let req = common_utils::Encode::::encode_to_string_of_json( + &req_obj, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; Ok(Some(req)) } @@ -612,17 +792,15 @@ impl ConnectorIntegration, res: Response, ) -> CustomResult, errors::ConnectorError> { - logger::debug!(target: "router::connector::nuvei", response=?res); let response: nuvei::NuveiPaymentsResponse = res .response - .parse_struct("nuvei NuveiPaymentsResponse") - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - types::ResponseRouterData { + .parse_struct("NuveiPaymentsResponse") + .switch()?; + types::RouterData::try_from(types::ResponseRouterData { response, data: data.clone(), http_code: res.status_code, - } - .try_into() + }) .change_context(errors::ConnectorError::ResponseHandlingFailed) } @@ -664,7 +842,31 @@ impl services::ConnectorRedirectResponse for Nuvei { fn get_flow_type( &self, _query_params: &str, + json_payload: Option, + action: services::PaymentAction, ) -> CustomResult { - Ok(payments::CallConnectorAction::Trigger) + match action { + services::PaymentAction::PSync => Ok(payments::CallConnectorAction::Trigger), + services::PaymentAction::CompleteAuthorize => { + if let Some(payload) = json_payload { + let redirect_response: nuvei::NuveiRedirectionResponse = + payload.parse_value("NuveiRedirectionResponse").switch()?; + let acs_response: nuvei::NuveiACSResponse = redirect_response + .cres + .parse_struct("NuveiACSResponse") + .switch()?; + match acs_response.trans_status { + None | Some(nuvei::LiabilityShift::Failed) => { + Ok(payments::CallConnectorAction::StatusUpdate( + enums::AttemptStatus::AuthenticationFailed, + )) + } + _ => Ok(payments::CallConnectorAction::Trigger), + } + } else { + Ok(payments::CallConnectorAction::Trigger) + } + } + } } } diff --git a/crates/router/src/connector/nuvei/transformers.rs b/crates/router/src/connector/nuvei/transformers.rs index 50cf95e13c..b4bceacfbe 100644 --- a/crates/router/src/connector/nuvei/transformers.rs +++ b/crates/router/src/connector/nuvei/transformers.rs @@ -1,15 +1,18 @@ use common_utils::{ crypto::{self, GenerateDigest}, date_time, + pii::Email, }; use error_stack::{IntoReport, ResultExt}; use masking::Secret; +use reqwest::Url; use serde::{Deserialize, Serialize}; use crate::{ - connector::utils::{PaymentsCancelRequestData, RouterData}, + connector::utils::{self, PaymentsAuthorizeRequestData, PaymentsCancelRequestData, RouterData}, consts, core::errors, + services, types::{self, api, storage::enums}, }; @@ -28,7 +31,7 @@ pub struct NuveiSessionRequest { pub checksum: String, } -#[derive(Debug, Serialize, Default, Deserialize)] +#[derive(Debug, Serialize, Default, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NuveiSessionResponse { pub session_token: String, @@ -42,7 +45,7 @@ pub struct NuveiSessionResponse { pub client_request_id: String, } -#[derive(Debug, Serialize, Default, Deserialize)] +#[derive(Debug, Serialize, Default)] #[serde(rename_all = "camelCase")] pub struct NuveiPaymentsRequest { pub time_stamp: String, @@ -57,9 +60,23 @@ pub struct NuveiPaymentsRequest { pub transaction_type: TransactionType, pub payment_option: PaymentOption, pub checksum: String, + pub billing_address: Option, + pub related_transaction_id: Option, } -#[derive(Debug, Serialize, Default, Deserialize)] +#[derive(Debug, Serialize, Default)] +pub struct NuveiInitPaymentRequest { + pub session_token: String, + pub merchant_id: String, + pub merchant_site_id: String, + pub client_request_id: String, + pub amount: String, + pub currency: String, + pub payment_option: PaymentOption, + pub checksum: String, +} + +#[derive(Debug, Serialize, Default)] #[serde(rename_all = "camelCase")] pub struct NuveiPaymentFlowRequest { pub time_stamp: String, @@ -72,7 +89,7 @@ pub struct NuveiPaymentFlowRequest { pub checksum: String, } -#[derive(Debug, Serialize, Default, Deserialize)] +#[derive(Debug, Serialize, Default)] #[serde(rename_all = "camelCase")] pub struct NuveiPaymentSyncRequest { pub session_token: String, @@ -85,22 +102,37 @@ pub enum TransactionType { Sale, } -#[derive(Debug, Default, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PaymentOption { - pub card: Card, + pub card: Option, + pub redirect_url: Option, pub user_payment_option_id: Option, pub device_details: Option, + pub alternative_payment_method: Option, pub billing_address: Option, } -#[derive(Debug, Default, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AlternativePaymentMethod { + pub payment_method: AlternativePaymentMethodType, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AlternativePaymentMethodType { + #[default] + ApmgwExpresscheckout, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct BillingAddress { - pub email: String, + pub email: Secret, pub country: String, } -#[derive(Debug, Default, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Card { pub card_number: Option>, @@ -123,41 +155,84 @@ pub struct Card { pub issuer_bank_name: Option, pub issuer_country: Option, pub is_prepaid: Option, + pub external_token: Option, } -#[derive(Debug, Default, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ExternalToken { + pub external_token_provider: ExternalTokenProvider, + pub mobile_token: String, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub enum ExternalTokenProvider { + #[default] + GooglePay, + ApplePay, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ThreeD { + pub method_completion_ind: Option, pub browser_details: Option, pub version: Option, + #[serde(rename = "notificationURL")] pub notification_url: Option, + #[serde(rename = "merchantURL")] pub merchant_url: Option, - pub platform_type: Option, + pub acs_url: Option, + pub c_req: Option, + pub platform_type: Option, + pub v2supported: Option, pub v2_additional_params: Option, + pub is_liability_on_issuer: Option, } -#[derive(Debug, Default, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub enum MethodCompletion { + #[serde(rename = "Y")] + Success, + #[serde(rename = "N")] + Failure, + #[serde(rename = "U")] + #[default] + Unavailable, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub enum PlatformType { + #[serde(rename = "01")] + App, + #[serde(rename = "02")] + #[default] + Browser, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct BrowserDetails { pub accept_header: String, - pub ip: String, + pub ip: Option, pub java_enabled: String, pub java_script_enabled: String, pub language: String, - pub color_depth: String, - pub screen_height: String, - pub screen_width: String, - pub time_zone: String, + pub color_depth: u8, + pub screen_height: u32, + pub screen_width: u32, + pub time_zone: i32, pub user_agent: String, } -#[derive(Debug, Default, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct V2AdditionalParams { pub challenge_window_size: String, } -#[derive(Debug, Default, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DeviceDetails { pub ip_address: String, @@ -172,6 +247,48 @@ impl From for TransactionType { } } +#[derive(Debug, Serialize, Deserialize)] +pub struct NuveiRedirectionResponse { + pub cres: String, +} +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NuveiACSResponse { + #[serde(rename = "threeDSServerTransID")] + pub three_ds_server_trans_id: String, + #[serde(rename = "acsTransID")] + pub acs_trans_id: String, + pub message_type: String, + pub message_version: String, + pub trans_status: Option, + pub message_extension: Vec, + pub acs_signed_content: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MessageExtension { + pub name: String, + pub id: String, + pub criticality_indicator: bool, + pub data: MessageExtensionData, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MessageExtensionData { + pub value_one: String, + pub value_two: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum LiabilityShift { + #[serde(rename = "Y", alias = "1")] + Success, + #[serde(rename = "N", alias = "0")] + Failed, +} + fn encode_payload( payload: Vec, ) -> Result> { @@ -192,7 +309,9 @@ impl TryFrom<&types::PaymentsAuthorizeSessionTokenRouterData> for NuveiSessionRe let merchant_id = connector_meta.merchant_id; let merchant_site_id = connector_meta.merchant_site_id; let client_request_id = item.attempt_id.clone(); - let time_stamp = date_time::date_as_yyyymmddhhmmss(); + let time_stamp = date_time::date_as_yyyymmddhhmmss() + .into_report() + .change_context(errors::ConnectorError::RequestEncodingFailed)?; let merchant_secret = connector_meta.merchant_secret; Ok(Self { merchant_id: merchant_id.clone(), @@ -214,7 +333,7 @@ impl TryFrom> for types::RouterData { - type Error = error_stack::Report; + type Error = error_stack::Report; fn try_from( item: types::ResponseRouterData, ) -> Result { @@ -229,53 +348,237 @@ impl } } -impl TryFrom<&types::PaymentsAuthorizeRouterData> for NuveiPaymentsRequest { +impl + TryFrom<( + &types::RouterData, + String, + )> for NuveiPaymentsRequest +{ type Error = error_stack::Report; - fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { + fn try_from( + data: ( + &types::RouterData, + String, + ), + ) -> Result { + let item = data.0; + let session_token = data.1; + if session_token.is_empty() { + return Err(errors::ConnectorError::MissingRequiredField { + field_name: "session_token", + } + .into()); + } let connector_meta: NuveiAuthType = NuveiAuthType::try_from(&item.connector_auth_type)?; let merchant_id = connector_meta.merchant_id; let merchant_site_id = connector_meta.merchant_site_id; let client_request_id = item.attempt_id.clone(); - let time_stamp = date_time::date_as_yyyymmddhhmmss(); + let time_stamp = date_time::date_as_yyyymmddhhmmss() + .into_report() + .change_context(errors::ConnectorError::RequestEncodingFailed)?; let merchant_secret = connector_meta.merchant_secret; - match item.request.payment_method_data.clone() { - api::PaymentMethodData::Card(card) => Ok(Self { - merchant_id: merchant_id.clone(), - merchant_site_id: merchant_site_id.clone(), - client_request_id: client_request_id.clone(), - amount: item.request.amount.clone().to_string(), - currency: item.request.currency.clone().to_string(), - transaction_type: item - .request - .capture_method - .map(TransactionType::from) - .unwrap_or_default(), + let request_data = match item.request.payment_method_data.clone() { + api::PaymentMethodData::Card(card) => get_card_info(item, &card), + api::PaymentMethodData::Wallet(wallet) => match wallet { + api_models::payments::WalletData::GooglePay(gpay_data) => Ok(Self { + payment_option: PaymentOption { + card: Some(Card { + external_token: Some(ExternalToken { + external_token_provider: ExternalTokenProvider::GooglePay, + mobile_token: gpay_data.tokenization_data.token, + }), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + }), + api_models::payments::WalletData::ApplePay(apple_data) => Ok(Self { + payment_option: PaymentOption { + card: Some(Card { + external_token: Some(ExternalToken { + external_token_provider: ExternalTokenProvider::ApplePay, + mobile_token: apple_data.payment_data, + }), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + }), + api_models::payments::WalletData::PaypalRedirect(_) => Ok(Self { + payment_option: PaymentOption { + alternative_payment_method: Some(AlternativePaymentMethod { + payment_method: AlternativePaymentMethodType::ApmgwExpresscheckout, + }), + ..Default::default() + }, + billing_address: Some(BillingAddress { + email: item.request.get_email()?, + country: item.get_billing_country()?, + }), + ..Default::default() + }), + _ => Err(errors::ConnectorError::NotSupported { + payment_method: "Wallet".to_string(), + connector: "Nuvei", + payment_experience: "RedirectToUrl".to_string(), + } + .into()), + }, + _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), + }?; + Ok(Self { + merchant_id: merchant_id.clone(), + merchant_site_id: merchant_site_id.clone(), + client_request_id: client_request_id.clone(), + amount: item.request.amount.clone().to_string(), + currency: item.request.currency.clone().to_string(), + transaction_type: item + .request + .capture_method + .map(TransactionType::from) + .unwrap_or_default(), + time_stamp: time_stamp.clone(), + session_token, + checksum: encode_payload(vec![ + merchant_id, + merchant_site_id, + client_request_id, + item.request.amount.to_string(), + item.request.currency.to_string(), + time_stamp, + merchant_secret, + ])?, + ..request_data + }) + } +} +fn get_card_info( + item: &types::RouterData, + card_details: &api_models::payments::Card, +) -> Result> { + let browser_info = item.request.get_browser_info()?; + let related_transaction_id = if item.request.enrolled_for_3ds { + item.request.related_transaction_id.clone() + } else { + None + }; + let three_d = if item.request.enrolled_for_3ds { + Some(ThreeD { + browser_details: Some(BrowserDetails { + accept_header: browser_info.accept_header, + ip: browser_info.ip_address, + java_enabled: browser_info.java_enabled.to_string().to_uppercase(), + java_script_enabled: browser_info.java_script_enabled.to_string().to_uppercase(), + language: browser_info.language, + color_depth: browser_info.color_depth, + screen_height: browser_info.screen_height, + screen_width: browser_info.screen_width, + time_zone: browser_info.time_zone, + user_agent: browser_info.user_agent, + }), + notification_url: item.complete_authorize_url.clone(), + merchant_url: item.return_url.clone(), + platform_type: Some(PlatformType::Browser), + method_completion_ind: Some(MethodCompletion::Unavailable), + ..Default::default() + }) + } else { + None + }; + let card = card_details.clone(); + Ok(NuveiPaymentsRequest { + related_transaction_id, + payment_option: PaymentOption { + card: Some(Card { + card_number: Some(card.card_number), + card_holder_name: Some(card.card_holder_name), + expiration_month: Some(card.card_exp_month), + expiration_year: Some(card.card_exp_year), + three_d, + cvv: Some(card.card_cvc), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + }) +} + +impl + TryFrom<( + &types::RouterData, + String, + )> for NuveiPaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + data: ( + &types::RouterData, + String, + ), + ) -> Result { + let item = data.0; + let session_token = data.1; + if session_token.is_empty() { + return Err(errors::ConnectorError::MissingRequiredField { + field_name: "session_token", + } + .into()); + } + let connector_meta: NuveiAuthType = NuveiAuthType::try_from(&item.connector_auth_type)?; + let merchant_id = connector_meta.merchant_id; + let merchant_site_id = connector_meta.merchant_site_id; + let client_request_id = item.attempt_id.clone(); + let time_stamp = date_time::date_as_yyyymmddhhmmss() + .into_report() + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let merchant_secret = connector_meta.merchant_secret; + let request_data = match item.request.payment_method_data.clone() { + Some(api::PaymentMethodData::Card(card)) => Ok(Self { + related_transaction_id: item.request.connector_transaction_id.clone(), payment_option: PaymentOption { - card: Card { + card: Some(Card { card_number: Some(card.card_number), card_holder_name: Some(card.card_holder_name), expiration_month: Some(card.card_exp_month), expiration_year: Some(card.card_exp_year), cvv: Some(card.card_cvc), ..Default::default() - }, + }), ..Default::default() }, - time_stamp: time_stamp.clone(), - session_token: item.get_session_token()?, - checksum: encode_payload(vec![ - merchant_id, - merchant_site_id, - client_request_id, - item.request.amount.to_string(), - item.request.currency.to_string(), - time_stamp, - merchant_secret, - ])?, ..Default::default() }), - _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), - } + _ => Err(errors::ConnectorError::NotImplemented( + "Payment methods".to_string(), + )), + }?; + Ok(Self { + merchant_id: merchant_id.clone(), + merchant_site_id: merchant_site_id.clone(), + client_request_id: client_request_id.clone(), + amount: item.request.amount.clone().to_string(), + currency: item.request.currency.clone().to_string(), + transaction_type: item + .request + .capture_method + .map(TransactionType::from) + .unwrap_or_default(), + time_stamp: time_stamp.clone(), + session_token, + checksum: encode_payload(vec![ + merchant_id, + merchant_site_id, + client_request_id, + item.request.amount.to_string(), + item.request.currency.to_string(), + time_stamp, + merchant_secret, + ])?, + ..request_data + }) } } @@ -286,7 +589,9 @@ impl TryFrom<&types::PaymentsCaptureRouterData> for NuveiPaymentFlowRequest { let merchant_id = connector_meta.merchant_id; let merchant_site_id = connector_meta.merchant_site_id; let client_request_id = item.attempt_id.clone(); - let time_stamp = date_time::date_as_yyyymmddhhmmss(); + let time_stamp = date_time::date_as_yyyymmddhhmmss() + .into_report() + .change_context(errors::ConnectorError::RequestEncodingFailed)?; let merchant_secret = connector_meta.merchant_secret; Ok(Self { merchant_id: merchant_id.clone(), @@ -317,7 +622,9 @@ impl TryFrom<&types::RefundExecuteRouterData> for NuveiPaymentFlowRequest { let merchant_id = connector_meta.merchant_id; let merchant_site_id = connector_meta.merchant_site_id; let client_request_id = item.attempt_id.clone(); - let time_stamp = date_time::date_as_yyyymmddhhmmss(); + let time_stamp = date_time::date_as_yyyymmddhhmmss() + .into_report() + .change_context(errors::ConnectorError::RequestEncodingFailed)?; let merchant_secret = connector_meta.merchant_secret; Ok(Self { merchant_id: merchant_id.clone(), @@ -344,7 +651,7 @@ impl TryFrom<&types::RefundExecuteRouterData> for NuveiPaymentFlowRequest { impl TryFrom<&types::PaymentsSyncRouterData> for NuveiPaymentSyncRequest { type Error = error_stack::Report; fn try_from(value: &types::PaymentsSyncRouterData) -> Result { - let meta: NuveiMeta = value.to_connector_meta()?; + let meta: NuveiMeta = utils::to_connector_meta(value.request.connector_meta.clone())?; Ok(Self { session_token: meta.session_token, }) @@ -358,7 +665,9 @@ impl TryFrom<&types::PaymentsCancelRouterData> for NuveiPaymentFlowRequest { let merchant_id = connector_meta.merchant_id; let merchant_site_id = connector_meta.merchant_site_id; let client_request_id = item.attempt_id.clone(); - let time_stamp = date_time::date_as_yyyymmddhhmmss(); + let time_stamp = date_time::date_as_yyyymmddhhmmss() + .into_report() + .change_context(errors::ConnectorError::RequestEncodingFailed)?; let merchant_secret = connector_meta.merchant_secret; let amount = item.request.get_amount()?.to_string(); let currency = item.request.get_currency()?.to_string(); @@ -427,6 +736,7 @@ pub enum NuveiTransactionStatus { Approved, Declined, Error, + Redirect, #[default] Processing, } @@ -441,7 +751,7 @@ impl From for enums::AttemptStatus { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NuveiPaymentsResponse { pub order_id: Option, @@ -472,16 +782,18 @@ pub struct NuveiPaymentsResponse { pub client_request_id: Option, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub enum NuveiTransactionType { Auth, Sale, Credit, + Auth3D, + InitAuth3D, Settle, Void, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct FraudDetails { pub final_decision: String, @@ -505,10 +817,14 @@ fn get_payment_status(response: &NuveiPaymentsResponse) -> enums::AttemptStatus enums::AttemptStatus::Failure } Some(NuveiTransactionType::Void) => enums::AttemptStatus::VoidFailed, + Some(NuveiTransactionType::Auth3D) => { + enums::AttemptStatus::AuthenticationFailed + } _ => enums::AttemptStatus::Pending, } } NuveiTransactionStatus::Processing => enums::AttemptStatus::Pending, + NuveiTransactionStatus::Redirect => enums::AttemptStatus::AuthenticationPending, }, None => match response.status { NuveiPaymentStatus::Failed | NuveiPaymentStatus::Error => enums::AttemptStatus::Failure, @@ -521,10 +837,22 @@ impl TryFrom> for types::RouterData { - type Error = error_stack::Report; + type Error = error_stack::Report; fn try_from( item: types::ResponseRouterData, ) -> Result { + let redirection_data = item + .response + .payment_option + .as_ref() + .and_then(|o| o.card.clone()) + .and_then(|card| card.three_d) + .and_then(|three_ds| three_ds.acs_url.zip(three_ds.c_req)) + .map(|(base_url, creq)| services::RedirectForm { + endpoint: base_url, + method: services::Method::Post, + form_fields: std::collections::HashMap::from([("creq".to_string(), creq)]), + }); Ok(Self { status: get_payment_status(&item.response), response: match item.response.status { @@ -556,21 +884,24 @@ impl status_code: item.http_code, }), _ => Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId( - item.response.transaction_id.ok_or(errors::ParsingError)?, + resource_id: item.response.transaction_id.map_or_else( + || types::ResponseId::NoResponseId, + types::ResponseId::ConnectorTransactionId, ), - redirection_data: None, + redirection_data, mandate_reference: None, - connector_metadata: Some( - serde_json::to_value(NuveiMeta { - session_token: item - .response - .session_token - .ok_or(errors::ParsingError)?, - }) - .into_report() - .change_context(errors::ParsingError)?, - ), + // we don't need to save session token for capture, void flow so ignoring if it is not present + connector_metadata: if let Some(token) = item.response.session_token { + Some( + serde_json::to_value(NuveiMeta { + session_token: token, + }) + .into_report() + .change_context(errors::ConnectorError::ResponseHandlingFailed)?, + ) + } else { + None + }, }), }, }, @@ -579,12 +910,32 @@ impl } } +impl TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData, + ) -> Result { + Ok(Self { + status: enums::AttemptStatus::AuthenticationFailed, + response: Err(types::ErrorResponse { + code: consts::NO_ERROR_CODE.to_string(), + message: "Authentication Failed".to_string(), + reason: None, + status_code: item.http_code, + }), + ..item.data + }) + } +} + impl From for enums::RefundStatus { fn from(item: NuveiTransactionStatus) -> Self { match item { NuveiTransactionStatus::Approved => Self::Success, NuveiTransactionStatus::Declined | NuveiTransactionStatus::Error => Self::Failure, - NuveiTransactionStatus::Processing => Self::Pending, + NuveiTransactionStatus::Processing | NuveiTransactionStatus::Redirect => Self::Pending, } } } @@ -592,49 +943,18 @@ impl From for enums::RefundStatus { impl TryFrom> for types::RefundsRouterData { - type Error = error_stack::Report; + type Error = error_stack::Report; fn try_from( item: types::RefundsResponseRouterData, ) -> Result { - let response = item.response; - let http_code = item.http_code; - let refund_status = response - .transaction_status - .clone() - .map(|a| a.into()) - .unwrap_or(enums::RefundStatus::Failure); - let refund_response = match response.status { - NuveiPaymentStatus::Error => Err(types::ErrorResponse { - code: response - .err_code - .map(|c| c.to_string()) - .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), - message: response - .reason - .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), - reason: None, - status_code: http_code, - }), - _ => match response.transaction_status { - Some(NuveiTransactionStatus::Error) => Err(types::ErrorResponse { - code: response - .gw_error_code - .map(|c| c.to_string()) - .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), - message: response - .gw_error_reason - .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), - reason: None, - status_code: http_code, - }), - _ => Ok(types::RefundsResponseData { - connector_refund_id: response.transaction_id.ok_or(errors::ParsingError)?, - refund_status, - }), - }, - }; Ok(Self { - response: refund_response, + response: get_refund_response( + item.response.clone(), + item.http_code, + item.response + .transaction_id + .ok_or(errors::ConnectorError::MissingConnectorTransactionID)?, + ), ..item.data }) } @@ -643,54 +963,61 @@ impl TryFrom> for types::RefundsRouterData { - type Error = error_stack::Report; + type Error = error_stack::Report; fn try_from( item: types::RefundsResponseRouterData, ) -> Result { - let response = item.response; - let http_code = item.http_code; - let refund_status = response - .transaction_status - .clone() - .map(|a| a.into()) - .unwrap_or(enums::RefundStatus::Failure); - let refund_response = match response.status { - NuveiPaymentStatus::Error => Err(types::ErrorResponse { - code: response - .err_code - .map(|c| c.to_string()) - .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), - message: response - .reason - .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), - reason: None, - status_code: http_code, - }), - _ => match response.transaction_status { - Some(NuveiTransactionStatus::Error) => Err(types::ErrorResponse { - code: response - .gw_error_code - .map(|c| c.to_string()) - .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), - message: response - .gw_error_reason - .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), - reason: None, - status_code: http_code, - }), - _ => Ok(types::RefundsResponseData { - connector_refund_id: response.transaction_id.ok_or(errors::ParsingError)?, - refund_status, - }), - }, - }; Ok(Self { - response: refund_response, + response: get_refund_response( + item.response.clone(), + item.http_code, + item.response + .transaction_id + .ok_or(errors::ConnectorError::MissingConnectorTransactionID)?, + ), ..item.data }) } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct NuveiErrorResponse {} +fn get_refund_response( + response: NuveiPaymentsResponse, + http_code: u16, + txn_id: String, +) -> Result { + let refund_status = response + .transaction_status + .clone() + .map(enums::RefundStatus::from) + .unwrap_or(enums::RefundStatus::Failure); + match response.status { + NuveiPaymentStatus::Error => Err(types::ErrorResponse { + code: response + .err_code + .map(|c| c.to_string()) + .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), + message: response + .reason + .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), + reason: None, + status_code: http_code, + }), + _ => match response.transaction_status { + Some(NuveiTransactionStatus::Error) => Err(types::ErrorResponse { + code: response + .gw_error_code + .map(|c| c.to_string()) + .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), + message: response + .gw_error_reason + .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), + reason: None, + status_code: http_code, + }), + _ => Ok(types::RefundsResponseData { + connector_refund_id: txn_id, + refund_status, + }), + }, + } +} diff --git a/crates/router/src/connector/payu.rs b/crates/router/src/connector/payu.rs index e29b55760e..32cbfcb371 100644 --- a/crates/router/src/connector/payu.rs +++ b/crates/router/src/connector/payu.rs @@ -7,10 +7,7 @@ use transformers as payu; use crate::{ configs::settings, - core::{ - errors::{self, CustomResult}, - payments, - }, + core::errors::{self, CustomResult}, headers, services::{self, ConnectorIntegration}, types::{ @@ -692,12 +689,3 @@ impl api::IncomingWebhook for Payu { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } - -impl services::ConnectorRedirectResponse for Payu { - fn get_flow_type( - &self, - _query_params: &str, - ) -> CustomResult { - Ok(payments::CallConnectorAction::Trigger) - } -} diff --git a/crates/router/src/connector/rapyd.rs b/crates/router/src/connector/rapyd.rs index 8c0c47409e..0c1d6fa44a 100644 --- a/crates/router/src/connector/rapyd.rs +++ b/crates/router/src/connector/rapyd.rs @@ -12,10 +12,7 @@ use crate::{ configs::settings, connector::utils as conn_utils, consts, - core::{ - errors::{self, CustomResult}, - payments, - }, + core::errors::{self, CustomResult}, db::StorageInterface, headers, services, types::{ @@ -806,12 +803,3 @@ impl api::IncomingWebhook for Rapyd { Ok(res_json) } } - -impl services::ConnectorRedirectResponse for Rapyd { - fn get_flow_type( - &self, - _query_params: &str, - ) -> CustomResult { - Ok(payments::CallConnectorAction::Trigger) - } -} diff --git a/crates/router/src/connector/shift4.rs b/crates/router/src/connector/shift4.rs index ee4386e210..b70c660578 100644 --- a/crates/router/src/connector/shift4.rs +++ b/crates/router/src/connector/shift4.rs @@ -10,10 +10,7 @@ use super::utils::RefundsRequestData; use crate::{ configs::settings, consts, - core::{ - errors::{self, CustomResult}, - payments, - }, + core::errors::{self, CustomResult}, headers, services::{self, ConnectorIntegration}, types::{ @@ -530,12 +527,3 @@ impl api::IncomingWebhook for Shift4 { Ok(details.data) } } - -impl services::ConnectorRedirectResponse for Shift4 { - fn get_flow_type( - &self, - _query_params: &str, - ) -> CustomResult { - Ok(payments::CallConnectorAction::Trigger) - } -} diff --git a/crates/router/src/connector/stripe.rs b/crates/router/src/connector/stripe.rs index 1ae8722b67..12f50e7c14 100644 --- a/crates/router/src/connector/stripe.rs +++ b/crates/router/src/connector/stripe.rs @@ -974,6 +974,8 @@ impl services::ConnectorRedirectResponse for Stripe { fn get_flow_type( &self, query_params: &str, + _json_payload: Option, + _action: services::PaymentAction, ) -> CustomResult { let query = serde_urlencoded::from_str::(query_params) diff --git a/crates/router/src/connector/trustpay.rs b/crates/router/src/connector/trustpay.rs index 902ebeee4e..53e0f7b440 100644 --- a/crates/router/src/connector/trustpay.rs +++ b/crates/router/src/connector/trustpay.rs @@ -611,6 +611,8 @@ impl services::ConnectorRedirectResponse for Trustpay { fn get_flow_type( &self, query_params: &str, + _json_payload: Option, + _action: services::PaymentAction, ) -> CustomResult { let query = serde_urlencoded::from_str::(query_params) diff --git a/crates/router/src/connector/trustpay/transformers.rs b/crates/router/src/connector/trustpay/transformers.rs index de0e476d6c..c20a9e27aa 100644 --- a/crates/router/src/connector/trustpay/transformers.rs +++ b/crates/router/src/connector/trustpay/transformers.rs @@ -264,7 +264,7 @@ fn get_bank_redirection_request_data( }, }, callback_urls: CallbackURLs { - success: format!("{}?status=SuccessOk", return_url), + success: format!("{return_url}?status=SuccessOk"), cancel: return_url.clone(), error: return_url, }, @@ -816,7 +816,7 @@ impl TryFrom<&types::RefundsRouterData> for TrustpayRefundRequest { }, payment_information: BankPaymentInformation { amount: Amount { - amount: format!("{:.2}", amount), + amount: format!("{amount:.2}"), currency: item.request.currency.to_string(), }, references: References { diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 752e8d9c8a..861605a16d 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -1,6 +1,10 @@ use std::collections::HashMap; -use common_utils::{ext_traits::ValueExt, pii}; +use base64::Engine; +use common_utils::{ + errors::ReportSwitchExt, + pii::{self, Email}, +}; use error_stack::{report, IntoReport, ResultExt}; use masking::Secret; use once_cell::sync::Lazy; @@ -8,10 +12,11 @@ use regex::Regex; use serde::Serializer; use crate::{ + consts, core::errors::{self, CustomResult}, pii::PeekInterface, types::{self, api, PaymentsCancelData, ResponseId}, - utils::OptionExt, + utils::{OptionExt, ValueExt}, }; pub fn missing_field_err( @@ -52,6 +57,7 @@ pub trait RouterData { where T: serde::de::DeserializeOwned; fn get_return_url(&self) -> Result; + fn is_three_ds(&self) -> bool; } impl RouterData for types::RouterData { @@ -116,29 +122,40 @@ impl RouterData for types::RouterData Result; -} - -impl PaymentsRequestData for types::PaymentsAuthorizeRouterData { - fn get_card(&self) -> Result { - match self.request.payment_method_data.clone() { - api::PaymentMethodData::Card(card) => Ok(card), - _ => Err(missing_field_err("card")()), - } + fn is_three_ds(&self) -> bool { + matches!( + self.auth_type, + storage_models::enums::AuthenticationType::ThreeDs + ) } } pub trait PaymentsAuthorizeRequestData { fn is_auto_capture(&self) -> bool; + fn get_email(&self) -> Result, Error>; + fn get_browser_info(&self) -> Result; + fn get_card(&self) -> Result; } impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData { fn is_auto_capture(&self) -> bool { self.capture_method == Some(storage_models::enums::CaptureMethod::Automatic) } + fn get_email(&self) -> Result, Error> { + self.email.clone().ok_or_else(missing_field_err("email")) + } + fn get_browser_info(&self) -> Result { + self.browser_info + .clone() + .ok_or_else(missing_field_err("browser_info")) + } + fn get_card(&self) -> Result { + match self.payment_method_data.clone() { + api::PaymentMethodData::Card(card) => Ok(card), + _ => Err(missing_field_err("card")()), + } + } } pub trait PaymentsSyncRequestData { @@ -352,6 +369,65 @@ pub fn get_header_key_value<'a>( ))? } +pub fn to_boolean(string: String) -> bool { + let str = string.as_str(); + match str { + "true" => true, + "false" => false, + "yes" => true, + "no" => false, + _ => false, + } +} + +pub fn get_connector_meta( + connector_meta: Option, +) -> Result { + connector_meta.ok_or_else(missing_field_err("connector_meta_data")) +} + +pub fn to_connector_meta(connector_meta: Option) -> Result +where + T: serde::de::DeserializeOwned, +{ + let json = connector_meta.ok_or_else(missing_field_err("connector_meta_data"))?; + json.parse_value(std::any::type_name::()).switch() +} + +pub fn to_connector_meta_from_secret( + connector_meta: Option>, +) -> Result +where + T: serde::de::DeserializeOwned, +{ + let connector_meta_secret = + connector_meta.ok_or_else(missing_field_err("connector_meta_data"))?; + let json = connector_meta_secret.peek().clone(); + json.parse_value(std::any::type_name::()).switch() +} + +impl common_utils::errors::ErrorSwitch for errors::ParsingError { + fn switch(&self) -> errors::ConnectorError { + errors::ConnectorError::ParsingFailed + } +} + +pub fn to_string(data: &T) -> Result +where + T: serde::Serialize, +{ + serde_json::to_string(data) + .into_report() + .change_context(errors::ConnectorError::ResponseHandlingFailed) +} + +pub fn base64_decode(data: String) -> Result, Error> { + consts::BASE64_ENGINE + .decode(data) + .into_report() + .change_context(errors::ConnectorError::ResponseDeserializationFailed) +} + pub fn to_currency_base_unit_from_optional_amount( amount: Option, currency: storage_models::enums::Currency, diff --git a/crates/router/src/connector/worldline.rs b/crates/router/src/connector/worldline.rs index b538fb46b7..8f452bda01 100644 --- a/crates/router/src/connector/worldline.rs +++ b/crates/router/src/connector/worldline.rs @@ -773,5 +773,3 @@ impl api::IncomingWebhook for Worldline { Ok(response) } } - -impl services::ConnectorRedirectResponse for Worldline {} diff --git a/crates/router/src/connector/worldpay.rs b/crates/router/src/connector/worldpay.rs index c0a1897ef0..a6a33ef69c 100644 --- a/crates/router/src/connector/worldpay.rs +++ b/crates/router/src/connector/worldpay.rs @@ -12,10 +12,7 @@ use self::{requests::*, response::*}; use super::utils::RefundsRequestData; use crate::{ configs::settings, - core::{ - errors::{self, CustomResult}, - payments, - }, + core::errors::{self, CustomResult}, headers, services::{self, ConnectorIntegration}, types::{ @@ -611,12 +608,3 @@ impl api::IncomingWebhook for Worldpay { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } - -impl services::ConnectorRedirectResponse for Worldpay { - fn get_flow_type( - &self, - _query_params: &str, - ) -> CustomResult { - Ok(payments::CallConnectorAction::Trigger) - } -} diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index 52f93999a3..e72d87cf57 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -227,6 +227,8 @@ pub enum ConnectorError { RequestEncodingFailed, #[error("Request encoding failed : {0}")] RequestEncodingFailedWithReason(String), + #[error("Parsing failed")] + ParsingFailed, #[error("Failed to deserialize connector response")] ResponseDeserializationFailed, #[error("Failed to execute a processing step: {0:?}")] diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 06f5d192f8..50dfca86ca 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -18,7 +18,7 @@ pub use self::operations::{ }; use self::{ flows::{ConstructFlowSpecificData, Feature}, - operations::{BoxedOperation, Operation}, + operations::{payment_complete_authorize, BoxedOperation, Operation}, }; use crate::{ core::{ @@ -193,7 +193,11 @@ where .await? } }; - vault::Vault::delete_locker_payment_method_by_lookup_key(state, &payment_data.token).await + if payment_data.payment_intent.status != storage_enums::IntentStatus::RequiresCustomerAction + { + vault::Vault::delete_locker_payment_method_by_lookup_key(state, &payment_data.token) + .await + } } Ok((payment_data, req, customer)) } @@ -247,80 +251,149 @@ fn is_start_pay(operation: &Op) -> bool { format!("{operation:?}").eq("PaymentStart") } -#[allow(clippy::too_many_arguments)] -pub async fn handle_payments_redirect_response<'a, F>( - state: &AppState, - merchant_account: storage::MerchantAccount, - req: api::PaymentsRetrieveRequest, -) -> RouterResponse -where - F: Send + Clone + 'a, -{ - let connector = req.connector.clone().get_required_value("connector")?; +#[derive(Clone, Debug)] +pub struct PaymentsRedirectResponseData { + pub connector: Option, + pub param: Option, + pub merchant_id: Option, + pub json_payload: Option, + pub resource_id: api::PaymentIdType, + pub force_sync: bool, +} - let query_params = req.param.clone().get_required_value("param")?; +#[async_trait::async_trait] +pub trait PaymentRedirectFlow: Sync { + async fn call_payment_flow( + &self, + state: &AppState, + merchant_account: storage::MerchantAccount, + req: PaymentsRedirectResponseData, + connector_action: CallConnectorAction, + ) -> RouterResponse; - let resource_id = api::PaymentIdTypeExt::get_payment_intent_id(&req.resource_id) - .change_context(errors::ApiErrorResponse::MissingRequiredField { - field_name: "payment_id", - })?; + fn get_payment_action(&self) -> services::PaymentAction; - let connector_data = api::ConnectorData::get_connector_by_name( - &state.conf.connectors, - &connector, - api::GetToken::Connector, - )?; + #[allow(clippy::too_many_arguments)] + async fn handle_payments_redirect_response( + &self, + state: &AppState, + merchant_account: storage::MerchantAccount, + req: PaymentsRedirectResponseData, + ) -> RouterResponse { + let connector = req.connector.clone().get_required_value("connector")?; - let flow_type = connector_data - .connector - .get_flow_type(&query_params) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to decide the response flow")?; + let query_params = req.param.clone().get_required_value("param")?; - let response = payments_response_for_redirection_flows( - state, - merchant_account.clone(), - req.clone(), - flow_type, - ) - .await; + let resource_id = api::PaymentIdTypeExt::get_payment_intent_id(&req.resource_id) + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "payment_id", + })?; - let payments_response = - match response.change_context(errors::ApiErrorResponse::NotImplemented { - message: errors::api_error_response::NotImplementedMessage::Default, - })? { + let connector_data = api::ConnectorData::get_connector_by_name( + &state.conf.connectors, + &connector, + api::GetToken::Connector, + )?; + + let flow_type = connector_data + .connector + .get_flow_type( + &query_params, + req.json_payload.clone(), + self.get_payment_action(), + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to decide the response flow")?; + + let response = self + .call_payment_flow(state, merchant_account.clone(), req.clone(), flow_type) + .await; + + let payments_response = match response? { services::ApplicationResponse::Json(response) => Ok(response), _ => Err(errors::ApiErrorResponse::InternalServerError) .into_report() .attach_printable("Failed to get the response in json"), }?; - let result = helpers::get_handle_response_url( - resource_id, - &merchant_account, - payments_response, - connector, - ) - .attach_printable("No redirection response")?; + let result = helpers::get_handle_response_url( + resource_id, + &merchant_account, + payments_response, + connector, + ) + .attach_printable("No redirection response")?; - Ok(services::ApplicationResponse::JsonForRedirection(result)) + Ok(services::ApplicationResponse::JsonForRedirection(result)) + } } -pub async fn payments_response_for_redirection_flows<'a>( - state: &AppState, - merchant_account: storage::MerchantAccount, - req: api::PaymentsRetrieveRequest, - flow_type: CallConnectorAction, -) -> RouterResponse { - payments_core::( - state, - merchant_account, - PaymentStatus, - req, - services::api::AuthFlow::Merchant, - flow_type, - ) - .await +#[derive(Clone, Debug)] +pub struct PaymentRedirectCompleteAuthorize; + +#[async_trait::async_trait] +impl PaymentRedirectFlow for PaymentRedirectCompleteAuthorize { + async fn call_payment_flow( + &self, + state: &AppState, + merchant_account: storage::MerchantAccount, + req: PaymentsRedirectResponseData, + connector_action: CallConnectorAction, + ) -> RouterResponse { + let payment_confirm_req = api::PaymentsRequest { + payment_id: Some(req.resource_id.clone()), + merchant_id: req.merchant_id.clone(), + ..Default::default() + }; + payments_core::( + state, + merchant_account, + payment_complete_authorize::CompleteAuthorize, + payment_confirm_req, + services::api::AuthFlow::Merchant, + connector_action, + ) + .await + } + + fn get_payment_action(&self) -> services::PaymentAction { + services::PaymentAction::CompleteAuthorize + } +} + +#[derive(Clone, Debug)] +pub struct PaymentRedirectSync; + +#[async_trait::async_trait] +impl PaymentRedirectFlow for PaymentRedirectSync { + async fn call_payment_flow( + &self, + state: &AppState, + merchant_account: storage::MerchantAccount, + req: PaymentsRedirectResponseData, + connector_action: CallConnectorAction, + ) -> RouterResponse { + let payment_sync_req = api::PaymentsRetrieveRequest { + resource_id: req.resource_id, + merchant_id: req.merchant_id, + param: req.param, + force_sync: req.force_sync, + connector: req.connector, + }; + payments_core::( + state, + merchant_account, + PaymentStatus, + payment_sync_req, + services::api::AuthFlow::Merchant, + connector_action, + ) + .await + } + + fn get_payment_action(&self) -> services::PaymentAction { + services::PaymentAction::PSync + } } #[allow(clippy::too_many_arguments)] @@ -479,6 +552,7 @@ where Ok(payment_data) } +#[derive(Clone, Debug)] pub enum CallConnectorAction { Trigger, Avoid, @@ -598,6 +672,7 @@ pub fn should_call_connector( storage_enums::IntentStatus::RequiresCapture ) } + "CompleteAuthorize" => true, "PaymentSession" => true, _ => false, } diff --git a/crates/router/src/core/payments/access_token.rs b/crates/router/src/core/payments/access_token.rs index 38d8320d9d..0ff7a1baee 100644 --- a/crates/router/src/core/payments/access_token.rs +++ b/crates/router/src/core/payments/access_token.rs @@ -38,6 +38,7 @@ pub fn router_data_type_conversion( connector_meta_data: router_data.connector_meta_data, description: router_data.description, router_return_url: router_data.router_return_url, + complete_authorize_url: router_data.complete_authorize_url, payment_id: router_data.payment_id, payment_method: router_data.payment_method, payment_method_id: router_data.payment_method_id, diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index f46be7e07c..75bd16a4f3 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -1,6 +1,7 @@ pub mod authorize_flow; pub mod cancel_flow; pub mod capture_flow; +pub mod complete_authorize_flow; pub mod psync_flow; pub mod session_flow; pub mod verfiy_flow; @@ -8,7 +9,11 @@ pub mod verfiy_flow; use async_trait::async_trait; use crate::{ - core::{errors::RouterResult, payments}, + connector, + core::{ + errors::{ConnectorError, CustomResult, RouterResult}, + payments, + }, routes::AppState, services, types::{self, api, storage}, @@ -50,3 +55,82 @@ pub trait Feature { Self: Sized, dyn api::Connector: services::ConnectorIntegration; } + +macro_rules! default_imp_for_complete_authorize{ + ($($path:ident::$connector:ident),*)=> { + $( + impl api::PaymentsCompleteAuthorize for $path::$connector {} + impl + services::ConnectorIntegration< + api::CompleteAuthorize, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + > for $path::$connector + {} + )* + }; +} + +default_imp_for_complete_authorize!( + connector::Aci, + connector::Adyen, + connector::Airwallex, + connector::Applepay, + connector::Authorizedotnet, + connector::Bambora, + connector::Bluesnap, + connector::Braintree, + connector::Checkout, + connector::Cybersource, + connector::Dlocal, + connector::Fiserv, + connector::Globalpay, + connector::Klarna, + connector::Multisafepay, + connector::Payu, + connector::Rapyd, + connector::Shift4, + connector::Stripe, + connector::Trustpay, + connector::Worldline, + connector::Worldpay +); + +macro_rules! default_imp_for_connector_redirect_response{ + ($($path:ident::$connector:ident),*)=> { + $( + impl services::ConnectorRedirectResponse for $path::$connector { + fn get_flow_type( + &self, + _query_params: &str, + _json_payload: Option, + _action: services::PaymentAction + ) -> CustomResult { + Ok(payments::CallConnectorAction::Trigger) + } + } + )* + }; +} + +default_imp_for_connector_redirect_response!( + connector::Aci, + connector::Adyen, + connector::Airwallex, + connector::Applepay, + connector::Authorizedotnet, + connector::Bambora, + connector::Bluesnap, + connector::Braintree, + connector::Cybersource, + connector::Dlocal, + connector::Fiserv, + connector::Globalpay, + connector::Klarna, + connector::Multisafepay, + connector::Payu, + connector::Rapyd, + connector::Shift4, + connector::Worldline, + connector::Worldpay +); diff --git a/crates/router/src/core/payments/flows/authorize_flow.rs b/crates/router/src/core/payments/flows/authorize_flow.rs index 215bcca82a..a63508ac7a 100644 --- a/crates/router/src/core/payments/flows/authorize_flow.rs +++ b/crates/router/src/core/payments/flows/authorize_flow.rs @@ -101,6 +101,7 @@ impl types::PaymentsAuthorizeRouterData { .execute_pretasks(self, state) .await .map_err(|error| error.to_payment_failed_response())?; + self.decide_authentication_type(); let resp = services::execute_connector_processing_step( state, connector_integration, @@ -118,6 +119,14 @@ impl types::PaymentsAuthorizeRouterData { _ => Ok(self.clone()), } } + + fn decide_authentication_type(&mut self) { + if self.auth_type == storage_models::enums::AuthenticationType::ThreeDs + && !self.request.enrolled_for_3ds + { + self.auth_type = storage_models::enums::AuthenticationType::NoThreeDs + } + } } impl mandate::MandateBehaviour for types::PaymentsAuthorizeData { diff --git a/crates/router/src/core/payments/flows/complete_authorize_flow.rs b/crates/router/src/core/payments/flows/complete_authorize_flow.rs new file mode 100644 index 0000000000..0e5f360703 --- /dev/null +++ b/crates/router/src/core/payments/flows/complete_authorize_flow.rs @@ -0,0 +1,104 @@ +use async_trait::async_trait; + +use super::{ConstructFlowSpecificData, Feature}; +use crate::{ + core::{ + errors::{ConnectorErrorExt, RouterResult}, + payments::{self, access_token, transformers, PaymentData}, + }, + routes::AppState, + services, + types::{self, api, storage}, +}; + +#[async_trait] +impl + ConstructFlowSpecificData< + api::CompleteAuthorize, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + > for PaymentData +{ + async fn construct_router_data<'a>( + &self, + state: &AppState, + connector_id: &str, + merchant_account: &storage::MerchantAccount, + ) -> RouterResult< + types::RouterData< + api::CompleteAuthorize, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + >, + > { + transformers::construct_payment_router_data::< + api::CompleteAuthorize, + types::CompleteAuthorizeData, + >(state, self.clone(), connector_id, merchant_account) + .await + } +} + +#[async_trait] +impl Feature + for types::RouterData< + api::CompleteAuthorize, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + > +{ + async fn decide_flows<'a>( + self, + state: &AppState, + connector: &api::ConnectorData, + customer: &Option, + call_connector_action: payments::CallConnectorAction, + _merchant_account: &storage::MerchantAccount, + ) -> RouterResult { + self.decide_flow( + state, + connector, + customer, + Some(true), + call_connector_action, + ) + .await + } + + async fn add_access_token<'a>( + &self, + state: &AppState, + connector: &api::ConnectorData, + merchant_account: &storage::MerchantAccount, + ) -> RouterResult { + access_token::add_access_token(state, connector, merchant_account, self).await + } +} + +impl types::PaymentsCompleteAuthorizeRouterData { + pub async fn decide_flow<'a, 'b>( + &'b self, + state: &'a AppState, + connector: &api::ConnectorData, + _maybe_customer: &Option, + _confirm: Option, + call_connector_action: payments::CallConnectorAction, + ) -> RouterResult { + let connector_integration: services::BoxedConnectorIntegration< + '_, + api::CompleteAuthorize, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + > = connector.connector.get_connector_integration(); + let resp = services::execute_connector_processing_step( + state, + connector_integration, + self, + call_connector_action, + ) + .await + .map_err(|error| error.to_payment_failed_response())?; + + Ok(resp) + } +} diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index e46eb0781d..c1d29377ae 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -351,6 +351,17 @@ pub fn create_redirect_url( server.base_url, payment_attempt.payment_id, payment_attempt.merchant_id, connector_name ) } +pub fn create_complete_authorize_url( + server: &Server, + payment_attempt: &storage::PaymentAttempt, + connector_name: &String, +) -> String { + format!( + "{}/payments/{}/{}/complete/{}", + server.base_url, payment_attempt.payment_id, payment_attempt.merchant_id, connector_name + ) +} + fn validate_recurring_mandate(req: api::MandateValidationFields) -> RouterResult<()> { req.mandate_id.check_value_present("mandate_id")?; diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index 688d08ba16..50e7648bb7 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -1,5 +1,6 @@ pub mod payment_cancel; pub mod payment_capture; +pub mod payment_complete_authorize; pub mod payment_confirm; pub mod payment_create; pub mod payment_method_validate; diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs new file mode 100644 index 0000000000..23de0ebdd4 --- /dev/null +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -0,0 +1,342 @@ +use std::marker::PhantomData; + +use async_trait::async_trait; +use error_stack::ResultExt; +use router_derive::PaymentOperation; +use router_env::{instrument, tracing}; + +use super::{BoxedOperation, Domain, GetTracker, Operation, UpdateTracker, ValidateRequest}; +use crate::{ + core::{ + errors::{self, CustomResult, RouterResult, StorageErrorExt}, + payments::{helpers, operations, CustomerDetails, PaymentAddress, PaymentData}, + utils as core_utils, + }, + db::StorageInterface, + routes::AppState, + types::{ + self, + api::{self, PaymentIdTypeExt}, + storage::{self, enums as storage_enums}, + transformers::ForeignInto, + }, + utils::{self, OptionExt}, +}; + +#[derive(Debug, Clone, Copy, PaymentOperation)] +#[operation(ops = "all", flow = "authorize")] +pub struct CompleteAuthorize; + +#[async_trait] +impl GetTracker, api::PaymentsRequest> for CompleteAuthorize { + #[instrument(skip_all)] + async fn get_trackers<'a>( + &'a self, + state: &'a AppState, + payment_id: &api::PaymentIdType, + request: &api::PaymentsRequest, + mandate_type: Option, + merchant_account: &storage::MerchantAccount, + ) -> RouterResult<( + BoxedOperation<'a, F, api::PaymentsRequest>, + PaymentData, + Option, + )> { + let db = &*state.store; + let merchant_id = &merchant_account.merchant_id; + let storage_scheme = merchant_account.storage_scheme; + let (mut payment_intent, mut payment_attempt, currency, amount, connector_response); + + let payment_id = payment_id + .get_payment_intent_id() + .change_context(errors::ApiErrorResponse::PaymentNotFound)?; + + payment_intent = db + .find_payment_intent_by_payment_id_merchant_id(&payment_id, merchant_id, storage_scheme) + .await + .map_err(|error| { + error.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) + })?; + payment_intent.setup_future_usage = request + .setup_future_usage + .map(ForeignInto::foreign_into) + .or(payment_intent.setup_future_usage); + + helpers::validate_payment_status_against_not_allowed_statuses( + &payment_intent.status, + &[ + storage_enums::IntentStatus::Failed, + storage_enums::IntentStatus::Succeeded, + ], + "confirm", + )?; + + let (token, payment_method, setup_mandate) = helpers::get_token_pm_type_mandate_details( + state, + request, + mandate_type.clone(), + merchant_account, + ) + .await?; + + helpers::authenticate_client_secret( + request.client_secret.as_ref(), + payment_intent.client_secret.as_ref(), + )?; + + let browser_info = request + .browser_info + .clone() + .map(|x| utils::Encode::::encode_to_value(&x)) + .transpose() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "browser_info", + })?; + + payment_attempt = db + .find_payment_attempt_by_payment_id_merchant_id( + &payment_id, + merchant_id, + storage_scheme, + ) + .await + .map_err(|error| { + error.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) + })?; + + let token = token.or_else(|| payment_attempt.payment_token.clone()); + + helpers::validate_pm_or_token_given( + &request.payment_method, + &request.payment_method_data, + &request.payment_method_type, + &mandate_type, + &token, + )?; + + payment_attempt.payment_method = payment_method.or(payment_attempt.payment_method); + payment_attempt.browser_info = browser_info; + payment_attempt.payment_method_type = request + .payment_method_type + .map(|pmt| pmt.foreign_into()) + .or(payment_attempt.payment_method_type); + payment_attempt.payment_experience = request + .payment_experience + .map(|experience| experience.foreign_into()); + currency = payment_attempt.currency.get_required_value("currency")?; + amount = payment_attempt.amount.into(); + + helpers::validate_customer_id_mandatory_cases( + request.shipping.is_some(), + request.billing.is_some(), + request.setup_future_usage.is_some(), + &payment_intent + .customer_id + .clone() + .or_else(|| request.customer_id.clone()), + )?; + + let shipping_address = helpers::get_address_for_payment_request( + db, + request.shipping.as_ref(), + payment_intent.shipping_address_id.as_deref(), + merchant_id, + &payment_intent.customer_id, + ) + .await?; + let billing_address = helpers::get_address_for_payment_request( + db, + request.billing.as_ref(), + payment_intent.billing_address_id.as_deref(), + merchant_id, + &payment_intent.customer_id, + ) + .await?; + + connector_response = db + .find_connector_response_by_payment_id_merchant_id_attempt_id( + &payment_attempt.payment_id, + &payment_attempt.merchant_id, + &payment_attempt.attempt_id, + storage_scheme, + ) + .await + .map_err(|error| { + error.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) + })?; + + payment_intent.shipping_address_id = shipping_address.clone().map(|i| i.address_id); + payment_intent.billing_address_id = billing_address.clone().map(|i| i.address_id); + payment_intent.return_url = request.return_url.as_ref().map(|a| a.to_string()); + + Ok(( + Box::new(self), + PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + connector_response, + amount, + email: request.email.clone(), + mandate_id: None, + setup_mandate, + token, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.foreign_into()), + billing: billing_address.as_ref().map(|a| a.foreign_into()), + }, + confirm: request.confirm, + payment_method_data: request.payment_method_data.clone(), + force_sync: None, + refunds: vec![], + sessions_token: vec![], + card_cvc: request.card_cvc.clone(), + }, + Some(CustomerDetails { + customer_id: request.customer_id.clone(), + name: request.name.clone(), + email: request.email.clone(), + phone: request.phone.clone(), + phone_country_code: request.phone_country_code.clone(), + }), + )) + } +} + +#[async_trait] +impl Domain for CompleteAuthorize { + #[instrument(skip_all)] + async fn get_or_create_customer_details<'a>( + &'a self, + db: &dyn StorageInterface, + payment_data: &mut PaymentData, + request: Option, + merchant_id: &str, + ) -> CustomResult< + ( + BoxedOperation<'a, F, api::PaymentsRequest>, + Option, + ), + errors::StorageError, + > { + helpers::create_customer_if_not_exist( + Box::new(self), + db, + payment_data, + request, + merchant_id, + ) + .await + } + + #[instrument(skip_all)] + async fn make_pm_data<'a>( + &'a self, + state: &'a AppState, + payment_data: &mut PaymentData, + _storage_scheme: storage_enums::MerchantStorageScheme, + ) -> RouterResult<( + BoxedOperation<'a, F, api::PaymentsRequest>, + Option, + )> { + let (op, payment_method_data) = + helpers::make_pm_data(Box::new(self), state, payment_data).await?; + + utils::when(payment_method_data.is_none(), || { + Err(errors::ApiErrorResponse::PaymentMethodNotFound) + })?; + + Ok((op, payment_method_data)) + } + + #[instrument(skip_all)] + async fn add_task_to_process_tracker<'a>( + &'a self, + _state: &'a AppState, + _payment_attempt: &storage::PaymentAttempt, + ) -> CustomResult<(), errors::ApiErrorResponse> { + Ok(()) + } + + async fn get_connector<'a>( + &'a self, + _merchant_account: &storage::MerchantAccount, + state: &AppState, + request: &api::PaymentsRequest, + previously_used_connector: Option<&String>, + ) -> CustomResult { + // Use a new connector in the confirm call or use the same one which was passed when + // creating the payment or if none is passed then use the routing algorithm + let request_connector = request + .connector + .as_ref() + .and_then(|connector| connector.first().map(|c| c.to_string())); + helpers::get_connector_default( + state, + request_connector.as_ref().or(previously_used_connector), + ) + .await + } +} + +#[async_trait] +impl UpdateTracker, api::PaymentsRequest> for CompleteAuthorize { + #[instrument(skip_all)] + async fn update_trackers<'b>( + &'b self, + _db: &dyn StorageInterface, + _payment_id: &api::PaymentIdType, + payment_data: PaymentData, + _customer: Option, + _storage_scheme: storage_enums::MerchantStorageScheme, + ) -> RouterResult<(BoxedOperation<'b, F, api::PaymentsRequest>, PaymentData)> + where + F: 'b + Send, + { + Ok((Box::new(self), payment_data)) + } +} + +impl ValidateRequest for CompleteAuthorize { + #[instrument(skip_all)] + fn validate_request<'a, 'b>( + &'b self, + request: &api::PaymentsRequest, + merchant_account: &'a storage::MerchantAccount, + ) -> RouterResult<( + BoxedOperation<'b, F, api::PaymentsRequest>, + operations::ValidateResult<'a>, + )> { + let given_payment_id = match &request.payment_id { + Some(id_type) => Some( + id_type + .get_payment_intent_id() + .change_context(errors::ApiErrorResponse::PaymentNotFound)?, + ), + None => None, + }; + + let request_merchant_id = request.merchant_id.as_deref(); + helpers::validate_merchant_id(&merchant_account.merchant_id, request_merchant_id) + .change_context(errors::ApiErrorResponse::InvalidDataFormat { + field_name: "merchant_id".to_string(), + expected_format: "merchant_id from merchant account".to_string(), + })?; + + helpers::validate_payment_method_fields_present(request)?; + + let mandate_type = helpers::validate_mandate(request)?; + let payment_id = core_utils::get_or_generate_id("payment_id", &given_payment_id, "pay")?; + + Ok(( + Box::new(self), + operations::ValidateResult { + merchant_id: &merchant_account.merchant_id, + payment_id: api::PaymentIdType::PaymentIntentId(payment_id), + mandate_type, + storage_scheme: merchant_account.storage_scheme, + }, + )) + } +} diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 43f08b9391..1455c03883 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -22,7 +22,7 @@ use crate::{ #[derive(Debug, Clone, Copy, router_derive::PaymentOperation)] #[operation( ops = "post_tracker", - flow = "syncdata,authorizedata,canceldata,capturedata,verifydata,sessiondata" + flow = "syncdata,authorizedata,canceldata,capturedata,completeauthorizedata,verifydata,sessiondata" )] pub struct PaymentResponse; @@ -262,6 +262,26 @@ impl PostUpdateTracker, types::VerifyRequestData> fo } } +#[async_trait] +impl PostUpdateTracker, types::CompleteAuthorizeData> + for PaymentResponse +{ + async fn update_tracker<'b>( + &'b self, + db: &dyn StorageInterface, + payment_id: &api::PaymentIdType, + payment_data: PaymentData, + response: types::RouterData, + storage_scheme: enums::MerchantStorageScheme, + ) -> RouterResult> + where + F: 'b + Send, + { + payment_response_update_tracker(db, payment_id, payment_data, response, storage_scheme) + .await + } +} + async fn payment_response_update_tracker( db: &dyn StorageInterface, _payment_id: &api::PaymentIdType, diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 9c0d3833cb..1889ecabe8 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -74,6 +74,11 @@ where &payment_data.payment_attempt, &merchant_connector_account.connector_name, )); + let complete_authorize_url = Some(helpers::create_complete_authorize_url( + &state.conf.server, + &payment_data.payment_attempt, + &merchant_connector_account.connector_name, + )); router_data = types::RouterData { flow: PhantomData, @@ -87,6 +92,7 @@ where description: payment_data.payment_intent.description.clone(), return_url: payment_data.payment_intent.return_url.clone(), router_return_url, + complete_authorize_url, payment_method_id: payment_data.payment_attempt.payment_method_id.clone(), address: payment_data.address.clone(), auth_type: payment_data @@ -440,6 +446,9 @@ impl TryFrom> for types::PaymentsAuthorizeData { email: payment_data.email, payment_experience: payment_data.payment_attempt.payment_experience, order_details, + session_token: None, + enrolled_for_3ds: false, + related_transaction_id: None, payment_method_type: payment_data.payment_attempt.payment_method_type, }) } @@ -458,6 +467,7 @@ impl TryFrom> for types::PaymentsSyncData { }, encoded_data: payment_data.connector_response.encoded_data, capture_method: payment_data.payment_attempt.capture_method, + connector_meta: payment_data.payment_attempt.connector_metadata, }) } } @@ -547,3 +557,34 @@ impl TryFrom> for types::VerifyRequestData { }) } } + +impl TryFrom> for types::CompleteAuthorizeData { + type Error = error_stack::Report; + + fn try_from(payment_data: PaymentData) -> Result { + let browser_info: Option = payment_data + .payment_attempt + .browser_info + .map(|b| b.parse_value("BrowserInformation")) + .transpose() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "browser_info", + })?; + Ok(Self { + setup_future_usage: payment_data.payment_intent.setup_future_usage, + mandate_id: payment_data.mandate_id.clone(), + off_session: payment_data.mandate_id.as_ref().map(|_| true), + setup_mandate_details: payment_data.setup_mandate.clone(), + confirm: payment_data.payment_attempt.confirm, + statement_descriptor_suffix: payment_data.payment_intent.statement_descriptor_suffix, + capture_method: payment_data.payment_attempt.capture_method, + amount: payment_data.amount.into(), + currency: payment_data.currency, + browser_info, + email: payment_data.email, + payment_method_data: payment_data.payment_method_data, + connector_transaction_id: payment_data.connector_response.connector_transaction_id, + connector_meta: payment_data.payment_attempt.connector_metadata, + }) + } +} diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index ff5b59e3f9..f82b19bcc4 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -60,6 +60,7 @@ pub async fn construct_refund_router_data<'a, F>( description: None, return_url: payment_intent.return_url.clone(), router_return_url: None, + complete_authorize_url: None, payment_method_id: payment_attempt.payment_method_id.clone(), // Does refund need shipping/billing address ? address: PaymentAddress::default(), diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 3adc51dad2..7f5c08133c 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -111,6 +111,11 @@ impl Payments { .service( web::resource("/{payment_id}/{merchant_id}/response/{connector}") .route(web::get().to(payments_redirect_response)), + ) + .service( + web::resource("/{payment_id}/{merchant_id}/complete/{connector}") + // .route(web::get().to(payments_redirect_response)) + .route(web::post().to(payments_complete_authorize)), ); } route diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 88b8a754d6..ccfc18692a 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -4,7 +4,10 @@ use router_env::{instrument, tracing, Flow}; use crate::{ self as app, - core::{errors::http_not_implemented, payments}, + core::{ + errors::http_not_implemented, + payments::{self, PaymentRedirectFlow}, + }, services::{api, authentication as auth}, types::api::{self as api_types, enums as api_enums, payments as payment_types}, }; @@ -405,10 +408,11 @@ pub async fn payments_redirect_response( let (payment_id, merchant_id, connector) = path.into_inner(); let param_string = req.query_string(); - let payload = payment_types::PaymentsRetrieveRequest { + let payload = payments::PaymentsRedirectResponseData { resource_id: payment_types::PaymentIdType::PaymentIntentId(payment_id), merchant_id: Some(merchant_id.clone()), force_sync: true, + json_payload: None, param: Some(param_string.to_string()), connector: Some(connector), }; @@ -417,7 +421,41 @@ pub async fn payments_redirect_response( &req, payload, |state, merchant_account, req| { - payments::handle_payments_redirect_response::( + payments::PaymentRedirectSync {}.handle_payments_redirect_response( + state, + merchant_account, + req, + ) + }, + &auth::MerchantIdAuth(merchant_id), + ) + .await +} + +#[instrument(skip_all)] +pub async fn payments_complete_authorize( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Form, + path: web::Path<(String, String, String)>, +) -> impl Responder { + let (payment_id, merchant_id, connector) = path.into_inner(); + let param_string = req.query_string(); + + let payload = payments::PaymentsRedirectResponseData { + resource_id: payment_types::PaymentIdType::PaymentIntentId(payment_id), + merchant_id: Some(merchant_id.clone()), + param: Some(param_string.to_string()), + json_payload: Some(json_payload.0), + force_sync: false, + connector: Some(connector), + }; + api::server_wrap( + state.get_ref(), + &req, + payload, + |state, merchant_account, req| { + payments::PaymentRedirectCompleteAuthorize {}.handle_payments_redirect_response( state, merchant_account, req, diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 5c20ed3aad..4a861d5e92 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -369,6 +369,12 @@ pub enum ApplicationResponse { Form(RedirectForm), } +#[derive(Debug, Eq, PartialEq)] +pub enum PaymentAction { + PSync, + CompleteAuthorize, +} + #[derive(Debug, Eq, PartialEq, Serialize)] pub struct ApplicationRedirectResponse { pub url: String, @@ -574,6 +580,8 @@ pub trait ConnectorRedirectResponse { fn get_flow_type( &self, _query_params: &str, + _json_payload: Option, + _action: PaymentAction, ) -> CustomResult { Ok(payments::CallConnectorAction::Avoid) } diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index f98fe0666f..cdaf00ad71 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -24,6 +24,10 @@ pub type PaymentsAuthorizeRouterData = RouterData; pub type PaymentsAuthorizeSessionTokenRouterData = RouterData; +pub type PaymentsCompleteAuthorizeRouterData = + RouterData; +pub type PaymentsInitRouterData = + RouterData; pub type PaymentsSyncRouterData = RouterData; pub type PaymentsCaptureRouterData = RouterData; @@ -53,11 +57,21 @@ pub type RefundsResponseRouterData = pub type PaymentsAuthorizeType = dyn services::ConnectorIntegration; +pub type PaymentsComeplteAuthorizeType = dyn services::ConnectorIntegration< + api::CompleteAuthorize, + CompleteAuthorizeData, + PaymentsResponseData, +>; pub type PaymentsPreAuthorizeType = dyn services::ConnectorIntegration< api::AuthorizeSessionToken, AuthorizeSessionTokenData, PaymentsResponseData, >; +pub type PaymentsInitType = dyn services::ConnectorIntegration< + api::InitPayment, + PaymentsAuthorizeData, + PaymentsResponseData, +>; pub type PaymentsSyncType = dyn services::ConnectorIntegration; pub type PaymentsCaptureType = @@ -90,6 +104,7 @@ pub struct RouterData { pub description: Option, pub return_url: Option, pub router_return_url: Option, + pub complete_authorize_url: Option, pub address: PaymentAddress, pub auth_type: storage_enums::AuthenticationType, pub connector_meta_data: Option, @@ -125,6 +140,9 @@ pub struct PaymentsAuthorizeData { pub setup_mandate_details: Option, pub browser_info: Option, pub order_details: Option, + pub session_token: Option, + pub enrolled_for_3ds: bool, + pub related_transaction_id: Option, pub payment_experience: Option, pub payment_method_type: Option, } @@ -146,11 +164,31 @@ pub struct AuthorizeSessionTokenData { } #[derive(Debug, Clone)] +pub struct CompleteAuthorizeData { + pub payment_method_data: Option, + pub amount: i64, + pub email: Option>, + pub currency: storage_enums::Currency, + pub confirm: bool, + pub statement_descriptor_suffix: Option, + pub capture_method: Option, + // Mandates + pub setup_future_usage: Option, + pub mandate_id: Option, + pub off_session: Option, + pub setup_mandate_details: Option, + pub browser_info: Option, + pub connector_transaction_id: Option, + pub connector_meta: Option, +} + +#[derive(Debug, Default, Clone)] pub struct PaymentsSyncData { //TODO : add fields based on the connector requirements pub connector_transaction_id: ResponseId, pub encoded_data: Option, pub capture_method: Option, + pub connector_meta: Option, } #[derive(Debug, Default, Clone)] @@ -396,16 +434,26 @@ impl Default for ErrorResponse { } } -impl From<&&mut PaymentsAuthorizeRouterData> for PaymentsAuthorizeSessionTokenRouterData { +impl From<&&mut PaymentsAuthorizeRouterData> for AuthorizeSessionTokenData { fn from(data: &&mut PaymentsAuthorizeRouterData) -> Self { + Self { + amount_to_capture: data.amount_captured, + currency: data.request.currency, + connector_transaction_id: data.payment_id.clone(), + amount: data.request.amount, + } + } +} + +impl From<(&&mut RouterData, T2)> + for RouterData +{ + fn from(item: (&&mut RouterData, T2)) -> Self { + let data = item.0; + let request = item.1; Self { flow: PhantomData, - request: AuthorizeSessionTokenData { - amount_to_capture: data.amount_captured, - currency: data.request.currency, - connector_transaction_id: data.payment_id.clone(), - amount: data.request.amount, - }, + request, merchant_id: data.merchant_id.clone(), connector: data.connector.clone(), attempt_id: data.attempt_id.clone(), @@ -415,6 +463,7 @@ impl From<&&mut PaymentsAuthorizeRouterData> for PaymentsAuthorizeSessionTokenRo description: data.description.clone(), return_url: data.return_url.clone(), router_return_url: data.router_return_url.clone(), + complete_authorize_url: data.complete_authorize_url.clone(), address: data.address.clone(), auth_type: data.auth_type, connector_meta_data: data.connector_meta_data.clone(), diff --git a/crates/router/src/types/api/payments.rs b/crates/router/src/types/api/payments.rs index fd8001d24e..047c9519c8 100644 --- a/crates/router/src/types/api/payments.rs +++ b/crates/router/src/types/api/payments.rs @@ -67,6 +67,11 @@ pub struct Authorize; #[derive(Debug, Clone)] pub struct AuthorizeSessionToken; + +#[derive(Debug, Clone)] +pub struct CompleteAuthorize; +#[derive(Debug, Clone)] +pub struct InitPayment; #[derive(Debug, Clone)] pub struct Capture; @@ -166,9 +171,19 @@ pub trait PreVerify: { } +pub trait PaymentsCompleteAuthorize: + api::ConnectorIntegration< + CompleteAuthorize, + types::CompleteAuthorizeData, + types::PaymentsResponseData, +> +{ +} + pub trait Payment: api_types::ConnectorCommon + PaymentAuthorize + + PaymentsCompleteAuthorize + PaymentSync + PaymentCapture + PaymentVoid diff --git a/crates/router/tests/connectors/aci.rs b/crates/router/tests/connectors/aci.rs index 9fbf3a881f..4212f5c1c0 100644 --- a/crates/router/tests/connectors/aci.rs +++ b/crates/router/tests/connectors/aci.rs @@ -29,6 +29,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { connector_auth_type: auth.into(), description: Some("This is a test".to_string()), router_return_url: None, + complete_authorize_url: None, return_url: None, request: types::PaymentsAuthorizeData { amount: 1000, @@ -53,6 +54,9 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { browser_info: None, order_details: None, email: None, + session_token: None, + enrolled_for_3ds: false, + related_transaction_id: None, payment_experience: None, payment_method_type: None, }, @@ -80,6 +84,7 @@ fn construct_refund_router_data() -> types::RefundsRouterData { attempt_id: uuid::Uuid::new_v4().to_string(), status: enums::AttemptStatus::default(), router_return_url: None, + complete_authorize_url: None, payment_method: enums::PaymentMethod::Card, auth_type: enums::AuthenticationType::NoThreeDs, connector_auth_type: auth.into(), diff --git a/crates/router/tests/connectors/adyen.rs b/crates/router/tests/connectors/adyen.rs index cff3c79606..55e3a706a4 100644 --- a/crates/router/tests/connectors/adyen.rs +++ b/crates/router/tests/connectors/adyen.rs @@ -83,6 +83,9 @@ impl AdyenTest { email: None, payment_experience: None, payment_method_type: None, + session_token: None, + enrolled_for_3ds: false, + related_transaction_id: None, }) } } diff --git a/crates/router/tests/connectors/airwallex.rs b/crates/router/tests/connectors/airwallex.rs index 9514402c48..207d46bf3f 100644 --- a/crates/router/tests/connectors/airwallex.rs +++ b/crates/router/tests/connectors/airwallex.rs @@ -124,8 +124,7 @@ async fn should_sync_authorized_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( txn_id.unwrap(), ), - encoded_data: None, - capture_method: None, + ..Default::default() }), get_default_payment_info(), ) @@ -255,8 +254,7 @@ async fn should_sync_auto_captured_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( txn_id.unwrap(), ), - encoded_data: None, - capture_method: None, + ..Default::default() }), get_default_payment_info(), ) diff --git a/crates/router/tests/connectors/authorizedotnet.rs b/crates/router/tests/connectors/authorizedotnet.rs index b74d502f76..c3f6374524 100644 --- a/crates/router/tests/connectors/authorizedotnet.rs +++ b/crates/router/tests/connectors/authorizedotnet.rs @@ -25,6 +25,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { attempt_id: uuid::Uuid::new_v4().to_string(), status: enums::AttemptStatus::default(), router_return_url: None, + complete_authorize_url: None, payment_method: enums::PaymentMethod::Card, connector_auth_type: auth.into(), auth_type: enums::AuthenticationType::NoThreeDs, @@ -53,6 +54,9 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { browser_info: None, order_details: None, email: None, + session_token: None, + enrolled_for_3ds: false, + related_transaction_id: None, payment_experience: None, payment_method_type: None, }, @@ -81,6 +85,7 @@ fn construct_refund_router_data() -> types::RefundsRouterData { attempt_id: uuid::Uuid::new_v4().to_string(), status: enums::AttemptStatus::default(), router_return_url: None, + complete_authorize_url: None, auth_type: enums::AuthenticationType::NoThreeDs, payment_method: enums::PaymentMethod::Card, connector_auth_type: auth.into(), diff --git a/crates/router/tests/connectors/bambora.rs b/crates/router/tests/connectors/bambora.rs index 6d8f62d4d4..22fffe4dcb 100644 --- a/crates/router/tests/connectors/bambora.rs +++ b/crates/router/tests/connectors/bambora.rs @@ -102,6 +102,7 @@ async fn should_sync_authorized_payment() { ), encoded_data: None, capture_method: None, + connector_meta: None, }), None, ) @@ -213,6 +214,7 @@ async fn should_sync_auto_captured_payment() { ), encoded_data: None, capture_method: Some(enums::CaptureMethod::Automatic), + connector_meta: None, }), None, ) diff --git a/crates/router/tests/connectors/bluesnap.rs b/crates/router/tests/connectors/bluesnap.rs index 98135572df..4c1fa64f99 100644 --- a/crates/router/tests/connectors/bluesnap.rs +++ b/crates/router/tests/connectors/bluesnap.rs @@ -94,8 +94,7 @@ async fn should_sync_authorized_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( txn_id.unwrap(), ), - encoded_data: None, - capture_method: None, + ..Default::default() }), None, ) @@ -229,8 +228,7 @@ async fn should_sync_auto_captured_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( txn_id.unwrap(), ), - encoded_data: None, - capture_method: None, + ..Default::default() }), None, ) diff --git a/crates/router/tests/connectors/checkout.rs b/crates/router/tests/connectors/checkout.rs index 2c28753cce..6434e451a9 100644 --- a/crates/router/tests/connectors/checkout.rs +++ b/crates/router/tests/connectors/checkout.rs @@ -22,6 +22,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { attempt_id: uuid::Uuid::new_v4().to_string(), status: enums::AttemptStatus::default(), router_return_url: None, + complete_authorize_url: None, auth_type: enums::AuthenticationType::NoThreeDs, payment_method: enums::PaymentMethod::Card, connector_auth_type: auth.into(), @@ -50,6 +51,9 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { browser_info: None, order_details: None, email: None, + session_token: None, + enrolled_for_3ds: false, + related_transaction_id: None, payment_experience: None, payment_method_type: None, }, @@ -78,6 +82,7 @@ fn construct_refund_router_data() -> types::RefundsRouterData { attempt_id: uuid::Uuid::new_v4().to_string(), status: enums::AttemptStatus::default(), router_return_url: None, + complete_authorize_url: None, payment_method: enums::PaymentMethod::Card, auth_type: enums::AuthenticationType::NoThreeDs, connector_auth_type: auth.into(), diff --git a/crates/router/tests/connectors/cybersource.rs b/crates/router/tests/connectors/cybersource.rs index 7b1ae4d948..7b38103081 100644 --- a/crates/router/tests/connectors/cybersource.rs +++ b/crates/router/tests/connectors/cybersource.rs @@ -122,8 +122,7 @@ async fn should_sync_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( "6699597903496176903954".to_string(), ), - encoded_data: None, - capture_method: None, + ..Default::default() }), None, ) diff --git a/crates/router/tests/connectors/dlocal.rs b/crates/router/tests/connectors/dlocal.rs index eae58e3801..7312d0462c 100644 --- a/crates/router/tests/connectors/dlocal.rs +++ b/crates/router/tests/connectors/dlocal.rs @@ -88,8 +88,7 @@ async fn should_sync_authorized_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( txn_id.unwrap(), ), - encoded_data: None, - capture_method: None, + ..Default::default() }), Some(get_payment_info()), ) @@ -200,8 +199,7 @@ async fn should_sync_auto_captured_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( txn_id.unwrap(), ), - encoded_data: None, - capture_method: None, + ..Default::default() }), Some(get_payment_info()), ) diff --git a/crates/router/tests/connectors/fiserv.rs b/crates/router/tests/connectors/fiserv.rs index 6d5098517c..8b1c397c41 100644 --- a/crates/router/tests/connectors/fiserv.rs +++ b/crates/router/tests/connectors/fiserv.rs @@ -119,8 +119,7 @@ async fn should_sync_authorized_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( txn_id.unwrap(), ), - encoded_data: None, - capture_method: None, + ..Default::default() }), get_default_payment_info(), ) @@ -249,8 +248,7 @@ async fn should_sync_auto_captured_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( txn_id.unwrap(), ), - encoded_data: None, - capture_method: None, + ..Default::default() }), get_default_payment_info(), ) diff --git a/crates/router/tests/connectors/globalpay.rs b/crates/router/tests/connectors/globalpay.rs index e4a1ba9af0..aaea15ab43 100644 --- a/crates/router/tests/connectors/globalpay.rs +++ b/crates/router/tests/connectors/globalpay.rs @@ -105,8 +105,7 @@ async fn should_sync_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( txn_id.unwrap(), ), - encoded_data: None, - capture_method: None, + ..Default::default() }), get_default_payment_info(), ) diff --git a/crates/router/tests/connectors/multisafepay.rs b/crates/router/tests/connectors/multisafepay.rs index f5f3e981de..5e7c165c8a 100644 --- a/crates/router/tests/connectors/multisafepay.rs +++ b/crates/router/tests/connectors/multisafepay.rs @@ -87,8 +87,7 @@ async fn should_sync_authorized_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( txn_id.unwrap(), ), - encoded_data: None, - capture_method: None, + ..Default::default() }), None, ) @@ -192,8 +191,7 @@ async fn should_sync_auto_captured_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( txn_id.unwrap(), ), - encoded_data: None, - capture_method: None, + ..Default::default() }), None, ) diff --git a/crates/router/tests/connectors/nuvei.rs b/crates/router/tests/connectors/nuvei.rs index 0b7556c307..04ab6eeba5 100644 --- a/crates/router/tests/connectors/nuvei.rs +++ b/crates/router/tests/connectors/nuvei.rs @@ -7,7 +7,7 @@ use serde_json::json; use crate::{ connector_auth, - utils::{self, ConnectorActions, PaymentInfo}, + utils::{self, ConnectorActions}, }; #[derive(Clone, Copy)] @@ -101,15 +101,12 @@ async fn should_sync_authorized_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( txn_id.unwrap(), ), - encoded_data: None, - capture_method: None, - }), - Some(PaymentInfo { - connector_meta_data: Some(json!({ + connector_meta: Some(json!({ "session_token": authorize_response.session_token.unwrap() })), ..Default::default() }), + None, ) .await .expect("PSync response"); @@ -196,15 +193,12 @@ async fn should_sync_auto_captured_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( txn_id.unwrap(), ), - encoded_data: None, - capture_method: None, - }), - Some(PaymentInfo { - connector_meta_data: Some(json!({ + connector_meta: Some(json!({ "session_token": authorize_response.session_token.unwrap() })), ..Default::default() }), + None, ) .await .unwrap(); diff --git a/crates/router/tests/connectors/payu.rs b/crates/router/tests/connectors/payu.rs index 589ab55790..018e2720cd 100644 --- a/crates/router/tests/connectors/payu.rs +++ b/crates/router/tests/connectors/payu.rs @@ -70,8 +70,7 @@ async fn should_authorize_card_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( transaction_id.clone(), ), - encoded_data: None, - capture_method: None, + ..Default::default() }), get_default_payment_info(), ) @@ -116,8 +115,7 @@ async fn should_authorize_gpay_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( transaction_id.clone(), ), - encoded_data: None, - capture_method: None, + ..Default::default() }), get_default_payment_info(), ) @@ -150,8 +148,7 @@ async fn should_capture_already_authorized_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( transaction_id.clone(), ), - encoded_data: None, - capture_method: None, + ..Default::default() }), get_default_payment_info(), ) @@ -170,8 +167,7 @@ async fn should_capture_already_authorized_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( transaction_id, ), - encoded_data: None, - capture_method: None, + ..Default::default() }), get_default_payment_info(), ) @@ -207,8 +203,7 @@ async fn should_sync_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( transaction_id, ), - encoded_data: None, - capture_method: None, + ..Default::default() }), get_default_payment_info(), ) @@ -251,8 +246,7 @@ async fn should_void_already_authorized_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( transaction_id, ), - encoded_data: None, - capture_method: None, + ..Default::default() }), get_default_payment_info(), ) @@ -286,8 +280,7 @@ async fn should_refund_succeeded_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( transaction_id.clone(), ), - encoded_data: None, - capture_method: None, + ..Default::default() }), get_default_payment_info(), ) @@ -308,8 +301,7 @@ async fn should_refund_succeeded_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( transaction_id.clone(), ), - encoded_data: None, - capture_method: None, + ..Default::default() }), get_default_payment_info(), ) diff --git a/crates/router/tests/connectors/shift4.rs b/crates/router/tests/connectors/shift4.rs index 23157d546e..73d2f4dffd 100644 --- a/crates/router/tests/connectors/shift4.rs +++ b/crates/router/tests/connectors/shift4.rs @@ -89,8 +89,7 @@ async fn should_sync_authorized_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( txn_id.unwrap(), ), - encoded_data: None, - capture_method: None, + ..Default::default() }), None, ) @@ -114,8 +113,7 @@ async fn should_sync_auto_captured_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( txn_id.unwrap(), ), - encoded_data: None, - capture_method: None, + ..Default::default() }), None, ) diff --git a/crates/router/tests/connectors/stripe.rs b/crates/router/tests/connectors/stripe.rs index 83e2c6bd8d..c317684c0d 100644 --- a/crates/router/tests/connectors/stripe.rs +++ b/crates/router/tests/connectors/stripe.rs @@ -99,8 +99,7 @@ async fn should_sync_authorized_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( txn_id.unwrap(), ), - encoded_data: None, - capture_method: None, + ..Default::default() }), None, ) @@ -124,8 +123,7 @@ async fn should_sync_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( txn_id.unwrap(), ), - encoded_data: None, - capture_method: None, + ..Default::default() }), None, ) diff --git a/crates/router/tests/connectors/trustpay.rs b/crates/router/tests/connectors/trustpay.rs index 8c530cd6fe..50ed42c5d0 100644 --- a/crates/router/tests/connectors/trustpay.rs +++ b/crates/router/tests/connectors/trustpay.rs @@ -119,8 +119,7 @@ async fn should_sync_auto_captured_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( txn_id.unwrap(), ), - encoded_data: None, - capture_method: None, + ..Default::default() }), None, ) diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index c85b77d46e..18ffb005d3 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -361,6 +361,7 @@ pub trait ConnectorActions: Connector { attempt_id: uuid::Uuid::new_v4().to_string(), status: enums::AttemptStatus::default(), router_return_url: info.clone().and_then(|a| a.router_return_url), + complete_authorize_url: None, auth_type: info .clone() .map_or(enums::AuthenticationType::NoThreeDs, |a| { @@ -486,6 +487,9 @@ impl Default for PaymentAuthorizeType { browser_info: Some(BrowserInfoType::default().0), order_details: None, email: None, + session_token: None, + enrolled_for_3ds: false, + related_transaction_id: None, payment_experience: None, payment_method_type: None, }; @@ -540,6 +544,7 @@ impl Default for PaymentSyncType { ), encoded_data: None, capture_method: None, + connector_meta: None, }; Self(data) } diff --git a/crates/router/tests/connectors/worldline.rs b/crates/router/tests/connectors/worldline.rs index 375b560115..cb14bb2c90 100644 --- a/crates/router/tests/connectors/worldline.rs +++ b/crates/router/tests/connectors/worldline.rs @@ -83,6 +83,9 @@ impl WorldlineTest { browser_info: None, order_details: None, email: None, + session_token: None, + enrolled_for_3ds: false, + related_transaction_id: None, payment_experience: None, payment_method_type: None, }) @@ -212,8 +215,8 @@ async fn should_sync_manual_auth_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( connector_payment_id, ), - encoded_data: None, capture_method: Some(enums::CaptureMethod::Manual), + ..Default::default() }), None, ) @@ -245,8 +248,8 @@ async fn should_sync_auto_auth_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( connector_payment_id, ), - encoded_data: None, capture_method: Some(enums::CaptureMethod::Automatic), + ..Default::default() }), None, ) diff --git a/crates/router/tests/connectors/worldpay.rs b/crates/router/tests/connectors/worldpay.rs index 62d1b5f62a..48f87d9c76 100644 --- a/crates/router/tests/connectors/worldpay.rs +++ b/crates/router/tests/connectors/worldpay.rs @@ -150,8 +150,7 @@ async fn should_sync_payment() { connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( "112233".to_string(), ), - encoded_data: None, - capture_method: None, + ..Default::default() }), None, ) diff --git a/crates/router_derive/src/macros/operation.rs b/crates/router_derive/src/macros/operation.rs index 32c0aae7e4..5a5cd426aa 100644 --- a/crates/router_derive/src/macros/operation.rs +++ b/crates/router_derive/src/macros/operation.rs @@ -16,6 +16,7 @@ enum Derives { Syncdata, Canceldata, Capturedata, + CompleteAuthorizeData, VerifyData, Start, Verify, @@ -34,6 +35,7 @@ impl From for Derives { "canceldata" => Self::Canceldata, "capture" => Self::Capture, "capturedata" => Self::Capturedata, + "completeauthorizedata" => Self::CompleteAuthorizeData, "start" => Self::Start, "verify" => Self::Verify, "verifydata" => Self::VerifyData, @@ -110,6 +112,9 @@ impl Conversion { Derives::Canceldata => syn::Ident::new("PaymentsCancelData", Span::call_site()), Derives::Capture => syn::Ident::new("PaymentsCaptureRequest", Span::call_site()), Derives::Capturedata => syn::Ident::new("PaymentsCaptureData", Span::call_site()), + Derives::CompleteAuthorizeData => { + syn::Ident::new("CompleteAuthorizeData", Span::call_site()) + } Derives::Start => syn::Ident::new("PaymentsStartRequest", Span::call_site()), Derives::Verify => syn::Ident::new("VerifyRequest", Span::call_site()), Derives::VerifyData => syn::Ident::new("VerifyRequestData", Span::call_site()), @@ -328,6 +333,7 @@ pub fn operation_derive_inner(input: DeriveInput) -> syn::Result