mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-11-01 02:57:02 +08:00 
			
		
		
		
	feat(connector): add afterpay, klarna, affirm support in adyen connector (#516)
This commit is contained in:
		| @ -561,7 +561,7 @@ impl services::ConnectorIntegration<api::Execute, types::RefundsData, types::Ref | |||||||
|     ) -> CustomResult<String, errors::ConnectorError> { |     ) -> CustomResult<String, errors::ConnectorError> { | ||||||
|         let connector_payment_id = req.request.connector_transaction_id.clone(); |         let connector_payment_id = req.request.connector_transaction_id.clone(); | ||||||
|         Ok(format!( |         Ok(format!( | ||||||
|             "{}v68/payments/{}/reversals", |             "{}v68/payments/{}/refunds", | ||||||
|             self.base_url(connectors), |             self.base_url(connectors), | ||||||
|             connector_payment_id, |             connector_payment_id, | ||||||
|         )) |         )) | ||||||
|  | |||||||
| @ -1,19 +1,21 @@ | |||||||
| use std::{collections::HashMap, str::FromStr}; | use std::{collections::HashMap, str::FromStr}; | ||||||
|  |  | ||||||
| use error_stack::{IntoReport, ResultExt}; | use error_stack::{IntoReport, ResultExt}; | ||||||
|  | use masking::PeekInterface; | ||||||
| use reqwest::Url; | use reqwest::Url; | ||||||
| use serde::{Deserialize, Serialize}; | use serde::{Deserialize, Serialize}; | ||||||
|  |  | ||||||
| use crate::{ | use crate::{ | ||||||
|  |     connector::utils::{self, PaymentsRequestData}, | ||||||
|     consts, |     consts, | ||||||
|     core::errors, |     core::errors, | ||||||
|     pii, services, |     pii::{self, Email, Secret}, | ||||||
|  |     services, | ||||||
|     types::{ |     types::{ | ||||||
|         self, |         self, | ||||||
|         api::{self, enums as api_enums}, |         api::{self, enums as api_enums}, | ||||||
|         storage::enums as storage_enums, |         storage::enums as storage_enums, | ||||||
|     }, |     }, | ||||||
|     utils::OptionExt, |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| // Adyen Types Definition | // Adyen Types Definition | ||||||
| @ -41,8 +43,39 @@ pub enum AuthType { | |||||||
|     PreAuth, |     PreAuth, | ||||||
| } | } | ||||||
| #[derive(Default, Debug, Serialize, Deserialize)] | #[derive(Default, Debug, Serialize, Deserialize)] | ||||||
|  | #[serde(rename_all = "camelCase")] | ||||||
| pub struct AdditionalData { | pub struct AdditionalData { | ||||||
|     authorisation_type: AuthType, |     authorisation_type: AuthType, | ||||||
|  |     manual_capture: bool, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Serialize)] | ||||||
|  | #[serde(rename_all = "camelCase")] | ||||||
|  | pub struct ShopperName { | ||||||
|  |     first_name: Option<Secret<String>>, | ||||||
|  |     last_name: Option<Secret<String>>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Default, Debug, Serialize)] | ||||||
|  | #[serde(rename_all = "camelCase")] | ||||||
|  | pub struct Address { | ||||||
|  |     city: Option<String>, | ||||||
|  |     country: Option<String>, | ||||||
|  |     house_number_or_name: Option<Secret<String>>, | ||||||
|  |     postal_code: Option<Secret<String>>, | ||||||
|  |     state_or_province: Option<Secret<String>>, | ||||||
|  |     street: Option<Secret<String>>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Serialize)] | ||||||
|  | #[serde(rename_all = "camelCase")] | ||||||
|  | pub struct LineItem { | ||||||
|  |     amount_excluding_tax: Option<i64>, | ||||||
|  |     amount_including_tax: Option<i64>, | ||||||
|  |     description: Option<String>, | ||||||
|  |     id: Option<String>, | ||||||
|  |     tax_amount: Option<i64>, | ||||||
|  |     quantity: Option<u16>, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Debug, Serialize)] | #[derive(Debug, Serialize)] | ||||||
| @ -58,6 +91,13 @@ pub struct AdyenPaymentRequest { | |||||||
|     #[serde(skip_serializing_if = "Option::is_none")] |     #[serde(skip_serializing_if = "Option::is_none")] | ||||||
|     recurring_processing_model: Option<AdyenRecurringModel>, |     recurring_processing_model: Option<AdyenRecurringModel>, | ||||||
|     additional_data: Option<AdditionalData>, |     additional_data: Option<AdditionalData>, | ||||||
|  |     shopper_name: Option<ShopperName>, | ||||||
|  |     shopper_email: Option<Secret<String, Email>>, | ||||||
|  |     telephone_number: Option<Secret<String>>, | ||||||
|  |     billing_address: Option<Address>, | ||||||
|  |     delivery_address: Option<Address>, | ||||||
|  |     country_code: Option<String>, | ||||||
|  |     line_items: Option<Vec<LineItem>>, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Debug, Serialize)] | #[derive(Debug, Serialize)] | ||||||
| @ -175,17 +215,20 @@ pub enum AdyenPaymentMethod { | |||||||
|     AdyenPaypal(AdyenPaypal), |     AdyenPaypal(AdyenPaypal), | ||||||
|     Gpay(AdyenGPay), |     Gpay(AdyenGPay), | ||||||
|     ApplePay(AdyenApplePay), |     ApplePay(AdyenApplePay), | ||||||
|  |     AfterPay(AdyenPayLaterData), | ||||||
|  |     AdyenKlarna(AdyenPayLaterData), | ||||||
|  |     AdyenAffirm(AdyenPayLaterData), | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Debug, Clone, Serialize, Deserialize)] | #[derive(Debug, Clone, Serialize, Deserialize)] | ||||||
| #[serde(rename_all = "camelCase")] | #[serde(rename_all = "camelCase")] | ||||||
| pub struct AdyenCard { | pub struct AdyenCard { | ||||||
|     #[serde(rename = "type")] |     #[serde(rename = "type")] | ||||||
|     payment_type: String, |     payment_type: PaymentType, | ||||||
|     number: Option<pii::Secret<String, pii::CardNumber>>, |     number: Secret<String, pii::CardNumber>, | ||||||
|     expiry_month: Option<pii::Secret<String>>, |     expiry_month: Secret<String>, | ||||||
|     expiry_year: Option<pii::Secret<String>>, |     expiry_year: Secret<String>, | ||||||
|     cvc: Option<pii::Secret<String>>, |     cvc: Option<Secret<String>>, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Default, Debug, Serialize, Deserialize)] | #[derive(Default, Debug, Serialize, Deserialize)] | ||||||
| @ -213,13 +256,13 @@ pub enum CancelStatus { | |||||||
| #[serde(rename_all = "camelCase")] | #[serde(rename_all = "camelCase")] | ||||||
| pub struct AdyenPaypal { | pub struct AdyenPaypal { | ||||||
|     #[serde(rename = "type")] |     #[serde(rename = "type")] | ||||||
|     payment_type: String, |     payment_type: PaymentType, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Debug, Clone, Serialize, Deserialize)] | #[derive(Debug, Clone, Serialize, Deserialize)] | ||||||
| pub struct AdyenGPay { | pub struct AdyenGPay { | ||||||
|     #[serde(rename = "type")] |     #[serde(rename = "type")] | ||||||
|     payment_type: String, |     payment_type: PaymentType, | ||||||
|     #[serde(rename = "googlePayToken")] |     #[serde(rename = "googlePayToken")] | ||||||
|     google_pay_token: String, |     google_pay_token: String, | ||||||
| } | } | ||||||
| @ -227,16 +270,24 @@ pub struct AdyenGPay { | |||||||
| #[derive(Debug, Clone, Serialize, Deserialize)] | #[derive(Debug, Clone, Serialize, Deserialize)] | ||||||
| pub struct AdyenApplePay { | pub struct AdyenApplePay { | ||||||
|     #[serde(rename = "type")] |     #[serde(rename = "type")] | ||||||
|     payment_type: String, |     payment_type: PaymentType, | ||||||
|     #[serde(rename = "applePayToken")] |     #[serde(rename = "applePayToken")] | ||||||
|     apple_pay_token: String, |     apple_pay_token: String, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Clone, Serialize, Deserialize)] | ||||||
|  | pub struct AdyenPayLaterData { | ||||||
|  |     #[serde(rename = "type")] | ||||||
|  |     payment_type: PaymentType, | ||||||
|  | } | ||||||
|  |  | ||||||
| // Refunds Request and Response | // Refunds Request and Response | ||||||
| #[derive(Default, Debug, Serialize, Deserialize)] | #[derive(Default, Debug, Serialize, Deserialize)] | ||||||
| #[serde(rename_all = "camelCase")] | #[serde(rename_all = "camelCase")] | ||||||
| pub struct AdyenRefundRequest { | pub struct AdyenRefundRequest { | ||||||
|     merchant_account: String, |     merchant_account: String, | ||||||
|  |     amount: Amount, | ||||||
|  |     merchant_refund_reason: Option<String>, | ||||||
|     reference: String, |     reference: String, | ||||||
| } | } | ||||||
|  |  | ||||||
| @ -255,6 +306,18 @@ pub struct AdyenAuthType { | |||||||
|     pub(super) merchant_account: String, |     pub(super) merchant_account: String, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Clone, Serialize, Deserialize)] | ||||||
|  | #[serde(rename_all = "lowercase")] | ||||||
|  | pub enum PaymentType { | ||||||
|  |     Scheme, | ||||||
|  |     Googlepay, | ||||||
|  |     Applepay, | ||||||
|  |     Paypal, | ||||||
|  |     Klarna, | ||||||
|  |     Affirm, | ||||||
|  |     Afterpaytouch, | ||||||
|  | } | ||||||
|  |  | ||||||
| impl TryFrom<&types::ConnectorAuthType> for AdyenAuthType { | impl TryFrom<&types::ConnectorAuthType> for AdyenAuthType { | ||||||
|     type Error = error_stack::Report<errors::ConnectorError>; |     type Error = error_stack::Report<errors::ConnectorError>; | ||||||
|     fn try_from(auth_type: &types::ConnectorAuthType) -> Result<Self, Self::Error> { |     fn try_from(auth_type: &types::ConnectorAuthType) -> Result<Self, Self::Error> { | ||||||
| @ -269,159 +332,314 @@ impl TryFrom<&types::ConnectorAuthType> for AdyenAuthType { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| impl TryFrom<&types::BrowserInformation> for AdyenBrowserInfo { |  | ||||||
|     type Error = error_stack::Report<errors::ConnectorError>; |  | ||||||
|     fn try_from(item: &types::BrowserInformation) -> Result<Self, Self::Error> { |  | ||||||
|         Ok(Self { |  | ||||||
|             accept_header: item.accept_header.clone(), |  | ||||||
|             language: item.language.clone(), |  | ||||||
|             screen_height: item.screen_height, |  | ||||||
|             screen_width: item.screen_width, |  | ||||||
|             color_depth: item.color_depth, |  | ||||||
|             user_agent: item.user_agent.clone(), |  | ||||||
|             time_zone_offset: item.time_zone, |  | ||||||
|             java_enabled: item.java_enabled, |  | ||||||
|         }) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Payment Request Transform | // Payment Request Transform | ||||||
| impl TryFrom<&types::PaymentsAuthorizeRouterData> for AdyenPaymentRequest { | impl TryFrom<&types::PaymentsAuthorizeRouterData> for AdyenPaymentRequest { | ||||||
|     type Error = error_stack::Report<errors::ConnectorError>; |     type Error = error_stack::Report<errors::ConnectorError>; | ||||||
|     fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> { |     fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> { | ||||||
|         let auth_type = AdyenAuthType::try_from(&item.connector_auth_type)?; |         match item.payment_method { | ||||||
|         let reference = item.payment_id.to_string(); |             storage_models::enums::PaymentMethodType::Card => get_card_specific_payment_data(item), | ||||||
|         let amount = Amount { |             storage_models::enums::PaymentMethodType::PayLater => { | ||||||
|             currency: item.request.currency.to_string(), |                 get_paylater_specific_payment_data(item) | ||||||
|             value: item.request.amount, |  | ||||||
|         }; |  | ||||||
|         let ccard = match item.request.payment_method_data { |  | ||||||
|             api::PaymentMethod::Card(ref ccard) => Some(ccard), |  | ||||||
|             api::PaymentMethod::BankTransfer |  | ||||||
|             | api::PaymentMethod::Wallet(_) |  | ||||||
|             | api::PaymentMethod::PayLater(_) |  | ||||||
|             | api::PaymentMethod::Paypal => None, |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         let wallet_data = match item.request.payment_method_data { |  | ||||||
|             api::PaymentMethod::Wallet(ref wallet_data) => Some(wallet_data), |  | ||||||
|             _ => None, |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         let shopper_interaction = match item.request.off_session { |  | ||||||
|             Some(true) => AdyenShopperInteraction::ContinuedAuthentication, |  | ||||||
|             _ => AdyenShopperInteraction::Ecommerce, |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         let recurring_processing_model = match item.request.setup_future_usage { |  | ||||||
|             Some(storage_enums::FutureUsage::OffSession) => { |  | ||||||
|                 Some(AdyenRecurringModel::UnscheduledCardOnFile) |  | ||||||
|             } |             } | ||||||
|             _ => None, |             storage_models::enums::PaymentMethodType::Wallet => { | ||||||
|         }; |                 get_wallet_specific_payment_data(item) | ||||||
|  |  | ||||||
|         let payment_type = match item.payment_method { |  | ||||||
|             storage_enums::PaymentMethodType::Card => "scheme".to_string(), |  | ||||||
|             storage_enums::PaymentMethodType::Wallet => wallet_data |  | ||||||
|                 .get_required_value("wallet_data") |  | ||||||
|                 .change_context(errors::ConnectorError::RequestEncodingFailed)? |  | ||||||
|                 .issuer_name |  | ||||||
|                 .to_string(), |  | ||||||
|             _ => "None".to_string(), |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         let payment_method = match item.payment_method { |  | ||||||
|             storage_enums::PaymentMethodType::Card => { |  | ||||||
|                 let card = AdyenCard { |  | ||||||
|                     payment_type, |  | ||||||
|                     number: ccard.map(|x| x.card_number.clone()), |  | ||||||
|                     expiry_month: ccard.map(|x| x.card_exp_month.clone()), |  | ||||||
|                     expiry_year: ccard.map(|x| x.card_exp_year.clone()), |  | ||||||
|                     cvc: ccard.map(|x| x.card_cvc.clone()), |  | ||||||
|                 }; |  | ||||||
|  |  | ||||||
|                 Ok(AdyenPaymentMethod::AdyenCard(card)) |  | ||||||
|             } |             } | ||||||
|  |             _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), | ||||||
|             storage_enums::PaymentMethodType::Wallet => match wallet_data |         } | ||||||
|                 .get_required_value("wallet_data") |  | ||||||
|                 .change_context(errors::ConnectorError::RequestEncodingFailed)? |  | ||||||
|                 .issuer_name |  | ||||||
|             { |  | ||||||
|                 api_enums::WalletIssuer::GooglePay => { |  | ||||||
|                     let gpay_data = AdyenGPay { |  | ||||||
|                         payment_type, |  | ||||||
|                         google_pay_token: wallet_data |  | ||||||
|                             .get_required_value("wallet_data") |  | ||||||
|                             .change_context(errors::ConnectorError::RequestEncodingFailed)? |  | ||||||
|                             .token |  | ||||||
|                             .to_owned() |  | ||||||
|                             .get_required_value("token") |  | ||||||
|                             .change_context(errors::ConnectorError::RequestEncodingFailed) |  | ||||||
|                             .attach_printable("No token passed")?, |  | ||||||
|                     }; |  | ||||||
|                     Ok(AdyenPaymentMethod::Gpay(gpay_data)) |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 api_enums::WalletIssuer::ApplePay => { |  | ||||||
|                     let apple_pay_data = AdyenApplePay { |  | ||||||
|                         payment_type, |  | ||||||
|                         apple_pay_token: wallet_data |  | ||||||
|                             .get_required_value("wallet_data") |  | ||||||
|                             .change_context(errors::ConnectorError::RequestEncodingFailed)? |  | ||||||
|                             .token |  | ||||||
|                             .to_owned() |  | ||||||
|                             .get_required_value("token") |  | ||||||
|                             .change_context(errors::ConnectorError::RequestEncodingFailed) |  | ||||||
|                             .attach_printable("No token passed")?, |  | ||||||
|                     }; |  | ||||||
|                     Ok(AdyenPaymentMethod::ApplePay(apple_pay_data)) |  | ||||||
|                 } |  | ||||||
|                 api_enums::WalletIssuer::Paypal => { |  | ||||||
|                     let wallet = AdyenPaypal { payment_type }; |  | ||||||
|                     Ok(AdyenPaymentMethod::AdyenPaypal(wallet)) |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             _ => Err(errors::ConnectorError::MissingRequiredField { |  | ||||||
|                 field_name: "payment_method", |  | ||||||
|             }), |  | ||||||
|         }?; |  | ||||||
|  |  | ||||||
|         let browser_info = if matches!(item.auth_type, storage_enums::AuthenticationType::ThreeDs) { |  | ||||||
|             item.request |  | ||||||
|                 .browser_info |  | ||||||
|                 .clone() |  | ||||||
|                 .map(|d| AdyenBrowserInfo::try_from(&d)) |  | ||||||
|                 .transpose()? |  | ||||||
|         } else { |  | ||||||
|             None |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         let additional_data = match item.request.capture_method { |  | ||||||
|             Some(storage_models::enums::CaptureMethod::Manual) => Some(AdditionalData { |  | ||||||
|                 authorisation_type: AuthType::PreAuth, |  | ||||||
|             }), |  | ||||||
|             _ => None, |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         Ok(Self { |  | ||||||
|             amount, |  | ||||||
|             merchant_account: auth_type.merchant_account, |  | ||||||
|             payment_method, |  | ||||||
|             reference, |  | ||||||
|             return_url: item.router_return_url.clone().ok_or( |  | ||||||
|                 errors::ConnectorError::MissingRequiredField { |  | ||||||
|                     field_name: "router_return_url", |  | ||||||
|                 }, |  | ||||||
|             )?, |  | ||||||
|             shopper_interaction, |  | ||||||
|             recurring_processing_model, |  | ||||||
|             browser_info, |  | ||||||
|             additional_data, |  | ||||||
|         }) |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | impl From<&types::PaymentsAuthorizeRouterData> for AdyenShopperInteraction { | ||||||
|  |     fn from(item: &types::PaymentsAuthorizeRouterData) -> Self { | ||||||
|  |         match item.request.off_session { | ||||||
|  |             Some(true) => Self::ContinuedAuthentication, | ||||||
|  |             _ => Self::Ecommerce, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn get_recurring_processing_model( | ||||||
|  |     item: &types::PaymentsAuthorizeRouterData, | ||||||
|  | ) -> Option<AdyenRecurringModel> { | ||||||
|  |     match item.request.setup_future_usage { | ||||||
|  |         Some(storage_enums::FutureUsage::OffSession) => { | ||||||
|  |             Some(AdyenRecurringModel::UnscheduledCardOnFile) | ||||||
|  |         } | ||||||
|  |         _ => None, | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn get_browser_info(item: &types::PaymentsAuthorizeRouterData) -> Option<AdyenBrowserInfo> { | ||||||
|  |     if matches!(item.auth_type, storage_enums::AuthenticationType::ThreeDs) { | ||||||
|  |         item.request | ||||||
|  |             .browser_info | ||||||
|  |             .as_ref() | ||||||
|  |             .map(|info| AdyenBrowserInfo { | ||||||
|  |                 accept_header: info.accept_header.clone(), | ||||||
|  |                 language: info.language.clone(), | ||||||
|  |                 screen_height: info.screen_height, | ||||||
|  |                 screen_width: info.screen_width, | ||||||
|  |                 color_depth: info.color_depth, | ||||||
|  |                 user_agent: info.user_agent.clone(), | ||||||
|  |                 time_zone_offset: info.time_zone, | ||||||
|  |                 java_enabled: info.java_enabled, | ||||||
|  |             }) | ||||||
|  |     } else { | ||||||
|  |         None | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn get_additional_data(item: &types::PaymentsAuthorizeRouterData) -> Option<AdditionalData> { | ||||||
|  |     match item.request.capture_method { | ||||||
|  |         Some(storage_models::enums::CaptureMethod::Manual) => Some(AdditionalData { | ||||||
|  |             authorisation_type: AuthType::PreAuth, | ||||||
|  |             manual_capture: true, | ||||||
|  |         }), | ||||||
|  |         _ => None, | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn get_amount_data(item: &types::PaymentsAuthorizeRouterData) -> Amount { | ||||||
|  |     Amount { | ||||||
|  |         currency: item.request.currency.to_string(), | ||||||
|  |         value: item.request.amount, | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn get_address_info(address: Option<&api_models::payments::Address>) -> Option<Address> { | ||||||
|  |     address.and_then(|add| { | ||||||
|  |         add.address.as_ref().map(|a| Address { | ||||||
|  |             city: a.city.clone(), | ||||||
|  |             country: a.country.clone(), | ||||||
|  |             house_number_or_name: a.line1.clone(), | ||||||
|  |             postal_code: a.zip.clone(), | ||||||
|  |             state_or_province: a.state.clone(), | ||||||
|  |             street: a.line2.clone(), | ||||||
|  |         }) | ||||||
|  |     }) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn get_line_items(item: &types::PaymentsAuthorizeRouterData) -> Vec<LineItem> { | ||||||
|  |     let order_details = item.request.order_details.as_ref(); | ||||||
|  |     let line_item = LineItem { | ||||||
|  |         amount_including_tax: Some(item.request.amount), | ||||||
|  |         amount_excluding_tax: None, | ||||||
|  |         description: order_details.map(|details| details.product_name.clone()), | ||||||
|  |         // We support only one product details in payment request as of now, therefore hard coded the id. | ||||||
|  |         // If we begin to support multiple product details in future then this logic should be made to create ID dynamically | ||||||
|  |         id: Some(String::from("Items #1")), | ||||||
|  |         tax_amount: None, | ||||||
|  |         quantity: order_details.map(|details| details.quantity), | ||||||
|  |     }; | ||||||
|  |     vec![line_item] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn get_telephone_number(item: &types::PaymentsAuthorizeRouterData) -> Option<Secret<String>> { | ||||||
|  |     let phone = item | ||||||
|  |         .address | ||||||
|  |         .billing | ||||||
|  |         .as_ref() | ||||||
|  |         .and_then(|billing| billing.phone.as_ref()); | ||||||
|  |     phone.as_ref().and_then(|phone| { | ||||||
|  |         phone.number.as_ref().and_then(|number| { | ||||||
|  |             phone | ||||||
|  |                 .country_code | ||||||
|  |                 .as_ref() | ||||||
|  |                 .map(|cc| Secret::new(format!("{}{}", cc, number.peek()))) | ||||||
|  |         }) | ||||||
|  |     }) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn get_shopper_name(item: &types::PaymentsAuthorizeRouterData) -> Option<ShopperName> { | ||||||
|  |     let address = item | ||||||
|  |         .address | ||||||
|  |         .billing | ||||||
|  |         .as_ref() | ||||||
|  |         .and_then(|billing| billing.address.as_ref()); | ||||||
|  |     Some(ShopperName { | ||||||
|  |         first_name: address.and_then(|address| address.first_name.clone()), | ||||||
|  |         last_name: address.and_then(|address| address.last_name.clone()), | ||||||
|  |     }) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn get_country_code(item: &types::PaymentsAuthorizeRouterData) -> Option<String> { | ||||||
|  |     let address = item | ||||||
|  |         .address | ||||||
|  |         .billing | ||||||
|  |         .as_ref() | ||||||
|  |         .and_then(|billing| billing.address.as_ref()); | ||||||
|  |     address.and_then(|address| address.country.clone()) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn get_payment_method_data( | ||||||
|  |     item: &types::PaymentsAuthorizeRouterData, | ||||||
|  | ) -> Result<AdyenPaymentMethod, error_stack::Report<errors::ConnectorError>> { | ||||||
|  |     match item.request.payment_method_data { | ||||||
|  |         api::PaymentMethod::Card(ref card) => { | ||||||
|  |             let adyen_card = AdyenCard { | ||||||
|  |                 payment_type: PaymentType::Scheme, | ||||||
|  |                 number: card.card_number.clone(), | ||||||
|  |                 expiry_month: card.card_exp_month.clone(), | ||||||
|  |                 expiry_year: card.card_exp_year.clone(), | ||||||
|  |                 cvc: Some(card.card_cvc.clone()), | ||||||
|  |             }; | ||||||
|  |             Ok(AdyenPaymentMethod::AdyenCard(adyen_card)) | ||||||
|  |         } | ||||||
|  |         api::PaymentMethod::Wallet(ref wallet_data) => match wallet_data.issuer_name { | ||||||
|  |             api_enums::WalletIssuer::GooglePay => { | ||||||
|  |                 let gpay_data = AdyenGPay { | ||||||
|  |                     payment_type: PaymentType::Googlepay, | ||||||
|  |                     google_pay_token: wallet_data | ||||||
|  |                         .token | ||||||
|  |                         .clone() | ||||||
|  |                         .ok_or_else(utils::missing_field_err("token"))?, | ||||||
|  |                 }; | ||||||
|  |                 Ok(AdyenPaymentMethod::Gpay(gpay_data)) | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             api_enums::WalletIssuer::ApplePay => { | ||||||
|  |                 let apple_pay_data = AdyenApplePay { | ||||||
|  |                     payment_type: PaymentType::Applepay, | ||||||
|  |                     apple_pay_token: wallet_data | ||||||
|  |                         .token | ||||||
|  |                         .clone() | ||||||
|  |                         .ok_or_else(utils::missing_field_err("token"))?, | ||||||
|  |                 }; | ||||||
|  |                 Ok(AdyenPaymentMethod::ApplePay(apple_pay_data)) | ||||||
|  |             } | ||||||
|  |             api_enums::WalletIssuer::Paypal => { | ||||||
|  |                 let wallet = AdyenPaypal { | ||||||
|  |                     payment_type: PaymentType::Paypal, | ||||||
|  |                 }; | ||||||
|  |                 Ok(AdyenPaymentMethod::AdyenPaypal(wallet)) | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |         api_models::payments::PaymentMethod::PayLater(ref pay_later_data) => match pay_later_data { | ||||||
|  |             api_models::payments::PayLaterData::KlarnaRedirect { .. } => { | ||||||
|  |                 let klarna = AdyenPayLaterData { | ||||||
|  |                     payment_type: PaymentType::Klarna, | ||||||
|  |                 }; | ||||||
|  |                 Ok(AdyenPaymentMethod::AdyenKlarna(klarna)) | ||||||
|  |             } | ||||||
|  |             api_models::payments::PayLaterData::AffirmRedirect { .. } => { | ||||||
|  |                 Ok(AdyenPaymentMethod::AdyenAffirm(AdyenPayLaterData { | ||||||
|  |                     payment_type: PaymentType::Affirm, | ||||||
|  |                 })) | ||||||
|  |             } | ||||||
|  |             api_models::payments::PayLaterData::AfterpayClearpayRedirect { .. } => { | ||||||
|  |                 Ok(AdyenPaymentMethod::AfterPay(AdyenPayLaterData { | ||||||
|  |                     payment_type: PaymentType::Afterpaytouch, | ||||||
|  |                 })) | ||||||
|  |             } | ||||||
|  |             _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), | ||||||
|  |         }, | ||||||
|  |         api_models::payments::PaymentMethod::BankTransfer | ||||||
|  |         | api_models::payments::PaymentMethod::Paypal => { | ||||||
|  |             Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn get_card_specific_payment_data( | ||||||
|  |     item: &types::PaymentsAuthorizeRouterData, | ||||||
|  | ) -> Result<AdyenPaymentRequest, error_stack::Report<errors::ConnectorError>> { | ||||||
|  |     let amount = get_amount_data(item); | ||||||
|  |     let auth_type = AdyenAuthType::try_from(&item.connector_auth_type)?; | ||||||
|  |     let shopper_interaction = AdyenShopperInteraction::from(item); | ||||||
|  |     let recurring_processing_model = get_recurring_processing_model(item); | ||||||
|  |     let browser_info = get_browser_info(item); | ||||||
|  |     let additional_data = get_additional_data(item); | ||||||
|  |     let return_url = item.get_return_url()?; | ||||||
|  |     let payment_method = get_payment_method_data(item)?; | ||||||
|  |     Ok(AdyenPaymentRequest { | ||||||
|  |         amount, | ||||||
|  |         merchant_account: auth_type.merchant_account, | ||||||
|  |         payment_method, | ||||||
|  |         reference: item.payment_id.to_string(), | ||||||
|  |         return_url, | ||||||
|  |         shopper_interaction, | ||||||
|  |         recurring_processing_model, | ||||||
|  |         browser_info, | ||||||
|  |         additional_data, | ||||||
|  |         telephone_number: None, | ||||||
|  |         shopper_name: None, | ||||||
|  |         shopper_email: None, | ||||||
|  |         billing_address: None, | ||||||
|  |         delivery_address: None, | ||||||
|  |         country_code: None, | ||||||
|  |         line_items: None, | ||||||
|  |     }) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn get_wallet_specific_payment_data( | ||||||
|  |     item: &types::PaymentsAuthorizeRouterData, | ||||||
|  | ) -> Result<AdyenPaymentRequest, error_stack::Report<errors::ConnectorError>> { | ||||||
|  |     let amount = get_amount_data(item); | ||||||
|  |     let auth_type = AdyenAuthType::try_from(&item.connector_auth_type)?; | ||||||
|  |     let browser_info = get_browser_info(item); | ||||||
|  |     let additional_data = get_additional_data(item); | ||||||
|  |     let payment_method = get_payment_method_data(item)?; | ||||||
|  |     let shopper_interaction = AdyenShopperInteraction::from(item); | ||||||
|  |     let recurring_processing_model = get_recurring_processing_model(item); | ||||||
|  |     let return_url = item.get_return_url()?; | ||||||
|  |     Ok(AdyenPaymentRequest { | ||||||
|  |         amount, | ||||||
|  |         merchant_account: auth_type.merchant_account, | ||||||
|  |         payment_method, | ||||||
|  |         reference: item.payment_id.to_string(), | ||||||
|  |         return_url, | ||||||
|  |         shopper_interaction, | ||||||
|  |         recurring_processing_model, | ||||||
|  |         browser_info, | ||||||
|  |         additional_data, | ||||||
|  |         telephone_number: None, | ||||||
|  |         shopper_name: None, | ||||||
|  |         shopper_email: None, | ||||||
|  |         billing_address: None, | ||||||
|  |         delivery_address: None, | ||||||
|  |         country_code: None, | ||||||
|  |         line_items: None, | ||||||
|  |     }) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn get_paylater_specific_payment_data( | ||||||
|  |     item: &types::PaymentsAuthorizeRouterData, | ||||||
|  | ) -> Result<AdyenPaymentRequest, error_stack::Report<errors::ConnectorError>> { | ||||||
|  |     let amount = get_amount_data(item); | ||||||
|  |     let auth_type = AdyenAuthType::try_from(&item.connector_auth_type)?; | ||||||
|  |     let browser_info = get_browser_info(item); | ||||||
|  |     let additional_data = get_additional_data(item); | ||||||
|  |     let payment_method = get_payment_method_data(item)?; | ||||||
|  |     let shopper_interaction = AdyenShopperInteraction::from(item); | ||||||
|  |     let recurring_processing_model = get_recurring_processing_model(item); | ||||||
|  |     let return_url = item.get_return_url()?; | ||||||
|  |     let shopper_name = get_shopper_name(item); | ||||||
|  |     let shopper_email = item.request.email.clone(); | ||||||
|  |     let billing_address = get_address_info(item.address.billing.as_ref()); | ||||||
|  |     let delivery_address = get_address_info(item.address.shipping.as_ref()); | ||||||
|  |     let country_code = get_country_code(item); | ||||||
|  |     let line_items = Some(get_line_items(item)); | ||||||
|  |     let telephone_number = get_telephone_number(item); | ||||||
|  |     Ok(AdyenPaymentRequest { | ||||||
|  |         amount, | ||||||
|  |         merchant_account: auth_type.merchant_account, | ||||||
|  |         payment_method, | ||||||
|  |         reference: item.payment_id.to_string(), | ||||||
|  |         return_url, | ||||||
|  |         shopper_interaction, | ||||||
|  |         recurring_processing_model, | ||||||
|  |         browser_info, | ||||||
|  |         additional_data, | ||||||
|  |         telephone_number, | ||||||
|  |         shopper_name, | ||||||
|  |         shopper_email, | ||||||
|  |         billing_address, | ||||||
|  |         delivery_address, | ||||||
|  |         country_code, | ||||||
|  |         line_items, | ||||||
|  |     }) | ||||||
|  | } | ||||||
|  |  | ||||||
| impl TryFrom<&types::PaymentsCancelRouterData> for AdyenCancelRequest { | impl TryFrom<&types::PaymentsCancelRouterData> for AdyenCancelRequest { | ||||||
|     type Error = error_stack::Report<errors::ConnectorError>; |     type Error = error_stack::Report<errors::ConnectorError>; | ||||||
|     fn try_from(item: &types::PaymentsCancelRouterData) -> Result<Self, Self::Error> { |     fn try_from(item: &types::PaymentsCancelRouterData) -> Result<Self, Self::Error> { | ||||||
| @ -483,7 +701,7 @@ pub fn get_adyen_response( | |||||||
|                 storage_enums::AttemptStatus::Charged |                 storage_enums::AttemptStatus::Charged | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         AdyenStatus::Refused => storage_enums::AttemptStatus::Failure, |         AdyenStatus::Refused | AdyenStatus::Cancelled => storage_enums::AttemptStatus::Failure, | ||||||
|         _ => storage_enums::AttemptStatus::Pending, |         _ => storage_enums::AttemptStatus::Pending, | ||||||
|     }; |     }; | ||||||
|     let error = if response.refusal_reason.is_some() || response.refusal_reason_code.is_some() { |     let error = if response.refusal_reason.is_some() || response.refusal_reason_code.is_some() { | ||||||
| @ -709,6 +927,11 @@ impl<F> TryFrom<&types::RefundsRouterData<F>> for AdyenRefundRequest { | |||||||
|         let auth_type = AdyenAuthType::try_from(&item.connector_auth_type)?; |         let auth_type = AdyenAuthType::try_from(&item.connector_auth_type)?; | ||||||
|         Ok(Self { |         Ok(Self { | ||||||
|             merchant_account: auth_type.merchant_account, |             merchant_account: auth_type.merchant_account, | ||||||
|  |             amount: Amount { | ||||||
|  |                 currency: item.request.currency.to_string(), | ||||||
|  |                 value: item.request.refund_amount, | ||||||
|  |             }, | ||||||
|  |             merchant_refund_reason: item.request.reason.clone(), | ||||||
|             reference: item.request.refund_id.clone(), |             reference: item.request.refund_id.clone(), | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -40,6 +40,7 @@ pub trait PaymentsRequestData { | |||||||
|     fn get_billing_country(&self) -> Result<String, Error>; |     fn get_billing_country(&self) -> Result<String, Error>; | ||||||
|     fn get_billing_phone(&self) -> Result<&api::PhoneDetails, Error>; |     fn get_billing_phone(&self) -> Result<&api::PhoneDetails, Error>; | ||||||
|     fn get_card(&self) -> Result<api::Card, Error>; |     fn get_card(&self) -> Result<api::Card, Error>; | ||||||
|  |     fn get_return_url(&self) -> Result<String, Error>; | ||||||
| } | } | ||||||
|  |  | ||||||
| pub trait RefundsRequestData { | pub trait RefundsRequestData { | ||||||
| @ -91,6 +92,12 @@ impl PaymentsRequestData for types::PaymentsAuthorizeRouterData { | |||||||
|             .as_ref() |             .as_ref() | ||||||
|             .ok_or_else(missing_field_err("billing")) |             .ok_or_else(missing_field_err("billing")) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     fn get_return_url(&self) -> Result<String, Error> { | ||||||
|  |         self.router_return_url | ||||||
|  |             .clone() | ||||||
|  |             .ok_or_else(missing_field_err("router_return_url")) | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| pub trait CardData { | pub trait CardData { | ||||||
|  | |||||||
							
								
								
									
										445
									
								
								crates/router/tests/connectors/adyen.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										445
									
								
								crates/router/tests/connectors/adyen.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,445 @@ | |||||||
|  | use api_models::payments::{Address, AddressDetails}; | ||||||
|  | use masking::Secret; | ||||||
|  | use router::types::{self, api, storage::enums, PaymentAddress}; | ||||||
|  |  | ||||||
|  | use crate::{ | ||||||
|  |     connector_auth, | ||||||
|  |     utils::{self, ConnectorActions, PaymentInfo}, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | #[derive(Clone, Copy)] | ||||||
|  | struct AdyenTest; | ||||||
|  | impl ConnectorActions for AdyenTest {} | ||||||
|  | impl utils::Connector for AdyenTest { | ||||||
|  |     fn get_data(&self) -> types::api::ConnectorData { | ||||||
|  |         use router::connector::Adyen; | ||||||
|  |         types::api::ConnectorData { | ||||||
|  |             connector: Box::new(&Adyen), | ||||||
|  |             connector_name: types::Connector::Adyen, | ||||||
|  |             get_token: types::api::GetToken::Connector, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn get_auth_token(&self) -> types::ConnectorAuthType { | ||||||
|  |         types::ConnectorAuthType::from( | ||||||
|  |             connector_auth::ConnectorAuthentication::new() | ||||||
|  |                 .adyen | ||||||
|  |                 .expect("Missing connector authentication configuration"), | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn get_name(&self) -> String { | ||||||
|  |         "adyen".to_string() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl AdyenTest { | ||||||
|  |     fn get_payment_info() -> Option<PaymentInfo> { | ||||||
|  |         Some(PaymentInfo { | ||||||
|  |             address: Some(PaymentAddress { | ||||||
|  |                 billing: Some(Address { | ||||||
|  |                     address: Some(AddressDetails { | ||||||
|  |                         country: Some("US".to_string()), | ||||||
|  |                         ..Default::default() | ||||||
|  |                     }), | ||||||
|  |                     phone: None, | ||||||
|  |                 }), | ||||||
|  |                 ..Default::default() | ||||||
|  |             }), | ||||||
|  |             router_return_url: Some(String::from("http://localhost:8080")), | ||||||
|  |             ..Default::default() | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn get_payment_authorize_data( | ||||||
|  |         card_number: &str, | ||||||
|  |         card_exp_month: &str, | ||||||
|  |         card_exp_year: &str, | ||||||
|  |         card_cvc: &str, | ||||||
|  |         capture_method: enums::CaptureMethod, | ||||||
|  |     ) -> Option<types::PaymentsAuthorizeData> { | ||||||
|  |         Some(types::PaymentsAuthorizeData { | ||||||
|  |             amount: 3500, | ||||||
|  |             currency: enums::Currency::USD, | ||||||
|  |             payment_method_data: types::api::PaymentMethod::Card(types::api::Card { | ||||||
|  |                 card_number: Secret::new(card_number.to_string()), | ||||||
|  |                 card_exp_month: Secret::new(card_exp_month.to_string()), | ||||||
|  |                 card_exp_year: Secret::new(card_exp_year.to_string()), | ||||||
|  |                 card_holder_name: Secret::new("John Doe".to_string()), | ||||||
|  |                 card_cvc: Secret::new(card_cvc.to_string()), | ||||||
|  |             }), | ||||||
|  |             confirm: true, | ||||||
|  |             statement_descriptor_suffix: None, | ||||||
|  |             setup_future_usage: None, | ||||||
|  |             mandate_id: None, | ||||||
|  |             off_session: None, | ||||||
|  |             setup_mandate_details: None, | ||||||
|  |             capture_method: Some(capture_method), | ||||||
|  |             browser_info: None, | ||||||
|  |             order_details: None, | ||||||
|  |             email: None, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static CONNECTOR: AdyenTest = AdyenTest {}; | ||||||
|  |  | ||||||
|  | // Cards Positive Tests | ||||||
|  | // Creates a payment using the manual capture flow (Non 3DS). | ||||||
|  | #[actix_web::test] | ||||||
|  | async fn should_only_authorize_payment() { | ||||||
|  |     let response = CONNECTOR | ||||||
|  |         .authorize_payment( | ||||||
|  |             AdyenTest::get_payment_authorize_data( | ||||||
|  |                 "4111111111111111", | ||||||
|  |                 "03", | ||||||
|  |                 "2030", | ||||||
|  |                 "737", | ||||||
|  |                 enums::CaptureMethod::Manual, | ||||||
|  |             ), | ||||||
|  |             AdyenTest::get_payment_info(), | ||||||
|  |         ) | ||||||
|  |         .await | ||||||
|  |         .expect("Authorize payment response"); | ||||||
|  |     assert_eq!(response.status, enums::AttemptStatus::Authorized); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Captures a payment using the manual capture flow (Non 3DS). | ||||||
|  | #[actix_web::test] | ||||||
|  | async fn should_capture_authorized_payment() { | ||||||
|  |     let response = CONNECTOR | ||||||
|  |         .authorize_and_capture_payment( | ||||||
|  |             AdyenTest::get_payment_authorize_data( | ||||||
|  |                 "370000000000002", | ||||||
|  |                 "03", | ||||||
|  |                 "2030", | ||||||
|  |                 "7373", | ||||||
|  |                 enums::CaptureMethod::Manual, | ||||||
|  |             ), | ||||||
|  |             None, | ||||||
|  |             AdyenTest::get_payment_info(), | ||||||
|  |         ) | ||||||
|  |         .await | ||||||
|  |         .expect("Capture payment response"); | ||||||
|  |     assert_eq!(response.status, enums::AttemptStatus::Charged); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Partially captures a payment using the manual capture flow (Non 3DS). | ||||||
|  | #[actix_web::test] | ||||||
|  | async fn should_partially_capture_authorized_payment() { | ||||||
|  |     let response = CONNECTOR | ||||||
|  |         .authorize_and_capture_payment( | ||||||
|  |             AdyenTest::get_payment_authorize_data( | ||||||
|  |                 "4293189100000008", | ||||||
|  |                 "03", | ||||||
|  |                 "2030", | ||||||
|  |                 "737", | ||||||
|  |                 enums::CaptureMethod::Manual, | ||||||
|  |             ), | ||||||
|  |             Some(types::PaymentsCaptureData { | ||||||
|  |                 amount_to_capture: Some(50), | ||||||
|  |                 ..utils::PaymentCaptureType::default().0 | ||||||
|  |             }), | ||||||
|  |             AdyenTest::get_payment_info(), | ||||||
|  |         ) | ||||||
|  |         .await | ||||||
|  |         .expect("Capture payment response"); | ||||||
|  |     assert_eq!(response.status, enums::AttemptStatus::Charged); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Voids a payment using the manual capture flow (Non 3DS). | ||||||
|  | #[actix_web::test] | ||||||
|  | async fn should_void_authorized_payment() { | ||||||
|  |     let response = CONNECTOR | ||||||
|  |         .authorize_and_void_payment( | ||||||
|  |             AdyenTest::get_payment_authorize_data( | ||||||
|  |                 "4293189100000008", | ||||||
|  |                 "03", | ||||||
|  |                 "2030", | ||||||
|  |                 "737", | ||||||
|  |                 enums::CaptureMethod::Manual, | ||||||
|  |             ), | ||||||
|  |             Some(types::PaymentsCancelData { | ||||||
|  |                 connector_transaction_id: String::from(""), | ||||||
|  |                 cancellation_reason: Some("requested_by_customer".to_string()), | ||||||
|  |             }), | ||||||
|  |             AdyenTest::get_payment_info(), | ||||||
|  |         ) | ||||||
|  |         .await | ||||||
|  |         .expect("Void payment response"); | ||||||
|  |     assert_eq!(response.status, enums::AttemptStatus::Voided); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Refunds a payment using the manual capture flow (Non 3DS). | ||||||
|  | #[actix_web::test] | ||||||
|  | async fn should_refund_manually_captured_payment() { | ||||||
|  |     let response = CONNECTOR | ||||||
|  |         .capture_payment_and_refund( | ||||||
|  |             AdyenTest::get_payment_authorize_data( | ||||||
|  |                 "370000000000002", | ||||||
|  |                 "03", | ||||||
|  |                 "2030", | ||||||
|  |                 "7373", | ||||||
|  |                 enums::CaptureMethod::Manual, | ||||||
|  |             ), | ||||||
|  |             None, | ||||||
|  |             Some(types::RefundsData { | ||||||
|  |                 refund_amount: 1500, | ||||||
|  |                 reason: Some("CUSTOMER REQUEST".to_string()), | ||||||
|  |                 ..utils::PaymentRefundType::default().0 | ||||||
|  |             }), | ||||||
|  |             AdyenTest::get_payment_info(), | ||||||
|  |         ) | ||||||
|  |         .await | ||||||
|  |         .unwrap(); | ||||||
|  |     assert_eq!( | ||||||
|  |         response.response.unwrap().refund_status, | ||||||
|  |         enums::RefundStatus::Success, | ||||||
|  |     ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Partially refunds a payment using the manual capture flow (Non 3DS). | ||||||
|  | #[actix_web::test] | ||||||
|  | async fn should_partially_refund_manually_captured_payment() { | ||||||
|  |     let response = CONNECTOR | ||||||
|  |         .capture_payment_and_refund( | ||||||
|  |             AdyenTest::get_payment_authorize_data( | ||||||
|  |                 "2222400070000005", | ||||||
|  |                 "03", | ||||||
|  |                 "2030", | ||||||
|  |                 "737", | ||||||
|  |                 enums::CaptureMethod::Manual, | ||||||
|  |             ), | ||||||
|  |             None, | ||||||
|  |             Some(types::RefundsData { | ||||||
|  |                 refund_amount: 1500, | ||||||
|  |                 reason: Some("CUSTOMER REQUEST".to_string()), | ||||||
|  |                 ..utils::PaymentRefundType::default().0 | ||||||
|  |             }), | ||||||
|  |             AdyenTest::get_payment_info(), | ||||||
|  |         ) | ||||||
|  |         .await | ||||||
|  |         .unwrap(); | ||||||
|  |     assert_eq!( | ||||||
|  |         response.response.unwrap().refund_status, | ||||||
|  |         enums::RefundStatus::Success, | ||||||
|  |     ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Creates a payment using the automatic capture flow (Non 3DS). | ||||||
|  | #[actix_web::test] | ||||||
|  | async fn should_make_payment() { | ||||||
|  |     let authorize_response = CONNECTOR | ||||||
|  |         .make_payment( | ||||||
|  |             AdyenTest::get_payment_authorize_data( | ||||||
|  |                 "2222400070000005", | ||||||
|  |                 "03", | ||||||
|  |                 "2030", | ||||||
|  |                 "737", | ||||||
|  |                 enums::CaptureMethod::Manual, | ||||||
|  |             ), | ||||||
|  |             AdyenTest::get_payment_info(), | ||||||
|  |         ) | ||||||
|  |         .await | ||||||
|  |         .unwrap(); | ||||||
|  |     assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Refunds a payment using the automatic capture flow (Non 3DS). | ||||||
|  | #[actix_web::test] | ||||||
|  | async fn should_refund_auto_captured_payment() { | ||||||
|  |     let response = CONNECTOR | ||||||
|  |         .make_payment_and_refund( | ||||||
|  |             AdyenTest::get_payment_authorize_data( | ||||||
|  |                 "2222400070000005", | ||||||
|  |                 "03", | ||||||
|  |                 "2030", | ||||||
|  |                 "737", | ||||||
|  |                 enums::CaptureMethod::Automatic, | ||||||
|  |             ), | ||||||
|  |             Some(types::RefundsData { | ||||||
|  |                 refund_amount: 1000, | ||||||
|  |                 reason: Some("CUSTOMER REQUEST".to_string()), | ||||||
|  |                 ..utils::PaymentRefundType::default().0 | ||||||
|  |             }), | ||||||
|  |             AdyenTest::get_payment_info(), | ||||||
|  |         ) | ||||||
|  |         .await | ||||||
|  |         .unwrap(); | ||||||
|  |     assert_eq!( | ||||||
|  |         response.response.unwrap().refund_status, | ||||||
|  |         enums::RefundStatus::Success, | ||||||
|  |     ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Partially refunds a payment using the automatic capture flow (Non 3DS). | ||||||
|  | #[actix_web::test] | ||||||
|  | async fn should_partially_refund_succeeded_payment() { | ||||||
|  |     let refund_response = CONNECTOR | ||||||
|  |         .make_payment_and_refund( | ||||||
|  |             AdyenTest::get_payment_authorize_data( | ||||||
|  |                 "4293189100000008", | ||||||
|  |                 "03", | ||||||
|  |                 "2030", | ||||||
|  |                 "737", | ||||||
|  |                 enums::CaptureMethod::Automatic, | ||||||
|  |             ), | ||||||
|  |             Some(types::RefundsData { | ||||||
|  |                 refund_amount: 500, | ||||||
|  |                 reason: Some("CUSTOMER REQUEST".to_string()), | ||||||
|  |                 ..utils::PaymentRefundType::default().0 | ||||||
|  |             }), | ||||||
|  |             AdyenTest::get_payment_info(), | ||||||
|  |         ) | ||||||
|  |         .await | ||||||
|  |         .unwrap(); | ||||||
|  |     assert_eq!( | ||||||
|  |         refund_response.response.unwrap().refund_status, | ||||||
|  |         enums::RefundStatus::Success, | ||||||
|  |     ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Creates multiple refunds against a payment using the automatic capture flow (Non 3DS). | ||||||
|  | #[actix_web::test] | ||||||
|  | async fn should_refund_succeeded_payment_multiple_times() { | ||||||
|  |     CONNECTOR | ||||||
|  |         .make_payment_and_multiple_refund( | ||||||
|  |             AdyenTest::get_payment_authorize_data( | ||||||
|  |                 "2222400070000005", | ||||||
|  |                 "03", | ||||||
|  |                 "2030", | ||||||
|  |                 "737", | ||||||
|  |                 enums::CaptureMethod::Automatic, | ||||||
|  |             ), | ||||||
|  |             Some(types::RefundsData { | ||||||
|  |                 refund_amount: 100, | ||||||
|  |                 reason: Some("CUSTOMER REQUEST".to_string()), | ||||||
|  |                 ..utils::PaymentRefundType::default().0 | ||||||
|  |             }), | ||||||
|  |             AdyenTest::get_payment_info(), | ||||||
|  |         ) | ||||||
|  |         .await; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Cards Negative scenerios | ||||||
|  | // Creates a payment with incorrect card number. | ||||||
|  | #[actix_web::test] | ||||||
|  | async fn should_fail_payment_for_incorrect_card_number() { | ||||||
|  |     let response = CONNECTOR | ||||||
|  |         .make_payment( | ||||||
|  |             Some(types::PaymentsAuthorizeData { | ||||||
|  |                 payment_method_data: types::api::PaymentMethod::Card(api::Card { | ||||||
|  |                     card_number: Secret::new("1234567891011".to_string()), | ||||||
|  |                     ..utils::CCardType::default().0 | ||||||
|  |                 }), | ||||||
|  |                 ..utils::PaymentAuthorizeType::default().0 | ||||||
|  |             }), | ||||||
|  |             AdyenTest::get_payment_info(), | ||||||
|  |         ) | ||||||
|  |         .await | ||||||
|  |         .unwrap(); | ||||||
|  |     assert_eq!( | ||||||
|  |         response.response.unwrap_err().message, | ||||||
|  |         "Invalid card number", | ||||||
|  |     ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Creates a payment with empty card number. | ||||||
|  | #[actix_web::test] | ||||||
|  | async fn should_fail_payment_for_empty_card_number() { | ||||||
|  |     let response = CONNECTOR | ||||||
|  |         .make_payment( | ||||||
|  |             Some(types::PaymentsAuthorizeData { | ||||||
|  |                 payment_method_data: types::api::PaymentMethod::Card(api::Card { | ||||||
|  |                     card_number: Secret::new(String::from("")), | ||||||
|  |                     ..utils::CCardType::default().0 | ||||||
|  |                 }), | ||||||
|  |                 ..utils::PaymentAuthorizeType::default().0 | ||||||
|  |             }), | ||||||
|  |             AdyenTest::get_payment_info(), | ||||||
|  |         ) | ||||||
|  |         .await | ||||||
|  |         .unwrap(); | ||||||
|  |     let x = response.response.unwrap_err(); | ||||||
|  |     assert_eq!(x.message, "Missing payment method details: number",); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Creates a payment with incorrect CVC. | ||||||
|  | #[actix_web::test] | ||||||
|  | async fn should_fail_payment_for_incorrect_cvc() { | ||||||
|  |     let response = CONNECTOR | ||||||
|  |         .make_payment( | ||||||
|  |             Some(types::PaymentsAuthorizeData { | ||||||
|  |                 payment_method_data: types::api::PaymentMethod::Card(api::Card { | ||||||
|  |                     card_cvc: Secret::new("12345".to_string()), | ||||||
|  |                     ..utils::CCardType::default().0 | ||||||
|  |                 }), | ||||||
|  |                 ..utils::PaymentAuthorizeType::default().0 | ||||||
|  |             }), | ||||||
|  |             AdyenTest::get_payment_info(), | ||||||
|  |         ) | ||||||
|  |         .await | ||||||
|  |         .unwrap(); | ||||||
|  |     assert_eq!( | ||||||
|  |         response.response.unwrap_err().message, | ||||||
|  |         "CVC is not the right length", | ||||||
|  |     ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Creates a payment with incorrect expiry month. | ||||||
|  | #[actix_web::test] | ||||||
|  | async fn should_fail_payment_for_invalid_exp_month() { | ||||||
|  |     let response = CONNECTOR | ||||||
|  |         .make_payment( | ||||||
|  |             Some(types::PaymentsAuthorizeData { | ||||||
|  |                 payment_method_data: types::api::PaymentMethod::Card(api::Card { | ||||||
|  |                     card_exp_month: Secret::new("20".to_string()), | ||||||
|  |                     ..utils::CCardType::default().0 | ||||||
|  |                 }), | ||||||
|  |                 ..utils::PaymentAuthorizeType::default().0 | ||||||
|  |             }), | ||||||
|  |             AdyenTest::get_payment_info(), | ||||||
|  |         ) | ||||||
|  |         .await | ||||||
|  |         .unwrap(); | ||||||
|  |     assert_eq!( | ||||||
|  |         response.response.unwrap_err().message, | ||||||
|  |         "The provided Expiry Date is not valid.: Expiry month should be between 1 and 12 inclusive: 20", | ||||||
|  |     ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Creates a payment with incorrect expiry year. | ||||||
|  | #[actix_web::test] | ||||||
|  | async fn should_fail_payment_for_incorrect_expiry_year() { | ||||||
|  |     let response = CONNECTOR | ||||||
|  |         .make_payment( | ||||||
|  |             Some(types::PaymentsAuthorizeData { | ||||||
|  |                 payment_method_data: types::api::PaymentMethod::Card(api::Card { | ||||||
|  |                     card_exp_year: Secret::new("2000".to_string()), | ||||||
|  |                     ..utils::CCardType::default().0 | ||||||
|  |                 }), | ||||||
|  |                 ..utils::PaymentAuthorizeType::default().0 | ||||||
|  |             }), | ||||||
|  |             AdyenTest::get_payment_info(), | ||||||
|  |         ) | ||||||
|  |         .await | ||||||
|  |         .unwrap(); | ||||||
|  |     assert_eq!(response.response.unwrap_err().message, "Expired Card",); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Captures a payment using invalid connector payment id. | ||||||
|  | #[actix_web::test] | ||||||
|  | async fn should_fail_capture_for_invalid_payment() { | ||||||
|  |     let capture_response = CONNECTOR | ||||||
|  |         .capture_payment("123456789".to_string(), None, AdyenTest::get_payment_info()) | ||||||
|  |         .await | ||||||
|  |         .unwrap(); | ||||||
|  |     assert_eq!( | ||||||
|  |         capture_response.response.unwrap_err().message, | ||||||
|  |         String::from("Original pspReference required for this operation") | ||||||
|  |     ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Connector dependent test cases goes here | ||||||
|  |  | ||||||
|  | // [#478]: add unit tests for non 3DS, wallets & webhooks in connector tests | ||||||
| @ -4,6 +4,7 @@ use serde::Deserialize; | |||||||
| #[derive(Debug, Deserialize, Clone)] | #[derive(Debug, Deserialize, Clone)] | ||||||
| pub(crate) struct ConnectorAuthentication { | pub(crate) struct ConnectorAuthentication { | ||||||
|     pub aci: Option<BodyKey>, |     pub aci: Option<BodyKey>, | ||||||
|  |     pub adyen: Option<BodyKey>, | ||||||
|     pub authorizedotnet: Option<BodyKey>, |     pub authorizedotnet: Option<BodyKey>, | ||||||
|     pub checkout: Option<BodyKey>, |     pub checkout: Option<BodyKey>, | ||||||
|     pub cybersource: Option<SignatureKey>, |     pub cybersource: Option<SignatureKey>, | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)] | #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)] | ||||||
|  |  | ||||||
| mod aci; | mod aci; | ||||||
|  | mod adyen; | ||||||
| mod authorizedotnet; | mod authorizedotnet; | ||||||
| mod checkout; | mod checkout; | ||||||
| mod connector_auth; | mod connector_auth; | ||||||
|  | |||||||
| @ -5,6 +5,10 @@ | |||||||
| api_key = "Bearer MyApiKey" | api_key = "Bearer MyApiKey" | ||||||
| key1 = "MyEntityId" | key1 = "MyEntityId" | ||||||
|  |  | ||||||
|  | [adyen] | ||||||
|  | api_key = "Bearer MyApiKey" | ||||||
|  | key1 = "MerchantId" | ||||||
|  |  | ||||||
| [authorizedotnet] | [authorizedotnet] | ||||||
| api_key = "MyMerchantName" | api_key = "MyMerchantName" | ||||||
| key1 = "MyTransactionKey" | key1 = "MyTransactionKey" | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Arjun Karthik
					Arjun Karthik