diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs index b9dc1476fdb8..75cb673463b8 100644 --- a/crates/api_models/src/events/payment.rs +++ b/crates/api_models/src/events/payment.rs @@ -357,7 +357,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 2ace2b956e4a..2e33e679d9d9 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -5878,6 +5878,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 @@ -5890,6 +5891,19 @@ 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, + /// This is a token which expires after 15 minutes, used from the client to authenticate and create sessions from the SDK + #[schema(value_type = String)] + pub client_secret: Secret, + /// 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/types.rs b/crates/common_utils/src/types.rs index ce2be525989e..9a25bb29d5e5 100644 --- a/crates/common_utils/src/types.rs +++ b/crates/common_utils/src/types.rs @@ -820,7 +820,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/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 883e23930977..30e0f13d9431 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -365,6 +365,11 @@ pub enum PaymentIntentUpdate { status: storage_enums::IntentStatus, updated_by: String, }, + /// Update the payment intent details on payment intent session token call, before calling the connector + MetadataUpdate { + metadata: masking::Secret, + updated_by: String, + }, } #[cfg(feature = "v1")] @@ -518,7 +523,7 @@ pub struct PaymentIntentUpdateInternal { // pub customer_id: Option, // pub return_url: Option<>, // pub setup_future_usage: Option, - // pub metadata: Option, + pub metadata: Option, pub modified_at: PrimitiveDateTime, // pub active_attempt_id: Option, // pub description: Option, @@ -592,7 +597,7 @@ impl PaymentIntentUpdate { // customer_id, // return_url, // setup_future_usage, - // metadata, + metadata, modified_at: _, // active_attempt_id, // description, @@ -618,7 +623,7 @@ impl PaymentIntentUpdate { // customer_id: customer_id.or(source.customer_id), // return_url: return_url.or(source.return_url), // setup_future_usage: setup_future_usage.or(source.setup_future_usage), - // metadata: metadata.or(source.metadata), + metadata: metadata.or(source.metadata), modified_at: common_utils::date_time::now(), // active_attempt_id: active_attempt_id.unwrap_or(source.active_attempt_id), // description: description.or(source.description), @@ -738,11 +743,22 @@ impl From for PaymentIntentUpdateInternal { PaymentIntentUpdate::ConfirmIntent { status, updated_by } => Self { status: Some(status), modified_at: common_utils::date_time::now(), + metadata: None, updated_by, }, PaymentIntentUpdate::ConfirmIntentPostUpdate { status, updated_by } => Self { status: Some(status), modified_at: common_utils::date_time::now(), + metadata: None, + updated_by, + }, + PaymentIntentUpdate::MetadataUpdate { + metadata, + updated_by, + } => Self { + status: None, + modified_at: common_utils::date_time::now(), + metadata: Some(metadata), updated_by, }, } diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index a21788e1bb0a..7e6e6ffb6652 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -2,7 +2,7 @@ use std::marker::PhantomData; #[cfg(feature = "v2")] -use api_models::payments::Address; +use api_models::payments::{Address, SessionToken}; use common_utils::{ self, crypto::Encryptable, @@ -509,6 +509,8 @@ where { pub flow: PhantomData, pub payment_intent: PaymentIntent, + pub email: Option, + pub sessions_token: Vec, } // TODO: Check if this can be merged with existing payment data diff --git a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs index 3b26ff94e547..13055e475c32 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs @@ -279,6 +279,10 @@ pub enum PaymentIntentUpdate { status: storage_enums::IntentStatus, updated_by: String, }, + MetadataUpdate { + metadata: Secret, + updated_by: String, + }, } #[cfg(feature = "v2")] @@ -373,6 +377,14 @@ impl From for PaymentIntentUpdateInternal { updated_by, ..Default::default() }, + PaymentIntentUpdate::MetadataUpdate { + metadata, + updated_by, + } => Self { + metadata: Some(metadata), + updated_by, + ..Default::default() + }, } } } @@ -588,6 +600,13 @@ impl From for DieselPaymentIntentUpdate { PaymentIntentUpdate::ConfirmIntentPostUpdate { status, updated_by } => { Self::ConfirmIntentPostUpdate { status, updated_by } } + PaymentIntentUpdate::MetadataUpdate { + metadata, + updated_by, + } => Self::MetadataUpdate { + metadata, + updated_by, + }, } } } diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 5d75001b118a..1390293b12c5 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; @@ -50,6 +52,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; @@ -172,7 +176,6 @@ where .await .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound) .attach_printable("Failed while fetching/creating customer")?; - let connector = get_connector_choice( &operation, state, @@ -185,7 +188,6 @@ where None, ) .await?; - // TODO: do not use if let let payment_data = if let Some(connector_call_type) = connector { match connector_call_type { @@ -2789,6 +2791,7 @@ where ) .await?; + #[cfg(feature = "v1")] payment_data.set_surcharge_details(session_surcharge_details.as_ref().and_then( |session_surcharge_details| { session_surcharge_details.fetch_surcharge_details( @@ -3201,6 +3204,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, @@ -3215,7 +3219,6 @@ where F: Clone, D: OperationSessionGetters + Send + Sync + Clone, { - #[cfg(feature = "v1")] let profile_id = payment_data .get_payment_intent() .profile_id @@ -3242,6 +3245,37 @@ where Ok(merchant_connector_account) } +#[cfg(feature = "v2")] +#[instrument(skip_all)] +pub async fn construct_profile_id_and_get_mca<'a, F, D>( + state: &'a SessionState, + merchant_account: &domain::MerchantAccount, + payment_data: &D, + connector_name: &str, + merchant_connector_id: Option<&id_type::MerchantConnectorAccountId>, + key_store: &domain::MerchantKeyStore, + _should_validate: bool, +) -> RouterResult +where + F: Clone, + D: OperationSessionGetters + Send + Sync + Clone, +{ + let profile_id = payment_data.get_payment_intent().profile_id.clone(); + + let merchant_connector_account = helpers::get_merchant_connector_account( + state, + merchant_account.get_id(), + None, + key_store, + &profile_id, + connector_name, + merchant_connector_id, + ) + .await?; + + Ok(merchant_connector_account) +} + fn is_payment_method_tokenization_enabled_for_connector( state: &SessionState, connector_name: &str, @@ -4439,7 +4473,43 @@ where }; Ok(connector) } +#[cfg(feature = "v2")] +#[allow(clippy::too_many_arguments)] +pub async fn get_connector_choice_for_sdk_session_token( + operation: &BoxedOperation<'_, F, Req, D>, + state: &SessionState, + req: &Req, + merchant_account: &domain::MerchantAccount, + _business_profile: &domain::Profile, + key_store: &domain::MerchantKeyStore, + payment_data: &mut D, + _eligible_connectors: Option>, + _mandate_type: Option, +) -> RouterResult> +where + F: Send + Clone, + D: OperationSessionGetters + OperationSessionSetters + Send + Sync + Clone, +{ + let connector_choice = operation + .to_domain()? + .get_connector( + merchant_account, + &state.clone(), + req, + payment_data.get_payment_intent(), + key_store, + ) + .await?; + let connector = Some(match connector_choice { + api::ConnectorChoice::SessionMultiple(connectors) => { + // todo perform session routing here + ConnectorCallType::SessionMultiple(connectors) + } + _ => return Err(errors::ApiErrorResponse::InternalServerError.into()), + }); + Ok(connector) +} async fn get_eligible_connector_for_nti( state: &SessionState, key_store: &domain::MerchantKeyStore, @@ -6538,7 +6608,7 @@ impl OperationSessionGetters for PaymentIntentData { } fn get_sessions_token(&self) -> Vec { - todo!() + self.sessions_token.clone() } fn get_token_data(&self) -> Option<&storage::PaymentTokenData> { @@ -6589,8 +6659,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 ba8054696a11..66198b87b99f 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,6 +28,50 @@ use crate::{ utils::OptionExt, }; +#[cfg(feature = "v2")] +#[async_trait] +impl + ConstructFlowSpecificData + for PaymentIntentData +{ + async fn construct_router_data<'a>( + &self, + state: &routes::SessionState, + connector_id: &str, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + customer: &Option, + merchant_connector_account: &helpers::MerchantConnectorAccountType, + merchant_recipient_data: Option, + header_payload: Option, + ) -> RouterResult { + Box::pin(transformers::construct_payment_router_data_for_sdk_session( + 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>( + &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 diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index b03f41ac0fd1..f7caf04581fd 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")] @@ -42,10 +44,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_intent::PaymentGetIntent; pub use self::payment_response::PaymentResponse; @@ -59,6 +57,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 bd82a67e6270..938719d24d20 100644 --- a/crates/router/src/core/payments/operations/payment_create_intent.rs +++ b/crates/router/src/core/payments/operations/payment_create_intent.rs @@ -174,6 +174,8 @@ impl GetTracker, PaymentsCrea let payment_data = payments::PaymentIntentData { flow: PhantomData, payment_intent, + email: None, + sessions_token: vec![], }; let get_trackers_response = operations::GetTrackerResponse { 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 55ddc3b482a6..9a53db090f71 100644 --- a/crates/router/src/core/payments/operations/payment_get_intent.rs +++ b/crates/router/src/core/payments/operations/payment_get_intent.rs @@ -108,6 +108,8 @@ impl GetTracker, PaymentsGetI let payment_data = payments::PaymentIntentData { flow: PhantomData, payment_intent, + email: None, + sessions_token: vec![], }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 0234b97032c7..59f083814cfb 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -13,7 +13,7 @@ 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; +use hyperswitch_domain_models::payments::{PaymentConfirmData, PaymentIntentData}; use router_derive; use router_env::{instrument, logger, metrics::add_attributes, tracing}; use storage_impl::DataModelExt; @@ -2144,6 +2144,56 @@ async fn update_payment_method_status_and_ntid( Ok(()) } +#[cfg(feature = "v2")] +impl Operation for &PaymentResponse { + type Data = PaymentIntentData; + fn to_post_update_tracker( + &self, + ) -> RouterResult< + &(dyn PostUpdateTracker + Send + Sync), + > { + Ok(*self) + } +} + +#[cfg(feature = "v2")] +impl Operation for PaymentResponse { + type Data = PaymentIntentData; + fn to_post_update_tracker( + &self, + ) -> RouterResult< + &(dyn PostUpdateTracker + Send + Sync), + > { + Ok(self) + } +} + +#[cfg(feature = "v2")] +#[async_trait] +impl PostUpdateTracker, types::PaymentsSessionData> + for PaymentResponse +{ + async fn update_tracker<'b>( + &'b self, + state: &'b SessionState, + mut payment_data: PaymentIntentData, + response: types::RouterData, + key_store: &domain::MerchantKeyStore, + storage_scheme: enums::MerchantStorageScheme, + locale: &Option, + #[cfg(all(feature = "v1", feature = "dynamic_routing"))] routable_connector: Vec< + RoutableConnectorChoice, + >, + #[cfg(all(feature = "v1", feature = "dynamic_routing"))] business_profile: &domain::Profile, + ) -> RouterResult> + where + F: 'b + Send + Sync, + { + // TODO: Implement this + Ok(payment_data) + } +} + #[cfg(feature = "v2")] impl Operation for &PaymentResponse { type Data = PaymentConfirmData; 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..ad848018a383 --- /dev/null +++ b/crates/router/src/core/payments/operations/payment_session_intent.rs @@ -0,0 +1,375 @@ +use std::marker::PhantomData; + +use api_models::{ + admin::PaymentMethodsEnabled, enums::FrmSuggestion, payments::PaymentsSessionRequest, +}; +use async_trait::async_trait; +use common_utils::{errors::CustomResult, ext_traits::ValueExt}; +use error_stack::ResultExt; +use masking::ExposeInterface; +use router_env::{instrument, logger, tracing}; + +use super::{BoxedOperation, Domain, GetTracker, Operation, UpdateTracker, ValidateRequest}; +use crate::{ + core::{ + errors::{self, RouterResult, StorageErrorExt}, + payments::{self, helpers, operations}, + }, + routes::{app::ReqState, SessionState}, + types::{ + api, domain, + storage::{self, enums}, + }, +}; + +#[derive(Debug, Clone, Copy)] +pub struct PaymentSessionIntent; + +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) + } + fn to_update_tracker( + &self, + ) -> RouterResult<&(dyn UpdateTracker + Send + Sync)> + { + 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) + } + fn to_update_tracker( + &self, + ) -> RouterResult<&(dyn UpdateTracker + Send + Sync)> + { + 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< + operations::GetTrackerResponse< + 'a, + F, + PaymentsSessionRequest, + payments::PaymentIntentData, + >, + > { + 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)?; + + helpers::validate_payment_status_against_not_allowed_statuses( + &payment_intent.status, + &[enums::IntentStatus::Failed, enums::IntentStatus::Succeeded], + "create a session token for", + )?; + + // do this in core function + // helpers::authenticate_client_secret(Some(&request.client_secret), &payment_intent)?; + + let payment_data = payments::PaymentIntentData { + flow: PhantomData, + payment_intent, + email: None, + sessions_token: vec![], + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + payment_data, + }; + + Ok(get_trackers_response) + } +} + +#[async_trait] +impl UpdateTracker, PaymentsSessionRequest> + for PaymentSessionIntent +{ + #[instrument(skip_all)] + async fn update_trackers<'b>( + &'b self, + state: &'b SessionState, + _req_state: ReqState, + mut payment_data: payments::PaymentIntentData, + _customer: Option, + storage_scheme: enums::MerchantStorageScheme, + _updated_customer: Option, + key_store: &domain::MerchantKeyStore, + _frm_suggestion: Option, + _header_payload: hyperswitch_domain_models::payments::HeaderPayload, + ) -> RouterResult<( + PaymentsCreateIntentOperation<'b, F>, + payments::PaymentIntentData, + )> + where + F: 'b + Send, + { + let metadata = payment_data.payment_intent.metadata.clone(); + payment_data.payment_intent = match metadata { + Some(metadata) => state + .store + .update_payment_intent( + &state.into(), + payment_data.payment_intent, + storage::PaymentIntentUpdate::MetadataUpdate { + metadata, + updated_by: storage_scheme.to_string(), + }, + key_store, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?, + None => payment_data.payment_intent, + }; + + Ok((Box::new(self), payment_data)) + } +} + +impl ValidateRequest> + for PaymentSessionIntent +{ + #[instrument(skip_all)] + fn validate_request<'a, 'b>( + &'b self, + _request: &PaymentsSessionRequest, + merchant_account: &'a domain::MerchantAccount, + ) -> RouterResult<( + PaymentsCreateIntentOperation<'b, F>, + operations::ValidateResult, + )> { + Ok(( + Box::new(self), + 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?; + payment_data.email = customer + .email + .clone() + .map(|email| common_utils::pii::Email::from(email)); + 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 get_connector<'a>( + &'a self, + merchant_account: &domain::MerchantAccount, + state: &SessionState, + _request: &PaymentsSessionRequest, + payment_intent: &storage::PaymentIntent, + 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 profile_id = &payment_intent.profile_id; + let filtered_connector_accounts = helpers::filter_mca_based_on_profile_and_connector_type( + all_connector_accounts, + profile_id, + common_enums::ConnectorType::PaymentProcessor, + ); + let mut connector_and_supporting_payment_method_type = Vec::new(); + filtered_connector_accounts + .iter() + .for_each(|connector_account| { + let res = connector_account + .payment_methods_enabled + .clone() + .unwrap_or_default() + .into_iter() + .map(|payment_methods_enabled| { + payment_methods_enabled + .parse_value::("payment_methods_enabled") + }) + .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::>(); + connector_and_supporting_payment_method_type.extend(res); + }); + 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::ConnectorChoice::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..88b404a86f80 --- /dev/null +++ b/crates/router/src/core/payments/session_operation.rs @@ -0,0 +1,204 @@ +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}, + get_connector_choice_for_sdk_session_token, operations, + operations::{BoxedOperation, Operation, PaymentResponse}, + transformers, OperationSessionGetters, OperationSessionSetters, + }, + }, + errors::RouterResponse, + routes::{app::ReqState, SessionState}, + services, + types::{ + self as router_types, + api::{self, ConnectorCallType}, + 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, + + // To perform router related operation for PaymentResponse + PaymentResponse: Operation, +{ + let (payment_data, _req, customer, connector_http_status_code, external_latency) = + payments_session_operation_core::<_, _, _, _, _>( + &state, + req_state, + merchant_account, + 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, + ) +} + +#[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, + + // To perform router related operation for PaymentResponse + PaymentResponse: Operation, + FData: Send + Sync + Clone, +{ + let operation: BoxedOperation<'_, F, Req, D> = Box::new(operation); + + let (operation, _validate_result) = operation + .to_validate_request()? + .validate_request(&req, &merchant_account)?; + + let operations::GetTrackerResponse { + operation, + 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 = get_connector_choice_for_sdk_session_token( + &operation, + state, + &req, + &merchant_account, + &profile, + &key_store, + &mut payment_data, + None, + None, + ) + .await?; + + // TODO: do not use if let + let payment_data = if let Some(connector_call_type) = connector { + match connector_call_type { + ConnectorCallType::PreDetermined(_connectors) => { + todo!() + } + ConnectorCallType::Retryable(_connectors) => todo!(), + 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? + } + } + } else { + todo!() + }; + + 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 45d0f75ccec8..60fa4882ef84 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -17,7 +17,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}; @@ -376,6 +376,149 @@ pub async fn construct_payment_router_data_for_authorize<'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: &helpers::MerchantConnectorAccountType, + _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() + .parse_value("ConnectorAuthType") + .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 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() + .peek() + .address + .as_ref() + .and_then(|address| address.country) + }), + // TODO: populate surcharge here + surcharge_details: None, + order_details, + email: payment_data.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, + }; + + Ok(router_data) +} + #[cfg(feature = "v2")] #[instrument(skip_all)] #[allow(clippy::too_many_arguments)] @@ -716,6 +859,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, + ) -> RouterResponse { + Ok(services::ApplicationResponse::JsonWithHeaders(( + Self { + session_token: payment_data.get_sessions_token(), + payment_id: payment_data.get_payment_intent().id.clone(), + client_secret: Secret::new(payment_data.get_payment_intent().client_secret.clone()), + }, + vec![], + ))) + } +} + #[cfg(feature = "v1")] impl ToResponse for api::PaymentsDynamicTaxCalculationResponse where diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index ba785fbee8e6..f5181ef90c96 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