mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-01 19:42:27 +08:00
feat(router): add start_redirection api for three_ds flow in v2 (#6470)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
0389ae74e1
commit
6f24bb4ee3
@ -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<serde_json::Value> {
|
||||
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,
|
||||
|
||||
@ -328,6 +328,7 @@ impl<F: Clone> UpdateTracker<F, PaymentConfirmData<F>, 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 {
|
||||
|
||||
@ -2245,6 +2245,13 @@ impl<F: Clone> PostUpdateTracker<F, PaymentConfirmData<F>, 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<F: Clone> PostUpdateTracker<F, PaymentConfirmData<F>, 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,
|
||||
|
||||
@ -628,6 +628,7 @@ where
|
||||
connector_http_status_code: Option<u16>,
|
||||
external_latency: Option<u128>,
|
||||
is_latency_header_enabled: Option<bool>,
|
||||
merchant_account: &domain::MerchantAccount,
|
||||
) -> RouterResponse<Self>;
|
||||
}
|
||||
|
||||
@ -793,6 +794,7 @@ where
|
||||
_connector_http_status_code: Option<u16>,
|
||||
_external_latency: Option<u128>,
|
||||
_is_latency_header_enabled: Option<bool>,
|
||||
_merchant_account: &domain::MerchantAccount,
|
||||
) -> RouterResponse<Self> {
|
||||
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<domain::Customer>,
|
||||
_base_url: &str,
|
||||
base_url: &str,
|
||||
operation: Op,
|
||||
_connector_request_reference_id_config: &ConnectorRequestReferenceIdConfig,
|
||||
_connector_http_status_code: Option<u16>,
|
||||
_external_latency: Option<u128>,
|
||||
_is_latency_header_enabled: Option<bool>,
|
||||
merchant_account: &domain::MerchantAccount,
|
||||
) -> RouterResponse<Self> {
|
||||
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,
|
||||
|
||||
@ -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)),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@ -140,7 +140,8 @@ impl From<Flow> for ApiIdentifier {
|
||||
| Flow::PaymentsConfirmIntent
|
||||
| Flow::PaymentsCreateIntent
|
||||
| Flow::PaymentsGetIntent
|
||||
| Flow::PaymentsPostSessionTokens => Self::Payments,
|
||||
| Flow::PaymentsPostSessionTokens
|
||||
| Flow::PaymentStartRedirection => Self::Payments,
|
||||
|
||||
Flow::PayoutsCreate
|
||||
| Flow::PayoutsRetrieve
|
||||
|
||||
@ -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<app::AppState>,
|
||||
req: actix_web::HttpRequest,
|
||||
payload: web::Query<api_models::payments::PaymentStartRedirectionParams>,
|
||||
path: web::Path<common_utils::id_type::GlobalPaymentId>,
|
||||
) -> 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(
|
||||
|
||||
@ -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<A> AuthenticateAndFetch<AuthenticationData, A> 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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user