diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index e14963c80d48..84562b99343d 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -13383,6 +13383,14 @@ "payment_method_subtype": { "$ref": "#/components/schemas/PaymentMethodType" }, + "next_action": { + "allOf": [ + { + "$ref": "#/components/schemas/NextActionData" + } + ], + "nullable": true + }, "connector_transaction_id": { "type": "string", "description": "A unique identifier for a payment provided by the connector", diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs index b9dc1476fdb8..ab09a05d0b1c 100644 --- a/crates/api_models/src/events/payment.rs +++ b/crates/api_models/src/events/payment.rs @@ -2,8 +2,8 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; #[cfg(feature = "v2")] use super::{ - PaymentsConfirmIntentResponse, PaymentsCreateIntentRequest, PaymentsGetIntentRequest, - PaymentsIntentResponse, + PaymentStartRedirectionRequest, PaymentsConfirmIntentResponse, PaymentsCreateIntentRequest, + PaymentsGetIntentRequest, PaymentsIntentResponse, }; #[cfg(all( any(feature = "v2", feature = "v1"), @@ -365,3 +365,12 @@ impl ApiEventMetric for PaymentsSessionResponse { }) } } + +#[cfg(feature = "v2")] +impl ApiEventMetric for PaymentStartRedirectionRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payment { + payment_id: self.id.clone(), + }) + } +} diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 3e95fb9f2315..2494952e7ddc 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -3876,9 +3876,16 @@ pub enum NextActionType { #[serde(tag = "type", rename_all = "snake_case")] pub enum NextActionData { /// Contains the url for redirection flow + #[cfg(feature = "v1")] RedirectToUrl { redirect_to_url: String, }, + /// Contains the url for redirection flow + #[cfg(feature = "v2")] + RedirectToUrl { + #[schema(value_type = String)] + redirect_to_url: Url, + }, /// Informs the next steps for bank transfer and also contains the charges details (ex: amount received, amount charged etc) DisplayBankTransferInformation { bank_transfer_steps_and_charges_details: BankTransferNextStepsData, @@ -4538,6 +4545,9 @@ pub struct PaymentsConfirmIntentResponse { #[schema(value_type = PaymentMethodType, example = "apple_pay")] pub payment_method_subtype: api_enums::PaymentMethodType, + /// Additional information required for redirection + pub next_action: Option, + /// A unique identifier for a payment provided by the connector #[schema(value_type = Option, example = "993672945374576J")] pub connector_transaction_id: Option, @@ -4558,6 +4568,22 @@ pub struct PaymentsConfirmIntentResponse { pub error: Option, } +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +#[cfg(feature = "v2")] +pub struct PaymentStartRedirectionRequest { + /// Global Payment ID + pub id: id_type::GlobalPaymentId, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +#[cfg(feature = "v2")] +pub struct PaymentStartRedirectionParams { + /// The identifier for the Merchant Account. + pub publishable_key: String, + /// The identifier for business profile + pub profile_id: id_type::ProfileId, +} + /// Fee information to be charged on the payment being collected #[derive(Setter, Clone, Default, Debug, PartialEq, serde::Serialize, ToSchema)] pub struct PaymentChargeResponse { diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index f08847651e76..e233cfe390bb 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -770,7 +770,7 @@ pub struct PaymentAttemptUpdateInternal { pub updated_by: String, pub merchant_connector_id: Option, pub connector: Option, - // authentication_data: Option, + pub authentication_data: Option, // encoded_data: Option, pub unified_code: Option>, pub unified_message: Option>, diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 883e23930977..65fa044478ec 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -33,7 +33,7 @@ pub struct PaymentIntent { pub last_synced: Option, pub setup_future_usage: Option, pub client_secret: common_utils::types::ClientSecret, - pub active_attempt_id: Option, + pub active_attempt_id: Option, #[diesel(deserialize_as = super::OptionalDieselArray>)] pub order_details: Option>>, pub allowed_payment_method_types: Option, @@ -250,7 +250,7 @@ pub struct PaymentIntentNew { pub last_synced: Option, pub setup_future_usage: Option, pub client_secret: common_utils::types::ClientSecret, - pub active_attempt_id: Option, + pub active_attempt_id: Option, #[diesel(deserialize_as = super::OptionalDieselArray>)] pub order_details: Option>>, pub allowed_payment_method_types: Option, @@ -359,6 +359,7 @@ pub enum PaymentIntentUpdate { ConfirmIntent { status: storage_enums::IntentStatus, updated_by: String, + active_attempt_id: common_utils::id_type::GlobalAttemptId, }, /// Update the payment intent details on payment intent confirmation, after calling the connector ConfirmIntentPostUpdate { @@ -520,7 +521,7 @@ pub struct PaymentIntentUpdateInternal { // pub setup_future_usage: Option, // pub metadata: Option, pub modified_at: PrimitiveDateTime, - // pub active_attempt_id: Option, + pub active_attempt_id: Option, // pub description: Option, // pub statement_descriptor: Option, // #[diesel(deserialize_as = super::OptionalDieselArray)] @@ -594,7 +595,7 @@ impl PaymentIntentUpdate { // setup_future_usage, // metadata, modified_at: _, - // active_attempt_id, + active_attempt_id, // description, // statement_descriptor, // order_details, @@ -620,7 +621,7 @@ impl PaymentIntentUpdate { // setup_future_usage: setup_future_usage.or(source.setup_future_usage), // metadata: metadata.or(source.metadata), modified_at: common_utils::date_time::now(), - // active_attempt_id: active_attempt_id.unwrap_or(source.active_attempt_id), + active_attempt_id: active_attempt_id.or(source.active_attempt_id), // description: description.or(source.description), // statement_descriptor: statement_descriptor.or(source.statement_descriptor), // order_details: order_details.or(source.order_details), @@ -735,15 +736,21 @@ impl PaymentIntentUpdate { impl From for PaymentIntentUpdateInternal { fn from(payment_intent_update: PaymentIntentUpdate) -> Self { match payment_intent_update { - PaymentIntentUpdate::ConfirmIntent { status, updated_by } => Self { + PaymentIntentUpdate::ConfirmIntent { + status, + updated_by, + active_attempt_id, + } => Self { status: Some(status), modified_at: common_utils::date_time::now(), updated_by, + active_attempt_id: Some(active_attempt_id), }, PaymentIntentUpdate::ConfirmIntentPostUpdate { status, updated_by } => Self { status: Some(status), modified_at: common_utils::date_time::now(), updated_by, + active_attempt_id: None, }, } } diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index a21788e1bb0a..c1c32e08f4e5 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -111,6 +111,24 @@ impl PaymentIntent { pub fn get_id(&self) -> &id_type::GlobalPaymentId { &self.id } + + #[cfg(feature = "v2")] + pub fn create_start_redirection_url( + &self, + base_url: &str, + publishable_key: String, + ) -> CustomResult { + let start_redirection_url = &format!( + "{}/v2/payments/{}/start_redirection?publishable_key={}&profile_id={}", + base_url, + self.get_id().get_string_repr(), + publishable_key, + self.profile_id.get_string_repr() + ); + url::Url::parse(start_redirection_url) + .change_context(errors::api_error_response::ApiErrorResponse::InternalServerError) + .attach_printable("Error creating start redirection url") + } } #[cfg(feature = "v2")] @@ -272,8 +290,8 @@ pub struct PaymentIntent { pub setup_future_usage: storage_enums::FutureUsage, /// The client secret that is generated for the payment. This is used to authenticate the payment from client facing apis. pub client_secret: common_utils::types::ClientSecret, - /// The active attempt for the payment intent. This is the payment attempt that is currently active for the payment intent. - pub active_attempt: Option>, + /// The active attempt id for the payment intent. This is the payment attempt that is currently active for the payment intent. + pub active_attempt_id: Option, /// The order details for the payment. pub order_details: Option>>, /// This is the list of payment method types that are allowed for the payment intent. @@ -421,7 +439,7 @@ impl PaymentIntent { last_synced: None, setup_future_usage: request.setup_future_usage.unwrap_or_default(), client_secret, - active_attempt: None, + active_attempt_id: None, order_details, allowed_payment_method_types, connector_metadata, diff --git a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs index 831e97b54a17..a65abe1495e1 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs @@ -1296,6 +1296,7 @@ pub enum PaymentAttemptUpdate { status: storage_enums::AttemptStatus, connector_payment_id: Option, updated_by: String, + authentication_data: Option, }, /// Update the payment attempt on confirming the intent, after calling the connector on error response ConfirmIntentError { @@ -1923,6 +1924,7 @@ impl From for diesel_models::PaymentAttemptUpdateInternal unified_message: None, connector_payment_id: None, connector: Some(connector), + authentication_data: None, }, PaymentAttemptUpdate::ConfirmIntentError { status, @@ -1941,11 +1943,13 @@ impl From for diesel_models::PaymentAttemptUpdateInternal unified_message: None, connector_payment_id: None, connector: None, + authentication_data: None, }, PaymentAttemptUpdate::ConfirmIntentResponse { status, connector_payment_id, updated_by, + authentication_data, } => Self { status: Some(status), error_message: None, @@ -1959,6 +1963,7 @@ impl From for diesel_models::PaymentAttemptUpdateInternal unified_message: None, connector_payment_id, connector: None, + authentication_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..4e3f861d76ef 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs @@ -274,6 +274,7 @@ pub enum PaymentIntentUpdate { ConfirmIntent { status: storage_enums::IntentStatus, updated_by: String, + active_attempt_id: id_type::GlobalAttemptId, }, ConfirmIntentPostUpdate { status: storage_enums::IntentStatus, @@ -363,9 +364,14 @@ pub struct PaymentIntentUpdateInternal { impl From for PaymentIntentUpdateInternal { fn from(payment_intent_update: PaymentIntentUpdate) -> Self { match payment_intent_update { - PaymentIntentUpdate::ConfirmIntent { status, updated_by } => Self { + PaymentIntentUpdate::ConfirmIntent { + status, + updated_by, + active_attempt_id, + } => Self { status: Some(status), updated_by, + active_attempt_id: Some(active_attempt_id), ..Default::default() }, PaymentIntentUpdate::ConfirmIntentPostUpdate { status, updated_by } => Self { @@ -582,9 +588,15 @@ use diesel_models::{ impl From for DieselPaymentIntentUpdate { fn from(value: PaymentIntentUpdate) -> Self { match value { - PaymentIntentUpdate::ConfirmIntent { status, updated_by } => { - Self::ConfirmIntent { status, updated_by } - } + PaymentIntentUpdate::ConfirmIntent { + status, + updated_by, + active_attempt_id, + } => Self::ConfirmIntent { + status, + updated_by, + active_attempt_id, + }, PaymentIntentUpdate::ConfirmIntentPostUpdate { status, updated_by } => { Self::ConfirmIntentPostUpdate { status, updated_by } } @@ -1134,7 +1146,7 @@ impl behaviour::Conversion for PaymentIntent { last_synced, setup_future_usage, client_secret, - active_attempt, + active_attempt_id, order_details, allowed_payment_method_types, connector_metadata, @@ -1182,7 +1194,7 @@ impl behaviour::Conversion for PaymentIntent { last_synced, setup_future_usage: Some(setup_future_usage), client_secret, - active_attempt_id: active_attempt.map(|attempt| attempt.get_id()), + active_attempt_id, order_details: order_details.map(|order_details| { order_details .into_iter() @@ -1319,9 +1331,7 @@ impl behaviour::Conversion for PaymentIntent { last_synced: storage_model.last_synced, setup_future_usage: storage_model.setup_future_usage.unwrap_or_default(), client_secret: storage_model.client_secret, - active_attempt: storage_model - .active_attempt_id - .map(RemoteStorageObject::ForeignID), + active_attempt_id: storage_model.active_attempt_id, order_details: storage_model.order_details.map(|order_details| { order_details .into_iter() @@ -1395,7 +1405,7 @@ impl behaviour::Conversion for PaymentIntent { last_synced: self.last_synced, setup_future_usage: Some(self.setup_future_usage), client_secret: self.client_secret, - active_attempt_id: self.active_attempt.map(|attempt| attempt.get_id()), + active_attempt_id: self.active_attempt_id, order_details: self.order_details, allowed_payment_method_types: self .allowed_payment_method_types diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index a025b4b3cc85..ebf49daa809d 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -37,6 +37,8 @@ use futures::future::join_all; use helpers::{decrypt_paze_token, ApplePayData}; #[cfg(feature = "v2")] use hyperswitch_domain_models::payments::{PaymentConfirmData, PaymentIntentData}; +#[cfg(feature = "v2")] +use hyperswitch_domain_models::router_response_types::RedirectForm; pub use hyperswitch_domain_models::{ mandates::{CustomerAcceptance, MandateData}, payment_address::PaymentAddress, @@ -1463,7 +1465,7 @@ where let (payment_data, _req, customer) = payments_intent_operation_core::<_, _, _, _>( &state, req_state, - merchant_account, + merchant_account.clone(), profile, key_store, operation.clone(), @@ -1481,6 +1483,7 @@ where None, None, header_payload.x_hs_latency, + &merchant_account, ) } @@ -1521,7 +1524,7 @@ where payments_operation_core::<_, _, _, _, _>( &state, req_state, - merchant_account, + merchant_account.clone(), key_store, profile, operation.clone(), @@ -1541,6 +1544,7 @@ where connector_http_status_code, external_latency, header_payload.x_hs_latency, + &merchant_account, ) } @@ -5966,6 +5970,73 @@ pub async fn payment_external_authentication( )) } +#[instrument(skip_all)] +#[cfg(feature = "v2")] +pub async fn payment_start_redirection( + state: SessionState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + req: api_models::payments::PaymentStartRedirectionRequest, +) -> RouterResponse { + 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, &req.id, &key_store, storage_scheme) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + //TODO: send valid html error pages in this case, or atleast redirect to valid html error pages + utils::when( + payment_intent.status != storage_enums::IntentStatus::RequiresCustomerAction, + || { + Err(errors::ApiErrorResponse::PaymentUnexpectedState { + current_flow: "PaymentStartRedirection".to_string(), + field_name: "status".to_string(), + current_value: payment_intent.status.to_string(), + states: ["requires_customer_action".to_string()].join(", "), + }) + }, + )?; + + let payment_attempt = db + .find_payment_attempt_by_id( + key_manager_state, + &key_store, + payment_intent + .active_attempt_id + .clone() + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable("missing active attempt in payment_intent")? + .get_string_repr(), + storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error while fetching payment_attempt")?; + let redirection_data = payment_attempt + .authentication_data + .clone() + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable("missing authentication_data in payment_attempt")?; + + let form: RedirectForm = serde_json::from_value(redirection_data.expose()).map_err(|err| { + logger::error!(error = ?err, "Failed to deserialize redirection data"); + errors::ApiErrorResponse::InternalServerError + })?; + + Ok(services::ApplicationResponse::Form(Box::new( + services::RedirectionFormData { + redirect_form: form, + payment_method_data: None, + amount: payment_attempt.amount_details.net_amount.to_string(), + currency: payment_intent.amount_details.currency.to_string(), + }, + ))) +} + #[instrument(skip_all)] pub async fn get_extended_card_info( state: SessionState, diff --git a/crates/router/src/core/payments/operations/payment_confirm_intent.rs b/crates/router/src/core/payments/operations/payment_confirm_intent.rs index 083bb5adca28..4c61becdf4a9 100644 --- a/crates/router/src/core/payments/operations/payment_confirm_intent.rs +++ b/crates/router/src/core/payments/operations/payment_confirm_intent.rs @@ -328,6 +328,7 @@ impl UpdateTracker, PaymentsConfirmIntentRequ hyperswitch_domain_models::payments::payment_intent::PaymentIntentUpdate::ConfirmIntent { status: intent_status, updated_by: storage_scheme.to_string(), + active_attempt_id: payment_data.payment_attempt.get_id().clone(), }; let payment_attempt_update = hyperswitch_domain_models::payments::payment_attempt::PaymentAttemptUpdate::ConfirmIntent { diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 4b784ea9463b..427a406dbf59 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -2245,6 +2245,13 @@ impl PostUpdateTracker, types::PaymentsAuthor types::ResponseId::ConnectorTransactionId(id) | types::ResponseId::EncodedData(id) => Some(id), }; + let authentication_data = (*redirection_data) + .as_ref() + .map(Encode::encode_to_value) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not parse the connector response")? + .map(masking::Secret::new); let payment_intent_update = hyperswitch_domain_models::payments::payment_intent::PaymentIntentUpdate::ConfirmIntentPostUpdate { status: intent_status, updated_by: storage_scheme.to_string() }; let updated_payment_intent = db @@ -2260,7 +2267,7 @@ impl PostUpdateTracker, types::PaymentsAuthor .attach_printable("Unable to update payment intent")?; payment_data.payment_intent = updated_payment_intent; - let payment_attempt_update = hyperswitch_domain_models::payments::payment_attempt::PaymentAttemptUpdate::ConfirmIntentResponse { status: attempt_status, connector_payment_id, updated_by: storage_scheme.to_string() }; + let payment_attempt_update = hyperswitch_domain_models::payments::payment_attempt::PaymentAttemptUpdate::ConfirmIntentResponse { status: attempt_status, connector_payment_id, updated_by: storage_scheme.to_string(), authentication_data }; let updated_payment_attempt = db .update_payment_attempt( key_manager_state, diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 45d0f75ccec8..d37b249cd8a9 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -628,6 +628,7 @@ where connector_http_status_code: Option, external_latency: Option, is_latency_header_enabled: Option, + merchant_account: &domain::MerchantAccount, ) -> RouterResponse; } @@ -793,6 +794,7 @@ where _connector_http_status_code: Option, _external_latency: Option, _is_latency_header_enabled: Option, + _merchant_account: &domain::MerchantAccount, ) -> RouterResponse { let payment_intent = payment_data.get_payment_intent(); Ok(services::ApplicationResponse::JsonWithHeaders(( @@ -862,12 +864,13 @@ where fn generate_response( payment_data: D, _customer: Option, - _base_url: &str, + 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 { let payment_intent = payment_data.get_payment_intent(); let payment_attempt = payment_data.get_payment_attempt(); @@ -896,6 +899,14 @@ where .clone() .map(api_models::payments::ErrorDetails::foreign_from); + // TODO: Add support for other next actions, currently only supporting redirect to url + let redirect_to_url = payment_intent + .create_start_redirection_url(base_url, merchant_account.publishable_key.clone())?; + let next_action = payment_attempt + .authentication_data + .as_ref() + .map(|_| api_models::payments::NextActionData::RedirectToUrl { redirect_to_url }); + let response = Self { id: payment_intent.id.clone(), status: payment_intent.status, @@ -906,6 +917,7 @@ where payment_method_data: None, payment_method_type: payment_attempt.payment_method_type, payment_method_subtype: payment_attempt.payment_method_subtype, + next_action, connector_transaction_id: payment_attempt.connector_payment_id.clone(), connector_reference_id: None, merchant_connector_id, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 5ad8f3864418..9f0a83a053f6 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -535,6 +535,10 @@ impl Payments { .service( web::resource("/create-external-sdk-tokens") .route(web::post().to(payments::payments_connector_session)), + ) + .service( + web::resource("/start_redirection") + .route(web::get().to(payments::payments_start_redirection)), ), ); diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 43ddc35b8ae6..4d3718b967dd 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -140,7 +140,8 @@ impl From for ApiIdentifier { | Flow::PaymentsConfirmIntent | Flow::PaymentsCreateIntent | Flow::PaymentsGetIntent - | Flow::PaymentsPostSessionTokens => Self::Payments, + | Flow::PaymentsPostSessionTokens + | Flow::PaymentStartRedirection => Self::Payments, Flow::PayoutsCreate | Flow::PayoutsRetrieve diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index ba785fbee8e6..c20f487a28e8 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -2057,6 +2057,56 @@ mod internal_payload_types { } } +#[cfg(feature = "v2")] +#[instrument(skip_all, fields(flow = ?Flow::PaymentStartRedirection, payment_id))] +pub async fn payments_start_redirection( + state: web::Data, + req: actix_web::HttpRequest, + payload: web::Query, + path: web::Path, +) -> impl Responder { + let flow = Flow::PaymentStartRedirection; + + let global_payment_id = path.into_inner(); + tracing::Span::current().record("payment_id", global_payment_id.get_string_repr()); + + let publishable_key = &payload.publishable_key; + let profile_id = &payload.profile_id; + + let payment_start_redirection_request = api_models::payments::PaymentStartRedirectionRequest { + id: global_payment_id.clone(), + }; + + let internal_payload = internal_payload_types::PaymentsGenericRequestWithResourceId { + global_payment_id: global_payment_id.clone(), + payload: payment_start_redirection_request.clone(), + }; + + let locking_action = internal_payload.get_locking_input(flow.clone()); + + Box::pin(api::server_wrap( + flow, + state, + &req, + payment_start_redirection_request.clone(), + |state, auth: auth::AuthenticationData, _req, req_state| async { + payments::payment_start_redirection( + state, + auth.merchant_account, + auth.key_store, + payment_start_redirection_request.clone(), + ) + .await + }, + &auth::PublishableKeyAndProfileIdAuth { + publishable_key: publishable_key.clone(), + profile_id: profile_id.clone(), + }, + locking_action, + )) + .await +} + #[cfg(feature = "v2")] #[instrument(skip_all, fields(flow = ?Flow::PaymentsConfirmIntent, payment_id))] pub async fn payment_confirm_intent( diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 586252ff0ad7..70974f0ae6f6 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -1288,6 +1288,61 @@ where } } +#[derive(Debug)] +#[cfg(feature = "v2")] +pub struct PublishableKeyAndProfileIdAuth { + pub publishable_key: String, + pub profile_id: id_type::ProfileId, +} + +#[async_trait] +#[cfg(feature = "v2")] +impl AuthenticateAndFetch for PublishableKeyAndProfileIdAuth +where + A: SessionStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + _request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(AuthenticationData, AuthenticationType)> { + let key_manager_state = &(&state.session_state()).into(); + let (merchant_account, key_store) = state + .store() + .find_merchant_account_by_publishable_key( + key_manager_state, + self.publishable_key.as_str(), + ) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(errors::ApiErrorResponse::Unauthorized) + } else { + e.change_context(errors::ApiErrorResponse::InternalServerError) + } + })?; + + let profile = state + .store() + .find_business_profile_by_profile_id(key_manager_state, &key_store, &self.profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::ProfileNotFound { + id: self.profile_id.get_string_repr().to_owned(), + })?; + + let merchant_id = merchant_account.get_id().clone(); + + Ok(( + AuthenticationData { + merchant_account, + key_store, + profile, + }, + AuthenticationType::PublishableKey { merchant_id }, + )) + } +} + #[derive(Debug)] pub struct PublishableKeyAuth; diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 2410be9750cb..5d59e7ddbac4 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -512,6 +512,8 @@ pub enum Flow { PaymentsConfirmIntent, /// Payments post session tokens flow PaymentsPostSessionTokens, + /// Payments start redirection flow + PaymentStartRedirection, } /// diff --git a/v2_migrations/2024-08-28-081838_update_v2_primary_key_constraints/down.sql b/v2_migrations/2024-08-28-081838_update_v2_primary_key_constraints/down.sql index 359d5ce6359c..d0bc53f8ca07 100644 --- a/v2_migrations/2024-08-28-081838_update_v2_primary_key_constraints/down.sql +++ b/v2_migrations/2024-08-28-081838_update_v2_primary_key_constraints/down.sql @@ -87,6 +87,7 @@ ALTER COLUMN currency DROP NOT NULL, ALTER COLUMN client_secret DROP NOT NULL, ALTER COLUMN profile_id DROP NOT NULL; ALTER TABLE payment_intent ALTER COLUMN active_attempt_id SET NOT NULL; +ALTER TABLE payment_intent ALTER COLUMN active_attempt_id SET DEFAULT 'xxx'; ALTER TABLE payment_intent ALTER COLUMN session_expiry DROP NOT NULL; ------------------------ Payment Attempt ----------------------- diff --git a/v2_migrations/2024-08-28-081838_update_v2_primary_key_constraints/up.sql b/v2_migrations/2024-08-28-081838_update_v2_primary_key_constraints/up.sql index d4a1e7879b6b..8e72e06b66ab 100644 --- a/v2_migrations/2024-08-28-081838_update_v2_primary_key_constraints/up.sql +++ b/v2_migrations/2024-08-28-081838_update_v2_primary_key_constraints/up.sql @@ -130,4 +130,5 @@ ALTER TABLE payment_intent ALTER COLUMN session_expiry SET NOT NULL; -- This migration is to make fields optional in payment_intent table ALTER TABLE payment_intent ALTER COLUMN active_attempt_id DROP NOT NULL; - +ALTER TABLE payment_intent +ALTER COLUMN active_attempt_id DROP DEFAULT;