Skip to content

Commit

Permalink
feat(payments_v2): add finish redirection endpoint (#6549)
Browse files Browse the repository at this point in the history
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
Co-authored-by: hrithikesh026 <[email protected]>
  • Loading branch information
3 people authored Nov 15, 2024
1 parent 0b63efb commit 0805a93
Show file tree
Hide file tree
Showing 29 changed files with 534 additions and 136 deletions.
8 changes: 7 additions & 1 deletion api-reference-v2/openapi_spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -13510,7 +13510,8 @@
],
"nullable": true
}
}
},
"additionalProperties": false
},
"PaymentsConfirmIntentResponse": {
"type": "object",
Expand Down Expand Up @@ -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
}
}
},
Expand Down
6 changes: 3 additions & 3 deletions crates/api_models/src/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2007,7 +2007,7 @@ pub struct ProfileCreate {

/// The URL to redirect after the completion of the operation
#[schema(value_type = Option<String>, max_length = 255, example = "https://www.example.com/success")]
pub return_url: Option<url::Url>,
pub return_url: Option<common_utils::types::Url>,

/// A boolean value to indicate if payment response hash needs to be enabled
#[schema(default = true, example = true)]
Expand Down Expand Up @@ -2244,7 +2244,7 @@ pub struct ProfileResponse {

/// The URL to redirect after the completion of the operation
#[schema(value_type = Option<String>, max_length = 255, example = "https://www.example.com/success")]
pub return_url: Option<String>,
pub return_url: Option<common_utils::types::Url>,

/// A boolean value to indicate if payment response hash needs to be enabled
#[schema(default = true, example = true)]
Expand Down Expand Up @@ -2474,7 +2474,7 @@ pub struct ProfileUpdate {

/// The URL to redirect after the completion of the operation
#[schema(value_type = Option<String>, max_length = 255, example = "https://www.example.com/success")]
pub return_url: Option<url::Url>,
pub return_url: Option<common_utils::types::Url>,

/// A boolean value to indicate if payment response hash needs to be enabled
#[schema(default = true, example = true)]
Expand Down
12 changes: 12 additions & 0 deletions crates/api_models/src/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<String>,
}

/// Error details for the payment
Expand Down Expand Up @@ -5208,6 +5213,7 @@ pub struct PgRedirectResponse {
pub amount: Option<MinorUnit>,
}

#[cfg(feature = "v1")]
#[derive(Debug, serde::Serialize, PartialEq, Eq, serde::Deserialize)]
pub struct RedirectionResponse {
pub return_url: String,
Expand All @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions crates/common_utils/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
5 changes: 5 additions & 0 deletions crates/common_utils/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,15 @@ pub enum ApiEventsType {
},
Routing,
ResourceListAPI,
#[cfg(feature = "v1")]
PaymentRedirectionResponse {
connector: Option<String>,
payment_id: Option<id_type::PaymentId>,
},
#[cfg(feature = "v2")]
PaymentRedirectionResponse {
payment_id: id_type::GlobalPaymentId,
},
Gsm,
// TODO: This has to be removed once the corresponding apiEventTypes are created
Miscellaneous,
Expand Down
82 changes: 73 additions & 9 deletions crates/common_utils/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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<DB> ToSql<sql_types::Text, DB> for Url
Expand Down Expand Up @@ -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<const MAX_LENGTH: u16, const MIN_LENGTH: u8>(String);
pub(crate) struct LengthString<const MAX_LENGTH: u16, const MIN_LENGTH: u16>(String);

/// Error generated from violation of constraints for MerchantReferenceId
#[derive(Debug, Error, PartialEq, Eq)]
Expand All @@ -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<const MAX_LENGTH: u16, const MIN_LENGTH: u8> LengthString<MAX_LENGTH, MIN_LENGTH> {
impl<const MAX_LENGTH: u16, const MIN_LENGTH: u16> LengthString<MAX_LENGTH, MIN_LENGTH> {
/// Generates new [MerchantReferenceId] from the given input string
pub fn from(input_string: Cow<'static, str>) -> Result<Self, LengthStringError> {
let trimmed_input_string = input_string.trim().to_string();
Expand All @@ -1089,7 +1102,7 @@ impl<const MAX_LENGTH: u16, const MIN_LENGTH: u8> LengthString<MAX_LENGTH, MIN_L
Err(LengthStringError::MaxLengthViolated(MAX_LENGTH))
})?;

when(length_of_input_string < u16::from(MIN_LENGTH), || {
when(length_of_input_string < MIN_LENGTH, || {
Err(LengthStringError::MinLengthViolated(MIN_LENGTH))
})?;

Expand All @@ -1101,7 +1114,7 @@ impl<const MAX_LENGTH: u16, const MIN_LENGTH: u8> LengthString<MAX_LENGTH, MIN_L
}
}

impl<'de, const MAX_LENGTH: u16, const MIN_LENGTH: u8> Deserialize<'de>
impl<'de, const MAX_LENGTH: u16, const MIN_LENGTH: u16> Deserialize<'de>
for LengthString<MAX_LENGTH, MIN_LENGTH>
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
Expand All @@ -1113,7 +1126,7 @@ impl<'de, const MAX_LENGTH: u16, const MIN_LENGTH: u8> Deserialize<'de>
}
}

impl<DB, const MAX_LENGTH: u16, const MIN_LENGTH: u8> FromSql<sql_types::Text, DB>
impl<DB, const MAX_LENGTH: u16, const MIN_LENGTH: u16> FromSql<sql_types::Text, DB>
for LengthString<MAX_LENGTH, MIN_LENGTH>
where
DB: Backend,
Expand All @@ -1125,7 +1138,7 @@ where
}
}

