diff --git a/config/development.toml b/config/development.toml index 4ecd69879c..ad7ab4b416 100644 --- a/config/development.toml +++ b/config/development.toml @@ -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 diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index a8c69d04e5..2a12439846 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -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), diff --git a/crates/router/src/compatibility/wrap.rs b/crates/router/src/compatibility/wrap.rs index 3ed5f8f67a..05246d3bdc 100644 --- a/crates/router/src/compatibility/wrap.rs +++ b/crates/router/src/compatibility/wrap.rs @@ -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) diff --git a/crates/router/src/connector/authorizedotnet.rs b/crates/router/src/connector/authorizedotnet.rs index bf732635c1..3e44328677 100644 --- a/crates/router/src/connector/authorizedotnet.rs +++ b/crates/router/src/connector/authorizedotnet.rs @@ -596,6 +596,7 @@ fn get_error_response( types::Response { response, status_code, + .. }: types::Response, ) -> CustomResult { let response: authorizedotnet::AuthorizedotnetPaymentsResponse = response diff --git a/crates/router/src/connector/bluesnap.rs b/crates/router/src/connector/bluesnap.rs index 8d87979300..abe5fe734e 100644 --- a/crates/router/src/connector/bluesnap.rs +++ b/crates/router/src/connector/bluesnap.rs @@ -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 for Bluesnap +{ + fn get_headers( + &self, + req: &types::ConnectorCustomerRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, 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 { + Ok(format!( + "{}services/2/vaulted-shoppers", + self.base_url(connectors), + )) + } + + fn get_request_body( + &self, + req: &types::ConnectorCustomerRouterData, + ) -> CustomResult, errors::ConnectorError> { + let connector_request = bluesnap::BluesnapCustomerRequest::try_from(req)?; + let bluesnap_req = + utils::Encode::::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, 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 + 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 { + self.build_error_response(res) + } +} + impl api::PaymentVoid for Bluesnap {} impl ConnectorIntegration @@ -414,14 +514,22 @@ impl ConnectorIntegration CustomResult { - 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 CustomResult { + 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 { + 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, 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 { + Ok(format!( + "{}services/2/transactions", + self.base_url(connectors), + )) + } + fn get_request_body( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + ) -> CustomResult, errors::ConnectorError> { + let connector_req = bluesnap::BluesnapPaymentsRequest::try_from(req)?; + let bluesnap_req = + utils::Encode::::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, 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 { 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, + _action: services::PaymentAction, + ) -> CustomResult { + 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, + )), + } + } +} diff --git a/crates/router/src/connector/bluesnap/transformers.rs b/crates/router/src/connector/bluesnap/transformers.rs index c2f851ce61..aef423be6e 100644 --- a/crates/router/src/connector/bluesnap/transformers.rs +++ b/crates/router/src/connector/bluesnap/transformers.rs @@ -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, +} + +#[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; + fn try_from(item: &types::PaymentsCompleteAuthorizeRouterData) -> Result { + 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, + 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, +} + +impl TryFrom<&types::ConnectorCustomerRouterData> for BluesnapCustomerRequest { + type Error = error_stack::Report; + fn try_from(item: &types::ConnectorCustomerRouterData) -> Result { + Ok(Self { + email: item.request.email.to_owned(), + }) + } +} + +#[derive(Debug, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BluesnapCustomerResponse { + vaulted_shopper_id: u64, +} +impl + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + BluesnapCustomerResponse, + T, + types::PaymentsResponseData, + >, + ) -> Result { + 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")] diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index ec1d76ffd6..d47bb0cc07 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -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; fn get_customer_id(&self) -> Result; + fn get_connector_customer_id(&self) -> Result; } impl RouterData for types::RouterData { @@ -151,6 +152,11 @@ impl RouterData for types::RouterData Result { + 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> { - 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> { - 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(value: &str, serializer: S) -> Result diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index ce71849c09..eb06a09400 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -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)] diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index a292e4503f..a03725b45f 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -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, diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 27b69dcffe..03d578daea 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -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, diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index 5a4fd81bb2..40c155c147 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -126,7 +126,7 @@ impl GetTracker, 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()), diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 53948e7af0..aad3b12079 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -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, diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index c0c30e9b88..ff2d10ddd4 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -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 { StatusOk, TextPlain(String), JsonForRedirection(api::RedirectionResponse), - Form(RedirectForm), + Form(Box), FileData((Vec, mime::Mime)), } +#[derive(Debug, Eq, PartialEq)] +pub struct RedirectionFormData { + pub redirect_form: RedirectForm, + pub payment_method_data: Option, + 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, + 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#""#)) + } + 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#""#)) + + (PreEscaped(r#" + + "#)) + + + h3 style="text-align: center;" { "Please wait while we process your payment..." } + } + + (PreEscaped(format!(" + "))) + }} + } } } diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index d67dd9f7b4..53c84fc3a2 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -555,6 +555,7 @@ pub struct ConnectorsList { #[derive(Clone, Debug)] pub struct Response { + pub headers: Option, pub response: bytes::Bytes, pub status_code: u16, } diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index 3104bc65ce..4e3615d5f6 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -119,3 +119,34 @@ impl 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> { + 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> { + 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) +}