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:
SamraatBansal
2023-04-13 13:40:30 +05:30
committed by GitHub
parent 01bc162d25
commit d1d58e33b7
19 changed files with 374 additions and 84 deletions

View File

@ -40,6 +40,7 @@ pub enum AttemptStatus {
Failure, Failure,
PaymentMethodAwaited, PaymentMethodAwaited,
ConfirmationAwaited, ConfirmationAwaited,
DeviceDataCollectionPending,
} }
#[derive( #[derive(
@ -767,9 +768,10 @@ impl From<AttemptStatus> for IntentStatus {
AttemptStatus::PaymentMethodAwaited => Self::RequiresPaymentMethod, AttemptStatus::PaymentMethodAwaited => Self::RequiresPaymentMethod,
AttemptStatus::Authorized => Self::RequiresCapture, AttemptStatus::Authorized => Self::RequiresCapture,
AttemptStatus::AuthenticationPending => Self::RequiresCustomerAction, AttemptStatus::AuthenticationPending | AttemptStatus::DeviceDataCollectionPending => {
Self::RequiresCustomerAction
}
AttemptStatus::Unresolved => Self::RequiresMerchantAction, AttemptStatus::Unresolved => Self::RequiresMerchantAction,
AttemptStatus::PartialCharged AttemptStatus::PartialCharged
| AttemptStatus::Started | AttemptStatus::Started
| AttemptStatus::AuthenticationSuccessful | AttemptStatus::AuthenticationSuccessful

View File

@ -1222,6 +1222,8 @@ pub struct Metadata {
#[schema(value_type = Object, example = r#"{ "city": "NY", "unit": "245" }"#)] #[schema(value_type = Object, example = r#"{ "city": "NY", "unit": "245" }"#)]
#[serde(flatten)] #[serde(flatten)]
pub data: pii::SecretSerdeValue, 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)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)]

View File

@ -88,7 +88,7 @@ impl ConnectorCommon for Airwallex {
} }
impl api::Payment for Airwallex {} impl api::Payment for Airwallex {}
impl api::PaymentsCompleteAuthorize for Airwallex {}
impl api::PreVerify for Airwallex {} impl api::PreVerify for Airwallex {}
impl ConnectorIntegration<api::Verify, types::VerifyRequestData, types::PaymentsResponseData> impl ConnectorIntegration<api::Verify, types::VerifyRequestData, types::PaymentsResponseData>
for Airwallex 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 api::PaymentCapture for Airwallex {}
impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::PaymentsResponseData> impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::PaymentsResponseData>
for Airwallex for Airwallex
@ -906,3 +995,18 @@ impl api::IncomingWebhook for Airwallex {
Ok(details.data.object) 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)
}
}
}
}

View File

