mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-30 01:27:31 +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
@ -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",
|
||||
|
||||
@ -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<ApiEventsType> {
|
||||
Some(ApiEventsType::Payment {
|
||||
payment_id: self.id.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<NextActionData>,
|
||||
|
||||
/// A unique identifier for a payment provided by the connector
|
||||
#[schema(value_type = Option<String>, example = "993672945374576J")]
|
||||
pub connector_transaction_id: Option<String>,
|
||||
@ -4558,6 +4568,22 @@ pub struct PaymentsConfirmIntentResponse {
|
||||
pub error: Option<ErrorDetails>,
|
||||
}
|
||||
|
||||
#[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 {
|
||||
|
||||
@ -770,7 +770,7 @@ pub struct PaymentAttemptUpdateInternal {
|
||||
pub updated_by: String,
|
||||
pub merchant_connector_id: Option<id_type::MerchantConnectorAccountId>,
|
||||
pub connector: Option<String>,
|
||||
// authentication_data: Option<serde_json::Value>,
|
||||
pub authentication_data: Option<pii::SecretSerdeValue>,
|
||||
// encoded_data: Option<String>,
|
||||
pub unified_code: Option<Option<String>>,
|
||||
pub unified_message: Option<Option<String>>,
|
||||
|
||||
@ -33,7 +33,7 @@ pub struct PaymentIntent {
|
||||
pub last_synced: Option<PrimitiveDateTime>,
|
||||
pub setup_future_usage: Option<storage_enums::FutureUsage>,
|
||||
pub client_secret: common_utils::types::ClientSecret,
|
||||
pub active_attempt_id: Option<String>,
|
||||
pub active_attempt_id: Option<common_utils::id_type::GlobalAttemptId>,
|
||||
#[diesel(deserialize_as = super::OptionalDieselArray<masking::Secret<OrderDetailsWithAmount>>)]
|
||||
pub order_details: Option<Vec<masking::Secret<OrderDetailsWithAmount>>>,
|
||||
pub allowed_payment_method_types: Option<pii::SecretSerdeValue>,
|
||||
@ -250,7 +250,7 @@ pub struct PaymentIntentNew {
|
||||
pub last_synced: Option<PrimitiveDateTime>,
|
||||
pub setup_future_usage: Option<storage_enums::FutureUsage>,
|
||||
pub client_secret: common_utils::types::ClientSecret,
|
||||
pub active_attempt_id: Option<String>,
|
||||
pub active_attempt_id: Option<common_utils::id_type::GlobalAttemptId>,
|
||||
#[diesel(deserialize_as = super::OptionalDieselArray<masking::Secret<OrderDetailsWithAmount>>)]
|
||||
pub order_details: Option<Vec<masking::Secret<OrderDetailsWithAmount>>>,
|
||||
pub allowed_payment_method_types: Option<pii::SecretSerdeValue>,
|
||||
@ -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<storage_enums::FutureUsage>,
|
||||
// pub metadata: Option<pii::SecretSerdeValue>,
|
||||
pub modified_at: PrimitiveDateTime,
|
||||
// pub active_attempt_id: Option<String>,
|
||||
pub active_attempt_id: Option<common_utils::id_type::GlobalAttemptId>,
|
||||
// pub description: Option<String>,
|
||||
// pub statement_descriptor: Option<String>,
|
||||
// #[diesel(deserialize_as = super::OptionalDieselArray<pii::SecretSerdeValue>)]
|
||||
@ -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<PaymentIntentUpdate> 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<url::Url, errors::api_error_response::ApiErrorResponse> {
|
||||
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<RemoteStorageObject<PaymentAttempt>>,
|
||||
/// 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<id_type::GlobalAttemptId>,
|
||||
/// The order details for the payment.
|
||||
pub order_details: Option<Vec<Secret<OrderDetailsWithAmount>>>,
|
||||
/// 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,
|
||||
|
||||
@ -1296,6 +1296,7 @@ pub enum PaymentAttemptUpdate {
|
||||
status: storage_enums::AttemptStatus,
|
||||
connector_payment_id: Option<String>,
|
||||
updated_by: String,
|
||||
authentication_data: Option<pii::SecretSerdeValue>,
|
||||
},
|
||||
/// Update the payment attempt on confirming the intent, after calling the connector on error response
|
||||
ConfirmIntentError {
|
||||
@ -1923,6 +1924,7 @@ impl From<PaymentAttemptUpdate> 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<PaymentAttemptUpdate> 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<PaymentAttemptUpdate> for diesel_models::PaymentAttemptUpdateInternal
|
||||
unified_message: None,
|
||||
connector_payment_id,
|
||||
connector: None,
|
||||
authentication_data,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<PaymentIntentUpdate> 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<PaymentIntentUpdate> 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
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -512,6 +512,8 @@ pub enum Flow {
|
||||
PaymentsConfirmIntent,
|
||||
/// Payments post session tokens flow
|
||||
PaymentsPostSessionTokens,
|
||||
/// Payments start redirection flow
|
||||
PaymentStartRedirection,
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
@ -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 -----------------------
|
||||
|
||||
@ -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;
|
||||
|
||||
Reference in New Issue
Block a user