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

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

View File

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