diff --git a/crates/router/src/connector/adyen.rs b/crates/router/src/connector/adyen.rs index 8a6dde9c4a..f126a75f43 100644 --- a/crates/router/src/connector/adyen.rs +++ b/crates/router/src/connector/adyen.rs @@ -561,7 +561,7 @@ impl services::ConnectorIntegration CustomResult { let connector_payment_id = req.request.connector_transaction_id.clone(); Ok(format!( - "{}v68/payments/{}/reversals", + "{}v68/payments/{}/refunds", self.base_url(connectors), connector_payment_id, )) diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index 245e1c3549..de81160477 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -1,19 +1,21 @@ use std::{collections::HashMap, str::FromStr}; use error_stack::{IntoReport, ResultExt}; +use masking::PeekInterface; use reqwest::Url; use serde::{Deserialize, Serialize}; use crate::{ + connector::utils::{self, PaymentsRequestData}, consts, core::errors, - pii, services, + pii::{self, Email, Secret}, + services, types::{ self, api::{self, enums as api_enums}, storage::enums as storage_enums, }, - utils::OptionExt, }; // Adyen Types Definition @@ -41,8 +43,39 @@ pub enum AuthType { PreAuth, } #[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct AdditionalData { authorisation_type: AuthType, + manual_capture: bool, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ShopperName { + first_name: Option>, + last_name: Option>, +} + +#[derive(Default, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Address { + city: Option, + country: Option, + house_number_or_name: Option>, + postal_code: Option>, + state_or_province: Option>, + street: Option>, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LineItem { + amount_excluding_tax: Option, + amount_including_tax: Option, + description: Option, + id: Option, + tax_amount: Option, + quantity: Option, } #[derive(Debug, Serialize)] @@ -58,6 +91,13 @@ pub struct AdyenPaymentRequest { #[serde(skip_serializing_if = "Option::is_none")] recurring_processing_model: Option, additional_data: Option, + shopper_name: Option, + shopper_email: Option>, + telephone_number: Option>, + billing_address: Option
, + delivery_address: Option
, + country_code: Option, + line_items: Option>, } #[derive(Debug, Serialize)] @@ -175,17 +215,20 @@ pub enum AdyenPaymentMethod { AdyenPaypal(AdyenPaypal), Gpay(AdyenGPay), ApplePay(AdyenApplePay), + AfterPay(AdyenPayLaterData), + AdyenKlarna(AdyenPayLaterData), + AdyenAffirm(AdyenPayLaterData), } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AdyenCard { #[serde(rename = "type")] - payment_type: String, - number: Option>, - expiry_month: Option>, - expiry_year: Option>, - cvc: Option>, + payment_type: PaymentType, + number: Secret, + expiry_month: Secret, + expiry_year: Secret, + cvc: Option>, } #[derive(Default, Debug, Serialize, Deserialize)] @@ -213,13 +256,13 @@ pub enum CancelStatus { #[serde(rename_all = "camelCase")] pub struct AdyenPaypal { #[serde(rename = "type")] - payment_type: String, + payment_type: PaymentType, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AdyenGPay { #[serde(rename = "type")] - payment_type: String, + payment_type: PaymentType, #[serde(rename = "googlePayToken")] google_pay_token: String, } @@ -227,16 +270,24 @@ pub struct AdyenGPay { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AdyenApplePay { #[serde(rename = "type")] - payment_type: String, + payment_type: PaymentType, #[serde(rename = "applePayToken")] apple_pay_token: String, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AdyenPayLaterData { + #[serde(rename = "type")] + payment_type: PaymentType, +} + // Refunds Request and Response #[derive(Default, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AdyenRefundRequest { merchant_account: String, + amount: Amount, + merchant_refund_reason: Option, reference: String, } @@ -255,6 +306,18 @@ pub struct AdyenAuthType { 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 { type Error = error_stack::Report; fn try_from(auth_type: &types::ConnectorAuthType) -> Result { @@ -269,159 +332,314 @@ impl TryFrom<&types::ConnectorAuthType> for AdyenAuthType { } } -impl TryFrom<&types::BrowserInformation> for AdyenBrowserInfo { - type Error = error_stack::Report; - fn try_from(item: &types::BrowserInformation) -> Result { - 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 impl TryFrom<&types::PaymentsAuthorizeRouterData> for AdyenPaymentRequest { type Error = error_stack::Report; fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { - let auth_type = AdyenAuthType::try_from(&item.connector_auth_type)?; - let reference = item.payment_id.to_string(); - let amount = Amount { - currency: item.request.currency.to_string(), - 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) + match item.payment_method { + storage_models::enums::PaymentMethodType::Card => get_card_specific_payment_data(item), + storage_models::enums::PaymentMethodType::PayLater => { + get_paylater_specific_payment_data(item) } - _ => None, - }; - - 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)) + storage_models::enums::PaymentMethodType::Wallet => { + get_wallet_specific_payment_data(item) } - - 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, - }) + _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), + } } } +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 { + match item.request.setup_future_usage { + Some(storage_enums::FutureUsage::OffSession) => { + Some(AdyenRecurringModel::UnscheduledCardOnFile) + } + _ => None, + } +} + +fn get_browser_info(item: &types::PaymentsAuthorizeRouterData) -> Option { + 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 { + 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.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 { + 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> { + 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 { + 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 { + 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> { + 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> { + 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> { + 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> { + 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 { type Error = error_stack::Report; fn try_from(item: &types::PaymentsCancelRouterData) -> Result { @@ -483,7 +701,7 @@ pub fn get_adyen_response( storage_enums::AttemptStatus::Charged } } - AdyenStatus::Refused => storage_enums::AttemptStatus::Failure, + AdyenStatus::Refused | AdyenStatus::Cancelled => storage_enums::AttemptStatus::Failure, _ => storage_enums::AttemptStatus::Pending, }; let error = if response.refusal_reason.is_some() || response.refusal_reason_code.is_some() { @@ -709,6 +927,11 @@ impl TryFrom<&types::RefundsRouterData> for AdyenRefundRequest { let auth_type = AdyenAuthType::try_from(&item.connector_auth_type)?; Ok(Self { 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(), }) } diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 1e7c350ce2..f7758b0401 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -40,6 +40,7 @@ pub trait PaymentsRequestData { fn get_billing_country(&self) -> Result; fn get_billing_phone(&self) -> Result<&api::PhoneDetails, Error>; fn get_card(&self) -> Result; + fn get_return_url(&self) -> Result; } pub trait RefundsRequestData { @@ -91,6 +92,12 @@ impl PaymentsRequestData for types::PaymentsAuthorizeRouterData { .as_ref() .ok_or_else(missing_field_err("billing")) } + + fn get_return_url(&self) -> Result { + self.router_return_url + .clone() + .ok_or_else(missing_field_err("router_return_url")) + } } pub trait CardData { diff --git a/crates/router/tests/connectors/adyen.rs b/crates/router/tests/connectors/adyen.rs new file mode 100644 index 0000000000..da0a412784 --- /dev/null +++ b/crates/router/tests/connectors/adyen.rs @@ -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 { + 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 { + 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 diff --git a/crates/router/tests/connectors/connector_auth.rs b/crates/router/tests/connectors/connector_auth.rs index 5ac367e68a..1a13893234 100644 --- a/crates/router/tests/connectors/connector_auth.rs +++ b/crates/router/tests/connectors/connector_auth.rs @@ -4,6 +4,7 @@ use serde::Deserialize; #[derive(Debug, Deserialize, Clone)] pub(crate) struct ConnectorAuthentication { pub aci: Option, + pub adyen: Option, pub authorizedotnet: Option, pub checkout: Option, pub cybersource: Option, diff --git a/crates/router/tests/connectors/main.rs b/crates/router/tests/connectors/main.rs index 87e15657a2..1844076982 100644 --- a/crates/router/tests/connectors/main.rs +++ b/crates/router/tests/connectors/main.rs @@ -1,6 +1,7 @@ #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)] mod aci; +mod adyen; mod authorizedotnet; mod checkout; mod connector_auth; diff --git a/crates/router/tests/connectors/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index a9587db05a..368b871ea8 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -5,6 +5,10 @@ api_key = "Bearer MyApiKey" key1 = "MyEntityId" +[adyen] +api_key = "Bearer MyApiKey" +key1 = "MerchantId" + [authorizedotnet] api_key = "MyMerchantName" key1 = "MyTransactionKey"