feat(connector): [Bluesnap] add cards 3DS support (#1057)

Co-authored-by: Jagan Elavarasan <jaganelavarasan@gmail.com>
Co-authored-by: Kartikeya Hegde <karthikey.hegde@juspay.in>
This commit is contained in:
SamraatBansal
2023-05-08 19:04:43 +05:30
committed by GitHub
parent 35196493c4
commit 9c331e411b
15 changed files with 596 additions and 69 deletions

View File

@ -180,33 +180,33 @@ ideal = { country = "NL", currency = "EUR" }
[pm_filters.braintree]
paypal = { currency = "AUD,BRL,CAD,CNY,CZK,DKK,EUR,HKD,HUF,ILS,JPY,MYR,MXN,TWD,NZD,NOK,PHP,PLN,GBP,RUB,SGD,SEK,CHF,THB,USD" }
credit = { not_available_flows = {capture_method="manual"} }
debit = { not_available_flows = {capture_method="manual"} }
credit = { not_available_flows = { capture_method = "manual" } }
debit = { not_available_flows = { capture_method = "manual" } }
[pm_filters.klarna]
klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NL,NZ,NO,PL,PT,ES,SE,CH,GB,US", currency = "AUD,EUR,EUR,CAD,CZK,DKK,EUR,EUR,EUR,EUR,EUR,EUR,EUR,NZD,NOK,PLN,EUR,EUR,SEK,CHF,GBP,USD" }
credit = { not_available_flows = {capture_method="manual"} }
debit = { not_available_flows = {capture_method="manual"} }
credit = { not_available_flows = { capture_method = "manual" } }
debit = { not_available_flows = { capture_method = "manual" } }
[pm_filters.zen]
credit = { not_available_flows = {capture_method="manual"} }
debit = { not_available_flows = {capture_method="manual"} }
credit = { not_available_flows = { capture_method = "manual" } }
debit = { not_available_flows = { capture_method = "manual" } }
[pm_filters.aci]
credit = { not_available_flows = {capture_method="manual"} }
debit = { not_available_flows = {capture_method="manual"} }
credit = { not_available_flows = { capture_method = "manual" } }
debit = { not_available_flows = { capture_method = "manual" } }
[pm_filters.mollie]
credit = { not_available_flows = {capture_method="manual"} }
debit = { not_available_flows = {capture_method="manual"} }
credit = { not_available_flows = { capture_method = "manual" } }
debit = { not_available_flows = { capture_method = "manual" } }
[pm_filters.multisafepay]
credit = { not_available_flows = {capture_method="manual"} }
debit = { not_available_flows = {capture_method="manual"} }
credit = { not_available_flows = { capture_method = "manual" } }
debit = { not_available_flows = { capture_method = "manual" } }
[pm_filters.trustpay]
credit = { not_available_flows = {capture_method="manual"} }
debit = { not_available_flows = {capture_method="manual"} }
credit = { not_available_flows = { capture_method = "manual" } }
debit = { not_available_flows = { capture_method = "manual" } }
[pm_filters.authorizedotnet]
google_pay = { currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" }
@ -221,15 +221,15 @@ bucket_name = ""
region = ""
[pm_filters.forte]
credit = {currency = "USD"}
debit = {currency = "USD"}
credit = { currency = "USD" }
debit = { currency = "USD" }
[tokenization]
stripe = { long_lived_token = false, payment_method = "wallet" }
checkout = { long_lived_token = false, payment_method = "wallet" }
[connector_customer]
connector_list = "stripe"
connector_list = "bluesnap, stripe"
[dummy_connector]
payment_ttl = 172800

View File

@ -511,7 +511,7 @@ pub enum BankDebitData {
},
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema)]
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum PaymentMethodData {
Card(Card),

View File

@ -71,9 +71,15 @@ where
),
}
}
Ok(api::ApplicationResponse::Form(form_data)) => api::build_redirection_form(&form_data)
.respond_to(request)
.map_into_boxed_body(),
Ok(api::ApplicationResponse::Form(redirection_data)) => api::build_redirection_form(
&redirection_data.redirect_form,
redirection_data.payment_method_data,
redirection_data.amount,
redirection_data.currency,
)
.respond_to(request)
.map_into_boxed_body(),
Err(error) => {
logger::error!(api_response_error=?error);
api::log_and_return_error_response(error)

View File

@ -596,6 +596,7 @@ fn get_error_response(
types::Response {
response,
status_code,
..
}: types::Response,
) -> CustomResult<types::ErrorResponse, errors::ConnectorError> {
let response: authorizedotnet::AuthorizedotnetPaymentsResponse = response

View File

@ -3,21 +3,28 @@ mod transformers;
use std::fmt::Debug;
use base64::Engine;
use common_utils::crypto;
use common_utils::{
crypto,
ext_traits::{StringExt, ValueExt},
};
use error_stack::{IntoReport, ResultExt};
use transformers as bluesnap;
use super::utils::RefundsRequestData;
use super::utils::{self as connector_utils, RefundsRequestData, RouterData};
use crate::{
configs::settings,
consts,
core::errors::{self, CustomResult},
core::{
errors::{self, CustomResult},
payments,
},
db::StorageInterface,
headers, logger,
services::{self, ConnectorIntegration},
types::{
self,
api::{self, ConnectorCommon, ConnectorCommonExt},
storage::enums,
ErrorResponse, Response,
},
utils::{self, BytesExt},
@ -128,6 +135,99 @@ impl ConnectorIntegration<api::Verify, types::VerifyRequestData, types::Payments
{
}
impl api::ConnectorCustomer for Bluesnap {}
impl
ConnectorIntegration<
api::CreateConnectorCustomer,
types::ConnectorCustomerData,
types::PaymentsResponseData,
> for Bluesnap
{
fn get_headers(
&self,
req: &types::ConnectorCustomerRouterData,
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::ConnectorCustomerRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!(
"{}services/2/vaulted-shoppers",
self.base_url(connectors),
))
}
fn get_request_body(
&self,
req: &types::ConnectorCustomerRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let connector_request = bluesnap::BluesnapCustomerRequest::try_from(req)?;
let bluesnap_req =
utils::Encode::<bluesnap::BluesnapCustomerRequest>::encode_to_string_of_json(
&connector_request,
)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(bluesnap_req))
}
fn build_request(
&self,
req: &types::ConnectorCustomerRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::ConnectorCustomerType::get_url(
self, req, connectors,
)?)
.attach_default_headers()
.headers(types::ConnectorCustomerType::get_headers(
self, req, connectors,
)?)
.body(types::ConnectorCustomerType::get_request_body(self, req)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::ConnectorCustomerRouterData,
res: Response,
) -> CustomResult<types::ConnectorCustomerRouterData, errors::ConnectorError>
where
types::PaymentsResponseData: Clone,
{
let response: bluesnap::BluesnapCustomerResponse = res
.response
.parse_struct("BluesnapCustomerResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}
fn get_error_response(
&self,
res: Response,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl api::PaymentVoid for Bluesnap {}
impl ConnectorIntegration<api::Void, types::PaymentsCancelData, types::PaymentsResponseData>
@ -414,14 +514,22 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
fn get_url(
&self,
_req: &types::PaymentsAuthorizeRouterData,
req: &types::PaymentsAuthorizeRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!(
"{}{}",
self.base_url(connectors),
"services/2/transactions"
))
match req.is_three_ds() {
true => Ok(format!(
"{}{}{}",
self.base_url(connectors),
"services/2/payment-fields-tokens?shopperId=",
req.get_connector_customer_id()?
)),
_ => Ok(format!(
"{}{}",
self.base_url(connectors),
"services/2/transactions"
)),
}
}
fn get_request_body(
@ -462,16 +570,126 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
data: &types::PaymentsAuthorizeRouterData,
res: Response,
) -> CustomResult<types::PaymentsAuthorizeRouterData, errors::ConnectorError> {
match (data.is_three_ds(), res.headers) {
(true, Some(headers)) => {
let location = connector_utils::get_http_header("Location", &headers)?;
let payment_fields_token = location
.split('/')
.last()
.ok_or(errors::ConnectorError::ResponseHandlingFailed)?
.to_string();
Ok(types::RouterData {
status: enums::AttemptStatus::AuthenticationPending,
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::NoResponseId,
redirection_data: Some(services::RedirectForm::BlueSnap {
payment_fields_token,
}),
mandate_reference: None,
connector_metadata: None,
network_txn_id: None,
}),
..data.clone()
})
}
_ => {
let response: bluesnap::BluesnapPaymentsResponse = res
.response
.parse_struct("BluesnapPaymentsResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}
}
}
fn get_error_response(
&self,
res: Response,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl api::PaymentsCompleteAuthorize for Bluesnap {}
impl
ConnectorIntegration<
api::CompleteAuthorize,
types::CompleteAuthorizeData,
types::PaymentsResponseData,
> for Bluesnap
{
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> {
Ok(format!(
"{}services/2/transactions",
self.base_url(connectors),
))
}
fn get_request_body(
&self,
req: &types::PaymentsCompleteAuthorizeRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let connector_req = bluesnap::BluesnapPaymentsRequest::try_from(req)?;
let bluesnap_req =
utils::Encode::<bluesnap::BluesnapPaymentsRequest>::encode_to_string_of_json(
&connector_req,
)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(bluesnap_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::PaymentsCompleteAuthorizeType::get_url(
self, req, connectors,
)?)
.attach_default_headers()
.headers(types::PaymentsCompleteAuthorizeType::get_headers(
self, req, connectors,
)?)
.body(types::PaymentsCompleteAuthorizeType::get_request_body(
self, req,
)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::PaymentsCompleteAuthorizeRouterData,
res: Response,
) -> CustomResult<types::PaymentsCompleteAuthorizeRouterData, errors::ConnectorError> {
let response: bluesnap::BluesnapPaymentsResponse = res
.response
.parse_struct("BluesnapPaymentsResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::ResponseRouterData {
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
}
.try_into()
})
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
@ -760,3 +978,31 @@ impl api::IncomingWebhook for Bluesnap {
Ok(res_json)
}
}
impl services::ConnectorRedirectResponse for Bluesnap {
fn get_flow_type(
&self,
_query_params: &str,
json_payload: Option<serde_json::Value>,
_action: services::PaymentAction,
) -> CustomResult<payments::CallConnectorAction, errors::ConnectorError> {
let redirection_response: bluesnap::BluesnapRedirectionResponse = json_payload
.ok_or(errors::ConnectorError::MissingConnectorRedirectionPayload {
field_name: "json_payload",
})?
.parse_value("BluesnapRedirectionResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
let redirection_result: bluesnap::BluesnapThreeDsResult = redirection_response
.authentication_response
.parse_struct("BluesnapThreeDsResult")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
match redirection_result.status.as_str() {
"Success" => Ok(payments::CallConnectorAction::Trigger),
_ => Ok(payments::CallConnectorAction::StatusUpdate(
enums::AttemptStatus::AuthenticationFailed,
)),
}
}
}

View File

@ -1,4 +1,8 @@
use base64::Engine;
use common_utils::{
ext_traits::{StringExt, ValueExt},
pii::Email,
};
use error_stack::ResultExt;
use serde::{Deserialize, Serialize};
@ -19,6 +23,13 @@ pub struct BluesnapPaymentsRequest {
payment_method: PaymentMethodDetails,
currency: enums::Currency,
card_transaction_type: BluesnapTxnType,
three_d_secure: Option<BluesnapThreeDSecureInfo>,
}
#[derive(Debug, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BluesnapThreeDSecureInfo {
three_d_secure_reference_id: String,
}
#[derive(Debug, Serialize, Eq, PartialEq)]
@ -116,10 +127,82 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for BluesnapPaymentsRequest {
payment_method,
currency: item.request.currency,
card_transaction_type: auth_mode,
three_d_secure: None,
})
}
}
impl TryFrom<&types::PaymentsCompleteAuthorizeRouterData> for BluesnapPaymentsRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsCompleteAuthorizeRouterData) -> Result<Self, Self::Error> {
let redirection_response: BluesnapRedirectionResponse = item
.request
.payload
.clone()
.ok_or(errors::ConnectorError::MissingConnectorRedirectionPayload {
field_name: "request.payload",
})?
.parse_value("BluesnapRedirectionResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
let redirection_result: BluesnapThreeDsResult = redirection_response
.authentication_response
.parse_struct("BluesnapThreeDsResult")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
let auth_mode = match item.request.capture_method {
Some(enums::CaptureMethod::Manual) => BluesnapTxnType::AuthOnly,
_ => BluesnapTxnType::AuthCapture,
};
let payment_method = if let Some(api::PaymentMethodData::Card(ccard)) =
item.request.payment_method_data.clone()
{
PaymentMethodDetails::CreditCard(Card {
card_number: ccard.card_number,
expiration_month: ccard.card_exp_month.clone(),
expiration_year: ccard.card_exp_year.clone(),
security_code: ccard.card_cvc,
})
} else {
Err(errors::ConnectorError::MissingConnectorRedirectionPayload {
field_name: "request.payment_method_data",
})?
};
Ok(Self {
amount: utils::to_currency_base_unit(item.request.amount, item.request.currency)?,
payment_method,
currency: item.request.currency,
card_transaction_type: auth_mode,
three_d_secure: Some(BluesnapThreeDSecureInfo {
three_d_secure_reference_id: redirection_result
.three_d_secure
.ok_or(errors::ConnectorError::MissingConnectorRedirectionPayload {
field_name: "three_d_secure_reference_id",
})?
.three_d_secure_reference_id,
}),
})
}
}
#[derive(Debug, Deserialize, PartialEq)]
pub struct BluesnapRedirectionResponse {
pub authentication_response: String,
}
#[derive(Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BluesnapThreeDsResult {
three_d_secure: Option<BluesnapThreeDsReference>,
pub status: String,
}
#[derive(Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BluesnapThreeDsReference {
three_d_secure_reference_id: String,
}
#[derive(Debug, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BluesnapVoidRequest {
@ -181,6 +264,49 @@ impl TryFrom<&types::ConnectorAuthType> for BluesnapAuthType {
}
}
}
#[derive(Debug, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BluesnapCustomerRequest {
email: Option<Email>,
}
impl TryFrom<&types::ConnectorCustomerRouterData> for BluesnapCustomerRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::ConnectorCustomerRouterData) -> Result<Self, Self::Error> {
Ok(Self {
email: item.request.email.to_owned(),
})
}
}
#[derive(Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BluesnapCustomerResponse {
vaulted_shopper_id: u64,
}
impl<F, T>
TryFrom<types::ResponseRouterData<F, BluesnapCustomerResponse, T, types::PaymentsResponseData>>
for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<
F,
BluesnapCustomerResponse,
T,
types::PaymentsResponseData,
>,
) -> Result<Self, Self::Error> {
Ok(Self {
response: Ok(types::PaymentsResponseData::ConnectorCustomerResponse {
connector_customer_id: item.response.vaulted_shopper_id.to_string(),
}),
..item.data
})
}
}
// PaymentsResponse
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]

