feat(connector): add afterpay, klarna, affirm support in adyen connector (#516)

This commit is contained in:
Arjun Karthik
2023-02-10 02:37:09 +05:30
committed by GitHub
parent 2310e12bf7
commit f6eac13b21
7 changed files with 837 additions and 156 deletions

View File

@ -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,
)) ))

View File

@ -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(),
}) })
} }

View File

@ -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 {

View 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

View File

@ -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>,

View File

@ -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;

View File

@ -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"