diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 1494908c5fcd..0edeb537dc1c 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -15344,7 +15344,6 @@ "type": "object", "required": [ "payment_id", - "client_secret", "session_token" ], "properties": { @@ -15352,10 +15351,6 @@ "type": "string", "description": "The identifier for the payment" }, - "client_secret": { - "type": "string", - "description": "This is a token which expires after 15 minutes, used from the client to authenticate and create sessions from the SDK" - }, "session_token": { "type": "array", "items": { diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs index 6fdb7d59b0f8..6015682f7c38 100644 --- a/crates/api_models/src/events/payment.rs +++ b/crates/api_models/src/events/payment.rs @@ -402,7 +402,6 @@ impl ApiEventMetric for PaymentsManualUpdateResponse { } } -#[cfg(feature = "v1")] impl ApiEventMetric for PaymentsSessionResponse { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::Payment { diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index c0cf816c68d0..45b9bac53acb 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -6084,6 +6084,7 @@ pub struct ApplepayErrorResponse { pub status_message: String, } +#[cfg(feature = "v1")] #[derive(Default, Debug, serde::Serialize, Clone, ToSchema)] pub struct PaymentsSessionResponse { /// The identifier for the payment @@ -6096,6 +6097,16 @@ pub struct PaymentsSessionResponse { pub session_token: Vec, } +#[cfg(feature = "v2")] +#[derive(Debug, serde::Serialize, Clone, ToSchema)] +pub struct PaymentsSessionResponse { + /// The identifier for the payment + #[schema(value_type = String)] + pub payment_id: id_type::GlobalPaymentId, + /// The list of session token object + pub session_token: Vec, +} + #[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] pub struct PaymentRetrieveBody { /// The identifier for the Merchant Account. diff --git a/crates/common_utils/src/macros.rs b/crates/common_utils/src/macros.rs index 21cec6f60fce..fe1289acba03 100644 --- a/crates/common_utils/src/macros.rs +++ b/crates/common_utils/src/macros.rs @@ -369,6 +369,41 @@ mod id_type { } } +/// Create new generic list wrapper +#[macro_export] +macro_rules! create_list_wrapper { + ( + $wrapper_name:ident, + $type_name: ty, + impl_functions: { + $($function_def: tt)* + } + ) => { + pub struct $wrapper_name(Vec<$type_name>); + impl $wrapper_name { + pub fn new(list: Vec<$type_name>) -> Self { + Self(list) + } + pub fn iter(&self) -> std::slice::Iter<'_, $type_name> { + self.0.iter() + } + $($function_def)* + } + impl Iterator for $wrapper_name { + type Item = $type_name; + fn next(&mut self) -> Option { + self.0.pop() + } + } + + impl FromIterator<$type_name> for $wrapper_name { + fn from_iter>(iter: T) -> Self { + Self(iter.into_iter().collect()) + } + } + }; +} + /// Get the type name for a type #[macro_export] macro_rules! type_name { diff --git a/crates/common_utils/src/types.rs b/crates/common_utils/src/types.rs index 84a70e44a32a..2a271acb62bb 100644 --- a/crates/common_utils/src/types.rs +++ b/crates/common_utils/src/types.rs @@ -845,7 +845,7 @@ mod client_secret_type { Ok(row) } } - + crate::impl_serializable_secret_id_type!(ClientSecret); #[cfg(test)] mod client_secret_tests { #![allow(clippy::expect_used)] diff --git a/crates/hyperswitch_domain_models/src/merchant_connector_account.rs b/crates/hyperswitch_domain_models/src/merchant_connector_account.rs index f0719ba35892..51c75113dcda 100644 --- a/crates/hyperswitch_domain_models/src/merchant_connector_account.rs +++ b/crates/hyperswitch_domain_models/src/merchant_connector_account.rs @@ -1,3 +1,7 @@ +#[cfg(feature = "v2")] +use api_models::admin; +#[cfg(feature = "v2")] +use common_utils::ext_traits::ValueExt; use common_utils::{ crypto::Encryptable, date_time, @@ -9,11 +13,15 @@ use common_utils::{ use diesel_models::{enums, merchant_connector_account::MerchantConnectorAccountUpdateInternal}; use error_stack::ResultExt; use masking::{PeekInterface, Secret}; +#[cfg(feature = "v2")] +use router_env::logger; use rustc_hash::FxHashMap; use serde_json::Value; use super::behaviour; #[cfg(feature = "v2")] +use crate::errors::api_error_response::ApiErrorResponse; +#[cfg(feature = "v2")] use crate::router_data; use crate::type_encryption::{crypto_operation, CryptoOperation}; @@ -90,6 +98,27 @@ impl MerchantConnectorAccount { self.id.clone() } + pub fn get_metadata(&self) -> Option { + self.metadata.clone() + } + + pub fn get_parsed_payment_methods_enabled( + &self, + ) -> Vec> { + self.payment_methods_enabled + .clone() + .unwrap_or_default() + .into_iter() + .map(|payment_methods_enabled| { + payment_methods_enabled + .parse_value::("payment_methods_enabled") + .change_context(ApiErrorResponse::InvalidDataValue { + field_name: "payment_methods_enabled", + }) + }) + .collect() + } + pub fn is_disabled(&self) -> bool { self.disabled.unwrap_or(false) } @@ -530,31 +559,73 @@ impl From for MerchantConnectorAccountUpdateInte } } -#[derive(Debug)] -pub struct MerchantConnectorAccounts(Vec); - -impl MerchantConnectorAccounts { - pub fn new(merchant_connector_accounts: Vec) -> Self { - Self(merchant_connector_accounts) - } - - pub fn is_merchant_connector_account_id_in_connector_mandate_details( - &self, - profile_id: Option<&id_type::ProfileId>, - connector_mandate_details: &diesel_models::PaymentsMandateReference, - ) -> bool { - let mca_ids = self - .0 - .iter() - .filter(|mca| { - mca.disabled.is_some_and(|disabled| !disabled) - && profile_id.is_some_and(|profile_id| *profile_id == mca.profile_id) - }) - .map(|mca| mca.get_id()) - .collect::>(); +common_utils::create_list_wrapper!( + MerchantConnectorAccounts, + MerchantConnectorAccount, + impl_functions: { + #[cfg(feature = "v2")] + pub fn get_connector_and_supporting_payment_method_type_for_session_call( + &self, + ) -> Vec<(&MerchantConnectorAccount, common_enums::PaymentMethodType)> { + let connector_and_supporting_payment_method_type = self.iter().flat_map(|connector_account| { + connector_account + .get_parsed_payment_methods_enabled() + // TODO: make payment_methods_enabled strict type in DB + .into_iter() + .filter_map(|parsed_payment_method_result| { + parsed_payment_method_result + .inspect_err(|err| { + logger::error!(session_token_parsing_error=?err); + }) + .ok() + }) + .flat_map(|parsed_payment_methods_enabled| { + parsed_payment_methods_enabled + .payment_method_types + .unwrap_or_default() + .into_iter() + .filter(|payment_method_type| { + let is_invoke_sdk_client = matches!( + payment_method_type.payment_experience, + Some(api_models::enums::PaymentExperience::InvokeSdkClient) + ); + is_invoke_sdk_client + }) + .map(|payment_method_type| { + (connector_account, payment_method_type.payment_method_type) + }) + .collect::>() + }) + .collect::>() + }).collect(); + connector_and_supporting_payment_method_type + } + pub fn filter_based_on_profile_and_connector_type( + self, + profile_id: &id_type::ProfileId, + connector_type: common_enums::ConnectorType, + ) -> Self { + self.into_iter() + .filter(|mca| &mca.profile_id == profile_id && mca.connector_type == connector_type) + .collect() + } + pub fn is_merchant_connector_account_id_in_connector_mandate_details( + &self, + profile_id: Option<&id_type::ProfileId>, + connector_mandate_details: &diesel_models::PaymentsMandateReference, + ) -> bool { + let mca_ids = self + .iter() + .filter(|mca| { + mca.disabled.is_some_and(|disabled| !disabled) + && profile_id.is_some_and(|profile_id| *profile_id == mca.profile_id) + }) + .map(|mca| mca.get_id()) + .collect::>(); - connector_mandate_details - .keys() - .any(|mca_id| mca_ids.contains(mca_id)) + connector_mandate_details + .keys() + .any(|mca_id| mca_ids.contains(mca_id)) + } } -} +); diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index 4cb934032194..b7a6c12500d1 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -1,6 +1,8 @@ #[cfg(feature = "v2")] use std::marker::PhantomData; +#[cfg(feature = "v2")] +use api_models::payments::SessionToken; #[cfg(feature = "v2")] use common_utils::ext_traits::ValueExt; use common_utils::{ @@ -566,6 +568,7 @@ where { pub flow: PhantomData, pub payment_intent: PaymentIntent, + pub sessions_token: Vec, } // TODO: Check if this can be merged with existing payment data diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index ada8160c6d94..5eb97e6eebe2 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -8,6 +8,8 @@ pub mod operations; #[cfg(feature = "retry")] pub mod retry; pub mod routing; +#[cfg(feature = "v2")] +pub mod session_operation; pub mod tokenization; pub mod transformers; pub mod types; @@ -56,6 +58,8 @@ use router_env::{instrument, metrics::add_attributes, tracing}; #[cfg(feature = "olap")] use router_types::transformers::ForeignFrom; use scheduler::utils as pt_utils; +#[cfg(feature = "v2")] +pub use session_operation::payments_session_core; #[cfg(feature = "olap")] use strum::IntoEnumIterator; use time; @@ -3111,6 +3115,119 @@ where } } +#[cfg(feature = "v2")] +#[allow(clippy::too_many_arguments)] +pub async fn call_multiple_connectors_service( + state: &SessionState, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + connectors: Vec, + _operation: &Op, + mut payment_data: D, + customer: &Option, + _session_surcharge_details: Option, + business_profile: &domain::Profile, + header_payload: HeaderPayload, +) -> RouterResult +where + Op: Debug, + F: Send + Clone, + + // To create connector flow specific interface data + D: OperationSessionGetters + OperationSessionSetters + Send + Sync + Clone, + D: ConstructFlowSpecificData, + RouterData: Feature, + + // To construct connector flow specific api + dyn api::Connector: + services::api::ConnectorIntegration, +{ + let call_connectors_start_time = Instant::now(); + let mut join_handlers = Vec::with_capacity(connectors.len()); + for session_connector_data in connectors.iter() { + let merchant_connector_id = session_connector_data + .connector + .merchant_connector_id + .as_ref() + .get_required_value("merchant_connector_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("connector id is not set")?; + // TODO: make this DB call parallel + let merchant_connector_account = state + .store + .find_merchant_connector_account_by_id(&state.into(), merchant_connector_id, key_store) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: merchant_connector_id.get_string_repr().to_owned(), + })?; + let connector_id = session_connector_data.connector.connector.id(); + let router_data = payment_data + .construct_router_data( + state, + connector_id, + merchant_account, + key_store, + customer, + &merchant_connector_account, + None, + None, + ) + .await?; + + let res = router_data.decide_flows( + state, + &session_connector_data.connector, + CallConnectorAction::Trigger, + None, + business_profile, + header_payload.clone(), + ); + + join_handlers.push(res); + } + + let result = join_all(join_handlers).await; + + for (connector_res, session_connector) in result.into_iter().zip(connectors) { + let connector_name = session_connector.connector.connector_name.to_string(); + match connector_res { + Ok(connector_response) => { + if let Ok(router_types::PaymentsResponseData::SessionResponse { + session_token, + .. + }) = connector_response.response.clone() + { + // If session token is NoSessionTokenReceived, it is not pushed into the sessions_token as there is no response or there can be some error + // In case of error, that error is already logged + if !matches!( + session_token, + api_models::payments::SessionToken::NoSessionTokenReceived, + ) { + payment_data.push_sessions_token(session_token); + } + } + if let Err(connector_error_response) = connector_response.response { + logger::error!( + "sessions_connector_error {} {:?}", + connector_name, + connector_error_response + ); + } + } + Err(api_error) => { + logger::error!("sessions_api_error {} {:?}", connector_name, api_error); + } + } + } + + let call_connectors_end_time = Instant::now(); + let call_connectors_duration = + call_connectors_end_time.saturating_duration_since(call_connectors_start_time); + tracing::info!(duration = format!("Duration taken: {}", call_connectors_duration.as_millis())); + + Ok(payment_data) +} + #[cfg(feature = "v1")] #[allow(clippy::too_many_arguments)] pub async fn call_multiple_connectors_service( @@ -3137,9 +3254,6 @@ where // To construct connector flow specific api dyn api::Connector: services::api::ConnectorIntegration, - - // To perform router related operation for PaymentResponse - PaymentResponse: Operation, { let call_connectors_start_time = Instant::now(); let mut join_handlers = Vec::with_capacity(connectors.len()); @@ -3574,6 +3688,7 @@ pub fn is_preprocessing_required_for_wallets(connector_name: String) -> bool { connector_name == *"trustpay" || connector_name == *"payme" } +#[cfg(feature = "v1")] #[instrument(skip_all)] pub async fn construct_profile_id_and_get_mca<'a, F, D>( state: &'a SessionState, @@ -3588,7 +3703,6 @@ where F: Clone, D: OperationSessionGetters + Send + Sync + Clone, { - #[cfg(feature = "v1")] let profile_id = payment_data .get_payment_intent() .profile_id @@ -6977,7 +7091,7 @@ impl OperationSessionGetters for PaymentIntentData { } fn get_sessions_token(&self) -> Vec { - todo!() + self.sessions_token.clone() } fn get_token_data(&self) -> Option<&storage::PaymentTokenData> { @@ -7033,8 +7147,8 @@ impl OperationSessionSetters for PaymentIntentData { todo!() } - fn push_sessions_token(&mut self, _token: api::SessionToken) { - todo!() + fn push_sessions_token(&mut self, token: api::SessionToken) { + self.sessions_token.push(token); } fn set_surcharge_details(&mut self, _surcharge_details: Option) { diff --git a/crates/router/src/core/payments/flows/session_flow.rs b/crates/router/src/core/payments/flows/session_flow.rs index 43f855182e89..265046f42d96 100644 --- a/crates/router/src/core/payments/flows/session_flow.rs +++ b/crates/router/src/core/payments/flows/session_flow.rs @@ -6,6 +6,8 @@ use common_utils::{ types::{AmountConvertor, StringMajorUnitForConnector}, }; use error_stack::{Report, ResultExt}; +#[cfg(feature = "v2")] +use hyperswitch_domain_models::payments::PaymentIntentData; use masking::ExposeInterface; use router_env::metrics::add_attributes; @@ -26,12 +28,12 @@ use crate::{ utils::OptionExt, }; +#[cfg(feature = "v2")] #[async_trait] impl ConstructFlowSpecificData - for PaymentData + for PaymentIntentData { - #[cfg(feature = "v1")] async fn construct_router_data<'a>( &self, state: &routes::SessionState, @@ -39,14 +41,11 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, customer: &Option, - merchant_connector_account: &helpers::MerchantConnectorAccountType, + merchant_connector_account: &domain::MerchantConnectorAccount, merchant_recipient_data: Option, header_payload: Option, ) -> RouterResult { - Box::pin(transformers::construct_payment_router_data::< - api::Session, - types::PaymentsSessionData, - >( + Box::pin(transformers::construct_payment_router_data_for_sdk_session( state, self.clone(), connector_id, @@ -60,7 +59,24 @@ impl .await } - #[cfg(feature = "v2")] + async fn get_merchant_recipient_data<'a>( + &self, + _state: &routes::SessionState, + _merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + _merchant_connector_account: &helpers::MerchantConnectorAccountType, + _connector: &api::ConnectorData, + ) -> RouterResult> { + Ok(None) + } +} + +#[cfg(feature = "v1")] +#[async_trait] +impl + ConstructFlowSpecificData + for PaymentData +{ async fn construct_router_data<'a>( &self, state: &routes::SessionState, @@ -68,11 +84,25 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, customer: &Option, - merchant_connector_account: &domain::MerchantConnectorAccount, + merchant_connector_account: &helpers::MerchantConnectorAccountType, merchant_recipient_data: Option, header_payload: Option, ) -> RouterResult { - todo!() + Box::pin(transformers::construct_payment_router_data::< + api::Session, + types::PaymentsSessionData, + >( + state, + self.clone(), + connector_id, + merchant_account, + key_store, + customer, + merchant_connector_account, + merchant_recipient_data, + header_payload, + )) + .await } async fn get_merchant_recipient_data<'a>( diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index e936f3725455..5cdfff60b451 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -17,6 +17,8 @@ pub mod payment_reject; pub mod payment_response; #[cfg(feature = "v1")] pub mod payment_session; +#[cfg(feature = "v2")] +pub mod payment_session_intent; #[cfg(feature = "v1")] pub mod payment_start; #[cfg(feature = "v1")] @@ -45,10 +47,6 @@ use async_trait::async_trait; use error_stack::{report, ResultExt}; use router_env::{instrument, tracing}; -#[cfg(feature = "v2")] -pub use self::payment_confirm_intent::PaymentIntentConfirm; -#[cfg(feature = "v2")] -pub use self::payment_create_intent::PaymentIntentCreate; #[cfg(feature = "v2")] pub use self::payment_get::PaymentGet; #[cfg(feature = "v2")] @@ -64,6 +62,11 @@ pub use self::{ payments_incremental_authorization::PaymentIncrementalAuthorization, tax_calculation::PaymentSessionUpdate, }; +#[cfg(feature = "v2")] +pub use self::{ + payment_confirm_intent::PaymentIntentConfirm, payment_create_intent::PaymentIntentCreate, + payment_session_intent::PaymentSessionIntent, +}; use super::{helpers, CustomerDetails, OperationSessionGetters, OperationSessionSetters}; use crate::{ core::errors::{self, CustomResult, RouterResult}, diff --git a/crates/router/src/core/payments/operations/payment_create_intent.rs b/crates/router/src/core/payments/operations/payment_create_intent.rs index bf5b4fb80c9e..988e040f3769 100644 --- a/crates/router/src/core/payments/operations/payment_create_intent.rs +++ b/crates/router/src/core/payments/operations/payment_create_intent.rs @@ -158,6 +158,7 @@ impl GetTracker, PaymentsCrea let payment_data = payments::PaymentIntentData { flow: PhantomData, payment_intent, + sessions_token: vec![], }; let get_trackers_response = operations::GetTrackerResponse { payment_data }; diff --git a/crates/router/src/core/payments/operations/payment_get_intent.rs b/crates/router/src/core/payments/operations/payment_get_intent.rs index 6424ff5a2b35..344f10434bdd 100644 --- a/crates/router/src/core/payments/operations/payment_get_intent.rs +++ b/crates/router/src/core/payments/operations/payment_get_intent.rs @@ -101,6 +101,7 @@ impl GetTracker, PaymentsGetI let payment_data = payments::PaymentIntentData { flow: PhantomData, payment_intent, + sessions_token: vec![], }; let get_trackers_response = operations::GetTrackerResponse { payment_data }; diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 8fb36764c970..e3f9ad5d9054 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -13,7 +13,9 @@ use error_stack::{report, ResultExt}; use futures::FutureExt; use hyperswitch_domain_models::payments::payment_attempt::PaymentAttempt; #[cfg(feature = "v2")] -use hyperswitch_domain_models::payments::{PaymentConfirmData, PaymentStatusData}; +use hyperswitch_domain_models::payments::{ + PaymentConfirmData, PaymentIntentData, PaymentStatusData, +}; use router_derive; use router_env::{instrument, logger, metrics::add_attributes, tracing}; use storage_impl::DataModelExt; diff --git a/crates/router/src/core/payments/operations/payment_session_intent.rs b/crates/router/src/core/payments/operations/payment_session_intent.rs new file mode 100644 index 000000000000..ee490408cb11 --- /dev/null +++ b/crates/router/src/core/payments/operations/payment_session_intent.rs @@ -0,0 +1,281 @@ +use std::marker::PhantomData; + +use api_models::payments::PaymentsSessionRequest; +use async_trait::async_trait; +use common_utils::errors::CustomResult; +use error_stack::ResultExt; +use router_env::{instrument, logger, tracing}; + +use super::{BoxedOperation, Domain, GetTracker, Operation, ValidateRequest}; +use crate::{ + core::{ + errors::{self, RouterResult, StorageErrorExt}, + payments::{self, operations, operations::ValidateStatusForOperation}, + }, + routes::SessionState, + types::{api, domain, storage::enums}, + utils::ext_traits::OptionExt, +}; + +#[derive(Debug, Clone, Copy)] +pub struct PaymentSessionIntent; + +impl ValidateStatusForOperation for PaymentSessionIntent { + /// Validate if the current operation can be performed on the current status of the payment intent + fn validate_status_for_operation( + &self, + intent_status: common_enums::IntentStatus, + ) -> Result<(), errors::ApiErrorResponse> { + match intent_status { + common_enums::IntentStatus::RequiresPaymentMethod => Ok(()), + common_enums::IntentStatus::Cancelled + | common_enums::IntentStatus::Processing + | common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::RequiresCapture + | common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::RequiresConfirmation + | common_enums::IntentStatus::PartiallyCapturedAndCapturable + | common_enums::IntentStatus::Succeeded + | common_enums::IntentStatus::Failed => { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: format!( + "You cannot create session token for this payment because it has status {intent_status}. Expected status is requires_payment_method.", + ), + }) + } + } + } +} + +impl Operation for &PaymentSessionIntent { + type Data = payments::PaymentIntentData; + fn to_validate_request( + &self, + ) -> RouterResult<&(dyn ValidateRequest + Send + Sync)> + { + Ok(*self) + } + fn to_get_tracker( + &self, + ) -> RouterResult<&(dyn GetTracker + Send + Sync)> { + Ok(*self) + } + fn to_domain(&self) -> RouterResult<&(dyn Domain)> { + Ok(*self) + } +} + +impl Operation for PaymentSessionIntent { + type Data = payments::PaymentIntentData; + fn to_validate_request( + &self, + ) -> RouterResult<&(dyn ValidateRequest + Send + Sync)> + { + Ok(self) + } + fn to_get_tracker( + &self, + ) -> RouterResult<&(dyn GetTracker + Send + Sync)> { + Ok(self) + } + fn to_domain(&self) -> RouterResult<&dyn Domain> { + Ok(self) + } +} + +type PaymentsCreateIntentOperation<'b, F> = + BoxedOperation<'b, F, PaymentsSessionRequest, payments::PaymentIntentData>; + +#[async_trait] +impl GetTracker, PaymentsSessionRequest> + for PaymentSessionIntent +{ + #[instrument(skip_all)] + async fn get_trackers<'a>( + &'a self, + state: &'a SessionState, + payment_id: &common_utils::id_type::GlobalPaymentId, + _request: &PaymentsSessionRequest, + merchant_account: &domain::MerchantAccount, + _profile: &domain::Profile, + key_store: &domain::MerchantKeyStore, + header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + ) -> RouterResult>> { + let db = &*state.store; + let key_manager_state = &state.into(); + let storage_scheme = merchant_account.storage_scheme; + + let payment_intent = db + .find_payment_intent_by_id(key_manager_state, payment_id, key_store, storage_scheme) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + self.validate_status_for_operation(payment_intent.status)?; + + let client_secret = header_payload + .client_secret + .as_ref() + .get_required_value("client_secret header")?; + payment_intent.validate_client_secret(client_secret)?; + + let payment_data = payments::PaymentIntentData { + flow: PhantomData, + payment_intent, + sessions_token: vec![], + }; + + let get_trackers_response = operations::GetTrackerResponse { payment_data }; + + Ok(get_trackers_response) + } +} + +impl ValidateRequest> + for PaymentSessionIntent +{ + #[instrument(skip_all)] + fn validate_request<'a, 'b>( + &'b self, + _request: &PaymentsSessionRequest, + merchant_account: &'a domain::MerchantAccount, + ) -> RouterResult { + Ok(operations::ValidateResult { + merchant_id: merchant_account.get_id().to_owned(), + storage_scheme: merchant_account.storage_scheme, + requeue: false, + }) + } +} + +#[async_trait] +impl Domain> + for PaymentSessionIntent +{ + #[instrument(skip_all)] + async fn get_customer_details<'a>( + &'a self, + state: &SessionState, + payment_data: &mut payments::PaymentIntentData, + merchant_key_store: &domain::MerchantKeyStore, + storage_scheme: enums::MerchantStorageScheme, + ) -> CustomResult< + ( + BoxedOperation<'a, F, PaymentsSessionRequest, payments::PaymentIntentData>, + Option, + ), + errors::StorageError, + > { + match payment_data.payment_intent.customer_id.clone() { + Some(id) => { + let customer = state + .store + .find_customer_by_global_id( + &state.into(), + id.get_string_repr(), + &payment_data.payment_intent.merchant_id, + merchant_key_store, + storage_scheme, + ) + .await?; + Ok((Box::new(self), Some(customer))) + } + None => Ok((Box::new(self), None)), + } + } + + #[instrument(skip_all)] + async fn make_pm_data<'a>( + &'a self, + _state: &'a SessionState, + _payment_data: &mut payments::PaymentIntentData, + _storage_scheme: enums::MerchantStorageScheme, + _merchant_key_store: &domain::MerchantKeyStore, + _customer: &Option, + _business_profile: &domain::Profile, + ) -> RouterResult<( + PaymentsCreateIntentOperation<'a, F>, + Option, + Option, + )> { + Ok((Box::new(self), None, None)) + } + + async fn perform_routing<'a>( + &'a self, + merchant_account: &domain::MerchantAccount, + _business_profile: &domain::Profile, + state: &SessionState, + payment_data: &mut payments::PaymentIntentData, + merchant_key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + let db = &state.store; + let all_connector_accounts = db + .find_merchant_connector_account_by_merchant_id_and_disabled_list( + &state.into(), + merchant_account.get_id(), + false, + merchant_key_store, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Database error when querying for merchant connector accounts")?; + let all_connector_accounts = domain::MerchantConnectorAccounts::new(all_connector_accounts); + let profile_id = &payment_data.payment_intent.profile_id; + let filtered_connector_accounts = all_connector_accounts + .filter_based_on_profile_and_connector_type( + profile_id, + common_enums::ConnectorType::PaymentProcessor, + ); + let connector_and_supporting_payment_method_type = filtered_connector_accounts + .get_connector_and_supporting_payment_method_type_for_session_call(); + let mut session_connector_data = + Vec::with_capacity(connector_and_supporting_payment_method_type.len()); + for (merchant_connector_account, payment_method_type) in + connector_and_supporting_payment_method_type + { + let connector_type = api::GetToken::from(payment_method_type); + if let Ok(connector_data) = api::ConnectorData::get_connector_by_name( + &state.conf.connectors, + &merchant_connector_account.connector_name.to_string(), + connector_type, + Some(merchant_connector_account.get_id()), + ) + .inspect_err(|err| { + logger::error!(session_token_error=?err); + }) { + let new_session_connector_data = + api::SessionConnectorData::new(payment_method_type, connector_data, None); + session_connector_data.push(new_session_connector_data) + }; + } + + Ok(api::ConnectorCallType::SessionMultiple( + session_connector_data, + )) + } + + #[instrument(skip_all)] + async fn guard_payment_against_blocklist<'a>( + &'a self, + _state: &SessionState, + _merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + _payment_data: &mut payments::PaymentIntentData, + ) -> CustomResult { + Ok(false) + } +} + +impl From for api::GetToken { + fn from(value: api_models::enums::PaymentMethodType) -> Self { + match value { + api_models::enums::PaymentMethodType::GooglePay => Self::GpayMetadata, + api_models::enums::PaymentMethodType::ApplePay => Self::ApplePayMetadata, + api_models::enums::PaymentMethodType::SamsungPay => Self::SamsungPayMetadata, + api_models::enums::PaymentMethodType::Paypal => Self::PaypalSdkMetadata, + api_models::enums::PaymentMethodType::Paze => Self::PazeMetadata, + _ => Self::Connector, + } + } +} diff --git a/crates/router/src/core/payments/session_operation.rs b/crates/router/src/core/payments/session_operation.rs new file mode 100644 index 000000000000..d7b6ad0d345b --- /dev/null +++ b/crates/router/src/core/payments/session_operation.rs @@ -0,0 +1,186 @@ +use std::fmt::Debug; + +pub use common_enums::enums::CallConnectorAction; +use common_utils::id_type; +use error_stack::ResultExt; +pub use hyperswitch_domain_models::{ + mandates::{CustomerAcceptance, MandateData}, + payment_address::PaymentAddress, + payments::HeaderPayload, + router_data::{PaymentMethodToken, RouterData}, + router_request_types::CustomerDetails, +}; +use router_env::{instrument, tracing}; + +use crate::{ + core::{ + errors::{self, utils::StorageErrorExt, RouterResult}, + payments::{ + call_multiple_connectors_service, + flows::{ConstructFlowSpecificData, Feature}, + operations, + operations::{BoxedOperation, Operation}, + transformers, OperationSessionGetters, OperationSessionSetters, + }, + }, + errors::RouterResponse, + routes::{app::ReqState, SessionState}, + services, + types::{self as router_types, api, domain}, +}; + +#[cfg(feature = "v2")] +#[allow(clippy::too_many_arguments)] +pub async fn payments_session_core( + state: SessionState, + req_state: ReqState, + merchant_account: domain::MerchantAccount, + profile: domain::Profile, + key_store: domain::MerchantKeyStore, + operation: Op, + req: Req, + payment_id: id_type::GlobalPaymentId, + call_connector_action: CallConnectorAction, + header_payload: HeaderPayload, +) -> RouterResponse +where + F: Send + Clone + Sync, + Req: Send + Sync, + FData: Send + Sync + Clone, + Op: Operation + Send + Sync + Clone, + Req: Debug, + D: OperationSessionGetters + OperationSessionSetters + Send + Sync + Clone, + Res: transformers::ToResponse, + // To create connector flow specific interface data + D: ConstructFlowSpecificData, + RouterData: Feature, + + // To construct connector flow specific api + dyn api::Connector: + services::api::ConnectorIntegration, +{ + let (payment_data, _req, customer, connector_http_status_code, external_latency) = + payments_session_operation_core::<_, _, _, _, _>( + &state, + req_state, + merchant_account.clone(), + key_store, + profile, + operation.clone(), + req, + payment_id, + call_connector_action, + header_payload.clone(), + ) + .await?; + + Res::generate_response( + payment_data, + customer, + &state.base_url, + operation, + &state.conf.connector_request_reference_id_config, + connector_http_status_code, + external_latency, + header_payload.x_hs_latency, + &merchant_account, + ) +} + +#[allow(clippy::too_many_arguments, clippy::type_complexity)] +#[instrument(skip_all, fields(payment_id, merchant_id))] +pub async fn payments_session_operation_core( + state: &SessionState, + _req_state: ReqState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + profile: domain::Profile, + operation: Op, + req: Req, + payment_id: id_type::GlobalPaymentId, + _call_connector_action: CallConnectorAction, + header_payload: HeaderPayload, +) -> RouterResult<(D, Req, Option, Option, Option)> +where + F: Send + Clone + Sync, + Req: Send + Sync, + Op: Operation + Send + Sync, + D: OperationSessionGetters + OperationSessionSetters + Send + Sync + Clone, + + // To create connector flow specific interface data + D: ConstructFlowSpecificData, + RouterData: Feature, + + // To construct connector flow specific api + dyn api::Connector: + services::api::ConnectorIntegration, + FData: Send + Sync + Clone, +{ + let operation: BoxedOperation<'_, F, Req, D> = Box::new(operation); + + let _validate_result = operation + .to_validate_request()? + .validate_request(&req, &merchant_account)?; + + let operations::GetTrackerResponse { mut payment_data } = operation + .to_get_tracker()? + .get_trackers( + state, + &payment_id, + &req, + &merchant_account, + &profile, + &key_store, + &header_payload, + ) + .await?; + + let (_operation, customer) = operation + .to_domain()? + .get_customer_details( + state, + &mut payment_data, + &key_store, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound) + .attach_printable("Failed while fetching/creating customer")?; + + let connector = operation + .to_domain()? + .perform_routing( + &merchant_account, + &profile, + &state.clone(), + &mut payment_data, + &key_store, + ) + .await?; + + let payment_data = match connector { + api::ConnectorCallType::PreDetermined(_connector) => { + todo!() + } + api::ConnectorCallType::Retryable(_connectors) => todo!(), + api::ConnectorCallType::Skip => todo!(), + api::ConnectorCallType::SessionMultiple(connectors) => { + // todo: call surcharge manager for session token call. + Box::pin(call_multiple_connectors_service( + state, + &merchant_account, + &key_store, + connectors, + &operation, + payment_data, + &customer, + None, + &profile, + header_payload.clone(), + )) + .await? + } + }; + + Ok((payment_data, req, customer, None, None)) +} diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 326975081016..4baeade05abf 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -7,8 +7,7 @@ use api_models::payments::{ use common_enums::{Currency, RequestIncrementalAuthorization}; use common_utils::{ consts::X_HS_LATENCY, - fp_utils, - pii::Email, + fp_utils, pii, types::{self as common_utils_type, AmountConvertor, MinorUnit, StringMajorUnitForConnector}, }; use diesel_models::{ @@ -17,7 +16,7 @@ use diesel_models::{ }; use error_stack::{report, ResultExt}; #[cfg(feature = "v2")] -use hyperswitch_domain_models::payments::PaymentConfirmData; +use hyperswitch_domain_models::payments::{PaymentConfirmData, PaymentIntentData}; #[cfg(feature = "v2")] use hyperswitch_domain_models::ApiModelToDieselModelConvertor; use hyperswitch_domain_models::{payments::payment_intent::CustomerData, router_request_types}; @@ -510,6 +509,152 @@ pub async fn construct_router_data_for_psync<'a>( Ok(router_data) } +#[cfg(feature = "v2")] +#[instrument(skip_all)] +#[allow(clippy::too_many_arguments)] +pub async fn construct_payment_router_data_for_sdk_session<'a>( + _state: &'a SessionState, + payment_data: PaymentIntentData, + connector_id: &str, + merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + customer: &'a Option, + merchant_connector_account: &domain::MerchantConnectorAccount, + _merchant_recipient_data: Option, + header_payload: Option, +) -> RouterResult { + fp_utils::when(merchant_connector_account.is_disabled(), || { + Err(errors::ApiErrorResponse::MerchantConnectorAccountDisabled) + })?; + + let auth_type: types::ConnectorAuthType = merchant_connector_account + .get_connector_account_details() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while parsing value for ConnectorAuthType")?; + + // TODO: Take Globalid and convert to connector reference id + let customer_id = customer + .to_owned() + .map(|customer| customer.id.clone()) + .map(std::borrow::Cow::Owned) + .map(common_utils::id_type::CustomerId::try_from) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "Invalid global customer generated, not able to convert to reference id", + )?; + let email = customer + .as_ref() + .and_then(|customer| customer.email.clone()) + .map(pii::Email::from); + let order_details = payment_data + .payment_intent + .order_details + .clone() + .map(|order_details| { + order_details + .into_iter() + .map(|order_detail| order_detail.expose()) + .collect() + }); + // TODO: few fields are repeated in both routerdata and request + let request = types::PaymentsSessionData { + amount: payment_data + .payment_intent + .amount_details + .order_amount + .get_amount_as_i64(), + currency: payment_data.payment_intent.amount_details.currency, + country: payment_data + .payment_intent + .billing_address + .and_then(|billing_address| { + billing_address + .get_inner() + .address + .as_ref() + .and_then(|address| address.country) + }), + // TODO: populate surcharge here + surcharge_details: None, + order_details, + email, + minor_amount: payment_data.payment_intent.amount_details.order_amount, + }; + + // TODO: evaluate the fields in router data, if they are required or not + let router_data = types::RouterData { + flow: PhantomData, + merchant_id: merchant_account.get_id().clone(), + // TODO: evaluate why we need customer id at the connector level. We already have connector customer id. + customer_id, + connector: connector_id.to_owned(), + // TODO: evaluate why we need payment id at the connector level. We already have connector reference id + payment_id: payment_data.payment_intent.id.get_string_repr().to_owned(), + // TODO: evaluate why we need attempt id at the connector level. We already have connector reference id + attempt_id: "".to_string(), + status: enums::AttemptStatus::Started, + payment_method: enums::PaymentMethod::Wallet, + connector_auth_type: auth_type, + description: payment_data + .payment_intent + .description + .as_ref() + .map(|description| description.get_string_repr()) + .map(ToOwned::to_owned), + // TODO: evaluate why we need to send merchant's return url here + // This should be the return url of application, since application takes care of the redirection + return_url: payment_data + .payment_intent + .return_url + .as_ref() + .map(|description| description.get_string_repr()) + .map(ToOwned::to_owned), + // TODO: Create unified address + address: hyperswitch_domain_models::payment_address::PaymentAddress::default(), + auth_type: payment_data.payment_intent.authentication_type, + connector_meta_data: merchant_connector_account.get_metadata(), + connector_wallets_details: None, + request, + response: Err(hyperswitch_domain_models::router_data::ErrorResponse::default()), + amount_captured: None, + minor_amount_captured: None, + access_token: None, + session_token: None, + reference_id: None, + payment_method_status: None, + payment_method_token: None, + connector_customer: None, + recurring_mandate_payment_data: None, + // TODO: This has to be generated as the reference id based on the connector configuration + // Some connectros might not accept accept the global id. This has to be done when generating the reference id + connector_request_reference_id: "".to_string(), + preprocessing_id: None, + #[cfg(feature = "payouts")] + payout_method_data: None, + #[cfg(feature = "payouts")] + quote_id: None, + // TODO: take this based on the env + test_mode: Some(true), + payment_method_balance: None, + connector_api_version: None, + connector_http_status_code: None, + external_latency: None, + apple_pay_flow: None, + frm_metadata: None, + refund_id: None, + dispute_id: None, + connector_response: None, + integrity_check: Ok(()), + additional_merchant_data: None, + header_payload, + connector_mandate_request_reference_id: None, + psd2_sca_exemption_type: None, + }; + + Ok(router_data) +} + #[cfg(feature = "v2")] #[instrument(skip_all)] #[allow(clippy::too_many_arguments)] @@ -852,6 +997,35 @@ where } } +#[cfg(feature = "v2")] +impl ToResponse for api::PaymentsSessionResponse +where + F: Clone, + Op: Debug, + D: OperationSessionGetters, +{ + #[allow(clippy::too_many_arguments)] + fn generate_response( + payment_data: D, + _customer: Option, + _base_url: &str, + _operation: Op, + _connector_request_reference_id_config: &ConnectorRequestReferenceIdConfig, + _connector_http_status_code: Option, + _external_latency: Option, + _is_latency_header_enabled: Option, + _merchant_account: &domain::MerchantAccount, + ) -> RouterResponse { + Ok(services::ApplicationResponse::JsonWithHeaders(( + Self { + session_token: payment_data.get_sessions_token(), + payment_id: payment_data.get_payment_intent().id.clone(), + }, + vec![], + ))) + } +} + #[cfg(feature = "v1")] impl ToResponse for api::PaymentsDynamicTaxCalculationResponse where @@ -1503,7 +1677,7 @@ where .and_then(|customer_data| customer_data.email.clone()) .or(customer_details_encrypted_data.email.or(customer .as_ref() - .and_then(|customer| customer.email.clone().map(Email::from)))), + .and_then(|customer| customer.email.clone().map(pii::Email::from)))), phone: customer_table_response .as_ref() .and_then(|customer_data| customer_data.phone.clone()) diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 1584cfae2b95..0f13e08d53e1 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -1676,7 +1676,6 @@ impl PayoutLink { route } } - pub struct Profile; #[cfg(all(feature = "olap", feature = "v2"))] impl Profile { diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 48b15c3b2047..93f50f3ed9a2 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -695,8 +695,61 @@ pub async fn payments_connector_session( state: web::Data, req: actix_web::HttpRequest, json_payload: web::Json, + path: web::Path, ) -> impl Responder { - "Session Response" + use hyperswitch_domain_models::payments::PaymentIntentData; + let flow = Flow::PaymentsSessionToken; + + let global_payment_id = path.into_inner(); + tracing::Span::current().record("payment_id", global_payment_id.get_string_repr()); + + let internal_payload = internal_payload_types::PaymentsGenericRequestWithResourceId { + global_payment_id, + payload: json_payload.into_inner(), + }; + + let header_payload = match HeaderPayload::foreign_try_from(req.headers()) { + Ok(headers) => headers, + Err(err) => { + return api::log_and_return_error_response(err); + } + }; + + let locking_action = internal_payload.get_locking_input(flow.clone()); + + Box::pin(api::server_wrap( + flow, + state, + &req, + internal_payload, + |state, auth: auth::AuthenticationData, req, req_state| { + let payment_id = req.global_payment_id; + let request = req.payload; + let operation = payments::operations::PaymentSessionIntent; + payments::payments_session_core::< + api_types::Session, + payment_types::PaymentsSessionResponse, + _, + _, + _, + PaymentIntentData, + >( + state, + req_state, + auth.merchant_account, + auth.profile, + auth.key_store, + operation, + request, + payment_id, + payments::CallConnectorAction::Trigger, + header_payload.clone(), + ) + }, + &auth::HeaderAuth(auth::PublishableKeyAuth), + locking_action, + )) + .await } #[cfg(feature = "v1")] @@ -1786,6 +1839,16 @@ impl GetLockingInput for payment_types::PaymentsSessionRequest { } } +#[cfg(feature = "v2")] +impl GetLockingInput for payment_types::PaymentsSessionRequest { + fn get_locking_input(&self, flow: F) -> api_locking::LockAction + where + F: types::FlowMetric, + { + api_locking::LockAction::NotApplicable + } +} + #[cfg(feature = "v1")] impl GetLockingInput for payment_types::PaymentsDynamicTaxCalculationRequest { fn get_locking_input(&self, flow: F) -> api_locking::LockAction