feat(connector): [Novalnet] Add zero auth mandate (#6631)

This commit is contained in:
Debarati Ghatak
2025-01-10 14:46:37 +05:30
committed by GitHub
parent d850f17b87
commit 7b306a9015
10 changed files with 473 additions and 27 deletions

View File

@ -28,7 +28,7 @@ use hyperswitch_domain_models::{
router_response_types::{PaymentsResponseData, RefundsResponseData},
types::{
PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsCaptureRouterData,
PaymentsSyncRouterData, RefundSyncRouterData, RefundsRouterData,
PaymentsSyncRouterData, RefundSyncRouterData, RefundsRouterData, SetupMandateRouterData,
},
};
use hyperswitch_interfaces::{
@ -231,6 +231,79 @@ impl ConnectorIntegration<AccessTokenAuth, AccessTokenRequestData, AccessToken>
impl ConnectorIntegration<SetupMandate, SetupMandateRequestData, PaymentsResponseData>
for Novalnet
{
fn get_headers(
&self,
req: &SetupMandateRouterData,
connectors: &Connectors,
) -> CustomResult<Vec<(String, masking::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: &SetupMandateRouterData,
connectors: &Connectors,
) -> CustomResult<String, errors::ConnectorError> {
let endpoint = self.base_url(connectors);
Ok(format!("{}/payment", endpoint))
}
fn get_request_body(
&self,
req: &SetupMandateRouterData,
_connectors: &Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let connector_req = novalnet::NovalnetPaymentsRequest::try_from(req)?;
Ok(RequestContent::Json(Box::new(connector_req)))
}
fn build_request(
&self,
req: &SetupMandateRouterData,
connectors: &Connectors,
) -> CustomResult<Option<Request>, errors::ConnectorError> {
Ok(Some(
RequestBuilder::new()
.method(Method::Post)
.url(&types::SetupMandateType::get_url(self, req, connectors)?)
.attach_default_headers()
.headers(types::SetupMandateType::get_headers(self, req, connectors)?)
.set_body(types::SetupMandateType::get_request_body(
self, req, connectors,
)?)
.build(),
))
}
fn handle_response(
&self,
data: &SetupMandateRouterData,
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<SetupMandateRouterData, errors::ConnectorError> {
let response: novalnet::NovalnetPaymentsResponse = res
.response
.parse_struct("Novalnet PaymentsAuthorizeResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
RouterData::try_from(ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}
fn get_error_response(
&self,
res: Response,
event_builder: Option<&mut ConnectorEvent>,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res, event_builder)
}
}
impl ConnectorIntegration<Authorize, PaymentsAuthorizeData, PaymentsResponseData> for Novalnet {

View File

@ -17,7 +17,7 @@ use hyperswitch_domain_models::{
},
types::{
PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsCaptureRouterData,
PaymentsSyncRouterData, RefundSyncRouterData, RefundsRouterData,
PaymentsSyncRouterData, RefundSyncRouterData, RefundsRouterData, SetupMandateRouterData,
},
};
use hyperswitch_interfaces::errors;
@ -28,8 +28,9 @@ use strum::Display;
use crate::{
types::{RefundsResponseRouterData, ResponseRouterData},
utils::{
self, ApplePay, PaymentsAuthorizeRequestData, PaymentsCancelRequestData,
PaymentsCaptureRequestData, PaymentsSyncRequestData, RefundsRequestData, RouterData as _,
self, AddressDetailsData, ApplePay, PaymentsAuthorizeRequestData,
PaymentsCancelRequestData, PaymentsCaptureRequestData, PaymentsSetupMandateRequestData,
PaymentsSyncRequestData, RefundsRequestData, RouterData as _,
},
};
@ -47,6 +48,19 @@ impl<T> From<(StringMinorUnit, T)> for NovalnetRouterData<T> {
}
}
const MINIMAL_CUSTOMER_DATA_PASSED: i64 = 1;
const CREATE_TOKEN_REQUIRED: i8 = 1;
const TEST_MODE_ENABLED: i8 = 1;
const TEST_MODE_DISABLED: i8 = 0;
fn get_test_mode(item: Option<bool>) -> i8 {
match item {
Some(true) => TEST_MODE_ENABLED,
Some(false) | None => TEST_MODE_DISABLED,
}
}
#[derive(Debug, Copy, Serialize, Deserialize, Clone)]
pub enum NovalNetPaymentTypes {
CREDITCARD,
@ -117,11 +131,18 @@ pub struct NovalnetCustom {
lang: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(untagged)]
pub enum NovalNetAmount {
StringMinor(StringMinorUnit),
Int(i64),
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct NovalnetPaymentsRequestTransaction {
test_mode: i8,
payment_type: NovalNetPaymentTypes,
amount: StringMinorUnit,
amount: NovalNetAmount,
currency: common_enums::Currency,
order_no: String,
payment_data: Option<NovalNetPaymentData>,
@ -171,10 +192,7 @@ impl TryFrom<&NovalnetRouterData<&PaymentsAuthorizeRouterData>> for NovalnetPaym
enums::AuthenticationType::ThreeDs => Some(1),
enums::AuthenticationType::NoThreeDs => None,
};
let test_mode = match item.router_data.test_mode {
Some(true) => 1,
Some(false) | None => 0,
};
let test_mode = get_test_mode(item.router_data.test_mode);
let billing = NovalnetPaymentsRequestBilling {
house_no: item.router_data.get_optional_billing_line1(),
@ -197,7 +215,7 @@ impl TryFrom<&NovalnetRouterData<&PaymentsAuthorizeRouterData>> for NovalnetPaym
mobile: item.router_data.get_optional_billing_phone_number(),
billing: Some(billing),
// no_nc is used to indicate if minimal customer data is passed or not
no_nc: 1,
no_nc: MINIMAL_CUSTOMER_DATA_PASSED,
};
let lang = item
@ -209,7 +227,7 @@ impl TryFrom<&NovalnetRouterData<&PaymentsAuthorizeRouterData>> for NovalnetPaym
let hook_url = item.router_data.request.get_webhook_url()?;
let return_url = item.router_data.request.get_router_return_url()?;
let create_token = if item.router_data.request.is_mandate_payment() {
Some(1)
Some(CREATE_TOKEN_REQUIRED)
} else {
None
};
@ -234,7 +252,7 @@ impl TryFrom<&NovalnetRouterData<&PaymentsAuthorizeRouterData>> for NovalnetPaym
let transaction = NovalnetPaymentsRequestTransaction {
test_mode,
payment_type: NovalNetPaymentTypes::CREDITCARD,
amount: item.amount.clone(),
amount: NovalNetAmount::StringMinor(item.amount.clone()),
currency: item.router_data.request.currency,
order_no: item.router_data.connector_request_reference_id.clone(),
hook_url: Some(hook_url),
@ -265,7 +283,7 @@ impl TryFrom<&NovalnetRouterData<&PaymentsAuthorizeRouterData>> for NovalnetPaym
let transaction = NovalnetPaymentsRequestTransaction {
test_mode,
payment_type: NovalNetPaymentTypes::GOOGLEPAY,
amount: item.amount.clone(),
amount: NovalNetAmount::StringMinor(item.amount.clone()),
currency: item.router_data.request.currency,
order_no: item.router_data.connector_request_reference_id.clone(),
hook_url: Some(hook_url),
@ -287,7 +305,7 @@ impl TryFrom<&NovalnetRouterData<&PaymentsAuthorizeRouterData>> for NovalnetPaym
let transaction = NovalnetPaymentsRequestTransaction {
test_mode,
payment_type: NovalNetPaymentTypes::APPLEPAY,
amount: item.amount.clone(),
amount: NovalNetAmount::StringMinor(item.amount.clone()),
currency: item.router_data.request.currency,
order_no: item.router_data.connector_request_reference_id.clone(),
hook_url: Some(hook_url),
@ -331,7 +349,7 @@ impl TryFrom<&NovalnetRouterData<&PaymentsAuthorizeRouterData>> for NovalnetPaym
let transaction = NovalnetPaymentsRequestTransaction {
test_mode,
payment_type: NovalNetPaymentTypes::PAYPAL,
amount: item.amount.clone(),
amount: NovalNetAmount::StringMinor(item.amount.clone()),
currency: item.router_data.request.currency,
order_no: item.router_data.connector_request_reference_id.clone(),
hook_url: Some(hook_url),
@ -389,7 +407,7 @@ impl TryFrom<&NovalnetRouterData<&PaymentsAuthorizeRouterData>> for NovalnetPaym
let transaction = NovalnetPaymentsRequestTransaction {
test_mode,
payment_type,
amount: item.amount.clone(),
amount: NovalNetAmount::StringMinor(item.amount.clone()),
currency: item.router_data.request.currency,
order_no: item.router_data.connector_request_reference_id.clone(),
hook_url: Some(hook_url),
@ -1380,3 +1398,194 @@ pub fn get_novalnet_dispute_status(status: WebhookEventType) -> WebhookDisputeSt
pub fn option_to_result<T>(opt: Option<T>) -> Result<T, errors::ConnectorError> {
opt.ok_or(errors::ConnectorError::WebhookBodyDecodingFailed)
}
impl TryFrom<&SetupMandateRouterData> for NovalnetPaymentsRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &SetupMandateRouterData) -> Result<Self, Self::Error> {
let auth = NovalnetAuthType::try_from(&item.connector_auth_type)?;
let merchant = NovalnetPaymentsRequestMerchant {
signature: auth.product_activation_key,
tariff: auth.tariff_id,
};
let enforce_3d = match item.auth_type {
enums::AuthenticationType::ThreeDs => Some(1),
enums::AuthenticationType::NoThreeDs => None,
};
let test_mode = get_test_mode(item.test_mode);
let req_address = item.get_billing_address()?.to_owned();
let billing = NovalnetPaymentsRequestBilling {
house_no: item.get_optional_billing_line1(),
street: item.get_optional_billing_line2(),
city: item.get_optional_billing_city().map(Secret::new),
zip: item.get_optional_billing_zip(),
country_code: item.get_optional_billing_country(),
};
let customer = NovalnetPaymentsRequestCustomer {
first_name: req_address.get_first_name()?.clone(),
last_name: req_address.get_last_name()?.clone(),
email: item.request.get_email()?.clone(),
mobile: item.get_optional_billing_phone_number(),
billing: Some(billing),
// no_nc is used to indicate if minimal customer data is passed or not
no_nc: MINIMAL_CUSTOMER_DATA_PASSED,
};
let lang = item
.request
.get_optional_language_from_browser_info()
.unwrap_or(consts::DEFAULT_LOCALE.to_string().to_string());
let custom = NovalnetCustom { lang };
let hook_url = item.request.get_webhook_url()?;
let return_url = item.request.get_return_url()?;
let create_token = Some(CREATE_TOKEN_REQUIRED);
match item.request.payment_method_data {
PaymentMethodData::Card(ref req_card) => {
let novalnet_card = NovalNetPaymentData::Card(NovalnetCard {
card_number: req_card.card_number.clone(),
card_expiry_month: req_card.card_exp_month.clone(),
card_expiry_year: req_card.card_exp_year.clone(),
card_cvc: req_card.card_cvc.clone(),
card_holder: req_address.get_full_name()?.clone(),
});
let transaction = NovalnetPaymentsRequestTransaction {
test_mode,
payment_type: NovalNetPaymentTypes::CREDITCARD,
amount: NovalNetAmount::Int(0),
currency: item.request.currency,
order_no: item.connector_request_reference_id.clone(),
hook_url: Some(hook_url),
return_url: Some(return_url.clone()),
error_return_url: Some(return_url.clone()),
payment_data: Some(novalnet_card),
enforce_3d,
create_token,
};
Ok(Self {
merchant,
transaction,
customer,
custom,
})
}
PaymentMethodData::Wallet(ref wallet_data) => match wallet_data {
WalletDataPaymentMethod::GooglePay(ref req_wallet) => {
let novalnet_google_pay: NovalNetPaymentData =
NovalNetPaymentData::GooglePay(NovalnetGooglePay {
wallet_data: Secret::new(req_wallet.tokenization_data.token.clone()),
});
let transaction = NovalnetPaymentsRequestTransaction {
test_mode,
payment_type: NovalNetPaymentTypes::GOOGLEPAY,
amount: NovalNetAmount::Int(0),
currency: item.request.currency,
order_no: item.connector_request_reference_id.clone(),
hook_url: Some(hook_url),
return_url: None,
error_return_url: None,
payment_data: Some(novalnet_google_pay),
enforce_3d,
create_token,
};
Ok(Self {
merchant,
transaction,
customer,
custom,
})
}
WalletDataPaymentMethod::ApplePay(payment_method_data) => {
let transaction = NovalnetPaymentsRequestTransaction {
test_mode,
payment_type: NovalNetPaymentTypes::APPLEPAY,
amount: NovalNetAmount::Int(0),
currency: item.request.currency,
order_no: item.connector_request_reference_id.clone(),
hook_url: Some(hook_url),
return_url: None,
error_return_url: None,
payment_data: Some(NovalNetPaymentData::ApplePay(NovalnetApplePay {
wallet_data: Secret::new(payment_method_data.payment_data.clone()),
})),
enforce_3d: None,
create_token,
};
Ok(Self {
merchant,
transaction,
customer,
custom,
})
}
WalletDataPaymentMethod::AliPayQr(_)
| WalletDataPaymentMethod::AliPayRedirect(_)
| WalletDataPaymentMethod::AliPayHkRedirect(_)
| WalletDataPaymentMethod::MomoRedirect(_)
| WalletDataPaymentMethod::KakaoPayRedirect(_)
| WalletDataPaymentMethod::GoPayRedirect(_)
| WalletDataPaymentMethod::GcashRedirect(_)
| WalletDataPaymentMethod::ApplePayRedirect(_)
| WalletDataPaymentMethod::ApplePayThirdPartySdk(_)
| WalletDataPaymentMethod::DanaRedirect {}
| WalletDataPaymentMethod::GooglePayRedirect(_)
| WalletDataPaymentMethod::GooglePayThirdPartySdk(_)
| WalletDataPaymentMethod::MbWayRedirect(_)
| WalletDataPaymentMethod::MobilePayRedirect(_) => {
Err(errors::ConnectorError::NotImplemented(
utils::get_unimplemented_payment_method_error_message("novalnet"),
))?
}
WalletDataPaymentMethod::PaypalRedirect(_) => {
let transaction = NovalnetPaymentsRequestTransaction {
test_mode,
payment_type: NovalNetPaymentTypes::PAYPAL,
amount: NovalNetAmount::Int(0),
currency: item.request.currency,
order_no: item.connector_request_reference_id.clone(),
hook_url: Some(hook_url),
return_url: Some(return_url.clone()),
error_return_url: Some(return_url.clone()),
payment_data: None,
enforce_3d: None,
create_token,
};
Ok(Self {
merchant,
transaction,
customer,
custom,
})
}
WalletDataPaymentMethod::PaypalSdk(_)
| WalletDataPaymentMethod::Paze(_)
| WalletDataPaymentMethod::SamsungPay(_)
| WalletDataPaymentMethod::TwintRedirect {}
| WalletDataPaymentMethod::VippsRedirect {}
| WalletDataPaymentMethod::TouchNGoRedirect(_)
| WalletDataPaymentMethod::WeChatPayRedirect(_)
| WalletDataPaymentMethod::CashappQr(_)
| WalletDataPaymentMethod::SwishQr(_)
| WalletDataPaymentMethod::WeChatPayQr(_)
| WalletDataPaymentMethod::Mifinity(_) => {
Err(errors::ConnectorError::NotImplemented(
utils::get_unimplemented_payment_method_error_message("novalnet"),
))?
}
},
_ => Err(errors::ConnectorError::NotImplemented(
utils::get_unimplemented_payment_method_error_message("novalnet"),
))?,
}
}
}

View File

@ -1595,6 +1595,9 @@ pub trait PaymentsSetupMandateRequestData {
fn get_email(&self) -> Result<Email, Error>;
fn get_router_return_url(&self) -> Result<String, Error>;
fn is_card(&self) -> bool;
fn get_return_url(&self) -> Result<String, Error>;
fn get_webhook_url(&self) -> Result<String, Error>;
fn get_optional_language_from_browser_info(&self) -> Option<String>;
}
impl PaymentsSetupMandateRequestData for SetupMandateRequestData {
@ -1614,6 +1617,21 @@ impl PaymentsSetupMandateRequestData for SetupMandateRequestData {
fn is_card(&self) -> bool {
matches!(self.payment_method_data, PaymentMethodData::Card(_))
}
fn get_return_url(&self) -> Result<String, Error> {
self.router_return_url
.clone()
.ok_or_else(missing_field_err("return_url"))
}
fn get_webhook_url(&self) -> Result<String, Error> {
self.webhook_url
.clone()
.ok_or_else(missing_field_err("webhook_url"))
}
fn get_optional_language_from_browser_info(&self) -> Option<String> {
self.browser_info
.clone()
.and_then(|browser_info| browser_info.language)
}
}
pub trait PaymentMethodTokenizationRequestData {

View File

@ -871,6 +871,7 @@ pub struct SetupMandateRequestData {
pub off_session: Option<bool>,
pub setup_mandate_details: Option<mandates::MandateData>,
pub router_return_url: Option<String>,
pub webhook_url: Option<String>,
pub browser_info: Option<BrowserInformation>,
pub email: Option<pii::Email>,
pub customer_name: Option<Secret<String>>,

View File

@ -6564,8 +6564,17 @@ pub async fn payment_external_authentication(
&payment_attempt.clone(),
payment_connector_name,
));
let webhook_url =
helpers::create_webhook_url(&state.base_url, merchant_id, &authentication_connector);
let mca_id_option = merchant_connector_account.get_mca_id(); // Bind temporary value
let merchant_connector_account_id_or_connector_name = mca_id_option
.as_ref()
.map(|mca_id| mca_id.get_string_repr())
.unwrap_or(&authentication_connector);
let webhook_url = helpers::create_webhook_url(
&state.base_url,
merchant_id,
merchant_connector_account_id_or_connector_name,
);
let authentication_details = business_profile
.authentication_connector_details

View File

@ -1241,17 +1241,18 @@ pub fn create_authorize_url(
}
pub fn create_webhook_url(
router_base_url: &String,
router_base_url: &str,
merchant_id: &id_type::MerchantId,
connector_name: impl std::fmt::Display,
merchant_connector_id_or_connector_name: &str,
) -> String {
format!(
"{}/webhooks/{}/{}",
router_base_url,
merchant_id.get_string_repr(),
connector_name
merchant_connector_id_or_connector_name,
)
}
pub fn create_complete_authorize_url(
router_base_url: &String,
payment_attempt: &PaymentAttempt,

View File

@ -225,7 +225,7 @@ pub async fn construct_payment_router_data_for_authorize<'a>(
let webhook_url = Some(helpers::create_webhook_url(
router_base_url,
&attempt.merchant_id,
connector_id,
merchant_connector_account.get_id().get_string_repr(),
));
let router_return_url = payment_data
@ -2751,11 +2751,17 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsAuthoriz
attempt,
connector_name,
));
let merchant_connector_account_id_or_connector_name = payment_data
.payment_attempt
.merchant_connector_id
.as_ref()
.map(|mca_id| mca_id.get_string_repr())
.unwrap_or(connector_name);
let webhook_url = Some(helpers::create_webhook_url(
router_base_url,
&attempt.merchant_id,
connector_name,
merchant_connector_account_id_or_connector_name,
));
let router_return_url = Some(helpers::create_redirect_url(
router_base_url,
@ -3575,6 +3581,18 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::SetupMandateRequ
.map(|customer| customer.clone().into_inner())
});
let amount = payment_data.payment_attempt.get_total_amount();
let merchant_connector_account_id_or_connector_name = payment_data
.payment_attempt
.merchant_connector_id
.as_ref()
.map(|mca_id| mca_id.get_string_repr())
.unwrap_or(connector_name);
let webhook_url = Some(helpers::create_webhook_url(
router_base_url,
&attempt.merchant_id,
merchant_connector_account_id_or_connector_name,
));
Ok(Self {
currency: payment_data.currency,
confirm: true,
@ -3604,6 +3622,7 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::SetupMandateRequ
),
metadata: payment_data.payment_intent.metadata.clone().map(Into::into),
shipping_cost: payment_data.payment_intent.shipping_cost,
webhook_url,
})
}
}
@ -3770,11 +3789,16 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsPreProce
.collect::<Result<Vec<_>, _>>()
})
.transpose()?;
let merchant_connector_account_id_or_connector_name = payment_data
.payment_attempt
.merchant_connector_id
.as_ref()
.map(|mca_id| mca_id.get_string_repr())
.unwrap_or(connector_name);
let webhook_url = Some(helpers::create_webhook_url(
router_base_url,
&attempt.merchant_id,
connector_name,
merchant_connector_account_id_or_connector_name,
));
let router_return_url = Some(helpers::create_redirect_url(
router_base_url,

View File

@ -32,7 +32,7 @@ pub async fn construct_relay_refund_router_data<'a, F>(
let webhook_url = Some(payments::helpers::create_webhook_url(
&state.base_url.clone(),
merchant_id,
connector_name,
connector_account.get_id().get_string_repr(),
));
let supported_connector = &state

View File

@ -288,11 +288,16 @@ pub async fn construct_refund_router_data<'a, F>(
.payment_method
.get_required_value("payment_method_type")
.change_context(errors::ApiErrorResponse::InternalServerError)?;
let merchant_connector_account_id_or_connector_name = payment_attempt
.merchant_connector_id
.as_ref()
.map(|mca_id| mca_id.get_string_repr())
.unwrap_or(connector_id);
let webhook_url = Some(helpers::create_webhook_url(
&state.base_url.clone(),
merchant_account.get_id(),
connector_id,
merchant_connector_account_id_or_connector_name,
));
let test_mode: Option<bool> = merchant_connector_account.is_test_mode_on();

View File

@ -6,6 +6,31 @@ const successfulThreeDSTestCardDetails = {
card_cvc: "123",
};
const successfulNo3DSCardDetails = {
card_number: "4242424242424242",
card_exp_month: "01",
card_exp_year: "50",
card_holder_name: "joseph Doe",
card_cvc: "123",
};
const singleUseMandateData = {
customer_acceptance: {
acceptance_type: "offline",
accepted_at: "1963-05-03T04:07:52.723Z",
online: {
ip_address: "125.0.0.1",
user_agent: "amet irure esse",
},
},
mandate_type: {
single_use: {
amount: 8000,
currency: "EUR",
},
},
};
export const connectorDetails = {
card_pm: {
PaymentIntent: {
@ -206,6 +231,87 @@ export const connectorDetails = {
},
},
},
SaveCardConfirmAutoCaptureOffSession: {
Request: {
setup_future_usage: "off_session",
},
Response: {
status: 200,
trigger_skip: true,
body: {
status: "requires_customer_action",
},
},
},
PaymentIntentOffSession: {
Request: {
currency: "EUR",
amount: 6500,
authentication_type: "no_three_ds",
customer_acceptance: null,
setup_future_usage: "off_session",
},
Response: {
status: 200,
trigger_skip: true,
body: {
status: "requires_payment_method",
setup_future_usage: "off_session",
},
},
},
ZeroAuthPaymentIntent: {
Request: {
amount: 0,
setup_future_usage: "off_session",
currency: "EUR",
},
Response: {
status: 200,
trigger_skip: true,
body: {
status: "requires_payment_method",
setup_future_usage: "off_session",
},
},
},
ZeroAuthMandate: {
Request: {
payment_method: "card",
payment_method_data: {
card: successfulNo3DSCardDetails,
},
currency: "USD",
mandate_data: singleUseMandateData,
},
Response: {
status: 200,
trigger_skip: true,
body: {
status: "succeeded",
},
},
},
ZeroAuthConfirmPayment: {
Request: {
payment_type: "setup_mandate",
payment_method: "card",
payment_method_type: "credit",
payment_method_data: {
card: successfulNo3DSCardDetails,
},
},
Response: {
status: 501,
body: {
error: {
type: "invalid_request",
message: "Setup Mandate flow for Novalnet is not implemented",
code: "IR_00",
},
},
},
},
},
pm_list: {
PmListResponse: {