Skip to content

Commit

Permalink
feat(connector): [Adyen] implement Swish for Adyen (#1701)
Browse files Browse the repository at this point in the history
Co-authored-by: Sangamesh <[email protected]>
Co-authored-by: swangi-kumari <[email protected]>
  • Loading branch information
3 people authored Aug 1, 2023
1 parent 5d6510e commit cf30255
Show file tree
Hide file tree
Showing 12 changed files with 123 additions and 31 deletions.
2 changes: 1 addition & 1 deletion .github/testcases/ui_tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -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\":\"[email protected]\",\"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\":\"[email protected]\",\"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,
Expand Down
1 change: 1 addition & 0 deletions config/config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" } }
Expand Down
1 change: 1 addition & 0 deletions config/development.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
1 change: 1 addition & 0 deletions config/docker_compose.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" } }
Expand Down
8 changes: 6 additions & 2 deletions crates/api_models/src/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
}
Expand Down
2 changes: 1 addition & 1 deletion crates/router/src/connector/adyen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1265,7 +1265,7 @@ impl api::IncomingWebhook for Adyen {
.change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?;
if adyen::is_transaction_event(&notif.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(&notif.event_code) {
Expand Down
81 changes: 67 additions & 14 deletions crates/router/src/connector/adyen/transformers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -27,6 +28,7 @@ use crate::{
transformers::ForeignFrom,
PaymentsAuthorizeData,
},
utils as crate_utils,
};

type Error = error_stack::Report<errors::ConnectorError>;
Expand Down Expand Up @@ -234,7 +236,7 @@ pub struct AdyenThreeDS {
#[serde(untagged)]
pub enum AdyenPaymentResponse {
Response(Response),
RedirectResponse(RedirectionResponse),
NextActionResponse(NextActionResponse),
RedirectionErrorResponse(RedirectionErrorResponse),
}

Expand All @@ -259,30 +261,33 @@ 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)]
#[serde(rename_all = "lowercase")]
pub enum ActionType {
Redirect,
Await,
#[serde(rename = "qrCode")]
QrCode,
}

#[derive(Default, Debug, Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -887,6 +894,7 @@ pub enum PaymentType {
Samsungpay,
Twint,
Vipps,
Swish,
}

#[derive(Debug, Eq, PartialEq, Serialize, Clone)]
Expand Down Expand Up @@ -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()),
}
}
Expand Down Expand Up @@ -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<
Expand All @@ -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,
Expand All @@ -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())),
Expand All @@ -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,
};
Expand Down Expand Up @@ -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>,
Expand All @@ -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)?
Expand Down
1 change: 1 addition & 0 deletions crates/router/src/core/payments/transformers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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| {
Expand Down
1 change: 1 addition & 0 deletions crates/router/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion crates/router/src/types/transformers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
29 changes: 28 additions & 1 deletion crates/router/tests/connectors/adyen_uk_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?;
Expand Down Expand Up @@ -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"]
Expand Down
Loading

0 comments on commit cf30255

Please sign in to comment.