From fce5ffa4e06bc6b8e413b13ec550613617e05568 Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Fri, 3 Jan 2025 13:20:45 +0530 Subject: [PATCH 1/6] fix(cache): address in-memory cache invalidation using global tenant as `key_prefix` (#6976) --- crates/redis_interface/src/commands.rs | 15 ++-- crates/router/src/core/admin.rs | 2 +- crates/router/src/core/cache.rs | 4 +- crates/router/src/core/routing.rs | 2 +- crates/router/src/core/routing/helpers.rs | 6 +- crates/router/src/db/configs.rs | 33 ++++----- crates/router/src/db/merchant_account.rs | 4 +- crates/storage_impl/src/redis/cache.rs | 85 +++++++++++++---------- crates/storage_impl/src/redis/pub_sub.rs | 5 -- 9 files changed, 73 insertions(+), 83 deletions(-) diff --git a/crates/redis_interface/src/commands.rs b/crates/redis_interface/src/commands.rs index 19497d6fbb83..3c7ffa16ada3 100644 --- a/crates/redis_interface/src/commands.rs +++ b/crates/redis_interface/src/commands.rs @@ -211,18 +211,13 @@ impl super::RedisConnectionPool { #[instrument(level = "DEBUG", skip(self))] pub async fn delete_multiple_keys( &self, - keys: Vec, + keys: &[String], ) -> CustomResult, errors::RedisError> { - let mut del_result = Vec::with_capacity(keys.len()); + let futures = keys.iter().map(|key| self.pool.del(self.add_prefix(key))); - for key in keys { - del_result.push( - self.pool - .del(self.add_prefix(&key)) - .await - .change_context(errors::RedisError::DeleteFailed)?, - ); - } + let del_result = futures::future::try_join_all(futures) + .await + .change_context(errors::RedisError::DeleteFailed)?; Ok(del_result) } diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 24a2b25d6ce9..19c3e6c1e228 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -4288,7 +4288,7 @@ impl ProfileWrapper { .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to update routing algorithm ref in business profile")?; - storage_impl::redis::cache::publish_into_redact_channel( + storage_impl::redis::cache::redact_from_redis_and_publish( db.get_cache_store().as_ref(), [routing_cache_key], ) diff --git a/crates/router/src/core/cache.rs b/crates/router/src/core/cache.rs index 8cda60cf7009..fbe75f3c9c0f 100644 --- a/crates/router/src/core/cache.rs +++ b/crates/router/src/core/cache.rs @@ -1,6 +1,6 @@ use common_utils::errors::CustomResult; use error_stack::{report, ResultExt}; -use storage_impl::redis::cache::{publish_into_redact_channel, CacheKind}; +use storage_impl::redis::cache::{redact_from_redis_and_publish, CacheKind}; use super::errors; use crate::{routes::SessionState, services}; @@ -10,7 +10,7 @@ pub async fn invalidate( key: &str, ) -> CustomResult, errors::ApiErrorResponse> { let store = state.store.as_ref(); - let result = publish_into_redact_channel( + let result = redact_from_redis_and_publish( store.get_cache_store().as_ref(), [CacheKind::All(key.into())], ) diff --git a/crates/router/src/core/routing.rs b/crates/router/src/core/routing.rs index 717dfd0c6eba..99bd2b00209b 100644 --- a/crates/router/src/core/routing.rs +++ b/crates/router/src/core/routing.rs @@ -1383,7 +1383,7 @@ pub async fn success_based_routing_update_configs( let cache_entries_to_redact = vec![cache::CacheKind::SuccessBasedDynamicRoutingCache( cache_key.into(), )]; - let _ = cache::publish_into_redact_channel( + let _ = cache::redact_from_redis_and_publish( state.store.get_cache_store().as_ref(), cache_entries_to_redact, ) diff --git a/crates/router/src/core/routing/helpers.rs b/crates/router/src/core/routing/helpers.rs index 0d66c3b6f17b..159def38621a 100644 --- a/crates/router/src/core/routing/helpers.rs +++ b/crates/router/src/core/routing/helpers.rs @@ -189,7 +189,7 @@ pub async fn update_merchant_active_algorithm_ref( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to update routing algorithm ref in merchant account")?; - cache::publish_into_redact_channel(db.get_cache_store().as_ref(), [config_key]) + cache::redact_from_redis_and_publish(db.get_cache_store().as_ref(), [config_key]) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to invalidate the config cache")?; @@ -256,7 +256,7 @@ pub async fn update_profile_active_algorithm_ref( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to update routing algorithm ref in business profile")?; - cache::publish_into_redact_channel(db.get_cache_store().as_ref(), [routing_cache_key]) + cache::redact_from_redis_and_publish(db.get_cache_store().as_ref(), [routing_cache_key]) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to invalidate routing cache")?; @@ -1031,7 +1031,7 @@ pub async fn disable_dynamic_routing_algorithm( }; // redact cache for dynamic routing config - let _ = cache::publish_into_redact_channel( + let _ = cache::redact_from_redis_and_publish( state.store.get_cache_store().as_ref(), cache_entries_to_redact, ) diff --git a/crates/router/src/db/configs.rs b/crates/router/src/db/configs.rs index 575481793ca1..9b8ab5231b67 100644 --- a/crates/router/src/db/configs.rs +++ b/crates/router/src/db/configs.rs @@ -1,16 +1,13 @@ use diesel_models::configs::ConfigUpdateInternal; use error_stack::{report, ResultExt}; use router_env::{instrument, tracing}; -use storage_impl::redis::{ - cache::{self, CacheKind, CONFIG_CACHE}, - kv_store::RedisConnInterface, - pub_sub::PubSubInterface, -}; +use storage_impl::redis::cache::{self, CacheKind, CONFIG_CACHE}; use super::{MockDb, Store}; use crate::{ connection, core::errors::{self, CustomResult}, + db::StorageInterface, types::storage, }; @@ -69,14 +66,11 @@ impl ConfigInterface for Store { .await .map_err(|error| report!(errors::StorageError::from(error)))?; - self.get_redis_conn() - .map_err(Into::::into)? - .publish( - cache::IMC_INVALIDATION_CHANNEL, - CacheKind::Config((&inserted.key).into()), - ) - .await - .map_err(Into::::into)?; + cache::redact_from_redis_and_publish( + self.get_cache_store().as_ref(), + [CacheKind::Config((&inserted.key).into())], + ) + .await?; Ok(inserted) } @@ -177,14 +171,11 @@ impl ConfigInterface for Store { .await .map_err(|error| report!(errors::StorageError::from(error)))?; - self.get_redis_conn() - .map_err(Into::::into)? - .publish( - cache::IMC_INVALIDATION_CHANNEL, - CacheKind::Config(key.into()), - ) - .await - .map_err(Into::::into)?; + cache::redact_from_redis_and_publish( + self.get_cache_store().as_ref(), + [CacheKind::Config((&deleted.key).into())], + ) + .await?; Ok(deleted) } diff --git a/crates/router/src/db/merchant_account.rs b/crates/router/src/db/merchant_account.rs index 4f4a3f1cf00b..1c104b22489f 100644 --- a/crates/router/src/db/merchant_account.rs +++ b/crates/router/src/db/merchant_account.rs @@ -801,7 +801,7 @@ async fn publish_and_redact_merchant_account_cache( cache_keys.extend(publishable_key.into_iter()); cache_keys.extend(cgraph_key.into_iter()); - cache::publish_into_redact_channel(store.get_cache_store().as_ref(), cache_keys).await?; + cache::redact_from_redis_and_publish(store.get_cache_store().as_ref(), cache_keys).await?; Ok(()) } @@ -822,6 +822,6 @@ async fn publish_and_redact_all_merchant_account_cache( .map(|s| CacheKind::Accounts(s.into())) .collect(); - cache::publish_into_redact_channel(store.get_cache_store().as_ref(), cache_keys).await?; + cache::redact_from_redis_and_publish(store.get_cache_store().as_ref(), cache_keys).await?; Ok(()) } diff --git a/crates/storage_impl/src/redis/cache.rs b/crates/storage_impl/src/redis/cache.rs index 93255fac9144..323d3d6df259 100644 --- a/crates/storage_impl/src/redis/cache.rs +++ b/crates/storage_impl/src/redis/cache.rs @@ -2,14 +2,17 @@ use std::{any::Any, borrow::Cow, fmt::Debug, sync::Arc}; use common_utils::{ errors::{self, CustomResult}, - ext_traits::{AsyncExt, ByteSliceExt}, + ext_traits::ByteSliceExt, }; use dyn_clone::DynClone; use error_stack::{Report, ResultExt}; use moka::future::Cache as MokaCache; use once_cell::sync::Lazy; use redis_interface::{errors::RedisError, RedisConnectionPool, RedisValue}; -use router_env::tracing::{self, instrument}; +use router_env::{ + logger, + tracing::{self, instrument}, +}; use crate::{ errors::StorageError, @@ -100,7 +103,7 @@ pub struct CacheRedact<'a> { pub kind: CacheKind<'a>, } -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum CacheKind<'a> { Config(Cow<'a, str>), Accounts(Cow<'a, str>), @@ -114,6 +117,23 @@ pub enum CacheKind<'a> { All(Cow<'a, str>), } +impl CacheKind<'_> { + pub(crate) fn get_key_without_prefix(&self) -> &str { + match self { + CacheKind::Config(key) + | CacheKind::Accounts(key) + | CacheKind::Routing(key) + | CacheKind::DecisionManager(key) + | CacheKind::Surcharge(key) + | CacheKind::CGraph(key) + | CacheKind::SuccessBasedDynamicRoutingCache(key) + | CacheKind::EliminationBasedDynamicRoutingCache(key) + | CacheKind::PmFiltersCGraph(key) + | CacheKind::All(key) => key, + } + } +} + impl<'a> TryFrom> for RedisValue { type Error = Report; fn try_from(v: CacheRedact<'a>) -> Result { @@ -343,48 +363,37 @@ where } #[instrument(skip_all)] -pub async fn redact_cache( +pub async fn redact_from_redis_and_publish< + 'a, + K: IntoIterator> + Send + Clone, +>( store: &(dyn RedisConnInterface + Send + Sync), - key: &'static str, - fun: F, - in_memory: Option<&Cache>, -) -> CustomResult -where - F: FnOnce() -> Fut + Send, - Fut: futures::Future> + Send, -{ - let data = fun().await?; - + keys: K, +) -> CustomResult { let redis_conn = store .get_redis_conn() .change_context(StorageError::RedisError( RedisError::RedisConnectionError.into(), )) .attach_printable("Failed to get redis connection")?; - let tenant_key = CacheKey { - key: key.to_string(), - prefix: redis_conn.key_prefix.clone(), - }; - in_memory.async_map(|cache| cache.remove(tenant_key)).await; - redis_conn - .delete_key(key) + let redis_keys_to_be_deleted = keys + .clone() + .into_iter() + .map(|val| val.get_key_without_prefix().to_owned()) + .collect::>(); + + let del_replies = redis_conn + .delete_multiple_keys(&redis_keys_to_be_deleted) .await - .change_context(StorageError::KVError)?; - Ok(data) -} + .map_err(StorageError::RedisError)?; -#[instrument(skip_all)] -pub async fn publish_into_redact_channel<'a, K: IntoIterator> + Send>( - store: &(dyn RedisConnInterface + Send + Sync), - keys: K, -) -> CustomResult { - let redis_conn = store - .get_redis_conn() - .change_context(StorageError::RedisError( - RedisError::RedisConnectionError.into(), - )) - .attach_printable("Failed to get redis connection")?; + let deletion_result = redis_keys_to_be_deleted + .into_iter() + .zip(del_replies) + .collect::>(); + + logger::debug!(redis_deletion_result=?deletion_result); let futures = keys.into_iter().map(|key| async { redis_conn @@ -411,7 +420,7 @@ where Fut: futures::Future> + Send, { let data = fun().await?; - publish_into_redact_channel(store, [key]).await?; + redact_from_redis_and_publish(store, [key]).await?; Ok(data) } @@ -424,10 +433,10 @@ pub async fn publish_and_redact_multiple<'a, T, F, Fut, K>( where F: FnOnce() -> Fut + Send, Fut: futures::Future> + Send, - K: IntoIterator> + Send, + K: IntoIterator> + Send + Clone, { let data = fun().await?; - publish_into_redact_channel(store, keys).await?; + redact_from_redis_and_publish(store, keys).await?; Ok(data) } diff --git a/crates/storage_impl/src/redis/pub_sub.rs b/crates/storage_impl/src/redis/pub_sub.rs index 42ad2ae0795a..373ac370e2fe 100644 --- a/crates/storage_impl/src/redis/pub_sub.rs +++ b/crates/storage_impl/src/redis/pub_sub.rs @@ -243,11 +243,6 @@ impl PubSubInterface for std::sync::Arc { } }; - self.delete_key(key.as_ref()) - .await - .map_err(|err| logger::error!("Error while deleting redis key: {err:?}")) - .ok(); - logger::debug!( key_prefix=?message.tenant.clone(), channel_name=?channel_name, From 2aa14e7fec19b31d84745b524cbf835ff16b8ce8 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Jan 2025 08:26:26 +0000 Subject: [PATCH 2/6] chore(version): 2025.01.03.0 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3de8bcfeb93a..74578ee9b405 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2025.01.03.0 + +### Bug Fixes + +- **cache:** Address in-memory cache invalidation using global tenant as `key_prefix` ([#6976](https://github.com/juspay/hyperswitch/pull/6976)) ([`fce5ffa`](https://github.com/juspay/hyperswitch/commit/fce5ffa4e06bc6b8e413b13ec550613617e05568)) + +**Full Changelog:** [`2024.12.31.0...2025.01.03.0`](https://github.com/juspay/hyperswitch/compare/2024.12.31.0...2025.01.03.0) + +- - - + ## 2024.12.31.0 ### Features From 60ed69c1cff706aaba248e1aba0219f70bb679bd Mon Sep 17 00:00:00 2001 From: Kashif Date: Fri, 3 Jan 2025 16:23:23 +0530 Subject: [PATCH 3/6] chore: add migrations for Currency type in DB (#6980) --- .../2025-01-03-084904_add_currencies/down.sql | 1 + .../2025-01-03-084904_add_currencies/up.sql | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 migrations/2025-01-03-084904_add_currencies/down.sql create mode 100644 migrations/2025-01-03-084904_add_currencies/up.sql diff --git a/migrations/2025-01-03-084904_add_currencies/down.sql b/migrations/2025-01-03-084904_add_currencies/down.sql new file mode 100644 index 000000000000..e0ac49d1ecfb --- /dev/null +++ b/migrations/2025-01-03-084904_add_currencies/down.sql @@ -0,0 +1 @@ +SELECT 1; diff --git a/migrations/2025-01-03-084904_add_currencies/up.sql b/migrations/2025-01-03-084904_add_currencies/up.sql new file mode 100644 index 000000000000..14167a32cfcb --- /dev/null +++ b/migrations/2025-01-03-084904_add_currencies/up.sql @@ -0,0 +1,18 @@ +DO $$ + DECLARE currency TEXT; + BEGIN + FOR currency IN + SELECT + unnest( + ARRAY ['AFN', 'BTN', 'CDF', 'ERN', 'IRR', 'ISK', 'KPW', 'SDG', 'SYP', 'TJS', 'TMT', 'ZWL'] + ) AS currency + LOOP + IF NOT EXISTS ( + SELECT 1 + FROM pg_enum + WHERE enumlabel = currency + AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'Currency') + ) THEN EXECUTE format('ALTER TYPE "Currency" ADD VALUE %L', currency); + END IF; + END LOOP; +END $$; \ No newline at end of file From 8c4cd07ea69395bffdb8c8ddf1313dc87a5dc740 Mon Sep 17 00:00:00 2001 From: likhinbopanna <131246334+likhinbopanna@users.noreply.github.com> Date: Sat, 4 Jan 2025 01:00:45 +0530 Subject: [PATCH 4/6] ci(cypress): fix adyen sofort in cypress (#6984) --- .../cypress/e2e/PaymentUtils/Adyen.js | 4 +++- .../cypress/e2e/PaymentUtils/Iatapay.js | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Adyen.js b/cypress-tests/cypress/e2e/PaymentUtils/Adyen.js index 56efbf1f6f21..95fa82f9675a 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Adyen.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Adyen.js @@ -909,7 +909,9 @@ export const connectorDetails = { Response: { status: 200, body: { - status: "requires_customer_action", + status: "failed", + error_code: "14_006", + error_message: "Required object 'paymentMethod' is not provided.", }, }, }, diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Iatapay.js b/cypress-tests/cypress/e2e/PaymentUtils/Iatapay.js index faa810e7bad5..477728969558 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Iatapay.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Iatapay.js @@ -88,6 +88,27 @@ export const connectorDetails = { }, }, }, + No3DSFailPayment: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 400, + body: { + error: { + type: "invalid_request", + message: + "Selected payment method through iatapay is not implemented", + code: "IR_00", + }, + }, + }, + }, }, upi_pm: { PaymentIntent: { From de2f209ae687d79fb7bf2575dcaa612718d1d1aa Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 00:32:33 +0000 Subject: [PATCH 5/6] chore(version): 2025.01.06.0 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74578ee9b405..ee0640a5641f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2025.01.06.0 + +### Miscellaneous Tasks + +- Add migrations for Currency type in DB ([#6980](https://github.com/juspay/hyperswitch/pull/6980)) ([`60ed69c`](https://github.com/juspay/hyperswitch/commit/60ed69c1cff706aaba248e1aba0219f70bb679bd)) + +**Full Changelog:** [`2025.01.03.0...2025.01.06.0`](https://github.com/juspay/hyperswitch/compare/2025.01.03.0...2025.01.06.0) + +- - - + ## 2025.01.03.0 ### Bug Fixes From 638e1f230a543a1ff2b7712d04b937a9a9db1969 Mon Sep 17 00:00:00 2001 From: Debarati Ghatak <88573135+cookieg13@users.noreply.github.com> Date: Mon, 6 Jan 2025 12:29:53 +0530 Subject: [PATCH 6/6] Ci(Cypress): Add PML test and Dynamic Fields Test for Novalnet (#6544) --- .../00000-PaymentMethodListTests.cy.js | 211 ++++++++++++++++-- .../e2e/PaymentMethodListUtils/Commons.js | 20 ++ .../{Stripe.js => Connector.js} | 0 .../e2e/PaymentMethodListUtils/Utils.js | 4 +- .../cypress/e2e/PaymentUtils/Novalnet.js | 163 ++++++++++++++ 5 files changed, 382 insertions(+), 16 deletions(-) rename cypress-tests/cypress/e2e/PaymentMethodListUtils/{Stripe.js => Connector.js} (100%) diff --git a/cypress-tests/cypress/e2e/PaymentMethodListTest/00000-PaymentMethodListTests.cy.js b/cypress-tests/cypress/e2e/PaymentMethodListTest/00000-PaymentMethodListTests.cy.js index 89b27a6b2716..0f754831d46e 100644 --- a/cypress-tests/cypress/e2e/PaymentMethodListTest/00000-PaymentMethodListTests.cy.js +++ b/cypress-tests/cypress/e2e/PaymentMethodListTest/00000-PaymentMethodListTests.cy.js @@ -6,6 +6,7 @@ import { cardCreditEnabled, cardCreditEnabledInUs, cardCreditEnabledInUsd, + cardCreditEnabledInEur, createPaymentBodyWithCurrency, createPaymentBodyWithCurrencyCountry, } from "../PaymentMethodListUtils/Commons"; @@ -68,7 +69,8 @@ describe("Payment Method list using Constraint Graph flow tests", () => { // creating payment with currency as EUR and no billing address it("create-payment-call-test", () => { - const data = getConnectorDetails("stripe")["pm_list"]["PaymentIntent"]; + const data = + getConnectorDetails("connector")["pm_list"]["PaymentIntent"]; const newData = { ...data, @@ -88,7 +90,7 @@ describe("Payment Method list using Constraint Graph flow tests", () => { // payment method list which should only have ideal with stripe it("payment-method-list-call-test", () => { const data = - getConnectorDetails("stripe")["pm_list"]["PmListResponse"][ + getConnectorDetails("connector")["pm_list"]["PmListResponse"][ "PmListWithStripeForIdeal" ]; cy.paymentMethodListTestLessThanEqualToOnePaymentMethod( @@ -151,7 +153,8 @@ describe("Payment Method list using Constraint Graph flow tests", () => { // creating payment with currency as INR and no billing address it("create-payment-call-test", () => { - const data = getConnectorDetails("stripe")["pm_list"]["PaymentIntent"]; + const data = + getConnectorDetails("connector")["pm_list"]["PaymentIntent"]; const newData = { ...data, @@ -171,7 +174,7 @@ describe("Payment Method list using Constraint Graph flow tests", () => { // payment method list which should only have ideal with stripe it("payment-method-list-call-test", () => { const data = - getConnectorDetails("stripe")["pm_list"]["PmListResponse"][ + getConnectorDetails("connector")["pm_list"]["PmListResponse"][ "PmListNull" ]; cy.paymentMethodListTestLessThanEqualToOnePaymentMethod( @@ -234,7 +237,8 @@ describe("Payment Method list using Constraint Graph flow tests", () => { // creating payment with currency as USD and billing address as US it("create-payment-call-test", () => { - const data = getConnectorDetails("stripe")["pm_list"]["PaymentIntent"]; + const data = + getConnectorDetails("connector")["pm_list"]["PaymentIntent"]; const newData = { ...data, @@ -254,7 +258,7 @@ describe("Payment Method list using Constraint Graph flow tests", () => { // payment method list which should only have credit with Stripe and Cybersource it("payment-method-list-call-test", () => { const data = - getConnectorDetails("stripe")["pm_list"]["PmListResponse"][ + getConnectorDetails("connector")["pm_list"]["PmListResponse"][ "PmListWithCreditTwoConnector" ]; cy.paymentMethodListTestTwoConnectorsForOnePaymentMethodCredit( @@ -317,7 +321,8 @@ describe("Payment Method list using Constraint Graph flow tests", () => { // creating payment with currency as EUR and billing address as US it("create-payment-call-test", () => { - const data = getConnectorDetails("stripe")["pm_list"]["PaymentIntent"]; + const data = + getConnectorDetails("connector")["pm_list"]["PaymentIntent"]; const newData = { ...data, @@ -337,7 +342,7 @@ describe("Payment Method list using Constraint Graph flow tests", () => { // payment method list which shouldn't have anything it("payment-method-list-call-test", () => { const data = - getConnectorDetails("stripe")["pm_list"]["PmListResponse"][ + getConnectorDetails("connector")["pm_list"]["PmListResponse"][ "PmListNull" ]; cy.paymentMethodListTestLessThanEqualToOnePaymentMethod( @@ -402,7 +407,8 @@ describe("Payment Method list using Constraint Graph flow tests", () => { // creating payment with currency as USD and billing address as IN it("create-payment-call-test", () => { - const data = getConnectorDetails("stripe")["pm_list"]["PaymentIntent"]; + const data = + getConnectorDetails("connector")["pm_list"]["PaymentIntent"]; const newData = { ...data, @@ -422,7 +428,7 @@ describe("Payment Method list using Constraint Graph flow tests", () => { // payment method list which should have credit with stripe and cybersource and no ideal it("payment-method-list-call-test", () => { const data = - getConnectorDetails("stripe")["pm_list"]["PmListResponse"][ + getConnectorDetails("connector")["pm_list"]["PmListResponse"][ "PmListWithCreditTwoConnector" ]; cy.paymentMethodListTestTwoConnectorsForOnePaymentMethodCredit( @@ -486,7 +492,8 @@ describe("Payment Method list using Constraint Graph flow tests", () => { // creating payment with currency as USD and billing address as IN it("create-payment-call-test", () => { - const data = getConnectorDetails("stripe")["pm_list"]["PaymentIntent"]; + const data = + getConnectorDetails("connector")["pm_list"]["PaymentIntent"]; const newData = { ...data, @@ -506,7 +513,7 @@ describe("Payment Method list using Constraint Graph flow tests", () => { // payment method list which should have credit with stripe and cybersource and no ideal it("payment-method-list-call-test", () => { const data = - getConnectorDetails("stripe")["pm_list"]["PmListResponse"][ + getConnectorDetails("connector")["pm_list"]["PmListResponse"][ "PmListWithCreditTwoConnector" ]; cy.paymentMethodListTestTwoConnectorsForOnePaymentMethodCredit( @@ -569,7 +576,8 @@ describe("Payment Method list using Constraint Graph flow tests", () => { // creating payment with currency as EUR and no billing address it("create-payment-call-test", () => { - const data = getConnectorDetails("stripe")["pm_list"]["PaymentIntent"]; + const data = + getConnectorDetails("connector")["pm_list"]["PaymentIntent"]; const newData = { ...data, @@ -589,7 +597,7 @@ describe("Payment Method list using Constraint Graph flow tests", () => { // payment method list which should only have ideal with stripe it("payment-method-list-call-test", () => { const data = - getConnectorDetails("stripe")["pm_list"]["PmListResponse"][ + getConnectorDetails("connector")["pm_list"]["PmListResponse"][ "PmListWithStripeForIdeal" ]; cy.paymentMethodListTestLessThanEqualToOnePaymentMethod( @@ -599,4 +607,179 @@ describe("Payment Method list using Constraint Graph flow tests", () => { }); } ); + + context( + ` + MCA1 -> Stripe configured with credit = { currency = "USD" }\n + MCA2 -> Novalnet configured with credit = { currency = "EUR" }\n + Payment is done with currency as as USD and no billing address\n + The resultant Payment Method list should only have credit with stripe\n + `, + () => { + before("seed global state", () => { + cy.task("getGlobalState").then((state) => { + globalState = new State(state); + }); + }); + + after("flush global state", () => { + cy.task("setGlobalState", globalState.data); + }); + + it("merchant-create-call-test", () => { + cy.merchantCreateCallTest(fixtures.merchantCreateBody, globalState); + }); + + it("api-key-create-call-test", () => { + cy.apiKeyCreateTest(fixtures.apiKeyCreateBody, globalState); + }); + + it("customer-create-call-test", () => { + cy.createCustomerCallTest(fixtures.customerCreateBody, globalState); + }); + + // stripe connector create with card credit enabled in USD + it("connector-create-call-test", () => { + cy.createNamedConnectorCallTest( + "payment_processor", + fixtures.createConnectorBody, + cardCreditEnabledInUsd, + globalState, + "stripe", + "stripe_US_default" + ); + }); + + // novalnet connector create with card credit enabled in EUR + it("connector-create-call-test", () => { + cy.createNamedConnectorCallTest( + "payment_processor", + fixtures.createConnectorBody, + cardCreditEnabledInEur, + globalState, + "novalnet", + "novalnet_DE_default" + ); + }); + + // creating payment with currency as USD and no billing email + // billing.email is mandatory for novalnet + it("create-payment-call-test", () => { + const data = + getConnectorDetails("connector")["pm_list"]["PaymentIntent"]; + const newData = { + ...data, + Request: data.RequestCurrencyUSD, + RequestCurrencyUSD: undefined, // we do not need this anymore + }; + + cy.createPaymentIntentTest( + createPaymentBodyWithCurrency("USD"), + newData, + "no_three_ds", + "automatic", + globalState + ); + }); + + // payment method list should only have credit with stripe + it("payment-method-list-call-test", () => { + const data = + getConnectorDetails("connector")["pm_list"]["PmListResponse"][ + "PmListWithCreditOneConnector" + ]; + cy.paymentMethodListTestLessThanEqualToOnePaymentMethod( + data, + globalState + ); + }); + } + ); + context( + ` + MCA1 -> Stripe configured with credit = { currency = "USD" }\n + MCA2 -> Novalnet configured with credit = { currency = "EUR" }\n + Payment is done with currency as as EUR and billing address for 3ds credit card\n + The resultant Payment Method list should only have credit with novalnet\n + `, + () => { + before("seed global state", () => { + cy.task("getGlobalState").then((state) => { + globalState = new State(state); + }); + }); + + after("flush global state", () => { + cy.task("setGlobalState", globalState.data); + }); + + it("merchant-create-call-test", () => { + cy.merchantCreateCallTest(fixtures.merchantCreateBody, globalState); + }); + + it("api-key-create-call-test", () => { + cy.apiKeyCreateTest(fixtures.apiKeyCreateBody, globalState); + }); + + it("customer-create-call-test", () => { + cy.createCustomerCallTest(fixtures.customerCreateBody, globalState); + }); + + // stripe connector create with card credit enabled in USD + it("connector-create-call-test", () => { + cy.createNamedConnectorCallTest( + "payment_processor", + fixtures.createConnectorBody, + cardCreditEnabledInUsd, + globalState, + "stripe", + "stripe_US_default" + ); + }); + + // novalnet connector create with card credit enabled in EUR + it("connector-create-call-test", () => { + cy.createNamedConnectorCallTest( + "payment_processor", + fixtures.createConnectorBody, + cardCreditEnabledInEur, + globalState, + "novalnet", + "novalnet_DE_default" + ); + }); + + // creating payment with currency as EUR and billing email + // billing.email is mandatory for novalnet + it("create-payment-call-test", () => { + const data = + getConnectorDetails("connector")["pm_list"]["PaymentIntent"]; + const newData = { + ...data, + Request: data.RequestCurrencyEUR, + RequestCurrencyEUR: undefined, // we do not need this anymore + }; + + cy.createPaymentIntentTest( + createPaymentBodyWithCurrencyCountry("EUR", "IN", "IN"), + newData, + "three_ds", + "automatic", + globalState + ); + }); + + // payment method list should only have credit with novalnet + it("payment-method-list-call-test", () => { + const data = + getConnectorDetails("connector")["pm_list"]["PmListResponse"][ + "PmListWithCreditOneConnector" + ]; + cy.paymentMethodListTestLessThanEqualToOnePaymentMethod( + data, + globalState + ); + }); + } + ); }); diff --git a/cypress-tests/cypress/e2e/PaymentMethodListUtils/Commons.js b/cypress-tests/cypress/e2e/PaymentMethodListUtils/Commons.js index ae72465b6065..e782b32368b2 100644 --- a/cypress-tests/cypress/e2e/PaymentMethodListUtils/Commons.js +++ b/cypress-tests/cypress/e2e/PaymentMethodListUtils/Commons.js @@ -58,6 +58,26 @@ export const cardCreditEnabledInUs = [ }, ]; +export const cardCreditEnabledInEur = [ + { + payment_method: "card", + payment_method_types: [ + { + payment_method_type: "credit", + card_networks: ["Visa"], + minimum_amount: 0, + accepted_currencies: { + type: "enable_only", + list: ["EUR"], + }, + maximum_amount: 68607706, + recurring_enabled: false, + installment_payment_enabled: true, + }, + ], + }, +]; + export const bankRedirectIdealEnabled = [ { payment_method: "bank_redirect", diff --git a/cypress-tests/cypress/e2e/PaymentMethodListUtils/Stripe.js b/cypress-tests/cypress/e2e/PaymentMethodListUtils/Connector.js similarity index 100% rename from cypress-tests/cypress/e2e/PaymentMethodListUtils/Stripe.js rename to cypress-tests/cypress/e2e/PaymentMethodListUtils/Connector.js diff --git a/cypress-tests/cypress/e2e/PaymentMethodListUtils/Utils.js b/cypress-tests/cypress/e2e/PaymentMethodListUtils/Utils.js index f7d199164fd4..64e127608a4a 100644 --- a/cypress-tests/cypress/e2e/PaymentMethodListUtils/Utils.js +++ b/cypress-tests/cypress/e2e/PaymentMethodListUtils/Utils.js @@ -1,9 +1,9 @@ import { connectorDetails as CommonConnectorDetails } from "./Commons.js"; -import { connectorDetails as stripeConnectorDetails } from "./Stripe.js"; +import { connectorDetails as ConnectorDetails } from "./Connector.js"; const connectorDetails = { commons: CommonConnectorDetails, - stripe: stripeConnectorDetails, + connector: ConnectorDetails, }; export default function getConnectorDetails(connectorId) { diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Novalnet.js b/cypress-tests/cypress/e2e/PaymentUtils/Novalnet.js index 0a8235141dcc..52e6acd6481e 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Novalnet.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Novalnet.js @@ -207,4 +207,167 @@ export const connectorDetails = { }, }, }, + pm_list: { + PmListResponse: { + PmListNull: { + payment_methods: [], + }, + pmListDynamicFieldWithoutBilling: { + payment_methods: [ + { + payment_method: "card", + payment_method_types: [ + { + payment_method_type: "credit", + card_networks: [ + { + eligible_connectors: ["novalnet"], + }, + ], + required_fields: { + "billing.address.first_name": { + required_field: + "payment_method_data.billing.address.first_name", + display_name: "first_name", + field_type: "user_full_name", + value: null, + }, + "billing.address.last_name": { + required_field: + "payment_method_data.billing.address.last_name", + display_name: "last_name", + field_type: "user_full_name", + value: null, + }, + "billing.email": { + required_field: "payment_method_data.billing.email", + display_name: "email_address", + field_type: "user_email_address", + value: "hyperswitch_sdk_demo_id@gmail.com", + }, + }, + }, + ], + }, + ], + }, + pmListDynamicFieldWithBilling: { + payment_methods: [ + { + payment_method: "card", + payment_method_types: [ + { + payment_method_type: "credit", + card_networks: [ + { + eligible_connectors: ["novalnet"], + }, + ], + required_fields: { + "billing.address.first_name": { + required_field: + "payment_method_data.billing.address.first_name", + display_name: "first_name", + field_type: "user_full_name", + value: "joseph", + }, + "billing.address.last_name": { + required_field: + "payment_method_data.billing.address.last_name", + display_name: "last_name", + field_type: "user_full_name", + value: "Doe", + }, + "billing.email": { + required_field: "payment_method_data.billing.email", + display_name: "email_address", + field_type: "user_email_address", + value: "hyperswitch.example@gmail.com", + }, + }, + }, + ], + }, + ], + }, + pmListDynamicFieldWithNames: { + payment_methods: [ + { + payment_method: "card", + payment_method_types: [ + { + payment_method_type: "credit", + card_networks: [ + { + eligible_connectors: ["novalnet"], + }, + ], + required_fields: { + "billing.address.first_name": { + required_field: + "payment_method_data.billing.address.first_name", + display_name: "first_name", + field_type: "user_full_name", + value: "joseph", + }, + "billing.address.last_name": { + required_field: + "payment_method_data.billing.address.last_name", + display_name: "last_name", + field_type: "user_full_name", + value: "Doe", + }, + "billing.email": { + required_field: "payment_method_data.billing.email", + display_name: "email_address", + field_type: "user_email_address", + value: "hyperswitch.example@gmail.com", + }, + }, + }, + ], + }, + ], + }, + pmListDynamicFieldWithEmail: { + payment_methods: [ + { + payment_method: "card", + payment_method_types: [ + { + payment_method_type: "credit", + card_networks: [ + { + eligible_connectors: ["novalnet"], + }, + ], + required_fields: { + "billing.address.first_name": { + required_field: + "payment_method_data.billing.address.first_name", + display_name: "first_name", + field_type: "user_full_name", + value: "joseph", + }, + "billing.address.last_name": { + required_field: + "payment_method_data.billing.address.last_name", + display_name: "last_name", + field_type: "user_full_name", + value: "Doe", + }, + "billing.email": { + required_field: "payment_method_data.billing.email", + display_name: "email_address", + field_type: "user_email_address", + value: "hyperswitch.example@gmail.com", + }, + }, + }, + ], + }, + ], + }, + }, + }, };