mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-27 19:46:48 +08:00
feat(connector): [Airwallex] add multiple redirect support for 3DS (#811)
Co-authored-by: Narayan Bhat <narayan.bhat@juspay.in> Co-authored-by: Jagan Elavarasan <jaganelavarasan@gmail.com>
This commit is contained in:
@ -40,6 +40,7 @@ pub enum AttemptStatus {
|
||||
Failure,
|
||||
PaymentMethodAwaited,
|
||||
ConfirmationAwaited,
|
||||
DeviceDataCollectionPending,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
@ -767,9 +768,10 @@ impl From<AttemptStatus> for IntentStatus {
|
||||
AttemptStatus::PaymentMethodAwaited => Self::RequiresPaymentMethod,
|
||||
|
||||
AttemptStatus::Authorized => Self::RequiresCapture,
|
||||
AttemptStatus::AuthenticationPending => Self::RequiresCustomerAction,
|
||||
AttemptStatus::AuthenticationPending | AttemptStatus::DeviceDataCollectionPending => {
|
||||
Self::RequiresCustomerAction
|
||||
}
|
||||
AttemptStatus::Unresolved => Self::RequiresMerchantAction,
|
||||
|
||||
AttemptStatus::PartialCharged
|
||||
| AttemptStatus::Started
|
||||
| AttemptStatus::AuthenticationSuccessful
|
||||
|
||||
@ -1222,6 +1222,8 @@ pub struct Metadata {
|
||||
#[schema(value_type = Object, example = r#"{ "city": "NY", "unit": "245" }"#)]
|
||||
#[serde(flatten)]
|
||||
pub data: pii::SecretSerdeValue,
|
||||
/// Payload coming in request as a metadata field
|
||||
pub payload: Option<pii::SecretSerdeValue>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)]
|
||||
|
||||
@ -88,7 +88,7 @@ impl ConnectorCommon for Airwallex {
|
||||
}
|
||||
|
||||
impl api::Payment for Airwallex {}
|
||||
|
||||
impl api::PaymentsCompleteAuthorize for Airwallex {}
|
||||
impl api::PreVerify for Airwallex {}
|
||||
impl ConnectorIntegration<api::Verify, types::VerifyRequestData, types::PaymentsResponseData>
|
||||
for Airwallex
|
||||
@ -461,6 +461,95 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
|
||||
}
|
||||
}
|
||||
|
||||
impl
|
||||
ConnectorIntegration<
|
||||
api::CompleteAuthorize,
|
||||
types::CompleteAuthorizeData,
|
||||
types::PaymentsResponseData,
|
||||
> for Airwallex
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::PaymentsCompleteAuthorizeRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
self.build_headers(req, connectors)
|
||||
}
|
||||
fn get_content_type(&self) -> &'static str {
|
||||
self.common_get_content_type()
|
||||
}
|
||||
fn get_url(
|
||||
&self,
|
||||
req: &types::PaymentsCompleteAuthorizeRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
let connector_payment_id = req
|
||||
.request
|
||||
.connector_transaction_id
|
||||
.clone()
|
||||
.ok_or(errors::ConnectorError::MissingConnectorTransactionID)?;
|
||||
Ok(format!(
|
||||
"{}api/v1/pa/payment_intents/{}/confirm_continue",
|
||||
self.base_url(connectors),
|
||||
connector_payment_id,
|
||||
))
|
||||
}
|
||||
fn get_request_body(
|
||||
&self,
|
||||
req: &types::PaymentsCompleteAuthorizeRouterData,
|
||||
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
||||
let req_obj = airwallex::AirwallexCompleteRequest::try_from(req)?;
|
||||
let req = utils::Encode::<airwallex::AirwallexCompleteRequest>::encode_to_string_of_json(
|
||||
&req_obj,
|
||||
)
|
||||
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||
Ok(Some(req))
|
||||
}
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &types::PaymentsCompleteAuthorizeRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
||||
Ok(Some(
|
||||
services::RequestBuilder::new()
|
||||
.method(services::Method::Post)
|
||||
.url(&types::PaymentsComeplteAuthorizeType::get_url(
|
||||
self, req, connectors,
|
||||
)?)
|
||||
.headers(types::PaymentsComeplteAuthorizeType::get_headers(
|
||||
self, req, connectors,
|
||||
)?)
|
||||
.body(types::PaymentsComeplteAuthorizeType::get_request_body(
|
||||
self, req,
|
||||
)?)
|
||||
.build(),
|
||||
))
|
||||
}
|
||||
fn handle_response(
|
||||
&self,
|
||||
data: &types::PaymentsCompleteAuthorizeRouterData,
|
||||
res: Response,
|
||||
) -> CustomResult<types::PaymentsCompleteAuthorizeRouterData, errors::ConnectorError> {
|
||||
let response: airwallex::AirwallexPaymentsResponse = res
|
||||
.response
|
||||
.parse_struct("AirwallexPaymentsResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
&self,
|
||||
res: Response,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl api::PaymentCapture for Airwallex {}
|
||||
impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::PaymentsResponseData>
|
||||
for Airwallex
|
||||
@ -906,3 +995,18 @@ impl api::IncomingWebhook for Airwallex {
|
||||
Ok(details.data.object)
|
||||
}
|
||||
}
|
||||
|
||||
impl services::ConnectorRedirectResponse for Airwallex {
|
||||
fn get_flow_type(
|
||||
&self,
|
||||
_query_params: &str,
|
||||
_json_payload: Option<serde_json::Value>,
|
||||
action: services::PaymentAction,
|
||||
) -> CustomResult<payments::CallConnectorAction, errors::ConnectorError> {
|
||||
match action {
|
||||
services::PaymentAction::PSync | services::PaymentAction::CompleteAuthorize => {
|
||||
Ok(payments::CallConnectorAction::Trigger)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,7 +110,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for AirwallexPaymentsRequest {
|
||||
request_id: Uuid::new_v4().to_string(),
|
||||
payment_method,
|
||||
payment_method_options,
|
||||
return_url: item.request.router_return_url.clone(),
|
||||
return_url: item.request.complete_authorize_url.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -140,6 +140,39 @@ impl<F, T> TryFrom<types::ResponseRouterData<F, AirwallexAuthUpdateResponse, T,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||
pub struct AirwallexCompleteRequest {
|
||||
request_id: String,
|
||||
three_ds: AirwallexThreeDsData,
|
||||
#[serde(rename = "type")]
|
||||
three_ds_type: AirwallexThreeDsType,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||
pub struct AirwallexThreeDsData {
|
||||
acs_response: Option<common_utils::pii::SecretSerdeValue>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||
pub enum AirwallexThreeDsType {
|
||||
#[default]
|
||||
#[serde(rename = "3ds_continue")]
|
||||
ThreeDSContinue,
|
||||
}
|
||||
|
||||
impl TryFrom<&types::PaymentsCompleteAuthorizeRouterData> for AirwallexCompleteRequest {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(item: &types::PaymentsCompleteAuthorizeRouterData) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
request_id: Uuid::new_v4().to_string(),
|
||||
three_ds: AirwallexThreeDsData {
|
||||
acs_response: item.request.payload.clone().map(Secret::new),
|
||||
},
|
||||
three_ds_type: AirwallexThreeDsType::ThreeDSContinue,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||
pub struct AirwallexPaymentsCaptureRequest {
|
||||
// Unique ID to be sent for each transaction/operation request to the connector
|
||||
@ -191,27 +224,43 @@ pub enum AirwallexPaymentStatus {
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl From<AirwallexPaymentStatus> for enums::AttemptStatus {
|
||||
fn from(item: AirwallexPaymentStatus) -> Self {
|
||||
match item {
|
||||
AirwallexPaymentStatus::Succeeded => Self::Charged,
|
||||
AirwallexPaymentStatus::Failed => Self::Failure,
|
||||
AirwallexPaymentStatus::Pending => Self::Pending,
|
||||
AirwallexPaymentStatus::RequiresPaymentMethod => Self::PaymentMethodAwaited,
|
||||
AirwallexPaymentStatus::RequiresCustomerAction => Self::AuthenticationPending,
|
||||
AirwallexPaymentStatus::RequiresCapture => Self::Authorized,
|
||||
AirwallexPaymentStatus::Cancelled => Self::Voided,
|
||||
}
|
||||
fn get_payment_status(response: &AirwallexPaymentsResponse) -> enums::AttemptStatus {
|
||||
match response.status.clone() {
|
||||
AirwallexPaymentStatus::Succeeded => enums::AttemptStatus::Charged,
|
||||
AirwallexPaymentStatus::Failed => enums::AttemptStatus::Failure,
|
||||
AirwallexPaymentStatus::Pending => enums::AttemptStatus::Pending,
|
||||
AirwallexPaymentStatus::RequiresPaymentMethod => enums::AttemptStatus::PaymentMethodAwaited,
|
||||
AirwallexPaymentStatus::RequiresCustomerAction => response.next_action.as_ref().map_or(
|
||||
enums::AttemptStatus::AuthenticationPending,
|
||||
|next_action| match next_action.stage {
|
||||
AirwallexNextActionStage::WaitingDeviceDataCollection => {
|
||||
enums::AttemptStatus::DeviceDataCollectionPending
|
||||
}
|
||||
AirwallexNextActionStage::WaitingUserInfoInput => {
|
||||
enums::AttemptStatus::AuthenticationPending
|
||||
}
|
||||
},
|
||||
),
|
||||
AirwallexPaymentStatus::RequiresCapture => enums::AttemptStatus::Authorized,
|
||||
AirwallexPaymentStatus::Cancelled => enums::AttemptStatus::Voided,
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum AirwallexNextActionStage {
|
||||
WaitingDeviceDataCollection,
|
||||
WaitingUserInfoInput,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct AirwallexRedirectFormData {
|
||||
#[serde(rename = "JWT")]
|
||||
jwt: String,
|
||||
jwt: Option<String>,
|
||||
#[serde(rename = "threeDSMethodData")]
|
||||
three_ds_method_data: String,
|
||||
token: String,
|
||||
three_ds_method_data: Option<String>,
|
||||
token: Option<String>,
|
||||
provider: Option<String>,
|
||||
version: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
@ -219,6 +268,7 @@ pub struct AirwallexPaymentsNextAction {
|
||||
url: Url,
|
||||
method: services::Method,
|
||||
data: AirwallexRedirectFormData,
|
||||
stage: AirwallexNextActionStage,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
@ -232,11 +282,46 @@ pub struct AirwallexPaymentsResponse {
|
||||
next_action: Option<AirwallexPaymentsNextAction>,
|
||||
}
|
||||
|
||||
fn get_redirection_form(
|
||||
response_url_data: AirwallexPaymentsNextAction,
|
||||
) -> Option<services::RedirectForm> {
|
||||
Some(services::RedirectForm {
|
||||
endpoint: response_url_data.url.to_string(),
|
||||
method: response_url_data.method,
|
||||
form_fields: std::collections::HashMap::from([
|
||||
//Some form fields might be empty based on the authentication type by the connector
|
||||
(
|
||||
"JWT".to_string(),
|
||||
response_url_data.data.jwt.unwrap_or_default(),
|
||||
),
|
||||
(
|
||||
"threeDSMethodData".to_string(),
|
||||
response_url_data
|
||||
.data
|
||||
.three_ds_method_data
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
(
|
||||
"token".to_string(),
|
||||
response_url_data.data.token.unwrap_or_default(),
|
||||
),
|
||||
(
|
||||
"provider".to_string(),
|
||||
response_url_data.data.provider.unwrap_or_default(),
|
||||
),
|
||||
(
|
||||
"version".to_string(),
|
||||
response_url_data.data.version.unwrap_or_default(),
|
||||
),
|
||||
]),
|
||||
})
|
||||
}
|
||||
|
||||
impl<F, T>
|
||||
TryFrom<types::ResponseRouterData<F, AirwallexPaymentsResponse, T, types::PaymentsResponseData>>
|
||||
for types::RouterData<F, T, types::PaymentsResponseData>
|
||||
{
|
||||
type Error = error_stack::Report<errors::ParsingError>;
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(
|
||||
item: types::ResponseRouterData<
|
||||
F,
|
||||
@ -245,23 +330,48 @@ impl<F, T>
|
||||
types::PaymentsResponseData,
|
||||
>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
let redirection_data =
|
||||
item.response
|
||||
.next_action
|
||||
.map(|response_url_data| services::RedirectForm {
|
||||
endpoint: response_url_data.url.to_string(),
|
||||
method: response_url_data.method,
|
||||
form_fields: std::collections::HashMap::from([
|
||||
("JWT".to_string(), response_url_data.data.jwt),
|
||||
(
|
||||
"threeDSMethodData".to_string(),
|
||||
response_url_data.data.three_ds_method_data,
|
||||
),
|
||||
("token".to_string(), response_url_data.data.token),
|
||||
]),
|
||||
});
|
||||
let (status, redirection_data) = item.response.next_action.clone().map_or(
|
||||
// If no next action is there, map the status and set redirection form as None
|
||||
(get_payment_status(&item.response), None),
|
||||
|response_url_data| {
|
||||
// If the connector sends a customer action response that is already under
|
||||
// process from our end it can cause an infinite loop to break this this check
|
||||
// is added and fail the payment
|
||||
if matches!(
|
||||
(
|
||||
response_url_data.stage.clone(),
|
||||
item.data.status,
|
||||
item.response.status.clone(),
|
||||
),
|
||||
// If the connector sends waiting for DDC and our status is already DDC Pending
|
||||
// that means we initiated the call to collect the data and now we expect a different response
|
||||
(
|
||||
AirwallexNextActionStage::WaitingDeviceDataCollection,
|
||||
enums::AttemptStatus::DeviceDataCollectionPending,
|
||||
_
|
||||
)
|
||||
// If the connector sends waiting for Customer Action and our status is already Authenticaition Pending
|
||||
// that means we initiated the call to authenticate and now we do not expect a requires_customer action
|
||||
// it will start a loop
|
||||
| (
|
||||
_,
|
||||
enums::AttemptStatus::AuthenticationPending,
|
||||
AirwallexPaymentStatus::RequiresCustomerAction,
|
||||
)
|
||||
) {
|
||||
// Fail the payment for above conditions
|
||||
(enums::AttemptStatus::AuthenticationFailed, None)
|
||||
} else {
|
||||
(
|
||||
//Build the redirect form and update the payment status
|
||||
get_payment_status(&item.response),
|
||||
get_redirection_form(response_url_data),
|
||||
)
|
||||
}
|
||||
},
|
||||
);
|
||||
Ok(Self {
|
||||
status: enums::AttemptStatus::from(item.response.status),
|
||||
status,
|
||||
reference_id: Some(item.response.id.clone()),
|
||||
response: Ok(types::PaymentsResponseData::TransactionResponse {
|
||||
resource_id: types::ResponseId::ConnectorTransactionId(item.response.id),
|
||||
|
||||
@ -6,6 +6,7 @@ pub mod transformers;
|
||||
|
||||
use std::{fmt::Debug, marker::PhantomData, time::Instant};
|
||||
|
||||
use api_models::payments::Metadata;
|
||||
use error_stack::{IntoReport, ResultExt};
|
||||
use futures::future::join_all;
|
||||
use router_env::tracing;
|
||||
@ -247,6 +248,14 @@ pub trait PaymentRedirectFlow: Sync {
|
||||
|
||||
fn get_payment_action(&self) -> services::PaymentAction;
|
||||
|
||||
fn generate_response(
|
||||
&self,
|
||||
payments_response: api_models::payments::PaymentsResponse,
|
||||
merchant_account: storage_models::merchant_account::MerchantAccount,
|
||||
payment_id: String,
|
||||
connector: String,
|
||||
) -> RouterResult<api::RedirectionResponse>;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn handle_payments_redirect_response(
|
||||
&self,
|
||||
@ -290,13 +299,8 @@ pub trait PaymentRedirectFlow: Sync {
|
||||
.attach_printable("Failed to get the response in json"),
|
||||
}?;
|
||||
|
||||
let result = helpers::get_handle_response_url(
|
||||
resource_id,
|
||||
&merchant_account,
|
||||
payments_response,
|
||||
connector,
|
||||
)
|
||||
.attach_printable("No redirection response")?;
|
||||
let result =
|
||||
self.generate_response(payments_response, merchant_account, resource_id, connector)?;
|
||||
|
||||
Ok(services::ApplicationResponse::JsonForRedirection(result))
|
||||
}
|
||||
@ -317,6 +321,11 @@ impl PaymentRedirectFlow for PaymentRedirectCompleteAuthorize {
|
||||
let payment_confirm_req = api::PaymentsRequest {
|
||||
payment_id: Some(req.resource_id.clone()),
|
||||
merchant_id: req.merchant_id.clone(),
|
||||
metadata: Some(Metadata {
|
||||
order_details: None,
|
||||
data: masking::Secret::new("{}".into()),
|
||||
payload: Some(req.json_payload.unwrap_or("{}".into()).into()),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
payments_core::<api::CompleteAuthorize, api::PaymentsResponse, _, _, _>(
|
||||
@ -333,6 +342,47 @@ impl PaymentRedirectFlow for PaymentRedirectCompleteAuthorize {
|
||||
fn get_payment_action(&self) -> services::PaymentAction {
|
||||
services::PaymentAction::CompleteAuthorize
|
||||
}
|
||||
|
||||
fn generate_response(
|
||||
&self,
|
||||
payments_response: api_models::payments::PaymentsResponse,
|
||||
merchant_account: storage_models::merchant_account::MerchantAccount,
|
||||
payment_id: String,
|
||||
connector: String,
|
||||
) -> RouterResult<api::RedirectionResponse> {
|
||||
// There might be multiple redirections needed for some flows
|
||||
// If the status is requires customer action, then send the startpay url again
|
||||
// The redirection data must have been provided and updated by the connector
|
||||
match payments_response.status {
|
||||
api_models::enums::IntentStatus::RequiresCustomerAction => {
|
||||
let startpay_url = payments_response
|
||||
.next_action
|
||||
.and_then(|next_action| next_action.redirect_to_url)
|
||||
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
||||
.into_report()
|
||||
.attach_printable(
|
||||
"did not receive redirect to url when status is requires customer action",
|
||||
)?;
|
||||
Ok(api::RedirectionResponse {
|
||||
return_url: String::new(),
|
||||
params: vec![],
|
||||
return_url_with_query_params: startpay_url,
|
||||
http_method: "GET".to_string(),
|
||||
headers: vec![],
|
||||
})
|
||||
}
|
||||
// If the status is terminal status, then redirect to merchant return url to provide status
|
||||
api_models::enums::IntentStatus::Succeeded
|
||||
| api_models::enums::IntentStatus::Failed
|
||||
| api_models::enums::IntentStatus::Cancelled | api_models::enums::IntentStatus::RequiresCapture=> helpers::get_handle_response_url(
|
||||
payment_id,
|
||||
&merchant_account,
|
||||
payments_response,
|
||||
connector,
|
||||
),
|
||||
_ => Err(errors::ApiErrorResponse::InternalServerError).into_report().attach_printable_lazy(|| format!("Could not proceed with payment as payment status {} cannot be handled during redirection",payments_response.status))?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@ -371,6 +421,21 @@ impl PaymentRedirectFlow for PaymentRedirectSync {
|
||||
.await
|
||||
}
|
||||
|
||||
fn generate_response(
|
||||
&self,
|
||||
payments_response: api_models::payments::PaymentsResponse,
|
||||
merchant_account: storage_models::merchant_account::MerchantAccount,
|
||||
payment_id: String,
|
||||
connector: String,
|
||||
) -> RouterResult<api::RedirectionResponse> {
|
||||
helpers::get_handle_response_url(
|
||||
payment_id,
|
||||
&merchant_account,
|
||||
payments_response,
|
||||
connector,
|
||||
)
|
||||
}
|
||||
|
||||
fn get_payment_action(&self) -> services::PaymentAction {
|
||||
services::PaymentAction::PSync
|
||||
}
|
||||
|
||||
@ -88,7 +88,6 @@ macro_rules! default_imp_for_complete_authorize{
|
||||
default_imp_for_complete_authorize!(
|
||||
connector::Aci,
|
||||
connector::Adyen,
|
||||
connector::Airwallex,
|
||||
connector::Applepay,
|
||||
connector::Authorizedotnet,
|
||||
connector::Bambora,
|
||||
@ -131,7 +130,6 @@ macro_rules! default_imp_for_connector_redirect_response{
|
||||
default_imp_for_connector_redirect_response!(
|
||||
connector::Aci,
|
||||
connector::Adyen,
|
||||
connector::Airwallex,
|
||||
connector::Applepay,
|
||||
connector::Authorizedotnet,
|
||||
connector::Bambora,
|
||||
|
||||
@ -339,7 +339,7 @@ pub fn create_startpay_url(
|
||||
payment_intent: &storage::PaymentIntent,
|
||||
) -> String {
|
||||
format!(
|
||||
"{}/payments/start/{}/{}/{}",
|
||||
"{}/payments/redirect/{}/{}/{}",
|
||||
server.base_url,
|
||||
payment_intent.payment_id,
|
||||
payment_intent.merchant_id,
|
||||
@ -355,7 +355,7 @@ pub fn create_redirect_url(
|
||||
) -> String {
|
||||
let creds_identifier_path = creds_identifier.map_or_else(String::new, |cd| format!("/{}", cd));
|
||||
format!(
|
||||
"{}/payments/{}/{}/response/{}",
|
||||
"{}/payments/{}/{}/redirect/response/{}",
|
||||
router_base_url, payment_attempt.payment_id, payment_attempt.merchant_id, connector_name,
|
||||
) + &creds_identifier_path
|
||||
}
|
||||
@ -376,7 +376,7 @@ pub fn create_complete_authorize_url(
|
||||
connector_name: &String,
|
||||
) -> String {
|
||||
format!(
|
||||
"{}/payments/{}/{}/complete/{}",
|
||||
"{}/payments/{}/{}/redirect/complete/{}",
|
||||
router_base_url, payment_attempt.payment_id, payment_attempt.merchant_id, connector_name
|
||||
)
|
||||
}
|
||||
@ -933,6 +933,7 @@ pub fn get_handle_response_url(
|
||||
connector: String,
|
||||
) -> RouterResult<api::RedirectionResponse> {
|
||||
let payments_return_url = response.return_url.as_ref();
|
||||
|
||||
let redirection_response = make_pg_redirect_response(payment_id, &response, connector);
|
||||
|
||||
let return_url = make_merchant_url_with_response(
|
||||
|
||||
@ -2,6 +2,7 @@ use std::marker::PhantomData;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use error_stack::ResultExt;
|
||||
use masking::ExposeOptionInterface;
|
||||
use router_derive::PaymentOperation;
|
||||
use router_env::{instrument, tracing};
|
||||
|
||||
@ -45,7 +46,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Co
|
||||
let db = &*state.store;
|
||||
let merchant_id = &merchant_account.merchant_id;
|
||||
let storage_scheme = merchant_account.storage_scheme;
|
||||
let (mut payment_intent, mut payment_attempt, currency, amount, connector_response);
|
||||
let (mut payment_intent, mut payment_attempt, currency, amount);
|
||||
|
||||
let payment_id = payment_id
|
||||
.get_payment_intent_id()
|
||||
@ -154,7 +155,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Co
|
||||
)
|
||||
.await?;
|
||||
|
||||
connector_response = db
|
||||
let mut connector_response = db
|
||||
.find_connector_response_by_payment_id_merchant_id_attempt_id(
|
||||
&payment_attempt.payment_id,
|
||||
&payment_attempt.merchant_id,
|
||||
@ -166,6 +167,13 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Co
|
||||
error.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)
|
||||
})?;
|
||||
|
||||
connector_response.encoded_data = request.metadata.clone().and_then(|secret_metadata| {
|
||||
secret_metadata
|
||||
.payload
|
||||
.expose_option()
|
||||
.map(|exposed_payload| exposed_payload.to_string())
|
||||
});
|
||||
|
||||
payment_intent.shipping_address_id = shipping_address.clone().map(|i| i.address_id);
|
||||
payment_intent.billing_address_id = billing_address.clone().map(|i| i.address_id);
|
||||
payment_intent.return_url = request.return_url.as_ref().map(|a| a.to_string());
|
||||
|
||||
@ -318,17 +318,10 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
|
||||
let payment_method = payment_data.payment_attempt.payment_method;
|
||||
let browser_info = payment_data.payment_attempt.browser_info.clone();
|
||||
|
||||
let (intent_status, attempt_status) = match payment_data.payment_attempt.authentication_type
|
||||
{
|
||||
Some(storage_enums::AuthenticationType::NoThreeDs) => (
|
||||
storage_enums::IntentStatus::Processing,
|
||||
storage_enums::AttemptStatus::Pending,
|
||||
),
|
||||
_ => (
|
||||
storage_enums::IntentStatus::RequiresCustomerAction,
|
||||
storage_enums::AttemptStatus::AuthenticationPending,
|
||||
),
|
||||
};
|
||||
let (intent_status, attempt_status) = (
|
||||
storage_enums::IntentStatus::Processing,
|
||||
storage_enums::AttemptStatus::Pending,
|
||||
);
|
||||
|
||||
let connector = payment_data.payment_attempt.connector.clone();
|
||||
let straight_through_algorithm = payment_data
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use std::{fmt::Debug, marker::PhantomData};
|
||||
|
||||
use error_stack::ResultExt;
|
||||
use error_stack::{IntoReport, ResultExt};
|
||||
use router_env::{instrument, tracing};
|
||||
|
||||
use super::{flows::Feature, PaymentAddress, PaymentData};
|
||||
@ -681,6 +681,14 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::CompleteAuthoriz
|
||||
.change_context(errors::ApiErrorResponse::InvalidDataValue {
|
||||
field_name: "browser_info",
|
||||
})?;
|
||||
|
||||
let json_payload = payment_data
|
||||
.connector_response
|
||||
.encoded_data
|
||||
.map(serde_json::to_value)
|
||||
.transpose()
|
||||
.into_report()
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)?;
|
||||
Ok(Self {
|
||||
setup_future_usage: payment_data.payment_intent.setup_future_usage,
|
||||
mandate_id: payment_data.mandate_id.clone(),
|
||||
@ -695,6 +703,7 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::CompleteAuthoriz
|
||||
email: payment_data.email,
|
||||
payment_method_data: payment_data.payment_method_data,
|
||||
connector_transaction_id: payment_data.connector_response.connector_transaction_id,
|
||||
payload: json_payload,
|
||||
connector_meta: payment_data.payment_attempt.connector_metadata,
|
||||
})
|
||||
}
|
||||
|
||||
@ -109,22 +109,21 @@ impl Payments {
|
||||
web::resource("/{payment_id}/capture").route(web::post().to(payments_capture)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/start/{payment_id}/{merchant_id}/{attempt_id}")
|
||||
web::resource("/redirect/{payment_id}/{merchant_id}/{attempt_id}")
|
||||
.route(web::get().to(payments_start)),
|
||||
)
|
||||
.service(
|
||||
web::resource(
|
||||
"/{payment_id}/{merchant_id}/response/{connector}/{creds_identifier}",
|
||||
"/{payment_id}/{merchant_id}/redirect/response/{connector}/{creds_identifier}",
|
||||
)
|
||||
.route(web::get().to(payments_redirect_response_with_creds_identifier)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/{payment_id}/{merchant_id}/response/{connector}")
|
||||
web::resource("/{payment_id}/{merchant_id}/redirect/response/{connector}")
|
||||
.route(web::get().to(payments_redirect_response)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/{payment_id}/{merchant_id}/complete/{connector}")
|
||||
.route(web::get().to(payments_complete_authorize))
|
||||
web::resource("/{payment_id}/{merchant_id}/redirect/complete/{connector}")
|
||||
.route(web::post().to(payments_complete_authorize)),
|
||||
);
|
||||
}
|
||||
|
||||
@ -60,12 +60,12 @@ pub async fn payments_create(
|
||||
.await
|
||||
}
|
||||
|
||||
// /// Payments - Start
|
||||
// /// Payments - Redirect
|
||||
// ///
|
||||
// /// The entry point for a payment which involves the redirection flow. This redirects the user to the authentication page
|
||||
// /// For a payment which involves the redirection flow. This redirects the user to the authentication page
|
||||
// #[utoipa::path(
|
||||
// get,
|
||||
// path = "/payments/start/{payment_id}/{merchant_id}/{attempt_id}",
|
||||
// path = "/payments/redirect/{payment_id}/{merchant_id}/{attempt_id}",
|
||||
// params(
|
||||
// ("payment_id" = String, Path, description = "The identifier for payment"),
|
||||
// ("merchant_id" = String, Path, description = "The identifier for merchant"),
|
||||
|
||||
@ -27,7 +27,7 @@ use crate::{
|
||||
logger,
|
||||
routes::{app::AppStateInfo, metrics, AppState},
|
||||
services::authentication as auth,
|
||||
types::{self, api, storage, ErrorResponse},
|
||||
types::{self, api, ErrorResponse},
|
||||
};
|
||||
|
||||
pub type BoxedConnectorIntegration<'a, T, Req, Resp> =
|
||||
@ -447,19 +447,6 @@ pub struct ApplicationRedirectResponse {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
impl From<&storage::PaymentAttempt> for ApplicationRedirectResponse {
|
||||
fn from(payment_attempt: &storage::PaymentAttempt) -> Self {
|
||||
Self {
|
||||
url: format!(
|
||||
"/payments/start/{}/{}/{}",
|
||||
&payment_attempt.payment_id,
|
||||
&payment_attempt.merchant_id,
|
||||
&payment_attempt.attempt_id
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct RedirectForm {
|
||||
pub endpoint: String,
|
||||
|
||||
@ -199,6 +199,7 @@ pub struct CompleteAuthorizeData {
|
||||
pub mandate_id: Option<api_models::payments::MandateIds>,
|
||||
pub off_session: Option<bool>,
|
||||
pub setup_mandate_details: Option<payments::MandateData>,
|
||||
pub payload: Option<serde_json::Value>,
|
||||
pub browser_info: Option<BrowserInformation>,
|
||||
pub connector_transaction_id: Option<String>,
|
||||
pub connector_meta: Option<serde_json::Value>,
|
||||
|
||||
@ -128,7 +128,10 @@ impl ForeignFrom<storage_enums::AttemptStatus> for storage_enums::IntentStatus {
|
||||
storage_enums::AttemptStatus::PaymentMethodAwaited => Self::RequiresPaymentMethod,
|
||||
|
||||
storage_enums::AttemptStatus::Authorized => Self::RequiresCapture,
|
||||
storage_enums::AttemptStatus::AuthenticationPending => Self::RequiresCustomerAction,
|
||||
storage_enums::AttemptStatus::AuthenticationPending
|
||||
| storage_enums::AttemptStatus::DeviceDataCollectionPending => {
|
||||
Self::RequiresCustomerAction
|
||||
}
|
||||
storage_enums::AttemptStatus::Unresolved => Self::RequiresMerchantAction,
|
||||
|
||||
storage_enums::AttemptStatus::PartialCharged
|
||||
|
||||
@ -63,6 +63,7 @@ fn payment_method_details() -> Option<types::PaymentsAuthorizeData> {
|
||||
}),
|
||||
capture_method: Some(storage_models::enums::CaptureMethod::Manual),
|
||||
router_return_url: Some("https://google.com".to_string()),
|
||||
complete_authorize_url: Some("https://google.com".to_string()),
|
||||
..utils::PaymentAuthorizeType::default().0
|
||||
})
|
||||
}
|
||||
|
||||
@ -57,6 +57,7 @@ pub enum AttemptStatus {
|
||||
Failure,
|
||||
PaymentMethodAwaited,
|
||||
ConfirmationAwaited,
|
||||
DeviceDataCollectionPending,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
DELETE FROM pg_enum
|
||||
WHERE enumlabel = 'device_data_collection_pending'
|
||||
AND enumtypid = (
|
||||
SELECT oid FROM pg_type WHERE typname = 'AttemptStatus'
|
||||
)
|
||||
@ -0,0 +1 @@
|
||||
ALTER TYPE "AttemptStatus" ADD VALUE IF NOT EXISTS 'device_data_collection_pending';
|
||||
Reference in New Issue
Block a user