diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 43bb5bfa49..d2fa50d862 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -13510,7 +13510,8 @@ ], "nullable": true } - } + }, + "additionalProperties": false }, "PaymentsConfirmIntentResponse": { "type": "object", @@ -15244,6 +15245,11 @@ "force_sync": { "type": "boolean", "description": "A boolean used to indicate if the payment status should be fetched from the connector\nIf this is set to true, the status will be fetched from the connector" + }, + "param": { + "type": "string", + "description": "These are the query params that are sent in case of redirect response.\nThese can be ingested by the connector to take necessary actions.", + "nullable": true } } }, diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index 96335f3471..7f5ae1556d 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -2007,7 +2007,7 @@ pub struct ProfileCreate { /// The URL to redirect after the completion of the operation #[schema(value_type = Option, max_length = 255, example = "https://www.example.com/success")] - pub return_url: Option, + pub return_url: Option, /// A boolean value to indicate if payment response hash needs to be enabled #[schema(default = true, example = true)] @@ -2244,7 +2244,7 @@ pub struct ProfileResponse { /// The URL to redirect after the completion of the operation #[schema(value_type = Option, max_length = 255, example = "https://www.example.com/success")] - pub return_url: Option, + pub return_url: Option, /// A boolean value to indicate if payment response hash needs to be enabled #[schema(default = true, example = true)] @@ -2474,7 +2474,7 @@ pub struct ProfileUpdate { /// The URL to redirect after the completion of the operation #[schema(value_type = Option, max_length = 255, example = "https://www.example.com/success")] - pub return_url: Option, + pub return_url: Option, /// A boolean value to indicate if payment response hash needs to be enabled #[schema(default = true, example = true)] diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 29c1b81df2..75dda21097 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -4517,6 +4517,7 @@ pub struct PaymentsResponse { /// Request for Payment Intent Confirm #[cfg(feature = "v2")] #[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] +#[serde(deny_unknown_fields)] pub struct PaymentsConfirmIntentRequest { /// The URL to which you want the user to be redirected after the completion of the payment operation /// If this url is not passed, the url configured in the business profile will be used @@ -4557,6 +4558,10 @@ pub struct PaymentsRetrieveRequest { /// If this is set to true, the status will be fetched from the connector #[serde(default)] pub force_sync: bool, + + /// These are the query params that are sent in case of redirect response. + /// These can be ingested by the connector to take necessary actions. + pub param: Option, } /// Error details for the payment @@ -5208,6 +5213,7 @@ pub struct PgRedirectResponse { pub amount: Option, } +#[cfg(feature = "v1")] #[derive(Debug, serde::Serialize, PartialEq, Eq, serde::Deserialize)] pub struct RedirectionResponse { pub return_url: String, @@ -5217,6 +5223,12 @@ pub struct RedirectionResponse { pub headers: Vec<(String, String)>, } +#[cfg(feature = "v2")] +#[derive(Debug, serde::Serialize, PartialEq, Eq, serde::Deserialize)] +pub struct RedirectionResponse { + pub return_url_with_query_params: String, +} + #[derive(Debug, serde::Deserialize)] pub struct PaymentsResponseForm { pub transaction_id: String, diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index 5fc2fea9c8..a50555ced9 100644 --- a/crates/common_utils/src/consts.rs +++ b/crates/common_utils/src/consts.rs @@ -159,6 +159,9 @@ pub const MAX_STATEMENT_DESCRIPTOR_LENGTH: u16 = 22; /// Payout flow identifier used for performing GSM operations pub const PAYOUT_FLOW_STR: &str = "payout_flow"; +/// length of the publishable key +pub const PUBLISHABLE_KEY_LENGTH: u16 = 39; + /// The number of bytes allocated for the hashed connector transaction ID. /// Total number of characters equals CONNECTOR_TRANSACTION_ID_HASH_BYTES times 2. pub const CONNECTOR_TRANSACTION_ID_HASH_BYTES: usize = 25; diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs index a83c1d481c..afe19d4123 100644 --- a/crates/common_utils/src/events.rs +++ b/crates/common_utils/src/events.rs @@ -66,10 +66,15 @@ pub enum ApiEventsType { }, Routing, ResourceListAPI, + #[cfg(feature = "v1")] PaymentRedirectionResponse { connector: Option, payment_id: Option, }, + #[cfg(feature = "v2")] + PaymentRedirectionResponse { + payment_id: id_type::GlobalPaymentId, + }, Gsm, // TODO: This has to be removed once the corresponding apiEventTypes are created Miscellaneous, diff --git a/crates/common_utils/src/types.rs b/crates/common_utils/src/types.rs index f7cdfd3617..84a70e44a3 100644 --- a/crates/common_utils/src/types.rs +++ b/crates/common_utils/src/types.rs @@ -37,7 +37,9 @@ use time::PrimitiveDateTime; use utoipa::ToSchema; use crate::{ - consts::{self, MAX_DESCRIPTION_LENGTH, MAX_STATEMENT_DESCRIPTOR_LENGTH}, + consts::{ + self, MAX_DESCRIPTION_LENGTH, MAX_STATEMENT_DESCRIPTOR_LENGTH, PUBLISHABLE_KEY_LENGTH, + }, errors::{CustomResult, ParsingError, PercentageError, ValidationError}, fp_utils::when, }; @@ -639,6 +641,17 @@ impl Url { pub fn into_inner(self) -> url::Url { self.0 } + + /// Add query params to the url + pub fn add_query_params(mut self, (key, value): (&str, &str)) -> Self { + let url = self + .0 + .query_pairs_mut() + .append_pair(key, value) + .finish() + .clone(); + Self(url) + } } impl ToSql for Url @@ -1064,7 +1077,7 @@ crate::impl_to_sql_from_sql_json!(ChargeRefunds); /// A common type of domain type that can be used for fields that contain a string with restriction of length #[derive(Debug, Clone, Serialize, Hash, PartialEq, Eq, AsExpression)] #[diesel(sql_type = sql_types::Text)] -pub(crate) struct LengthString(String); +pub(crate) struct LengthString(String); /// Error generated from violation of constraints for MerchantReferenceId #[derive(Debug, Error, PartialEq, Eq)] @@ -1075,10 +1088,10 @@ pub(crate) enum LengthStringError { #[error("the minimum required length for this field is {0}")] /// Minimum length of string violated - MinLengthViolated(u8), + MinLengthViolated(u16), } -impl LengthString { +impl LengthString { /// Generates new [MerchantReferenceId] from the given input string pub fn from(input_string: Cow<'static, str>) -> Result { let trimmed_input_string = input_string.trim().to_string(); @@ -1089,7 +1102,7 @@ impl LengthString LengthString Deserialize<'de> +impl<'de, const MAX_LENGTH: u16, const MIN_LENGTH: u16> Deserialize<'de> for LengthString { fn deserialize(deserializer: D) -> Result @@ -1113,7 +1126,7 @@ impl<'de, const MAX_LENGTH: u16, const MIN_LENGTH: u8> Deserialize<'de> } } -impl FromSql +impl FromSql for LengthString where DB: Backend, @@ -1125,7 +1138,7 @@ where } } -impl ToSql +impl ToSql for LengthString where DB: Backend, @@ -1136,7 +1149,7 @@ where } } -impl Queryable +impl Queryable for LengthString where DB: Backend, @@ -1518,3 +1531,54 @@ pub trait ConnectorTransactionIdTrait { self.get_optional_connector_transaction_id() } } + +/// Domain type for PublishableKey +#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize, AsExpression)] +#[diesel(sql_type = sql_types::Text)] +pub struct PublishableKey(LengthString); + +impl PublishableKey { + /// Create a new PublishableKey Domain type without any length check from a static str + pub fn generate(env_prefix: &'static str) -> Self { + let publishable_key_string = format!("pk_{env_prefix}_{}", uuid::Uuid::now_v7().simple()); + Self(LengthString::new_unchecked(publishable_key_string)) + } + + /// Get the string representation of the PublishableKey + pub fn get_string_repr(&self) -> &str { + &self.0 .0 + } +} + +impl Queryable for PublishableKey +where + DB: Backend, + Self: FromSql, +{ + type Row = Self; + + fn build(row: Self::Row) -> deserialize::Result { + Ok(row) + } +} + +impl FromSql for PublishableKey +where + DB: Backend, + LengthString: FromSql, +{ + fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result { + let val = LengthString::::from_sql(bytes)?; + Ok(Self(val)) + } +} + +impl ToSql for PublishableKey +where + DB: Backend, + LengthString: ToSql, +{ + fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> diesel::serialize::Result { + self.0.to_sql(out) + } +} diff --git a/crates/diesel_models/src/business_profile.rs b/crates/diesel_models/src/business_profile.rs index ecb5362064..05c124c4c9 100644 --- a/crates/diesel_models/src/business_profile.rs +++ b/crates/diesel_models/src/business_profile.rs @@ -255,7 +255,7 @@ pub struct Profile { pub profile_name: String, pub created_at: time::PrimitiveDateTime, pub modified_at: time::PrimitiveDateTime, - pub return_url: Option, + pub return_url: Option, pub enable_payment_response_hash: bool, pub payment_response_hash_key: Option, pub redirect_to_merchant_with_http_post: bool, @@ -314,7 +314,7 @@ pub struct ProfileNew { pub profile_name: String, pub created_at: time::PrimitiveDateTime, pub modified_at: time::PrimitiveDateTime, - pub return_url: Option, + pub return_url: Option, pub enable_payment_response_hash: bool, pub payment_response_hash_key: Option, pub redirect_to_merchant_with_http_post: bool, @@ -358,7 +358,7 @@ pub struct ProfileNew { pub struct ProfileUpdateInternal { pub profile_name: Option, pub modified_at: time::PrimitiveDateTime, - pub return_url: Option, + pub return_url: Option, pub enable_payment_response_hash: Option, pub payment_response_hash_key: Option, pub redirect_to_merchant_with_http_post: Option, diff --git a/crates/hyperswitch_domain_models/src/business_profile.rs b/crates/hyperswitch_domain_models/src/business_profile.rs index fb846e2886..3e8213a358 100644 --- a/crates/hyperswitch_domain_models/src/business_profile.rs +++ b/crates/hyperswitch_domain_models/src/business_profile.rs @@ -668,7 +668,7 @@ pub struct Profile { pub profile_name: String, pub created_at: time::PrimitiveDateTime, pub modified_at: time::PrimitiveDateTime, - pub return_url: Option, + pub return_url: Option, pub enable_payment_response_hash: bool, pub payment_response_hash_key: Option, pub redirect_to_merchant_with_http_post: bool, @@ -709,7 +709,7 @@ pub struct ProfileSetter { pub profile_name: String, pub created_at: time::PrimitiveDateTime, pub modified_at: time::PrimitiveDateTime, - pub return_url: Option, + pub return_url: Option, pub enable_payment_response_hash: bool, pub payment_response_hash_key: Option, pub redirect_to_merchant_with_http_post: bool, @@ -815,7 +815,7 @@ impl Profile { #[derive(Debug)] pub struct ProfileGeneralUpdate { pub profile_name: Option, - pub return_url: Option, + pub return_url: Option, pub enable_payment_response_hash: Option, pub payment_response_hash_key: Option, pub redirect_to_merchant_with_http_post: Option, diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index 8b4c351daa..5c629f6198 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -113,6 +113,7 @@ impl PaymentIntent { } #[cfg(feature = "v2")] + /// This is the url to which the customer will be redirected to, to complete the redirection flow pub fn create_start_redirection_url( &self, base_url: &str, @@ -129,6 +130,24 @@ impl PaymentIntent { .change_context(errors::api_error_response::ApiErrorResponse::InternalServerError) .attach_printable("Error creating start redirection url") } + + #[cfg(feature = "v2")] + /// This is the url to which the customer will be redirected to, after completing the redirection flow + pub fn create_finish_redirection_url( + &self, + base_url: &str, + publishable_key: &str, + ) -> CustomResult { + let finish_redirection_url = format!( + "{base_url}/v2/payments/{}/finish_redirection/{publishable_key}/{}", + self.id.get_string_repr(), + self.profile_id.get_string_repr() + ); + + url::Url::parse(&finish_redirection_url) + .change_context(errors::api_error_response::ApiErrorResponse::InternalServerError) + .attach_printable("Error creating finish redirection url") + } } #[cfg(feature = "v2")] diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs index a13f476950..96f41f75ee 100644 --- a/crates/router/src/analytics.rs +++ b/crates/router/src/analytics.rs @@ -792,6 +792,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] /// # Panics /// /// Panics if `json_payload` array does not contain one `GetSdkEventMetricRequest` element. @@ -830,6 +831,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] /// # Panics /// /// Panics if `json_payload` array does not contain one `GetActivePaymentsMetricRequest` element. @@ -869,6 +871,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] /// # Panics /// /// Panics if `json_payload` array does not contain one `GetAuthEventMetricRequest` element. @@ -1148,6 +1151,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] pub async fn get_sdk_event_filters( state: web::Data, req: actix_web::HttpRequest, @@ -1241,6 +1245,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] pub async fn get_profile_sdk_events( state: web::Data, req: actix_web::HttpRequest, diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 49b16aaa3f..97a40f38ac 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -3604,7 +3604,7 @@ impl ProfileCreateBridge for api::ProfileCreate { profile_name, created_at: current_time, modified_at: current_time, - return_url: self.return_url.map(|return_url| return_url.to_string()), + return_url: self.return_url, enable_payment_response_hash: self.enable_payment_response_hash.unwrap_or(true), payment_response_hash_key: Some(payment_response_hash_key), redirect_to_merchant_with_http_post: self @@ -3968,7 +3968,7 @@ impl ProfileUpdateBridge for api::ProfileUpdate { Ok(domain::ProfileUpdate::Update(Box::new( domain::ProfileGeneralUpdate { profile_name: self.profile_name, - return_url: self.return_url.map(|return_url| return_url.to_string()), + return_url: self.return_url, enable_payment_response_hash: self.enable_payment_response_hash, payment_response_hash_key: self.payment_response_hash_key, redirect_to_merchant_with_http_post: self.redirect_to_merchant_with_http_post, diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 2d480d71fe..ed41650840 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -49,6 +49,8 @@ pub use hyperswitch_domain_models::{ router_request_types::CustomerDetails, }; use masking::{ExposeInterface, PeekInterface, Secret}; +#[cfg(feature = "v2")] +use operations::ValidateStatusForOperation; use redis_interface::errors::RedisError; use router_env::{instrument, metrics::add_attributes, tracing}; #[cfg(feature = "olap")] @@ -123,7 +125,7 @@ pub async fn payments_operation_core( profile: domain::Profile, operation: Op, req: Req, - payment_id: id_type::GlobalPaymentId, + get_tracker_response: operations::GetTrackerResponse, call_connector_action: CallConnectorAction, header_payload: HeaderPayload, ) -> RouterResult<(D, Req, Option, Option, Option)> @@ -150,27 +152,8 @@ where { let operation: BoxedOperation<'_, F, Req, D> = Box::new(operation); - // Validate the request fields - let (operation, validate_result) = operation - .to_validate_request()? - .validate_request(&req, &merchant_account)?; - // Get the trackers related to track the state of the payment - 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 operations::GetTrackerResponse { mut payment_data } = get_tracker_response; let (_operation, customer) = operation .to_domain()? @@ -207,7 +190,6 @@ where &mut payment_data, &customer, call_connector_action.clone(), - &validate_result, None, header_payload.clone(), #[cfg(feature = "frm")] @@ -1029,7 +1011,7 @@ where tracing::Span::current().record("merchant_id", merchant_account.get_id().get_string_repr()); - let (operation, _validate_result) = operation + let _validate_result = operation .to_validate_request()? .validate_request(&req, &merchant_account)?; @@ -1037,10 +1019,7 @@ where tracing::Span::current().record("global_payment_id", payment_id.get_string_repr()); - let operations::GetTrackerResponse { - operation, - mut payment_data, - } = operation + let operations::GetTrackerResponse { mut payment_data } = operation .to_get_tracker()? .get_trackers( state, @@ -1520,6 +1499,24 @@ where RouterData: hyperswitch_domain_models::router_data::TrackerPostUpdateObjects, { + // Validate the request fields + let validate_result = operation + .to_validate_request()? + .validate_request(&req, &merchant_account)?; + + let get_tracker_response = operation + .to_get_tracker()? + .get_trackers( + &state, + &payment_id, + &req, + &merchant_account, + &profile, + &key_store, + &header_payload, + ) + .await?; + let (payment_data, _req, customer, connector_http_status_code, external_latency) = payments_operation_core::<_, _, _, _, _>( &state, @@ -1529,7 +1526,7 @@ where profile, operation.clone(), req, - payment_id, + get_tracker_response, call_connector_action, header_payload.clone(), ) @@ -1552,6 +1549,7 @@ fn is_start_pay(operation: &Op) -> bool { format!("{operation:?}").eq("PaymentStart") } +#[cfg(feature = "v1")] #[derive(Clone, Debug, serde::Serialize)] pub struct PaymentsRedirectResponseData { pub connector: Option, @@ -1563,11 +1561,20 @@ pub struct PaymentsRedirectResponseData { pub creds_identifier: Option, } +#[cfg(feature = "v2")] +#[derive(Clone, Debug, serde::Serialize)] +pub struct PaymentsRedirectResponseData { + pub payment_id: id_type::GlobalPaymentId, + pub query_params: String, + pub json_payload: Option, +} + #[async_trait::async_trait] pub trait PaymentRedirectFlow: Sync { // Associated type for call_payment_flow response type PaymentFlowResponse; + #[cfg(feature = "v1")] #[allow(clippy::too_many_arguments)] async fn call_payment_flow( &self, @@ -1581,8 +1588,20 @@ pub trait PaymentRedirectFlow: Sync { payment_id: id_type::PaymentId, ) -> RouterResult; + #[cfg(feature = "v2")] + async fn call_payment_flow( + &self, + state: &SessionState, + req_state: ReqState, + merchant_account: domain::MerchantAccount, + merchant_key_store: domain::MerchantKeyStore, + profile: domain::Profile, + req: PaymentsRedirectResponseData, + ) -> RouterResult; + fn get_payment_action(&self) -> services::PaymentAction; + #[cfg(feature = "v1")] fn generate_response( &self, payment_flow_response: &Self::PaymentFlowResponse, @@ -1590,7 +1609,13 @@ pub trait PaymentRedirectFlow: Sync { connector: String, ) -> RouterResult>; - #[allow(clippy::too_many_arguments)] + #[cfg(feature = "v2")] + fn generate_response( + &self, + payment_flow_response: &Self::PaymentFlowResponse, + ) -> RouterResult>; + + #[cfg(feature = "v1")] async fn handle_payments_redirect_response( &self, state: SessionState, @@ -1656,6 +1681,39 @@ pub trait PaymentRedirectFlow: Sync { self.generate_response(&payment_flow_response, resource_id, connector) } + + #[cfg(feature = "v2")] + async fn handle_payments_redirect_response( + &self, + state: SessionState, + req_state: ReqState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + profile: domain::Profile, + request: PaymentsRedirectResponseData, + ) -> RouterResponse { + metrics::REDIRECTION_TRIGGERED.add( + &metrics::CONTEXT, + 1, + &add_attributes([( + "merchant_id", + merchant_account.get_id().get_string_repr().to_owned(), + )]), + ); + + let payment_flow_response = self + .call_payment_flow( + &state, + req_state, + merchant_account, + key_store, + profile, + request, + ) + .await?; + + self.generate_response(&payment_flow_response) + } } #[derive(Clone, Debug)] @@ -1894,6 +1952,167 @@ impl PaymentRedirectFlow for PaymentRedirectSync { } } +#[cfg(feature = "v2")] +impl ValidateStatusForOperation for &PaymentRedirectSync { + fn validate_status_for_operation( + &self, + intent_status: common_enums::IntentStatus, + ) -> Result<(), errors::ApiErrorResponse> { + match intent_status { + common_enums::IntentStatus::RequiresCustomerAction => Ok(()), + common_enums::IntentStatus::Succeeded + | common_enums::IntentStatus::Failed + | common_enums::IntentStatus::Cancelled + | common_enums::IntentStatus::Processing + | common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::RequiresCapture + | common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::RequiresConfirmation + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => { + Err(errors::ApiErrorResponse::PaymentUnexpectedState { + current_flow: format!("{self:?}"), + field_name: "status".to_string(), + current_value: intent_status.to_string(), + states: ["requires_customer_action".to_string()].join(", "), + }) + } + } + } +} + +#[cfg(feature = "v2")] +#[async_trait::async_trait] +impl PaymentRedirectFlow for PaymentRedirectSync { + type PaymentFlowResponse = + router_types::RedirectPaymentFlowResponse>; + + async fn call_payment_flow( + &self, + state: &SessionState, + req_state: ReqState, + merchant_account: domain::MerchantAccount, + merchant_key_store: domain::MerchantKeyStore, + profile: domain::Profile, + req: PaymentsRedirectResponseData, + ) -> RouterResult { + let payment_id = req.payment_id.clone(); + + let payment_sync_request = api::PaymentsRetrieveRequest { + param: Some(req.query_params.clone()), + force_sync: true, + }; + + let operation = operations::PaymentGet; + let boxed_operation: BoxedOperation< + '_, + api::PSync, + api::PaymentsRetrieveRequest, + PaymentStatusData, + > = Box::new(operation); + + let get_tracker_response = boxed_operation + .to_get_tracker()? + .get_trackers( + state, + &payment_id, + &payment_sync_request, + &merchant_account, + &profile, + &merchant_key_store, + &HeaderPayload::default(), + ) + .await?; + + let payment_data = &get_tracker_response.payment_data; + self.validate_status_for_operation(payment_data.payment_intent.status)?; + + let payment_attempt = payment_data + .payment_attempt + .as_ref() + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable("payment_attempt not found in get_tracker_response")?; + + let connector = payment_attempt + .connector + .as_ref() + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "connector is not set in payment attempt in finish redirection flow", + )?; + + // This connector data is ephemeral, the call payment flow will get new connector data + // with merchant account details, so the connector_id can be safely set to None here + let connector_data = api::ConnectorData::get_connector_by_name( + &state.conf.connectors, + connector, + api::GetToken::Connector, + None, + )?; + + let call_connector_action = connector_data + .connector + .get_flow_type( + &req.query_params, + req.json_payload.clone(), + self.get_payment_action(), + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to decide the response flow")?; + + let (payment_data, _, _, _, _) = + Box::pin(payments_operation_core::( + state, + req_state, + merchant_account, + merchant_key_store.clone(), + profile.clone(), + operation, + payment_sync_request, + get_tracker_response, + call_connector_action, + HeaderPayload::default(), + )) + .await?; + + Ok(router_types::RedirectPaymentFlowResponse { + payment_data, + profile, + }) + } + fn generate_response( + &self, + payment_flow_response: &Self::PaymentFlowResponse, + ) -> RouterResult> { + let payment_intent = &payment_flow_response.payment_data.payment_intent; + let profile = &payment_flow_response.profile; + + let return_url = payment_intent + .return_url + .as_ref() + .or(profile.return_url.as_ref()) + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable("return url not found in payment intent and profile")? + .to_owned(); + + let return_url = return_url + .add_query_params(("id", payment_intent.id.get_string_repr())) + .add_query_params(("status", &payment_intent.status.to_string())); + + let return_url_str = return_url.into_inner().to_string(); + + Ok(services::ApplicationResponse::JsonForRedirection( + api::RedirectionResponse { + return_url_with_query_params: return_url_str, + }, + )) + } + + fn get_payment_action(&self) -> services::PaymentAction { + services::PaymentAction::PSync + } +} + #[derive(Clone, Debug)] pub struct PaymentAuthenticateCompleteAuthorize; @@ -2410,7 +2629,6 @@ pub async fn call_connector_service( payment_data: &mut D, customer: &Option, call_connector_action: CallConnectorAction, - validate_result: &operations::ValidateResult, schedule_time: Option, header_payload: HeaderPayload, frm_suggestion: Option, diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 478211ec14..f00f03d929 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -2904,6 +2904,7 @@ pub(super) fn validate_payment_list_request_for_joins( Ok(()) } +#[cfg(feature = "v1")] pub fn get_handle_response_url( payment_id: id_type::PaymentId, business_profile: &domain::Profile, @@ -2926,6 +2927,7 @@ pub fn get_handle_response_url( make_url_with_signature(&return_url, business_profile) } +#[cfg(feature = "v1")] pub fn make_merchant_url_with_response( business_profile: &domain::Profile, redirection_response: api::PgRedirectResponse, @@ -3034,6 +3036,7 @@ pub fn make_pg_redirect_response( } } +#[cfg(feature = "v1")] pub fn make_url_with_signature( redirect_url: &str, business_profile: &domain::Profile, diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index edf0485a77..e936f37254 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -144,16 +144,15 @@ pub trait ValidateRequest { #[cfg(feature = "v2")] pub trait ValidateRequest { - fn validate_request<'b>( - &'b self, + fn validate_request( + &self, request: &R, merchant_account: &domain::MerchantAccount, - ) -> RouterResult<(BoxedOperation<'b, F, R, D>, ValidateResult)>; + ) -> RouterResult; } #[cfg(feature = "v2")] -pub struct GetTrackerResponse<'a, F: Clone, R, D> { - pub operation: BoxedOperation<'a, F, R, D>, +pub struct GetTrackerResponse { pub payment_data: D, } @@ -183,8 +182,6 @@ pub trait GetTracker: Send { header_payload: &hyperswitch_domain_models::payments::HeaderPayload, ) -> RouterResult>; - // TODO: this need not return the operation, since operation does not change in v2 - // Operation remains the same from start to finish #[cfg(feature = "v2")] #[allow(clippy::too_many_arguments)] async fn get_trackers<'a>( @@ -196,7 +193,7 @@ pub trait GetTracker: Send { profile: &domain::Profile, mechant_key_store: &domain::MerchantKeyStore, header_payload: &hyperswitch_domain_models::payments::HeaderPayload, - ) -> RouterResult>; + ) -> RouterResult>; } #[async_trait] 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 c89f0bdc5c..0a03fc741f 100644 --- a/crates/router/src/core/payments/operations/payment_confirm_intent.rs +++ b/crates/router/src/core/payments/operations/payment_confirm_intent.rs @@ -131,14 +131,14 @@ impl ValidateRequest RouterResult<(BoxedConfirmOperation<'b, F>, operations::ValidateResult)> { + ) -> RouterResult { let validate_result = operations::ValidateResult { merchant_id: merchant_account.get_id().to_owned(), storage_scheme: merchant_account.storage_scheme, requeue: false, }; - Ok((Box::new(self), validate_result)) + Ok(validate_result) } } @@ -156,9 +156,7 @@ impl GetTracker, PaymentsConfirmIntent profile: &domain::Profile, key_store: &domain::MerchantKeyStore, header_payload: &hyperswitch_domain_models::payments::HeaderPayload, - ) -> RouterResult< - operations::GetTrackerResponse<'a, F, PaymentsConfirmIntentRequest, PaymentConfirmData>, - > { + ) -> RouterResult>> { let db = &*state.store; let key_manager_state = &state.into(); @@ -211,10 +209,7 @@ impl GetTracker, PaymentsConfirmIntent payment_method_data, }; - let get_trackers_response = operations::GetTrackerResponse { - operation: Box::new(self), - payment_data, - }; + let get_trackers_response = operations::GetTrackerResponse { payment_data }; Ok(get_trackers_response) } 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 d57d24e3ed..b46992a6ae 100644 --- a/crates/router/src/core/payments/operations/payment_create_intent.rs +++ b/crates/router/src/core/payments/operations/payment_create_intent.rs @@ -95,14 +95,7 @@ impl GetTracker, PaymentsCrea profile: &domain::Profile, key_store: &domain::MerchantKeyStore, _header_payload: &hyperswitch_domain_models::payments::HeaderPayload, - ) -> RouterResult< - operations::GetTrackerResponse< - 'a, - F, - PaymentsCreateIntentRequest, - payments::PaymentIntentData, - >, - > { + ) -> RouterResult>> { let db = &*state.store; let key_manager_state = &state.into(); @@ -176,10 +169,7 @@ impl GetTracker, PaymentsCrea payment_intent, }; - let get_trackers_response = operations::GetTrackerResponse { - operation: Box::new(self), - payment_data, - }; + let get_trackers_response = operations::GetTrackerResponse { payment_data }; Ok(get_trackers_response) } @@ -221,18 +211,12 @@ impl &'b self, _request: &PaymentsCreateIntentRequest, 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, - }, - )) + ) -> RouterResult { + Ok(operations::ValidateResult { + merchant_id: merchant_account.get_id().to_owned(), + storage_scheme: merchant_account.storage_scheme, + requeue: false, + }) } } diff --git a/crates/router/src/core/payments/operations/payment_get.rs b/crates/router/src/core/payments/operations/payment_get.rs index 2a41756804..a69c3187c7 100644 --- a/crates/router/src/core/payments/operations/payment_get.rs +++ b/crates/router/src/core/payments/operations/payment_get.rs @@ -109,14 +109,14 @@ impl ValidateRequest RouterResult<(BoxedConfirmOperation<'b, F>, operations::ValidateResult)> { + ) -> RouterResult { let validate_result = operations::ValidateResult { merchant_id: merchant_account.get_id().to_owned(), storage_scheme: merchant_account.storage_scheme, requeue: false, }; - Ok((Box::new(self), validate_result)) + Ok(validate_result) } } @@ -132,9 +132,7 @@ impl GetTracker, PaymentsRetrieveReques _profile: &domain::Profile, key_store: &domain::MerchantKeyStore, header_payload: &hyperswitch_domain_models::payments::HeaderPayload, - ) -> RouterResult< - operations::GetTrackerResponse<'a, F, PaymentsRetrieveRequest, PaymentStatusData>, - > { + ) -> RouterResult>> { let db = &*state.store; let key_manager_state = &state.into(); @@ -172,10 +170,7 @@ impl GetTracker, PaymentsRetrieveReques should_sync_with_connector, }; - let get_trackers_response = operations::GetTrackerResponse { - operation: Box::new(self), - payment_data, - }; + let get_trackers_response = operations::GetTrackerResponse { payment_data }; Ok(get_trackers_response) } 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 ca09d021b7..6424ff5a2b 100644 --- a/crates/router/src/core/payments/operations/payment_get_intent.rs +++ b/crates/router/src/core/payments/operations/payment_get_intent.rs @@ -89,14 +89,7 @@ impl GetTracker, PaymentsGetI _profile: &domain::Profile, key_store: &domain::MerchantKeyStore, _header_payload: &hyperswitch_domain_models::payments::HeaderPayload, - ) -> RouterResult< - operations::GetTrackerResponse< - 'a, - F, - PaymentsGetIntentRequest, - payments::PaymentIntentData, - >, - > { + ) -> RouterResult>> { let db = &*state.store; let key_manager_state = &state.into(); let storage_scheme = merchant_account.storage_scheme; @@ -110,10 +103,7 @@ impl GetTracker, PaymentsGetI payment_intent, }; - let get_trackers_response = operations::GetTrackerResponse { - operation: Box::new(self), - payment_data, - }; + let get_trackers_response = operations::GetTrackerResponse { payment_data }; Ok(get_trackers_response) } @@ -154,18 +144,12 @@ impl ValidateRequest RouterResult<( - PaymentsGetIntentOperation<'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, - }, - )) + ) -> RouterResult { + Ok(operations::ValidateResult { + merchant_id: merchant_account.get_id().to_owned(), + storage_scheme: merchant_account.storage_scheme, + requeue: false, + }) } } diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 8f2280355f..b09034e28c 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -226,12 +226,12 @@ pub async fn construct_payment_router_data_for_authorize<'a>( connector_id, )); - let router_return_url = Some(helpers::create_redirect_url( - router_base_url, - attempt, - connector_id, - None, - )); + let router_return_url = payment_data + .payment_intent + .create_finish_redirection_url(router_base_url, &merchant_account.publishable_key) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to construct finish redirection url")? + .to_string(); let connector_request_reference_id = payment_data .payment_intent @@ -269,7 +269,7 @@ pub async fn construct_payment_router_data_for_authorize<'a>( enrolled_for_3ds: true, related_transaction_id: None, payment_method_type: Some(payment_data.payment_attempt.payment_method_subtype), - router_return_url, + router_return_url: Some(router_return_url), webhook_url, complete_authorize_url, customer_id: None, diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index aad2537c3d..e0ae1c531a 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -2223,6 +2223,7 @@ pub async fn list_user_authentication_methods( )) } +#[cfg(feature = "v1")] pub async fn get_sso_auth_url( state: SessionState, request: user_api::GetSsoAuthUrlRequest, diff --git a/crates/router/src/events/api_logs.rs b/crates/router/src/events/api_logs.rs index e818b7440b..76cbcb958c 100644 --- a/crates/router/src/events/api_logs.rs +++ b/crates/router/src/events/api_logs.rs @@ -124,6 +124,7 @@ impl_api_event_type!( ) ); +#[cfg(feature = "v1")] impl ApiEventMetric for PaymentsRedirectResponseData { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::PaymentRedirectionResponse { @@ -136,6 +137,15 @@ impl ApiEventMetric for PaymentsRedirectResponseData { } } +#[cfg(feature = "v2")] +impl ApiEventMetric for PaymentsRedirectResponseData { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::PaymentRedirectionResponse { + payment_id: self.payment_id.clone(), + }) + } +} + impl ApiEventMetric for DisputeId { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::Dispute { diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index cc9d5b04eb..7efaab21d5 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -113,7 +113,7 @@ pub fn mk_app( > { let mut server_app = get_application_builder(request_body_limit, state.conf.cors.clone()); - #[cfg(feature = "dummy_connector")] + #[cfg(all(feature = "dummy_connector", feature = "v1"))] { use routes::DummyConnector; server_app = server_app.service(DummyConnector::server(state.clone())); diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index d747f96b9f..ebf6c7d384 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -472,7 +472,7 @@ impl Health { #[cfg(feature = "dummy_connector")] pub struct DummyConnector; -#[cfg(feature = "dummy_connector")] +#[cfg(all(feature = "dummy_connector", feature = "v1"))] impl DummyConnector { pub fn server(state: AppState) -> Scope { let mut routes_with_restricted_access = web::scope(""); @@ -540,6 +540,10 @@ impl Payments { .service( web::resource("/start_redirection") .route(web::get().to(payments::payments_start_redirection)), + ) + .service( + web::resource("/finish_redirection/{publishable_key}/{profile_id}") + .route(web::get().to(payments::payments_finish_redirection)), ), ); diff --git a/crates/router/src/routes/dummy_connector.rs b/crates/router/src/routes/dummy_connector.rs index e376bcd7f9..709c76486c 100644 --- a/crates/router/src/routes/dummy_connector.rs +++ b/crates/router/src/routes/dummy_connector.rs @@ -13,6 +13,7 @@ mod errors; pub mod types; mod utils; +#[cfg(all(feature = "dummy_connector", feature = "v1"))] #[instrument(skip_all, fields(flow = ?types::Flow::DummyPaymentCreate))] pub async fn dummy_connector_authorize_payment( state: web::Data, @@ -33,6 +34,8 @@ pub async fn dummy_connector_authorize_payment( ) .await } + +#[cfg(all(feature = "dummy_connector", feature = "v1"))] #[instrument(skip_all, fields(flow = ?types::Flow::DummyPaymentCreate))] pub async fn dummy_connector_complete_payment( state: web::Data, @@ -57,6 +60,8 @@ pub async fn dummy_connector_complete_payment( )) .await } + +#[cfg(all(feature = "dummy_connector", feature = "v1"))] #[instrument(skip_all, fields(flow = ?types::Flow::DummyPaymentCreate))] pub async fn dummy_connector_payment( state: web::Data, @@ -76,6 +81,8 @@ pub async fn dummy_connector_payment( )) .await } + +#[cfg(all(feature = "dummy_connector", feature = "v1"))] #[instrument(skip_all, fields(flow = ?types::Flow::DummyPaymentRetrieve))] pub async fn dummy_connector_payment_data( state: web::Data, @@ -96,6 +103,8 @@ pub async fn dummy_connector_payment_data( ) .await } + +#[cfg(all(feature = "dummy_connector", feature = "v1"))] #[instrument(skip_all, fields(flow = ?types::Flow::DummyRefundCreate))] pub async fn dummy_connector_refund( state: web::Data, @@ -117,6 +126,8 @@ pub async fn dummy_connector_refund( ) .await } + +#[cfg(all(feature = "dummy_connector", feature = "v1"))] #[instrument(skip_all, fields(flow = ?types::Flow::DummyRefundRetrieve))] pub async fn dummy_connector_refund_data( state: web::Data, diff --git a/crates/router/src/routes/dummy_connector/core.rs b/crates/router/src/routes/dummy_connector/core.rs index f8e500e5b2..aadd7dcaa7 100644 --- a/crates/router/src/routes/dummy_connector/core.rs +++ b/crates/router/src/routes/dummy_connector/core.rs @@ -9,6 +9,7 @@ use crate::{ utils::OptionExt, }; +#[cfg(all(feature = "dummy_connector", feature = "v1"))] pub async fn payment( state: SessionState, req: types::DummyConnectorPaymentRequest, @@ -54,6 +55,7 @@ pub async fn payment_data( Ok(api::ApplicationResponse::Json(payment_data.into())) } +#[cfg(all(feature = "dummy_connector", feature = "v1"))] pub async fn payment_authorize( state: SessionState, req: types::DummyConnectorPaymentConfirmRequest, @@ -82,6 +84,7 @@ pub async fn payment_authorize( } } +#[cfg(all(feature = "dummy_connector", feature = "v1"))] pub async fn payment_complete( state: SessionState, req: types::DummyConnectorPaymentCompleteRequest, @@ -144,6 +147,7 @@ pub async fn payment_complete( )) } +#[cfg(all(feature = "dummy_connector", feature = "v1"))] pub async fn refund_payment( state: SessionState, req: types::DummyConnectorRefundRequest, @@ -197,6 +201,7 @@ pub async fn refund_payment( Ok(api::ApplicationResponse::Json(refund_data)) } +#[cfg(all(feature = "dummy_connector", feature = "v1"))] pub async fn refund_data( state: SessionState, req: types::DummyConnectorRefundRetrieveRequest, diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index d674770f4e..48b15c3b20 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -1842,6 +1842,23 @@ impl GetLockingInput for payments::PaymentsRedirectResponseData { } } +#[cfg(feature = "v2")] +impl GetLockingInput for payments::PaymentsRedirectResponseData { + fn get_locking_input(&self, flow: F) -> api_locking::LockAction + where + F: types::FlowMetric, + lock_utils::ApiIdentifier: From, + { + api_locking::LockAction::Hold { + input: api_locking::LockingInput { + unique_locking_key: self.payment_id.get_string_repr().to_owned(), + api_identifier: lock_utils::ApiIdentifier::from(flow), + override_lock_retries: None, + }, + } + } +} + #[cfg(feature = "v1")] impl GetLockingInput for payment_types::PaymentsCompleteAuthorizeRequest { fn get_locking_input(&self, flow: F) -> api_locking::LockAction @@ -2254,3 +2271,54 @@ pub async fn payment_status( )) .await } + +#[cfg(feature = "v2")] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsRedirect, payment_id))] +pub async fn payments_finish_redirection( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: Option>, + path: web::Path<( + common_utils::id_type::GlobalPaymentId, + String, + common_utils::id_type::ProfileId, + )>, +) -> impl Responder { + let flow = Flow::PaymentsRedirect; + let (payment_id, publishable_key, profile_id) = path.into_inner(); + let param_string = req.query_string(); + + tracing::Span::current().record("payment_id", payment_id.get_string_repr()); + + let payload = payments::PaymentsRedirectResponseData { + payment_id, + json_payload: json_payload.map(|payload| payload.0), + query_params: param_string.to_string(), + }; + + let locking_action = payload.get_locking_input(flow.clone()); + + api::server_wrap( + flow, + state, + &req, + payload, + |state, auth, req, req_state| { + ::handle_payments_redirect_response( + &payments::PaymentRedirectSync {}, + state, + req_state, + auth.merchant_account, + auth.key_store, + auth.profile, + req, + ) + }, + &auth::PublishableKeyAndProfileIdAuth { + publishable_key: publishable_key.clone(), + profile_id: profile_id.clone(), + }, + locking_action, + ) + .await +} diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index f52d0dca7a..068c2f30c7 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -691,6 +691,7 @@ pub async fn check_two_factor_auth_status_with_attempts( .await } +#[cfg(feature = "v1")] pub async fn get_sso_auth_url( state: web::Data, req: HttpRequest, diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 2cf8b721a0..bdd650389a 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -1880,7 +1880,7 @@ pub fn build_redirection_form( var collectionReference = data[collectionField]; return submitCollectionReference(collectionReference); }} else {{ - console.error("Collection field not found in event data (" + collectionField + ")"); + console.error("Collection field not found in event data (" + collectionField + ")"); }} }} catch (error) {{ console.error("Error parsing event data: ", error); @@ -1914,7 +1914,7 @@ pub fn build_redirection_form( } (PreEscaped(format!(r#"