mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-28 04:04:55 +08:00
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:
@ -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
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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")]
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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()),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
")))
|
||||
}}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user