@ -110,7 +110,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for AirwallexPaymentsRequest {
request_id: Uuid::new_v4().to_string(), request_id: Uuid::new_v4().to_string(),
payment_method, payment_method,
payment_method_options, 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)] #[derive(Default, Debug, Serialize, Eq, PartialEq)]
pub struct AirwallexPaymentsCaptureRequest { pub struct AirwallexPaymentsCaptureRequest {
// Unique ID to be sent for each transaction/operation request to the connector // Unique ID to be sent for each transaction/operation request to the connector
@ -191,27 +224,43 @@ pub enum AirwallexPaymentStatus {
Cancelled, Cancelled,
} }
impl From<AirwallexPaymentStatus> for enums::AttemptStatus { fn get_payment_status(response: &AirwallexPaymentsResponse) -> enums::AttemptStatus {
fn from(item: AirwallexPaymentStatus) -> Self { match response.status.clone() {
match item { AirwallexPaymentStatus::Succeeded => enums::AttemptStatus::Charged,
AirwallexPaymentStatus::Succeeded => Self::Charged, AirwallexPaymentStatus::Failed => enums::AttemptStatus::Failure,
AirwallexPaymentStatus::Failed => Self::Failure, AirwallexPaymentStatus::Pending => enums::AttemptStatus::Pending,
AirwallexPaymentStatus::Pending => Self::Pending, AirwallexPaymentStatus::RequiresPaymentMethod => enums::AttemptStatus::PaymentMethodAwaited,
AirwallexPaymentStatus::RequiresPaymentMethod => Self::PaymentMethodAwaited, AirwallexPaymentStatus::RequiresCustomerAction => response.next_action.as_ref().map_or(
AirwallexPaymentStatus::RequiresCustomerAction => Self::AuthenticationPending, enums::AttemptStatus::AuthenticationPending,
AirwallexPaymentStatus::RequiresCapture => Self::Authorized, |next_action| match next_action.stage {
AirwallexPaymentStatus::Cancelled => Self::Voided, 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)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AirwallexRedirectFormData { pub struct AirwallexRedirectFormData {
#[serde(rename = "JWT")] #[serde(rename = "JWT")]
jwt: String, jwt: Option<String>,
#[serde(rename = "threeDSMethodData")] #[serde(rename = "threeDSMethodData")]
three_ds_method_data: String, three_ds_method_data: Option<String>,
token: String, token: Option<String>,
provider: Option<String>,
version: Option<String>,
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
@ -219,6 +268,7 @@ pub struct AirwallexPaymentsNextAction {
url: Url, url: Url,
method: services::Method, method: services::Method,
data: AirwallexRedirectFormData, data: AirwallexRedirectFormData,
stage: AirwallexNextActionStage,
} }
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
@ -232,11 +282,46 @@ pub struct AirwallexPaymentsResponse {
next_action: Option<AirwallexPaymentsNextAction>, 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> impl<F, T>
TryFrom<types::ResponseRouterData<F, AirwallexPaymentsResponse, T, types::PaymentsResponseData>> TryFrom<types::ResponseRouterData<F, AirwallexPaymentsResponse, T, types::PaymentsResponseData>>
for types::RouterData<F, 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( fn try_from(
item: types::ResponseRouterData< item: types::ResponseRouterData<
F, F,
@ -245,23 +330,48 @@ impl<F, T>
types::PaymentsResponseData, types::PaymentsResponseData,
>, >,
) -> Result<Self, Self::Error> { ) -> Result<Self, Self::Error> {
let redirection_data = let (status, redirection_data) = item.response.next_action.clone().map_or(
item.response // If no next action is there, map the status and set redirection form as None
.next_action (get_payment_status(&item.response), None),
.map(|response_url_data| services::RedirectForm { |response_url_data| {
endpoint: response_url_data.url.to_string(), // If the connector sends a customer action response that is already under
method: response_url_data.method, // process from our end it can cause an infinite loop to break this this check
form_fields: std::collections::HashMap::from([ // is added and fail the payment
("JWT".to_string(), response_url_data.data.jwt), if matches!(
( (
"threeDSMethodData".to_string(), response_url_data.stage.clone(),
response_url_data.data.three_ds_method_data, item.data.status,
), item.response.status.clone(),
("token".to_string(), response_url_data.data.token), ),
]), // 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 { Ok(Self {
status: enums::AttemptStatus::from(item.response.status), status,
reference_id: Some(item.response.id.clone()), reference_id: Some(item.response.id.clone()),
response: Ok(types::PaymentsResponseData::TransactionResponse { response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(item.response.id), resource_id: types::ResponseId::ConnectorTransactionId(item.response.id),

View File

@ -6,6 +6,7 @@ pub mod transformers;
use std::{fmt::Debug, marker::PhantomData, time::Instant}; use std::{fmt::Debug, marker::PhantomData, time::Instant};
use api_models::payments::Metadata;
use error_stack::{IntoReport, ResultExt}; use error_stack::{IntoReport, ResultExt};
use futures::future::join_all; use futures::future::join_all;
use router_env::tracing; use router_env::tracing;
@ -247,6 +248,14 @@ pub trait PaymentRedirectFlow: Sync {
fn get_payment_action(&self) -> services::PaymentAction; 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)] #[allow(clippy::too_many_arguments)]
async fn handle_payments_redirect_response( async fn handle_payments_redirect_response(
&self, &self,
@ -290,13 +299,8 @@ pub trait PaymentRedirectFlow: Sync {
.attach_printable("Failed to get the response in json"), .attach_printable("Failed to get the response in json"),
}?; }?;
let result = helpers::get_handle_response_url( let result =
resource_id, self.generate_response(payments_response, merchant_account, resource_id, connector)?;
&merchant_account,
payments_response,
connector,
)
.attach_printable("No redirection response")?;
Ok(services::ApplicationResponse::JsonForRedirection(result)) Ok(services::ApplicationResponse::JsonForRedirection(result))
} }
@ -317,6 +321,11 @@ impl PaymentRedirectFlow for PaymentRedirectCompleteAuthorize {
let payment_confirm_req = api::PaymentsRequest { let payment_confirm_req = api::PaymentsRequest {
payment_id: Some(req.resource_id.clone()), payment_id: Some(req.resource_id.clone()),
merchant_id: req.merchant_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() ..Default::default()
}; };
payments_core::<api::CompleteAuthorize, api::PaymentsResponse, _, _, _>( payments_core::<api::CompleteAuthorize, api::PaymentsResponse, _, _, _>(
@ -333,6 +342,47 @@ impl PaymentRedirectFlow for PaymentRedirectCompleteAuthorize {
fn get_payment_action(&self) -> services::PaymentAction { fn get_payment_action(&self) -> services::PaymentAction {
services::PaymentAction::CompleteAuthorize 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)] #[derive(Clone, Debug)]
@ -371,6 +421,21 @@ impl PaymentRedirectFlow for PaymentRedirectSync {
.await .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 { fn get_payment_action(&self) -> services::PaymentAction {
services::PaymentAction::PSync services::PaymentAction::PSync
} }

View File

@ -88,7 +88,6 @@ macro_rules! default_imp_for_complete_authorize{
default_imp_for_complete_authorize!( default_imp_for_complete_authorize!(
connector::Aci, connector::Aci,
connector::Adyen, connector::Adyen,
connector::Airwallex,
connector::Applepay, connector::Applepay,
connector::Authorizedotnet, connector::Authorizedotnet,
connector::Bambora, connector::Bambora,
@ -131,7 +130,6 @@ macro_rules! default_imp_for_connector_redirect_response{
default_imp_for_connector_redirect_response!( default_imp_for_connector_redirect_response!(
connector::Aci, connector::Aci,
connector::Adyen, connector::Adyen,
connector::Airwallex,
connector::Applepay, connector::Applepay,
connector::Authorizedotnet, connector::Authorizedotnet,
connector::Bambora, connector::Bambora,

View File

@ -339,7 +339,7 @@ pub fn create_startpay_url(
payment_intent: &storage::PaymentIntent, payment_intent: &storage::PaymentIntent,
) -> String { ) -> String {
format!( format!(
"{}/payments/start/{}/{}/{}", "{}/payments/redirect/{}/{}/{}",
server.base_url, server.base_url,
payment_intent.payment_id, payment_intent.payment_id,
payment_intent.merchant_id, payment_intent.merchant_id,
@ -355,7 +355,7 @@ pub fn create_redirect_url(
) -> String { ) -> String {
let creds_identifier_path = creds_identifier.map_or_else(String::new, |cd| format!("/{}", cd)); let creds_identifier_path = creds_identifier.map_or_else(String::new, |cd| format!("/{}", cd));
format!( format!(
"{}/payments/{}/{}/response/{}", "{}/payments/{}/{}/redirect/response/{}",
router_base_url, payment_attempt.payment_id, payment_attempt.merchant_id, connector_name, router_base_url, payment_attempt.payment_id, payment_attempt.merchant_id, connector_name,
) + &creds_identifier_path ) + &creds_identifier_path
} }
@ -376,7 +376,7 @@ pub fn create_complete_authorize_url(
connector_name: &String, connector_name: &String,
) -> String { ) -> String {
format!( format!(
"{}/payments/{}/{}/complete/{}", "{}/payments/{}/{}/redirect/complete/{}",
router_base_url, payment_attempt.payment_id, payment_attempt.merchant_id, connector_name 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, connector: String,
) -> RouterResult<api::RedirectionResponse> { ) -> RouterResult<api::RedirectionResponse> {
let payments_return_url = response.return_url.as_ref(); let payments_return_url = response.return_url.as_ref();
let redirection_response = make_pg_redirect_response(payment_id, &response, connector); let redirection_response = make_pg_redirect_response(payment_id, &response, connector);
let return_url = make_merchant_url_with_response( let return_url = make_merchant_url_with_response(

View File

@ -2,6 +2,7 @@ use std::marker::PhantomData;
use async_trait::async_trait; use async_trait::async_trait;
use error_stack::ResultExt; use error_stack::ResultExt;
use masking::ExposeOptionInterface;
use router_derive::PaymentOperation; use router_derive::PaymentOperation;
use router_env::{instrument, tracing}; 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 db = &*state.store;
let merchant_id = &merchant_account.merchant_id; let merchant_id = &merchant_account.merchant_id;
let storage_scheme = merchant_account.storage_scheme; 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 let payment_id = payment_id
.get_payment_intent_id() .get_payment_intent_id()
@ -154,7 +155,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Co
) )
.await?; .await?;
connector_response = db let mut connector_response = db
.find_connector_response_by_payment_id_merchant_id_attempt_id( .find_connector_response_by_payment_id_merchant_id_attempt_id(
&payment_attempt.payment_id, &payment_attempt.payment_id,
&payment_attempt.merchant_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) 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.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.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()); payment_intent.return_url = request.return_url.as_ref().map(|a| a.to_string());

View File

@ -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 payment_method = payment_data.payment_attempt.payment_method;
let browser_info = payment_data.payment_attempt.browser_info.clone(); let browser_info = payment_data.payment_attempt.browser_info.clone();
let (intent_status, attempt_status) = match payment_data.payment_attempt.authentication_type let (intent_status, attempt_status) = (
{ storage_enums::IntentStatus::Processing,
Some(storage_enums::AuthenticationType::NoThreeDs) => ( storage_enums::AttemptStatus::Pending,
storage_enums::IntentStatus::Processing, );
storage_enums::AttemptStatus::Pending,
),
_ => (
storage_enums::IntentStatus::RequiresCustomerAction,
storage_enums::AttemptStatus::AuthenticationPending,
),
};
let connector = payment_data.payment_attempt.connector.clone(); let connector = payment_data.payment_attempt.connector.clone();
let straight_through_algorithm = payment_data let straight_through_algorithm = payment_data

View File

@ -1,6 +1,6 @@
use std::{fmt::Debug, marker::PhantomData}; use std::{fmt::Debug, marker::PhantomData};
use error_stack::ResultExt; use error_stack::{IntoReport, ResultExt};
use router_env::{instrument, tracing}; use router_env::{instrument, tracing};
use super::{flows::Feature, PaymentAddress, PaymentData}; use super::{flows::Feature, PaymentAddress, PaymentData};
@ -681,6 +681,14 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::CompleteAuthoriz
.change_context(errors::ApiErrorResponse::InvalidDataValue { .change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "browser_info", 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 { Ok(Self {
setup_future_usage: payment_data.payment_intent.setup_future_usage, setup_future_usage: payment_data.payment_intent.setup_future_usage,
mandate_id: payment_data.mandate_id.clone(), mandate_id: payment_data.mandate_id.clone(),
@ -695,6 +703,7 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::CompleteAuthoriz
email: payment_data.email, email: payment_data.email,
payment_method_data: payment_data.payment_method_data, payment_method_data: payment_data.payment_method_data,
connector_transaction_id: payment_data.connector_response.connector_transaction_id, connector_transaction_id: payment_data.connector_response.connector_transaction_id,
payload: json_payload,
connector_meta: payment_data.payment_attempt.connector_metadata, connector_meta: payment_data.payment_attempt.connector_metadata,
}) })
} }

View File

@ -109,22 +109,21 @@ impl Payments {
web::resource("/{payment_id}/capture").route(web::post().to(payments_capture)), web::resource("/{payment_id}/capture").route(web::post().to(payments_capture)),
) )
.service( .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)), .route(web::get().to(payments_start)),
) )
.service( .service(
web::resource( 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)), .route(web::get().to(payments_redirect_response_with_creds_identifier)),
) )
.service( .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)), .route(web::get().to(payments_redirect_response)),
) )
.service( .service(
web::resource("/{payment_id}/{merchant_id}/complete/{connector}") web::resource("/{payment_id}/{merchant_id}/redirect/complete/{connector}")
.route(web::get().to(payments_complete_authorize))
.route(web::post().to(payments_complete_authorize)), .route(web::post().to(payments_complete_authorize)),
); );
} }

View File

@ -60,12 +60,12 @@ pub async fn payments_create(
.await .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( // #[utoipa::path(
// get, // get,
// path = "/payments/start/{payment_id}/{merchant_id}/{attempt_id}", // path = "/payments/redirect/{payment_id}/{merchant_id}/{attempt_id}",
// params( // params(
// ("payment_id" = String, Path, description = "The identifier for payment"), // ("payment_id" = String, Path, description = "The identifier for payment"),
// ("merchant_id" = String, Path, description = "The identifier for merchant"), // ("merchant_id" = String, Path, description = "The identifier for merchant"),

View File

@ -27,7 +27,7 @@ use crate::{
logger, logger,
routes::{app::AppStateInfo, metrics, AppState}, routes::{app::AppStateInfo, metrics, AppState},
services::authentication as auth, services::authentication as auth,
types::{self, api, storage, ErrorResponse}, types::{self, api, ErrorResponse},
}; };
pub type BoxedConnectorIntegration<'a, T, Req, Resp> = pub type BoxedConnectorIntegration<'a, T, Req, Resp> =
@ -447,19 +447,6 @@ pub struct ApplicationRedirectResponse {
pub url: String, 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)] #[derive(Debug, Eq, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
pub struct RedirectForm { pub struct RedirectForm {
pub endpoint: String, pub endpoint: String,

View File

@ -199,6 +199,7 @@ pub struct CompleteAuthorizeData {
pub mandate_id: Option<api_models::payments::MandateIds>, pub mandate_id: Option<api_models::payments::MandateIds>,
pub off_session: Option<bool>, pub off_session: Option<bool>,
pub setup_mandate_details: Option<payments::MandateData>, pub setup_mandate_details: Option<payments::MandateData>,
pub payload: Option<serde_json::Value>,
pub browser_info: Option<BrowserInformation>, pub browser_info: Option<BrowserInformation>,
pub connector_transaction_id: Option<String>, pub connector_transaction_id: Option<String>,
pub connector_meta: Option<serde_json::Value>, pub connector_meta: Option<serde_json::Value>,

View File

@ -128,7 +128,10 @@ impl ForeignFrom<storage_enums::AttemptStatus> for storage_enums::IntentStatus {
storage_enums::AttemptStatus::PaymentMethodAwaited => Self::RequiresPaymentMethod, storage_enums::AttemptStatus::PaymentMethodAwaited => Self::RequiresPaymentMethod,
storage_enums::AttemptStatus::Authorized => Self::RequiresCapture, 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::Unresolved => Self::RequiresMerchantAction,
storage_enums::AttemptStatus::PartialCharged storage_enums::AttemptStatus::PartialCharged

View File

@ -63,6 +63,7 @@ fn payment_method_details() -> Option<types::PaymentsAuthorizeData> {
}), }),
capture_method: Some(storage_models::enums::CaptureMethod::Manual), capture_method: Some(storage_models::enums::CaptureMethod::Manual),
router_return_url: Some("https://google.com".to_string()), router_return_url: Some("https://google.com".to_string()),
complete_authorize_url: Some("https://google.com".to_string()),
..utils::PaymentAuthorizeType::default().0 ..utils::PaymentAuthorizeType::default().0
}) })
} }

View File

@ -57,6 +57,7 @@ pub enum AttemptStatus {
Failure, Failure,
PaymentMethodAwaited, PaymentMethodAwaited,
ConfirmationAwaited, ConfirmationAwaited,
DeviceDataCollectionPending,
} }
#[derive( #[derive(

View File

@ -0,0 +1,5 @@
DELETE FROM pg_enum
WHERE enumlabel = 'device_data_collection_pending'
AND enumtypid = (
SELECT oid FROM pg_type WHERE typname = 'AttemptStatus'
)

View File

@ -0,0 +1 @@
ALTER TYPE "AttemptStatus" ADD VALUE IF NOT EXISTS 'device_data_collection_pending';