View File

@ -18,7 +18,7 @@ use crate::{
core::errors::{self, CustomResult},
pii::PeekInterface,
types::{self, api, PaymentsCancelData, ResponseId},
utils::{OptionExt, ValueExt},
utils::{self, OptionExt, ValueExt},
};
pub fn missing_field_err(
@ -63,6 +63,7 @@ pub trait RouterData {
fn is_three_ds(&self) -> bool;
fn get_payment_method_token(&self) -> Result<String, Error>;
fn get_customer_id(&self) -> Result<String, Error>;
fn get_connector_customer_id(&self) -> Result<String, Error>;
}
impl<Flow, Request, Response> RouterData for types::RouterData<Flow, Request, Response> {
@ -151,6 +152,11 @@ impl<Flow, Request, Response> RouterData for types::RouterData<Flow, Request, Re
.to_owned()
.ok_or_else(missing_field_err("customer_id"))
}
fn get_connector_customer_id(&self) -> Result<String, Error> {
self.connector_customer
.to_owned()
.ok_or_else(missing_field_err("connector_customer_id"))
}
}
pub trait PaymentsAuthorizeRequestData {
@ -560,8 +566,20 @@ pub fn get_header_key_value<'a>(
key: &str,
headers: &'a actix_web::http::header::HeaderMap,
) -> CustomResult<&'a str, errors::ConnectorError> {
headers
.get(key)
get_header_field(headers.get(key))
}
pub fn get_http_header<'a>(
key: &str,
headers: &'a http::HeaderMap,
) -> CustomResult<&'a str, errors::ConnectorError> {
get_header_field(headers.get(key))
}
fn get_header_field(
field: Option<&http::HeaderValue>,
) -> CustomResult<&str, errors::ConnectorError> {
field
.map(|header_value| {
header_value
.to_str()
@ -640,27 +658,16 @@ pub fn to_currency_base_unit(
amount: i64,
currency: storage_models::enums::Currency,
) -> Result<String, error_stack::Report<errors::ConnectorError>> {
let amount_f64 = to_currency_base_unit_asf64(amount, currency)?;
Ok(format!("{amount_f64:.2}"))
utils::to_currency_base_unit(amount, currency)
.change_context(errors::ConnectorError::RequestEncodingFailed)
}
pub fn to_currency_base_unit_asf64(
amount: i64,
currency: storage_models::enums::Currency,
) -> Result<f64, error_stack::Report<errors::ConnectorError>> {
let amount_u32 = u32::try_from(amount)
.into_report()
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
let amount_f64 = f64::from(amount_u32);
let amount = match currency {
storage_models::enums::Currency::JPY | storage_models::enums::Currency::KRW => amount_f64,
storage_models::enums::Currency::BHD
| storage_models::enums::Currency::JOD
| storage_models::enums::Currency::KWD
| storage_models::enums::Currency::OMR => amount_f64 / 1000.00,
_ => amount_f64 / 100.00,
};
Ok(amount)
utils::to_currency_base_unit_asf64(amount, currency)
.change_context(errors::ConnectorError::RequestEncodingFailed)
}
pub fn str_to_f32<S>(value: &str, serializer: S) -> Result<S::Ok, S::Error>

View File

@ -299,6 +299,8 @@ pub enum ConnectorError {
MissingConnectorRelatedTransactionID { id: String },
#[error("File Validation failed")]
FileValidationFailed { reason: String },
#[error("Missing 3DS redirection payload: {field_name}")]
MissingConnectorRedirectionPayload { field_name: &'static str },
}
#[derive(Debug, thiserror::Error)]

View File

@ -117,9 +117,16 @@ where
get_connector_tokenization_action(state, &operation, payment_data, &validate_result)
.await?;
let connector_string = connector
.as_ref()
.and_then(|connector_type| match connector_type {
api::ConnectorCallType::Single(connector) => Some(connector.connector_name.to_string()),
_ => None,
});
let updated_customer = call_create_connector_customer(
state,
&payment_data.payment_attempt.connector.clone(),
&connector_string,
&customer,
&merchant_account,
&mut payment_data,

View File

@ -107,7 +107,6 @@ default_imp_for_complete_authorize!(
connector::Aci,
connector::Adyen,
connector::Authorizedotnet,
connector::Bluesnap,
connector::Braintree,
connector::Checkout,
connector::Coinbase,
@ -153,7 +152,6 @@ default_imp_for_create_customer!(
connector::Airwallex,
connector::Authorizedotnet,
connector::Bambora,
connector::Bluesnap,
connector::Braintree,
connector::Checkout,
connector::Coinbase,
@ -203,7 +201,6 @@ default_imp_for_connector_redirect_response!(
connector::Aci,
connector::Adyen,
connector::Authorizedotnet,
connector::Bluesnap,
connector::Braintree,
connector::Coinbase,
connector::Cybersource,

View File

@ -126,7 +126,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsStartRequest> f
mandate_id: None,
connector_response,
setup_mandate: None,
token: None,
token: payment_attempt.payment_token.clone(),
address: PaymentAddress {
shipping: shipping_address.as_ref().map(|a| a.foreign_into()),
billing: billing_address.as_ref().map(|a| a.foreign_into()),

View File

@ -18,7 +18,7 @@ use crate::{
storage::{self, enums},
transformers::{ForeignFrom, ForeignInto},
},
utils::{OptionExt, ValueExt},
utils::{self, OptionExt, ValueExt},
};
#[instrument(skip_all)]
@ -254,8 +254,12 @@ where
let currency = payment_attempt
.currency
.as_ref()
.get_required_value("currency")?
.to_string();
.get_required_value("currency")?;
let amount = utils::to_currency_base_unit(payment_attempt.amount, *currency).change_context(
errors::ApiErrorResponse::InvalidDataValue {
field_name: "amount",
},
)?;
let mandate_id = payment_attempt.mandate_id.clone();
let refunds_response = if refunds.is_empty() {
None
@ -269,7 +273,12 @@ where
let redirection_data = redirection_data.get_required_value("redirection_data")?;
let form: RedirectForm = serde_json::from_value(redirection_data)
.map_err(|_| errors::ApiErrorResponse::InternalServerError)?;
services::ApplicationResponse::Form(form)
services::ApplicationResponse::Form(Box::new(services::RedirectionFormData {
redirect_form: form,
payment_method_data,
amount,
currency: currency.to_string(),
}))
} else {
let mut next_action_response = None;
if payment_intent.status == enums::IntentStatus::RequiresCustomerAction {
@ -317,7 +326,7 @@ where
.set_connector(routed_through)
.set_client_secret(payment_intent.client_secret.map(masking::Secret::new))
.set_created(Some(payment_intent.created_at))
.set_currency(currency)
.set_currency(currency.to_string())
.set_customer_id(customer.as_ref().map(|cus| cus.clone().customer_id))
.set_email(
customer
@ -404,7 +413,7 @@ where
amount_received: payment_intent.amount_captured,
client_secret: payment_intent.client_secret.map(masking::Secret::new),
created: Some(payment_intent.created_at),
currency,
currency: currency.to_string(),
customer_id: payment_intent.customer_id,
description: payment_intent.description,
refunds: refunds_response,

View File

@ -12,7 +12,7 @@ use std::{
use actix_web::{body, HttpRequest, HttpResponse, Responder};
use common_utils::errors::ReportSwitchExt;
use error_stack::{report, IntoReport, Report, ResultExt};
use masking::{ExposeOptionInterface, PeekInterface};
use masking::{ExposeInterface, ExposeOptionInterface, PeekInterface};
use router_env::{instrument, tracing, Tag};
use serde::Serialize;
use serde_json::json;
@ -182,6 +182,7 @@ where
match call_connector_action {
payments::CallConnectorAction::HandleResponse(res) => {
let response = types::Response {
headers: None,
response: res.into(),
status_code: 200,
};
@ -377,6 +378,7 @@ async fn handle_response(
.map(|response| async {
logger::info!(?response);
let status_code = response.status().as_u16();
let headers = Some(response.headers().to_owned());
match status_code {
200..=202 | 302 | 204 => {
logger::debug!(response=?response);
@ -389,6 +391,7 @@ async fn handle_response(
.change_context(errors::ApiClientError::ResponseDecodingFailed)
.attach_printable("Error while waiting for response")?;
Ok(Ok(types::Response {
headers,
response,
status_code,
}))
@ -408,6 +411,7 @@ async fn handle_response(
// _ => errors::ApiClientError::UnexpectedServerResponse,
// };
Ok(Err(types::Response {
headers,
response: bytes,
status_code,
}))
@ -433,6 +437,7 @@ async fn handle_response(
Err(report!(error).attach_printable("Client error response received"))
*/
Ok(Err(types::Response {
headers,
response: bytes,
status_code,
}))
@ -451,10 +456,18 @@ pub enum ApplicationResponse<R> {
StatusOk,
TextPlain(String),
JsonForRedirection(api::RedirectionResponse),
Form(RedirectForm),
Form(Box<RedirectionFormData>),
FileData((Vec<u8>, mime::Mime)),
}
#[derive(Debug, Eq, PartialEq)]
pub struct RedirectionFormData {
pub redirect_form: RedirectForm,
pub payment_method_data: Option<api::PaymentMethodData>,
pub amount: String,
pub currency: String,
}
#[derive(Debug, Eq, PartialEq)]
pub enum PaymentAction {
PSync,
@ -476,6 +489,9 @@ pub enum RedirectForm {
Html {
html_data: String,
},
BlueSnap {
payment_fields_token: String, // payment-field-token
},
}
impl From<(url::Url, Method)> for RedirectForm {
@ -588,10 +604,14 @@ where
),
}
}
Ok(ApplicationResponse::Form(response)) => build_redirection_form(&response)
.respond_to(request)
.map_into_boxed_body(),
Ok(ApplicationResponse::Form(redirection_data)) => build_redirection_form(
&redirection_data.redirect_form,
redirection_data.payment_method_data,
redirection_data.amount,
redirection_data.currency,
)
.respond_to(request)
.map_into_boxed_body(),
Err(error) => log_and_return_error_response(error),
};
@ -696,7 +716,12 @@ impl Authenticate for api_models::payments::PaymentsCancelRequest {}
impl Authenticate for api_models::payments::PaymentsCaptureRequest {}
impl Authenticate for api_models::payments::PaymentsStartRequest {}
pub fn build_redirection_form(form: &RedirectForm) -> maud::Markup {
pub fn build_redirection_form(
form: &RedirectForm,
payment_method_data: Option<api_models::payments::PaymentMethodData>,
amount: String,
currency: String,
) -> maud::Markup {
use maud::PreEscaped;
match form {
@ -760,6 +785,75 @@ pub fn build_redirection_form(form: &RedirectForm) -> maud::Markup {
}
},
RedirectForm::Html { html_data } => PreEscaped(html_data.to_string()),
RedirectForm::BlueSnap {
payment_fields_token,
} => {
let card_details = if let Some(api::PaymentMethodData::Card(ccard)) =
payment_method_data
{
format!(
"var newCard={{ccNumber: \"{}\",cvv: \"{}\",expDate: \"{}/{}\",amount: {},currency: \"{}\"}};",
ccard.card_number.expose(),
ccard.card_cvc.expose(),
ccard.card_exp_month.clone().expose(),
ccard.card_exp_year.expose(),
amount,
currency
)
} else {
"".to_string()
};
maud::html! {
(maud::DOCTYPE)
html {
head {
meta name="viewport" content="width=device-width, initial-scale=1";
(PreEscaped(r#"<script src="https://sandpay.bluesnap.com/web-sdk/5/bluesnap.js"></script>"#))
}
body style="background-color: #ffffff; padding: 20px; font-family: Arial, Helvetica, Sans-Serif;" {
div id="loader1" class="lottie" style="height: 150px; display: block; position: relative; margin-top: 150px; margin-left: auto; margin-right: auto;" { "" }
(PreEscaped(r#"<script src="https://cdnjs.cloudflare.com/ajax/libs/bodymovin/5.7.4/lottie.min.js"></script>"#))
(PreEscaped(r#"
<script>
var anime = bodymovin.loadAnimation({
container: document.getElementById('loader1'),
renderer: 'svg',
loop: true,
autoplay: true,
name: 'hyperswitch loader',
animationData: {"v":"4.8.0","meta":{"g":"LottieFiles AE 3.1.1","a":"","k":"","d":"","tc":""},"fr":29.9700012207031,"ip":0,"op":31.0000012626559,"w":400,"h":250,"nm":"loader_shape","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"circle 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[278.25,202.671,0],"ix":2},"a":{"a":0,"k":[23.72,23.72,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[12.935,0],[0,-12.936],[-12.935,0],[0,12.935]],"o":[[-12.952,0],[0,12.935],[12.935,0],[0,-12.936]],"v":[[0,-23.471],[-23.47,0.001],[0,23.471],[23.47,0.001]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.427451010311,0.976470648074,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[10]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":19.99,"s":[100]},{"t":29.9800012211104,"s":[10]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.72,23.721],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48.0000019550801,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"square 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[196.25,201.271,0],"ix":2},"a":{"a":0,"k":[22.028,22.03,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.914,0],[0,0],[0,-1.914],[0,0],[-1.914,0],[0,0],[0,1.914],[0,0]],"o":[[0,0],[-1.914,0],[0,0],[0,1.914],[0,0],[1.914,0],[0,0],[0,-1.914]],"v":[[18.313,-21.779],[-18.312,-21.779],[-21.779,-18.313],[-21.779,18.314],[-18.312,21.779],[18.313,21.779],[21.779,18.314],[21.779,-18.313]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.427451010311,0.976470648074,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":5,"s":[10]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":14.99,"s":[100]},{"t":24.9800010174563,"s":[10]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[22.028,22.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":47.0000019143492,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Triangle 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[116.25,200.703,0],"ix":2},"a":{"a":0,"k":[27.11,21.243,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.558,-0.879],[0,0],[-1.133,0],[0,0],[0.609,0.947],[0,0]],"o":[[-0.558,-0.879],[0,0],[-0.609,0.947],[0,0],[1.133,0],[0,0],[0,0]],"v":[[1.209,-20.114],[-1.192,-20.114],[-26.251,18.795],[-25.051,20.993],[25.051,20.993],[26.251,18.795],[1.192,-20.114]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.427451010311,0.976470648074,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[10]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":9.99,"s":[100]},{"t":19.9800008138021,"s":[10]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[27.11,21.243],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48.0000019550801,"st":0,"bm":0}],"markers":[]}
})
</script>
"#))
h3 style="text-align: center;" { "Please wait while we process your payment..." }
}
(PreEscaped(format!("<script>
bluesnap.threeDsPaymentsSetup(\"{payment_fields_token}\",
function(sdkResponse) {{
console.log(sdkResponse);
var f = document.createElement('form');
f.action=window.location.pathname.replace(/payments\\/redirect\\/(\\w+)\\/(\\w+)\\/\\w+/, \"payments/$1/$2/redirect/complete/bluesnap\");
f.method='POST';
var i=document.createElement('input');
i.type='hidden';
i.name='authentication_response';
i.value=JSON.stringify(sdkResponse);
f.appendChild(i);
document.body.appendChild(f);
f.submit();
}});
{card_details}
bluesnap.threeDsPaymentsSubmitData(newCard);
</script>
")))
}}
}
}
}

View File

@ -555,6 +555,7 @@ pub struct ConnectorsList {
#[derive(Clone, Debug)]
pub struct Response {
pub headers: Option<http::HeaderMap>,
pub response: bytes::Bytes,
pub status_code: u16,
}

View File

@ -119,3 +119,34 @@ impl<E> ConnectorResponseExt
})
}
}
/// Convert the amount to its base denomination based on Currency and return String
pub fn to_currency_base_unit(
amount: i64,
currency: storage_models::enums::Currency,
) -> Result<String, error_stack::Report<errors::ValidationError>> {
let amount_f64 = to_currency_base_unit_asf64(amount, currency)?;
Ok(format!("{amount_f64:.2}"))
}
/// Convert the amount to its base denomination based on Currency and return f64
pub fn to_currency_base_unit_asf64(
amount: i64,
currency: storage_models::enums::Currency,
) -> Result<f64, error_stack::Report<errors::ValidationError>> {
let amount_u32 = u32::try_from(amount).into_report().change_context(
errors::ValidationError::InvalidValue {
message: amount.to_string(),
},
)?;
let amount_f64 = f64::from(amount_u32);
let amount = match currency {
storage_models::enums::Currency::JPY | storage_models::enums::Currency::KRW => amount_f64,
storage_models::enums::Currency::BHD
| storage_models::enums::Currency::JOD
| storage_models::enums::Currency::KWD
| storage_models::enums::Currency::OMR => amount_f64 / 1000.00,
_ => amount_f64 / 100.00,
};
Ok(amount)
}