impl<DB, const MAX_LENGTH: u16, const MIN_LENGTH: u8> ToSql<sql_types::Text, DB>
impl<DB, const MAX_LENGTH: u16, const MIN_LENGTH: u16> ToSql<sql_types::Text, DB>
for LengthString<MAX_LENGTH, MIN_LENGTH>
where
DB: Backend,
Expand All @@ -1136,7 +1149,7 @@ where
}
}

impl<DB, const MAX_LENGTH: u16, const MIN_LENGTH: u8> Queryable<sql_types::Text, DB>
impl<DB, const MAX_LENGTH: u16, const MIN_LENGTH: u16> Queryable<sql_types::Text, DB>
for LengthString<MAX_LENGTH, MIN_LENGTH>
where
DB: Backend,
Expand Down Expand Up @@ -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<PUBLISHABLE_KEY_LENGTH, PUBLISHABLE_KEY_LENGTH>);

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<DB> Queryable<sql_types::Text, DB> for PublishableKey
where
DB: Backend,
Self: FromSql<sql_types::Text, DB>,
{
type Row = Self;

fn build(row: Self::Row) -> deserialize::Result<Self> {
Ok(row)
}
}

impl<DB> FromSql<sql_types::Text, DB> for PublishableKey
where
DB: Backend,
LengthString<PUBLISHABLE_KEY_LENGTH, PUBLISHABLE_KEY_LENGTH>: FromSql<sql_types::Text, DB>,
{
fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result<Self> {
let val = LengthString::<PUBLISHABLE_KEY_LENGTH, PUBLISHABLE_KEY_LENGTH>::from_sql(bytes)?;
Ok(Self(val))
}
}

impl<DB> ToSql<sql_types::Text, DB> for PublishableKey
where
DB: Backend,
LengthString<PUBLISHABLE_KEY_LENGTH, PUBLISHABLE_KEY_LENGTH>: ToSql<sql_types::Text, DB>,
{
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> diesel::serialize::Result {
self.0.to_sql(out)
}
}
6 changes: 3 additions & 3 deletions crates/diesel_models/src/business_profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
pub return_url: Option<common_utils::types::Url>,
pub enable_payment_response_hash: bool,
pub payment_response_hash_key: Option<String>,
pub redirect_to_merchant_with_http_post: bool,
Expand Down Expand Up @@ -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<String>,
pub return_url: Option<common_utils::types::Url>,
pub enable_payment_response_hash: bool,
pub payment_response_hash_key: Option<String>,
pub redirect_to_merchant_with_http_post: bool,
Expand Down Expand Up @@ -358,7 +358,7 @@ pub struct ProfileNew {
pub struct ProfileUpdateInternal {
pub profile_name: Option<String>,
pub modified_at: time::PrimitiveDateTime,
pub return_url: Option<String>,
pub return_url: Option<common_utils::types::Url>,
pub enable_payment_response_hash: Option<bool>,
pub payment_response_hash_key: Option<String>,
pub redirect_to_merchant_with_http_post: Option<bool>,
Expand Down
6 changes: 3 additions & 3 deletions crates/hyperswitch_domain_models/src/business_profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
pub return_url: Option<common_utils::types::Url>,
pub enable_payment_response_hash: bool,
pub payment_response_hash_key: Option<String>,
pub redirect_to_merchant_with_http_post: bool,
Expand Down Expand Up @@ -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<String>,
pub return_url: Option<common_utils::types::Url>,
pub enable_payment_response_hash: bool,
pub payment_response_hash_key: Option<String>,
pub redirect_to_merchant_with_http_post: bool,
Expand Down Expand Up @@ -815,7 +815,7 @@ impl Profile {
#[derive(Debug)]
pub struct ProfileGeneralUpdate {
pub profile_name: Option<String>,
pub return_url: Option<String>,
pub return_url: Option<common_utils::types::Url>,
pub enable_payment_response_hash: Option<bool>,
pub payment_response_hash_key: Option<String>,
pub redirect_to_merchant_with_http_post: Option<bool>,
Expand Down
19 changes: 19 additions & 0 deletions crates/hyperswitch_domain_models/src/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<url::Url, errors::api_error_response::ApiErrorResponse> {
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")]
Expand Down
5 changes: 5 additions & 0 deletions crates/router/src/analytics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,7 @@ pub mod routes {
.await
}

#[cfg(feature = "v1")]
/// # Panics
///
/// Panics if `json_payload` array does not contain one `GetSdkEventMetricRequest` element.
Expand Down Expand Up @@ -830,6 +831,7 @@ pub mod routes {
.await
}

#[cfg(feature = "v1")]
/// # Panics
///
/// Panics if `json_payload` array does not contain one `GetActivePaymentsMetricRequest` element.
Expand Down Expand Up @@ -869,6 +871,7 @@ pub mod routes {
.await
}

#[cfg(feature = "v1")]
/// # Panics
///
/// Panics if `json_payload` array does not contain one `GetAuthEventMetricRequest` element.
Expand Down Expand Up @@ -1148,6 +1151,7 @@ pub mod routes {
.await
}

#[cfg(feature = "v1")]
pub async fn get_sdk_event_filters(
state: web::Data<AppState>,
req: actix_web::HttpRequest,
Expand Down Expand Up @@ -1241,6 +1245,7 @@ pub mod routes {
.await
}

#[cfg(feature = "v1")]
pub async fn get_profile_sdk_events(
state: web::Data<AppState>,
req: actix_web::HttpRequest,
Expand Down
Loading

0 comments on commit 0805a93

Please sign in to comment.