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:
AkshayaFoiger
2023-08-01 12:19:41 +05:30
committed by GitHub
parent 5d6510eddf
commit cf3025562f
12 changed files with 123 additions and 31 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"
}
}
}
]
},