mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-01 19:42:27 +08:00
feat(connector): [NMI] Implement 3DS for Cards (#3143)
Co-authored-by: Arjun Karthik <m.arjunkarthik@gmail.com> Co-authored-by: Gnanasundari24 <118818938+Gnanasundari24@users.noreply.github.com>
This commit is contained in:
@ -187,6 +187,90 @@ impl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl api::PaymentsPreProcessing for Nmi {}
|
||||||
|
|
||||||
|
impl
|
||||||
|
ConnectorIntegration<
|
||||||
|
api::PreProcessing,
|
||||||
|
types::PaymentsPreProcessingData,
|
||||||
|
types::PaymentsResponseData,
|
||||||
|
> for Nmi
|
||||||
|
{
|
||||||
|
fn get_headers(
|
||||||
|
&self,
|
||||||
|
req: &types::PaymentsPreProcessingRouterData,
|
||||||
|
connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<Vec<(String, request::Maskable<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::PaymentsPreProcessingRouterData,
|
||||||
|
connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<String, errors::ConnectorError> {
|
||||||
|
Ok(format!("{}api/transact.php", self.base_url(connectors)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_request_body(
|
||||||
|
&self,
|
||||||
|
req: &types::PaymentsPreProcessingRouterData,
|
||||||
|
_connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<RequestContent, errors::ConnectorError> {
|
||||||
|
let connector_req = nmi::NmiVaultRequest::try_from(req)?;
|
||||||
|
Ok(RequestContent::FormUrlEncoded(Box::new(connector_req)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_request(
|
||||||
|
&self,
|
||||||
|
req: &types::PaymentsPreProcessingRouterData,
|
||||||
|
connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
||||||
|
let req = Some(
|
||||||
|
services::RequestBuilder::new()
|
||||||
|
.method(services::Method::Post)
|
||||||
|
.attach_default_headers()
|
||||||
|
.headers(types::PaymentsPreProcessingType::get_headers(
|
||||||
|
self, req, connectors,
|
||||||
|
)?)
|
||||||
|
.url(&types::PaymentsPreProcessingType::get_url(
|
||||||
|
self, req, connectors,
|
||||||
|
)?)
|
||||||
|
.set_body(types::PaymentsPreProcessingType::get_request_body(
|
||||||
|
self, req, connectors,
|
||||||
|
)?)
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
Ok(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_response(
|
||||||
|
&self,
|
||||||
|
data: &types::PaymentsPreProcessingRouterData,
|
||||||
|
res: types::Response,
|
||||||
|
) -> CustomResult<types::PaymentsPreProcessingRouterData, errors::ConnectorError> {
|
||||||
|
let response: nmi::NmiVaultResponse = serde_urlencoded::from_bytes(&res.response)
|
||||||
|
.into_report()
|
||||||
|
.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: types::Response,
|
||||||
|
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||||
|
self.build_error_response(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::PaymentsResponseData>
|
impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::PaymentsResponseData>
|
||||||
for Nmi
|
for Nmi
|
||||||
{
|
{
|
||||||
@ -265,6 +349,91 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl api::PaymentsCompleteAuthorize for Nmi {}
|
||||||
|
|
||||||
|
impl
|
||||||
|
ConnectorIntegration<
|
||||||
|
api::CompleteAuthorize,
|
||||||
|
types::CompleteAuthorizeData,
|
||||||
|
types::PaymentsResponseData,
|
||||||
|
> for Nmi
|
||||||
|
{
|
||||||
|
fn get_headers(
|
||||||
|
&self,
|
||||||
|
req: &types::PaymentsCompleteAuthorizeRouterData,
|
||||||
|
connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<Vec<(String, request::Maskable<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!("{}api/transact.php", self.base_url(connectors)))
|
||||||
|
}
|
||||||
|
fn get_request_body(
|
||||||
|
&self,
|
||||||
|
req: &types::PaymentsCompleteAuthorizeRouterData,
|
||||||
|
_connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<RequestContent, errors::ConnectorError> {
|
||||||
|
let connector_router_data = nmi::NmiRouterData::try_from((
|
||||||
|
&self.get_currency_unit(),
|
||||||
|
req.request.currency,
|
||||||
|
req.request.amount,
|
||||||
|
req,
|
||||||
|
))?;
|
||||||
|
let connector_req = nmi::NmiCompleteRequest::try_from(&connector_router_data)?;
|
||||||
|
Ok(RequestContent::FormUrlEncoded(Box::new(connector_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,
|
||||||
|
)?)
|
||||||
|
.set_body(types::PaymentsCompleteAuthorizeType::get_request_body(
|
||||||
|
self, req, connectors,
|
||||||
|
)?)
|
||||||
|
.build(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_response(
|
||||||
|
&self,
|
||||||
|
data: &types::PaymentsCompleteAuthorizeRouterData,
|
||||||
|
res: types::Response,
|
||||||
|
) -> CustomResult<types::PaymentsCompleteAuthorizeRouterData, errors::ConnectorError> {
|
||||||
|
let response: nmi::NmiCompleteResponse = serde_urlencoded::from_bytes(&res.response)
|
||||||
|
.into_report()
|
||||||
|
.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: types::Response,
|
||||||
|
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||||
|
self.build_error_response(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
|
impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
|
||||||
for Nmi
|
for Nmi
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
use cards::CardNumber;
|
use cards::CardNumber;
|
||||||
use common_utils::ext_traits::XmlExt;
|
use common_utils::{errors::CustomResult, ext_traits::XmlExt};
|
||||||
use error_stack::{IntoReport, Report, ResultExt};
|
use error_stack::{IntoReport, Report, ResultExt};
|
||||||
use masking::Secret;
|
use masking::{ExposeInterface, Secret};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
connector::utils::{self, PaymentsAuthorizeRequestData},
|
connector::utils::{self, PaymentsAuthorizeRequestData, PaymentsCompleteAuthorizeRequestData},
|
||||||
core::errors,
|
core::errors,
|
||||||
|
services,
|
||||||
types::{self, api, storage::enums, transformers::ForeignFrom, ConnectorAuthType},
|
types::{self, api, storage::enums, transformers::ForeignFrom, ConnectorAuthType},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -25,17 +26,22 @@ pub enum TransactionType {
|
|||||||
|
|
||||||
pub struct NmiAuthType {
|
pub struct NmiAuthType {
|
||||||
pub(super) api_key: Secret<String>,
|
pub(super) api_key: Secret<String>,
|
||||||
|
pub(super) public_key: Option<Secret<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&ConnectorAuthType> for NmiAuthType {
|
impl TryFrom<&ConnectorAuthType> for NmiAuthType {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
fn try_from(auth_type: &ConnectorAuthType) -> Result<Self, Self::Error> {
|
fn try_from(auth_type: &ConnectorAuthType) -> Result<Self, Self::Error> {
|
||||||
if let types::ConnectorAuthType::HeaderKey { api_key } = auth_type {
|
match auth_type {
|
||||||
Ok(Self {
|
types::ConnectorAuthType::HeaderKey { api_key } => Ok(Self {
|
||||||
api_key: api_key.to_owned(),
|
api_key: api_key.to_owned(),
|
||||||
})
|
public_key: None,
|
||||||
} else {
|
}),
|
||||||
Err(errors::ConnectorError::FailedToObtainAuthType.into())
|
types::ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self {
|
||||||
|
api_key: api_key.to_owned(),
|
||||||
|
public_key: Some(key1.to_owned()),
|
||||||
|
}),
|
||||||
|
_ => Err(errors::ConnectorError::FailedToObtainAuthType.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -71,6 +77,291 @@ impl<T>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct NmiVaultRequest {
|
||||||
|
security_key: Secret<String>,
|
||||||
|
ccnumber: CardNumber,
|
||||||
|
ccexp: Secret<String>,
|
||||||
|
customer_vault: CustomerAction,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum CustomerAction {
|
||||||
|
AddCustomer,
|
||||||
|
UpdateCustomer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&types::PaymentsPreProcessingRouterData> for NmiVaultRequest {
|
||||||
|
type Error = Error;
|
||||||
|
fn try_from(item: &types::PaymentsPreProcessingRouterData) -> Result<Self, Self::Error> {
|
||||||
|
let auth_type: NmiAuthType = (&item.connector_auth_type).try_into()?;
|
||||||
|
let (ccnumber, ccexp) = get_card_details(item.request.payment_method_data.clone())?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
security_key: auth_type.api_key,
|
||||||
|
ccnumber,
|
||||||
|
ccexp,
|
||||||
|
customer_vault: CustomerAction::AddCustomer,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_card_details(
|
||||||
|
payment_method_data: Option<api::PaymentMethodData>,
|
||||||
|
) -> CustomResult<(CardNumber, Secret<String>), errors::ConnectorError> {
|
||||||
|
match payment_method_data {
|
||||||
|
Some(api::PaymentMethodData::Card(ref card_details)) => Ok((
|
||||||
|
card_details.card_number.clone(),
|
||||||
|
utils::CardData::get_card_expiry_month_year_2_digit_with_delimiter(
|
||||||
|
card_details,
|
||||||
|
"".to_string(),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
_ => Err(errors::ConnectorError::NotImplemented(
|
||||||
|
utils::get_unimplemented_payment_method_error_message("Nmi"),
|
||||||
|
))
|
||||||
|
.into_report(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct NmiVaultResponse {
|
||||||
|
pub response: Response,
|
||||||
|
pub responsetext: String,
|
||||||
|
pub customer_vault_id: String,
|
||||||
|
pub response_code: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl
|
||||||
|
TryFrom<
|
||||||
|
types::ResponseRouterData<
|
||||||
|
api::PreProcessing,
|
||||||
|
NmiVaultResponse,
|
||||||
|
types::PaymentsPreProcessingData,
|
||||||
|
types::PaymentsResponseData,
|
||||||
|
>,
|
||||||
|
> for types::PaymentsPreProcessingRouterData
|
||||||
|
{
|
||||||
|
type Error = Error;
|
||||||
|
fn try_from(
|
||||||
|
item: types::ResponseRouterData<
|
||||||
|
api::PreProcessing,
|
||||||
|
NmiVaultResponse,
|
||||||
|
types::PaymentsPreProcessingData,
|
||||||
|
types::PaymentsResponseData,
|
||||||
|
>,
|
||||||
|
) -> Result<Self, Self::Error> {
|
||||||
|
let auth_type: NmiAuthType = (&item.data.connector_auth_type).try_into()?;
|
||||||
|
let amount_data =
|
||||||
|
item.data
|
||||||
|
.request
|
||||||
|
.amount
|
||||||
|
.ok_or(errors::ConnectorError::MissingRequiredField {
|
||||||
|
field_name: "amount",
|
||||||
|
})?;
|
||||||
|
let currency_data =
|
||||||
|
item.data
|
||||||
|
.request
|
||||||
|
.currency
|
||||||
|
.ok_or(errors::ConnectorError::MissingRequiredField {
|
||||||
|
field_name: "currency",
|
||||||
|
})?;
|
||||||
|
let (response, status) = match item.response.response {
|
||||||
|
Response::Approved => (
|
||||||
|
Ok(types::PaymentsResponseData::TransactionResponse {
|
||||||
|
resource_id: types::ResponseId::NoResponseId,
|
||||||
|
redirection_data: Some(services::RedirectForm::Nmi {
|
||||||
|
amount: utils::to_currency_base_unit_asf64(
|
||||||
|
amount_data,
|
||||||
|
currency_data.to_owned(),
|
||||||
|
)?
|
||||||
|
.to_string(),
|
||||||
|
currency: currency_data,
|
||||||
|
customer_vault_id: item.response.customer_vault_id,
|
||||||
|
public_key: auth_type.public_key.ok_or(
|
||||||
|
errors::ConnectorError::InvalidConnectorConfig {
|
||||||
|
config: "public_key",
|
||||||
|
},
|
||||||
|
)?,
|
||||||
|
}),
|
||||||
|
mandate_reference: None,
|
||||||
|
connector_metadata: None,
|
||||||
|
network_txn_id: None,
|
||||||
|
connector_response_reference_id: None,
|
||||||
|
incremental_authorization_allowed: None,
|
||||||
|
}),
|
||||||
|
enums::AttemptStatus::AuthenticationPending,
|
||||||
|
),
|
||||||
|
Response::Declined | Response::Error => (
|
||||||
|
Err(types::ErrorResponse {
|
||||||
|
code: item.response.response_code,
|
||||||
|
message: item.response.responsetext,
|
||||||
|
reason: None,
|
||||||
|
status_code: item.http_code,
|
||||||
|
attempt_status: None,
|
||||||
|
connector_transaction_id: None,
|
||||||
|
}),
|
||||||
|
enums::AttemptStatus::Failure,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
Ok(Self {
|
||||||
|
status,
|
||||||
|
response,
|
||||||
|
..item.data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct NmiCompleteRequest {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
transaction_type: TransactionType,
|
||||||
|
security_key: Secret<String>,
|
||||||
|
cardholder_auth: CardHolderAuthType,
|
||||||
|
cavv: String,
|
||||||
|
xid: String,
|
||||||
|
three_ds_version: ThreeDsVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum CardHolderAuthType {
|
||||||
|
Verified,
|
||||||
|
Attempted,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub enum ThreeDsVersion {
|
||||||
|
#[serde(rename = "2.0.0")]
|
||||||
|
VersionTwo,
|
||||||
|
#[serde(rename = "2.2.0")]
|
||||||
|
VersionTwoPointTwo,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct NmiRedirectResponseData {
|
||||||
|
cavv: String,
|
||||||
|
xid: String,
|
||||||
|
card_holder_auth: CardHolderAuthType,
|
||||||
|
three_ds_version: ThreeDsVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&NmiRouterData<&types::PaymentsCompleteAuthorizeRouterData>> for NmiCompleteRequest {
|
||||||
|
type Error = Error;
|
||||||
|
fn try_from(
|
||||||
|
item: &NmiRouterData<&types::PaymentsCompleteAuthorizeRouterData>,
|
||||||
|
) -> Result<Self, Self::Error> {
|
||||||
|
let transaction_type = match item.router_data.request.is_auto_capture()? {
|
||||||
|
true => TransactionType::Sale,
|
||||||
|
false => TransactionType::Auth,
|
||||||
|
};
|
||||||
|
let auth_type: NmiAuthType = (&item.router_data.connector_auth_type).try_into()?;
|
||||||
|
let payload_data = item
|
||||||
|
.router_data
|
||||||
|
.request
|
||||||
|
.get_redirect_response_payload()?
|
||||||
|
.expose();
|
||||||
|
|
||||||
|
let three_ds_data: NmiRedirectResponseData = serde_json::from_value(payload_data)
|
||||||
|
.into_report()
|
||||||
|
.change_context(errors::ConnectorError::MissingConnectorRedirectionPayload {
|
||||||
|
field_name: "three_ds_data",
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
transaction_type,
|
||||||
|
security_key: auth_type.api_key,
|
||||||
|
cardholder_auth: three_ds_data.card_holder_auth,
|
||||||
|
cavv: three_ds_data.cavv,
|
||||||
|
xid: three_ds_data.xid,
|
||||||
|
three_ds_version: three_ds_data.three_ds_version,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct NmiCompleteResponse {
|
||||||
|
pub response: Response,
|
||||||
|
pub responsetext: String,
|
||||||
|
pub authcode: Option<String>,
|
||||||
|
pub transactionid: String,
|
||||||
|
pub avsresponse: Option<String>,
|
||||||
|
pub cvvresponse: Option<String>,
|
||||||
|
pub orderid: String,
|
||||||
|
pub response_code: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl
|
||||||
|
TryFrom<
|
||||||
|
types::ResponseRouterData<
|
||||||
|
api::CompleteAuthorize,
|
||||||
|
NmiCompleteResponse,
|
||||||
|
types::CompleteAuthorizeData,
|
||||||
|
types::PaymentsResponseData,
|
||||||
|
>,
|
||||||
|
> for types::PaymentsCompleteAuthorizeRouterData
|
||||||
|
{
|
||||||
|
type Error = Error;
|
||||||
|
fn try_from(
|
||||||
|
item: types::ResponseRouterData<
|
||||||
|
api::CompleteAuthorize,
|
||||||
|
NmiCompleteResponse,
|
||||||
|
types::CompleteAuthorizeData,
|
||||||
|
types::PaymentsResponseData,
|
||||||
|
>,
|
||||||
|
) -> Result<Self, Self::Error> {
|
||||||
|
let (response, status) = match item.response.response {
|
||||||
|
Response::Approved => (
|
||||||
|
Ok(types::PaymentsResponseData::TransactionResponse {
|
||||||
|
resource_id: types::ResponseId::ConnectorTransactionId(
|
||||||
|
item.response.transactionid,
|
||||||
|
),
|
||||||
|
redirection_data: None,
|
||||||
|
mandate_reference: None,
|
||||||
|
connector_metadata: None,
|
||||||
|
network_txn_id: None,
|
||||||
|
connector_response_reference_id: None,
|
||||||
|
incremental_authorization_allowed: None,
|
||||||
|
}),
|
||||||
|
if let Some(diesel_models::enums::CaptureMethod::Automatic) =
|
||||||
|
item.data.request.capture_method
|
||||||
|
{
|
||||||
|
enums::AttemptStatus::CaptureInitiated
|
||||||
|
} else {
|
||||||
|
enums::AttemptStatus::Authorizing
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Response::Declined | Response::Error => (
|
||||||
|
Err(types::ErrorResponse::foreign_from((
|
||||||
|
item.response,
|
||||||
|
item.http_code,
|
||||||
|
))),
|
||||||
|
enums::AttemptStatus::Failure,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
Ok(Self {
|
||||||
|
status,
|
||||||
|
response,
|
||||||
|
..item.data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ForeignFrom<(NmiCompleteResponse, u16)> for types::ErrorResponse {
|
||||||
|
fn foreign_from((response, http_code): (NmiCompleteResponse, u16)) -> Self {
|
||||||
|
Self {
|
||||||
|
code: response.response_code,
|
||||||
|
message: response.responsetext,
|
||||||
|
reason: None,
|
||||||
|
status_code: http_code,
|
||||||
|
attempt_status: None,
|
||||||
|
connector_transaction_id: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct NmiPaymentsRequest {
|
pub struct NmiPaymentsRequest {
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
||||||
|
|||||||
@ -1479,6 +1479,13 @@ where
|
|||||||
let is_error_in_response = router_data.response.is_err();
|
let is_error_in_response = router_data.response.is_err();
|
||||||
// If is_error_in_response is true, should_continue_payment should be false, we should throw the error
|
// If is_error_in_response is true, should_continue_payment should be false, we should throw the error
|
||||||
(router_data, !is_error_in_response)
|
(router_data, !is_error_in_response)
|
||||||
|
} else if connector.connector_name == router_types::Connector::Nmi
|
||||||
|
&& !matches!(format!("{operation:?}").as_str(), "CompleteAuthorize")
|
||||||
|
&& router_data.auth_type == storage_enums::AuthenticationType::ThreeDs
|
||||||
|
{
|
||||||
|
router_data = router_data.preprocessing_steps(state, connector).await?;
|
||||||
|
|
||||||
|
(router_data, false)
|
||||||
} else {
|
} else {
|
||||||
(router_data, should_continue_payment)
|
(router_data, should_continue_payment)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -165,7 +165,6 @@ default_imp_for_complete_authorize!(
|
|||||||
connector::Klarna,
|
connector::Klarna,
|
||||||
connector::Multisafepay,
|
connector::Multisafepay,
|
||||||
connector::Nexinets,
|
connector::Nexinets,
|
||||||
connector::Nmi,
|
|
||||||
connector::Noon,
|
connector::Noon,
|
||||||
connector::Opayo,
|
connector::Opayo,
|
||||||
connector::Opennode,
|
connector::Opennode,
|
||||||
@ -886,7 +885,6 @@ default_imp_for_pre_processing_steps!(
|
|||||||
connector::Mollie,
|
connector::Mollie,
|
||||||
connector::Multisafepay,
|
connector::Multisafepay,
|
||||||
connector::Nexinets,
|
connector::Nexinets,
|
||||||
connector::Nmi,
|
|
||||||
connector::Noon,
|
connector::Noon,
|
||||||
connector::Nuvei,
|
connector::Nuvei,
|
||||||
connector::Opayo,
|
connector::Opayo,
|
||||||
|
|||||||
@ -12,6 +12,7 @@ use std::{
|
|||||||
use actix_web::{body, web, FromRequest, HttpRequest, HttpResponse, Responder, ResponseError};
|
use actix_web::{body, web, FromRequest, HttpRequest, HttpResponse, Responder, ResponseError};
|
||||||
use api_models::enums::CaptureMethod;
|
use api_models::enums::CaptureMethod;
|
||||||
pub use client::{proxy_bypass_urls, ApiClient, MockApiClient, ProxyClient};
|
pub use client::{proxy_bypass_urls, ApiClient, MockApiClient, ProxyClient};
|
||||||
|
use common_enums::Currency;
|
||||||
pub use common_utils::request::{ContentType, Method, Request, RequestBuilder};
|
pub use common_utils::request::{ContentType, Method, Request, RequestBuilder};
|
||||||
use common_utils::{
|
use common_utils::{
|
||||||
consts::X_HS_LATENCY,
|
consts::X_HS_LATENCY,
|
||||||
@ -19,7 +20,7 @@ use common_utils::{
|
|||||||
request::RequestContent,
|
request::RequestContent,
|
||||||
};
|
};
|
||||||
use error_stack::{report, IntoReport, Report, ResultExt};
|
use error_stack::{report, IntoReport, Report, ResultExt};
|
||||||
use masking::PeekInterface;
|
use masking::{PeekInterface, Secret};
|
||||||
use router_env::{instrument, tracing, tracing_actix_web::RequestId, Tag};
|
use router_env::{instrument, tracing, tracing_actix_web::RequestId, Tag};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
@ -772,6 +773,12 @@ pub enum RedirectForm {
|
|||||||
card_token: String,
|
card_token: String,
|
||||||
bin: String,
|
bin: String,
|
||||||
},
|
},
|
||||||
|
Nmi {
|
||||||
|
amount: String,
|
||||||
|
currency: Currency,
|
||||||
|
public_key: Secret<String>,
|
||||||
|
customer_vault_id: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<(url::Url, Method)> for RedirectForm {
|
impl From<(url::Url, Method)> for RedirectForm {
|
||||||
@ -1495,6 +1502,79 @@ pub fn build_redirection_form(
|
|||||||
)))
|
)))
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
RedirectForm::Nmi {
|
||||||
|
amount,
|
||||||
|
currency,
|
||||||
|
public_key,
|
||||||
|
customer_vault_id,
|
||||||
|
} => {
|
||||||
|
let public_key_val = public_key.peek();
|
||||||
|
maud::html! {
|
||||||
|
(maud::DOCTYPE)
|
||||||
|
head {
|
||||||
|
(PreEscaped(r#"<script src="https://secure.networkmerchants.com/js/v1/Gateway.js"></script>"#))
|
||||||
|
}
|
||||||
|
(PreEscaped(format!("<script>
|
||||||
|
const gateway = Gateway.create('{public_key_val}');
|
||||||
|
|
||||||
|
// Initialize the ThreeDSService
|
||||||
|
const threeDS = gateway.get3DSecure();
|
||||||
|
|
||||||
|
const options = {{
|
||||||
|
customerVaultId: '{customer_vault_id}',
|
||||||
|
currency: '{currency}',
|
||||||
|
amount: '{amount}'
|
||||||
|
}};
|
||||||
|
|
||||||
|
var responseForm = document.createElement('form');
|
||||||
|
responseForm.action=window.location.pathname.replace(/payments\\/redirect\\/(\\w+)\\/(\\w+)\\/\\w+/, \"payments/$1/$2/redirect/complete/nmi\");
|
||||||
|
responseForm.method='POST';
|
||||||
|
|
||||||
|
const threeDSsecureInterface = threeDS.createUI(options);
|
||||||
|
threeDSsecureInterface.start('body');
|
||||||
|
|
||||||
|
threeDSsecureInterface.on('challenge', function(e) {{
|
||||||
|
console.log('Challenged');
|
||||||
|
}});
|
||||||
|
|
||||||
|
threeDSsecureInterface.on('complete', function(e) {{
|
||||||
|
|
||||||
|
var item1=document.createElement('input');
|
||||||
|
item1.type='hidden';
|
||||||
|
item1.name='cavv';
|
||||||
|
item1.value=e.cavv;
|
||||||
|
responseForm.appendChild(item1);
|
||||||
|
|
||||||
|
var item2=document.createElement('input');
|
||||||
|
item2.type='hidden';
|
||||||
|
item2.name='xid';
|
||||||
|
item2.value=e.xid;
|
||||||
|
responseForm.appendChild(item2);
|
||||||
|
|
||||||
|
var item3=document.createElement('input');
|
||||||
|
item3.type='hidden';
|
||||||
|
item3.name='cardHolderAuth';
|
||||||
|
item3.value=e.cardHolderAuth;
|
||||||
|
responseForm.appendChild(item3);
|
||||||
|
|
||||||
|
var item4=document.createElement('input');
|
||||||
|
item4.type='hidden';
|
||||||
|
item4.name='threeDsVersion';
|
||||||
|
item4.value=e.threeDsVersion;
|
||||||
|
responseForm.appendChild(item4);
|
||||||
|
|
||||||
|
document.body.appendChild(responseForm);
|
||||||
|
responseForm.submit();
|
||||||
|
}});
|
||||||
|
|
||||||
|
threeDSsecureInterface.on('failure', function(e) {{
|
||||||
|
responseForm.submit();
|
||||||
|
}});
|
||||||
|
|
||||||
|
</script>"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user