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:
Sai Harsha Vardhan
2024-11-08 19:20:25 +05:30
committed by GitHub
parent 0389ae74e1
commit 6f24bb4ee3
19 changed files with 316 additions and 28 deletions

View File

@ -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,

View File

@ -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 {

View File

@ -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,

View File

@ -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,

View File

@ -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)),
),
);

View File

@ -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

View File

@ -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(

View File

@ -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;