mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-27 19:46:48 +08:00
feat(connector): [Adyen] implement Swish for Adyen (#1701)
Co-authored-by: Sangamesh <sangamesh.kulkarni@juspay.in> Co-authored-by: swangi-kumari <swangi.12015941@lpu.in>
This commit is contained in:
2
.github/testcases/ui_tests.json
vendored
2
.github/testcases/ui_tests.json
vendored
@ -1258,7 +1258,7 @@
|
||||
"id": 210,
|
||||
"name": "Adyen Swish",
|
||||
"connector": "adyen_uk",
|
||||
"request": "{\"amount\":6540,\"currency\":\"SEK\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com/\",\"payment_method\":\"wallet\",\"payment_method_type\":\"swish\",\"payment_method_data\":{\"wallet\":{\"swish\":{}}}}"
|
||||
"request": "{\"amount\":6540,\"currency\":\"SEK\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com/\",\"payment_method\":\"wallet\",\"payment_method_type\":\"swish\",\"payment_method_data\":{\"wallet\":{\"swish_qr\":{}}}}"
|
||||
},
|
||||
"211": {
|
||||
"id": 211,
|
||||
|
||||
@ -330,6 +330,7 @@ online_banking_fpx = {country = "MY", currency = "MYR"}
|
||||
online_banking_thailand = {country = "TH", currency = "THB"}
|
||||
touch_n_go = {country = "MY", currency = "MYR"}
|
||||
atome = {country = "MY,SG", currency = "MYR,SGD"}
|
||||
swish = {country = "SE", currency = "SEK"}
|
||||
|
||||
[pm_filters.zen]
|
||||
credit = { not_available_flows = { capture_method = "manual" } }
|
||||
|
||||
@ -262,6 +262,7 @@ online_banking_fpx = {country = "MY", currency = "MYR"}
|
||||
online_banking_thailand = {country = "TH", currency = "THB"}
|
||||
touch_n_go = {country = "MY", currency = "MYR"}
|
||||
atome = {country = "MY,SG", currency = "MYR,SGD"}
|
||||
swish = {country = "SE", currency = "SEK"}
|
||||
|
||||
[pm_filters.braintree]
|
||||
paypal = { currency = "AUD,BRL,CAD,CNY,CZK,DKK,EUR,HKD,HUF,ILS,JPY,MYR,MXN,TWD,NZD,NOK,PHP,PLN,GBP,RUB,SGD,SEK,CHF,THB,USD" }
|
||||
|
||||
@ -212,6 +212,7 @@ online_banking_fpx = {country = "MY", currency = "MYR"}
|
||||
online_banking_thailand = {country = "TH", currency = "THB"}
|
||||
touch_n_go = {country = "MY", currency = "MYR"}
|
||||
atome = {country = "MY,SG", currency = "MYR,SGD"}
|
||||
swish = {country = "SE", currency = "SEK"}
|
||||
|
||||
[pm_filters.zen]
|
||||
credit = { not_available_flows = { capture_method = "manual" } }
|
||||
|
||||
@ -873,7 +873,6 @@ pub enum BankRedirectData {
|
||||
#[schema(example = "en")]
|
||||
preferred_language: String,
|
||||
},
|
||||
Swish {},
|
||||
Trustly {
|
||||
/// The country for bank payment
|
||||
#[schema(value_type = CountryAlpha2, example = "US")]
|
||||
@ -1032,6 +1031,8 @@ pub enum WalletData {
|
||||
WeChatPayRedirect(Box<WeChatPayRedirection>),
|
||||
/// The wallet data for WeChat Pay Display QrCode
|
||||
WeChatPayQr(Box<WeChatPayQr>),
|
||||
// The wallet data for Swish
|
||||
SwishQr(SwishQrData),
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)]
|
||||
@ -1129,6 +1130,9 @@ pub struct PayPalWalletData {
|
||||
#[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)]
|
||||
pub struct TouchNGoRedirection {}
|
||||
|
||||
#[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)]
|
||||
pub struct SwishQrData {}
|
||||
|
||||
#[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)]
|
||||
pub struct GpayTokenizationData {
|
||||
/// The type of the token
|
||||
@ -1378,7 +1382,7 @@ pub struct BankTransferNextStepsData {
|
||||
pub receiver: ReceiverDetails,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub struct QrCodeNextStepsInstruction {
|
||||
pub image_data_url: Url,
|
||||
}
|
||||
|
||||
@ -1265,7 +1265,7 @@ impl api::IncomingWebhook for Adyen {
|
||||
.change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?;
|
||||
if adyen::is_transaction_event(¬if.event_code) {
|
||||
return Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
|
||||
api_models::payments::PaymentIdType::ConnectorTransactionId(notif.psp_reference),
|
||||
api_models::payments::PaymentIdType::PaymentAttemptId(notif.merchant_reference),
|
||||
));
|
||||
}
|
||||
if adyen::is_refund_event(¬if.event_code) {
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
use api_models::payouts::PayoutMethodData;
|
||||
use api_models::{enums, payments, webhooks};
|
||||
use cards::CardNumber;
|
||||
use error_stack::ResultExt;
|
||||
use masking::PeekInterface;
|
||||
use reqwest::Url;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -27,6 +28,7 @@ use crate::{
|
||||
transformers::ForeignFrom,
|
||||
PaymentsAuthorizeData,
|
||||
},
|
||||
utils as crate_utils,
|
||||
};
|
||||
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
@ -234,7 +236,7 @@ pub struct AdyenThreeDS {
|
||||
#[serde(untagged)]
|
||||
pub enum AdyenPaymentResponse {
|
||||
Response(Response),
|
||||
RedirectResponse(RedirectionResponse),
|
||||
NextActionResponse(NextActionResponse),
|
||||
RedirectionErrorResponse(RedirectionErrorResponse),
|
||||
}
|
||||
|
||||
@ -259,23 +261,24 @@ pub struct RedirectionErrorResponse {
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RedirectionResponse {
|
||||
pub struct NextActionResponse {
|
||||
result_code: AdyenStatus,
|
||||
action: AdyenRedirectionAction,
|
||||
action: AdyenNextAction,
|
||||
refusal_reason: Option<String>,
|
||||
refusal_reason_code: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdyenRedirectionAction {
|
||||
payment_method_type: String,
|
||||
pub struct AdyenNextAction {
|
||||
payment_method_type: PaymentType,
|
||||
url: Option<Url>,
|
||||
method: Option<services::Method>,
|
||||
#[serde(rename = "type")]
|
||||
type_of_response: ActionType,
|
||||
data: Option<std::collections::HashMap<String, String>>,
|
||||
payment_data: Option<String>,
|
||||
qr_code_data: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@ -283,6 +286,8 @@ pub struct AdyenRedirectionAction {
|
||||
pub enum ActionType {
|
||||
Redirect,
|
||||
Await,
|
||||
#[serde(rename = "qrCode")]
|
||||
QrCode,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
@ -347,6 +352,8 @@ pub enum AdyenPaymentMethod<'a> {
|
||||
SamsungPay(Box<SamsungPayPmData>),
|
||||
Twint(Box<TwintWalletData>),
|
||||
Vipps(Box<VippsWalletData>),
|
||||
#[serde(rename = "swish")]
|
||||
Swish,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
@ -887,6 +894,7 @@ pub enum PaymentType {
|
||||
Samsungpay,
|
||||
Twint,
|
||||
Vipps,
|
||||
Swish,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Serialize, Clone)]
|
||||
@ -1451,6 +1459,7 @@ impl<'a> TryFrom<&api::WalletData> for AdyenPaymentMethod<'a> {
|
||||
};
|
||||
Ok(AdyenPaymentMethod::Dana(Box::new(data)))
|
||||
}
|
||||
api_models::payments::WalletData::SwishQr(_) => Ok(AdyenPaymentMethod::Swish),
|
||||
_ => Err(errors::ConnectorError::NotImplemented("Payment method".to_string()).into()),
|
||||
}
|
||||
}
|
||||
@ -2101,8 +2110,8 @@ pub fn get_adyen_response(
|
||||
Ok((status, error, payments_response_data))
|
||||
}
|
||||
|
||||
pub fn get_redirection_response(
|
||||
response: RedirectionResponse,
|
||||
pub fn get_next_action_response(
|
||||
response: NextActionResponse,
|
||||
is_manual_capture: bool,
|
||||
status_code: u16,
|
||||
) -> errors::CustomResult<
|
||||
@ -2113,15 +2122,19 @@ pub fn get_redirection_response(
|
||||
),
|
||||
errors::ConnectorError,
|
||||
> {
|
||||
let status =
|
||||
storage_enums::AttemptStatus::foreign_from((is_manual_capture, response.result_code));
|
||||
let status = storage_enums::AttemptStatus::foreign_from((
|
||||
is_manual_capture,
|
||||
response.result_code.clone(),
|
||||
));
|
||||
let error = if response.refusal_reason.is_some() || response.refusal_reason_code.is_some() {
|
||||
Some(types::ErrorResponse {
|
||||
code: response
|
||||
.refusal_reason_code
|
||||
.clone()
|
||||
.unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()),
|
||||
message: response
|
||||
.refusal_reason
|
||||
.clone()
|
||||
.unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()),
|
||||
reason: None,
|
||||
status_code,
|
||||
@ -2130,8 +2143,8 @@ pub fn get_redirection_response(
|
||||
None
|
||||
};
|
||||
|
||||
let redirection_data = response.action.url.map(|url| {
|
||||
let form_fields = response.action.data.unwrap_or_else(|| {
|
||||
let redirection_data = response.action.url.clone().map(|url| {
|
||||
let form_fields = response.action.data.clone().unwrap_or_else(|| {
|
||||
std::collections::HashMap::from_iter(
|
||||
url.query_pairs()
|
||||
.map(|(key, value)| (key.to_string(), value.to_string())),
|
||||
@ -2144,12 +2157,13 @@ pub fn get_redirection_response(
|
||||
}
|
||||
});
|
||||
|
||||
let connector_metadata = get_connector_metadata(&response)?;
|
||||
// We don't get connector transaction id for redirections in Adyen.
|
||||
let payments_response_data = types::PaymentsResponseData::TransactionResponse {
|
||||
resource_id: types::ResponseId::NoResponseId,
|
||||
redirection_data,
|
||||
mandate_reference: None,
|
||||
connector_metadata: None,
|
||||
connector_metadata,
|
||||
network_txn_id: None,
|
||||
connector_response_reference_id: None,
|
||||
};
|
||||
@ -2188,6 +2202,45 @@ pub fn get_redirection_error_response(
|
||||
Ok((status, error, payments_response_data))
|
||||
}
|
||||
|
||||
pub fn get_connector_metadata(
|
||||
response: &NextActionResponse,
|
||||
) -> errors::CustomResult<Option<serde_json::Value>, errors::ConnectorError> {
|
||||
let connector_metadata = match response.action.type_of_response {
|
||||
ActionType::QrCode => {
|
||||
let metadata = get_qr_metadata(response);
|
||||
Some(metadata)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
.transpose()
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)?;
|
||||
|
||||
Ok(connector_metadata)
|
||||
}
|
||||
|
||||
pub fn get_qr_metadata(
|
||||
response: &NextActionResponse,
|
||||
) -> errors::CustomResult<serde_json::Value, errors::ConnectorError> {
|
||||
let image_data = response
|
||||
.action
|
||||
.qr_code_data
|
||||
.clone()
|
||||
.map(crate_utils::QrImage::new_from_data)
|
||||
.transpose()
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)?;
|
||||
|
||||
let image_data_url = image_data
|
||||
.and_then(|image_data| Url::parse(image_data.data.as_str()).ok())
|
||||
.ok_or(errors::ConnectorError::ResponseHandlingFailed)?;
|
||||
|
||||
let qr_code_instructions = payments::QrCodeNextStepsInstruction { image_data_url };
|
||||
|
||||
common_utils::ext_traits::Encode::<payments::QrCodeNextStepsInstruction>::encode_to_value(
|
||||
&qr_code_instructions,
|
||||
)
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||
}
|
||||
|
||||
impl<F, Req>
|
||||
TryFrom<(
|
||||
types::ResponseRouterData<F, AdyenPaymentResponse, Req, types::PaymentsResponseData>,
|
||||
@ -2207,8 +2260,8 @@ impl<F, Req>
|
||||
AdyenPaymentResponse::Response(response) => {
|
||||
get_adyen_response(response, is_manual_capture, item.http_code)?
|
||||
}
|
||||
AdyenPaymentResponse::RedirectResponse(response) => {
|
||||
get_redirection_response(response, is_manual_capture, item.http_code)?
|
||||
AdyenPaymentResponse::NextActionResponse(response) => {
|
||||
get_next_action_response(response, is_manual_capture, item.http_code)?
|
||||
}
|
||||
AdyenPaymentResponse::RedirectionErrorResponse(response) => {
|
||||
get_redirection_error_response(response, is_manual_capture, item.http_code)?
|
||||
|
||||
@ -377,6 +377,7 @@ where
|
||||
|
||||
if payment_intent.status == enums::IntentStatus::RequiresCustomerAction
|
||||
|| bank_transfer_next_steps.is_some()
|
||||
|| next_action_containing_qr_code.is_some()
|
||||
{
|
||||
next_action_response = bank_transfer_next_steps
|
||||
.map(|bank_transfer| {
|
||||
|
||||
@ -203,6 +203,7 @@ Never share your secret api keys. Keep them guarded and secure.
|
||||
api_models::payments::FeatureMetadata,
|
||||
api_models::payments::ApplepayConnectorMetadataRequest,
|
||||
api_models::payments::SessionTokenInfo,
|
||||
api_models::payments::SwishQrData,
|
||||
api_models::payments::AirwallexData,
|
||||
api_models::payments::NoonData,
|
||||
api_models::payments::OrderDetails,
|
||||
|
||||
@ -178,6 +178,7 @@ impl ForeignFrom<api_enums::PaymentMethodType> for api_enums::PaymentMethod {
|
||||
| api_enums::PaymentMethodType::Twint
|
||||
| api_enums::PaymentMethodType::Vipps
|
||||
| api_enums::PaymentMethodType::TouchNGo
|
||||
| api_enums::PaymentMethodType::Swish
|
||||
| api_enums::PaymentMethodType::WeChatPay
|
||||
| api_enums::PaymentMethodType::GoPay
|
||||
| api_enums::PaymentMethodType::Gcash
|
||||
@ -203,7 +204,6 @@ impl ForeignFrom<api_enums::PaymentMethodType> for api_enums::PaymentMethod {
|
||||
| api_enums::PaymentMethodType::OnlineBankingPoland
|
||||
| api_enums::PaymentMethodType::OnlineBankingSlovakia
|
||||
| api_enums::PaymentMethodType::Przelewy24
|
||||
| api_enums::PaymentMethodType::Swish
|
||||
| api_enums::PaymentMethodType::Trustly
|
||||
| api_enums::PaymentMethodType::Bizum
|
||||
| api_enums::PaymentMethodType::Interac => Self::BankRedirect,
|
||||
|
||||
@ -606,7 +606,28 @@ async fn should_make_adyen_touch_n_go_payment(web_driver: WebDriver) -> Result<(
|
||||
Event::Trigger(Trigger::Goto(&format!("{CHEKOUT_BASE_URL}/saved/185"))),
|
||||
Event::Trigger(Trigger::Click(By::Id("card-submit-btn"))),
|
||||
Event::Trigger(Trigger::Click(By::Css("button[value='authorised']"))),
|
||||
Event::Assert(Assert::IsPresent("succeeded")),
|
||||
Event::Assert(Assert::IsPresent("Google")),
|
||||
Event::Assert(Assert::ContainsAny(
|
||||
Selector::QueryParamStr,
|
||||
vec!["status=succeeded"],
|
||||
)),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn should_make_adyen_swish_payment(web_driver: WebDriver) -> Result<(), WebDriverError> {
|
||||
let conn = AdyenSeleniumTest {};
|
||||
conn.make_redirection_payment(
|
||||
web_driver,
|
||||
vec![
|
||||
Event::Trigger(Trigger::Goto(&format!("{CHEKOUT_BASE_URL}/saved/210"))),
|
||||
Event::Trigger(Trigger::Click(By::Id("card-submit-btn"))),
|
||||
Event::Assert(Assert::IsPresent("status")),
|
||||
Event::Assert(Assert::IsPresent("processing")),
|
||||
Event::Assert(Assert::IsPresent("Next Action Type")),
|
||||
Event::Assert(Assert::IsPresent("qr_code_information")),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
@ -670,6 +691,12 @@ fn should_make_adyen_alipay_hk_payment_test() {
|
||||
tester!(should_make_adyen_alipay_hk_payment);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn should_make_adyen_swish_payment_test() {
|
||||
tester!(should_make_adyen_swish_payment);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
#[ignore = "Failing from connector side"]
|
||||
|
||||
@ -3046,17 +3046,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"swish"
|
||||
],
|
||||
"properties": {
|
||||
"swish": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
@ -9893,6 +9882,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"SwishQrData": {
|
||||
"type": "object"
|
||||
},
|
||||
"ThirdPartySdkSessionResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@ -10271,6 +10263,17 @@
|
||||
"$ref": "#/components/schemas/WeChatPayQr"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"swish_qr"
|
||||
],
|
||||
"properties": {
|
||||
"swish_qr": {
|
||||
"$ref": "#/components/schemas/SwishQrData"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user