From 0eb81f04b3e8f41483d85bc68dae3088245d16be Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 12 Nov 2023 14:49:16 +0000 Subject: [PATCH 001/146] chore(version): v1.76.0 --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 412b42afc2eb..c5926cfe86ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,29 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.76.0 (2023-11-12) + +### Features + +- **analytics:** Analytics APIs ([#2792](https://github.com/juspay/hyperswitch/pull/2792)) ([`f847802`](https://github.com/juspay/hyperswitch/commit/f847802339bfedb24cbaa47ad55e31d80cefddca)) +- **router:** Added Payment link new design ([#2731](https://github.com/juspay/hyperswitch/pull/2731)) ([`2a4f5d1`](https://github.com/juspay/hyperswitch/commit/2a4f5d13717a78dc2e2e4fc9a492a45b92151dbe)) +- **user:** Setup user tables ([#2803](https://github.com/juspay/hyperswitch/pull/2803)) ([`20c4226`](https://github.com/juspay/hyperswitch/commit/20c4226a36e4650a3ba8811b758ac5f7969bcfb3)) + +### Refactors + +- **connector:** [Zen] change error message from NotSupported to NotImplemented ([#2831](https://github.com/juspay/hyperswitch/pull/2831)) ([`b5ea8db`](https://github.com/juspay/hyperswitch/commit/b5ea8db2d2b7e7544931704a7191b42d3a8299be)) +- **core:** Remove connector response table and use payment_attempt instead ([#2644](https://github.com/juspay/hyperswitch/pull/2644)) ([`966369b`](https://github.com/juspay/hyperswitch/commit/966369b6f2c205b59524c23ad3b21ebab547631f)) +- **events:** Update api events to follow snake case naming ([#2828](https://github.com/juspay/hyperswitch/pull/2828)) ([`b3d5062`](https://github.com/juspay/hyperswitch/commit/b3d5062dc07676ec12e903b1999fdd9138c0891d)) + +### Documentation + +- **README:** Add bootstrap button for cloudformation deployment ([#2827](https://github.com/juspay/hyperswitch/pull/2827)) ([`e67e808`](https://github.com/juspay/hyperswitch/commit/e67e808d70d41c371fff168824e5a4dbb8b3a040)) + +**Full Changelog:** [`v1.75.0...v1.76.0`](https://github.com/juspay/hyperswitch/compare/v1.75.0...v1.76.0) + +- - - + + ## 1.75.0 (2023-11-09) ### Features From f88eee7362be2cc3e8e8dc2bb7bfd263892ff01e Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Mon, 13 Nov 2023 11:17:35 +0530 Subject: [PATCH 002/146] feat(router): Add new JWT authentication variants and use them (#2835) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .typos.toml | 1 + Cargo.lock | 56 + crates/api_models/src/events.rs | 1 + crates/api_models/src/events/user.rs | 14 + crates/api_models/src/lib.rs | 1 + crates/api_models/src/user.rs | 21 + crates/data_models/Cargo.toml | 2 +- crates/router/Cargo.toml | 3 + crates/router/src/consts.rs | 6 + crates/router/src/consts/user.rs | 8 + crates/router/src/core.rs | 2 + crates/router/src/core/errors.rs | 4 + crates/router/src/core/errors/user.rs | 78 + crates/router/src/core/user.rs | 81 + crates/router/src/lib.rs | 1 + crates/router/src/routes.rs | 4 +- crates/router/src/routes/admin.rs | 91 +- crates/router/src/routes/api_keys.rs | 32 +- crates/router/src/routes/app.rs | 16 +- crates/router/src/routes/lock_utils.rs | 3 + crates/router/src/routes/payments.rs | 19 +- crates/router/src/routes/refunds.rs | 8 +- crates/router/src/routes/routing.rs | 44 +- crates/router/src/routes/user.rs | 31 + crates/router/src/services.rs | 2 + crates/router/src/services/authentication.rs | 101 +- crates/router/src/services/jwt.rs | 42 + crates/router/src/types/domain.rs | 4 + crates/router/src/types/domain/user.rs | 483 ++++ crates/router/src/utils.rs | 2 + crates/router/src/utils/user.rs | 1 + .../router/src/utils/user/blocker_emails.txt | 2349 +++++++++++++++++ crates/router/src/utils/user/password.rs | 43 + crates/router_env/src/logger/types.rs | 2 + 34 files changed, 3489 insertions(+), 67 deletions(-) create mode 100644 crates/api_models/src/events/user.rs create mode 100644 crates/api_models/src/user.rs create mode 100644 crates/router/src/consts/user.rs create mode 100644 crates/router/src/core/errors/user.rs create mode 100644 crates/router/src/core/user.rs create mode 100644 crates/router/src/routes/user.rs create mode 100644 crates/router/src/services/jwt.rs create mode 100644 crates/router/src/types/domain/user.rs create mode 100644 crates/router/src/utils/user.rs create mode 100644 crates/router/src/utils/user/blocker_emails.txt create mode 100644 crates/router/src/utils/user/password.rs diff --git a/.typos.toml b/.typos.toml index 0d6e6fd8e38c..1ac38a005c9e 100644 --- a/.typos.toml +++ b/.typos.toml @@ -40,4 +40,5 @@ afe = "afe" # Commit id extend-exclude = [ "config/redis.conf", # `typos` also checked "AKE" in the file, which is present as a quoted string "openapi/open_api_spec.yaml", # no longer updated + "crates/router/src/utils/user/blocker_emails.txt", # this file contains various email domains ] diff --git a/Cargo.lock b/Cargo.lock index c96ce2c18258..ae7afa85d7d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -436,6 +436,18 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f907281554a3d0312bb7aab855a8e0ef6cbf1614d06de54105039ca8b34460e" +[[package]] +name = "argon2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ba4cac0a46bc1d2912652a751c47f2a9f3a7fe89bcae2275d418f5270402f9" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "arrayref" version = "0.3.7" @@ -1145,6 +1157,12 @@ dependencies = [ "vsimd", ] +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bb8" version = "0.8.1" @@ -1205,6 +1223,15 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "blake3" version = "1.4.0" @@ -3854,6 +3881,17 @@ dependencies = [ "regex", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "paste" version = "1.0.14" @@ -4562,6 +4600,7 @@ dependencies = [ "actix-rt", "actix-web", "api_models", + "argon2", "async-bb8-diesel", "async-trait", "awc", @@ -4637,10 +4676,12 @@ dependencies = [ "time", "tokio", "toml 0.7.4", + "unicode-segmentation", "url", "utoipa", "utoipa-swagger-ui", "uuid", + "validator", "wiremock", "x509-parser", ] @@ -6376,6 +6417,21 @@ dependencies = [ "serde", ] +[[package]] +name = "validator" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b92f40481c04ff1f4f61f304d61793c7b56ff76ac1469f1beb199b1445b253bd" +dependencies = [ + "idna", + "lazy_static", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", +] + [[package]] name = "valuable" version = "0.1.0" diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index 23e7c9dc706a..ad07340615b4 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -5,6 +5,7 @@ pub mod payment; pub mod payouts; pub mod refund; pub mod routing; +pub mod user; use common_utils::{ events::{ApiEventMetric, ApiEventsType}, diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs new file mode 100644 index 000000000000..2a896cc38776 --- /dev/null +++ b/crates/api_models/src/events/user.rs @@ -0,0 +1,14 @@ +use common_utils::events::{ApiEventMetric, ApiEventsType}; + +use crate::user::{ConnectAccountRequest, ConnectAccountResponse}; + +impl ApiEventMetric for ConnectAccountResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::User { + merchant_id: self.merchant_id.clone(), + user_id: self.user_id.clone(), + }) + } +} + +impl ApiEventMetric for ConnectAccountRequest {} diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index 75509ed7386d..bcc3913ea824 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -21,5 +21,6 @@ pub mod payments; pub mod payouts; pub mod refunds; pub mod routing; +pub mod user; pub mod verifications; pub mod webhooks; diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs new file mode 100644 index 000000000000..91f7702c654e --- /dev/null +++ b/crates/api_models/src/user.rs @@ -0,0 +1,21 @@ +use common_utils::pii; +use masking::Secret; + +#[derive(serde::Deserialize, Debug, Clone, serde::Serialize)] +pub struct ConnectAccountRequest { + pub email: pii::Email, + pub password: Secret, +} + +#[derive(serde::Serialize, Debug, Clone)] +pub struct ConnectAccountResponse { + pub token: Secret, + pub merchant_id: String, + pub name: Secret, + pub email: pii::Email, + pub verification_days_left: Option, + pub user_role: String, + //this field is added for audit/debug reasons + #[serde(skip_serializing)] + pub user_id: String, +} diff --git a/crates/data_models/Cargo.toml b/crates/data_models/Cargo.toml index 254c194182f3..c7c872771689 100644 --- a/crates/data_models/Cargo.toml +++ b/crates/data_models/Cargo.toml @@ -27,4 +27,4 @@ serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.96" strum = { version = "0.25", features = [ "derive" ] } thiserror = "1.0.40" -time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } \ No newline at end of file +time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 7456944a8e4e..d765a5b5c5ed 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -38,6 +38,7 @@ actix-cors = "0.6.4" actix-multipart = "0.6.0" actix-rt = "2.8.0" actix-web = "4.3.1" +argon2 = { version = "0.5.0", features = ["std"] } async-bb8-diesel = "0.1.0" async-trait = "0.1.68" aws-config = { version = "0.55.3", optional = true } @@ -89,10 +90,12 @@ thiserror = "1.0.40" time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] } tera = "1.19.1" +unicode-segmentation = "1.10.1" url = { version = "2.4.0", features = ["serde"] } utoipa = { version = "3.3.0", features = ["preserve_order", "time"] } utoipa-swagger-ui = { version = "3.1.3", features = ["actix-web"] } uuid = { version = "1.3.3", features = ["serde", "v4"] } +validator = "0.16.0" openssl = "0.10.55" x509-parser = "0.15.0" sha-1 = { version = "0.9"} diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 7b20c3865d15..410e3c1113b1 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -1,3 +1,6 @@ +#[cfg(feature = "olap")] +pub mod user; + // ID generation pub(crate) const ID_LENGTH: usize = 20; pub(crate) const MAX_ID_LENGTH: usize = 64; @@ -52,3 +55,6 @@ pub const ROUTING_CONFIG_ID_LENGTH: usize = 10; pub const LOCKER_REDIS_PREFIX: &str = "LOCKER_PM_TOKEN"; pub const LOCKER_REDIS_EXPIRY_SECONDS: u32 = 60 * 15; // 15 minutes + +#[cfg(any(feature = "olap", feature = "oltp"))] +pub const JWT_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24 * 2; // 2 days diff --git a/crates/router/src/consts/user.rs b/crates/router/src/consts/user.rs new file mode 100644 index 000000000000..3a71fed01a12 --- /dev/null +++ b/crates/router/src/consts/user.rs @@ -0,0 +1,8 @@ +#[cfg(feature = "olap")] +pub const MAX_NAME_LENGTH: usize = 70; +#[cfg(feature = "olap")] +pub const MAX_COMPANY_NAME_LENGTH: usize = 70; + +// USER ROLES +#[cfg(any(feature = "olap", feature = "oltp"))] +pub const ROLE_ID_ORGANIZATION_ADMIN: &str = "org_admin"; diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index 817fafdae520..b7023fe5ae46 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -18,6 +18,8 @@ pub mod payments; pub mod payouts; pub mod refunds; pub mod routing; +#[cfg(feature = "olap")] +pub mod user; pub mod utils; #[cfg(all(feature = "olap", feature = "kms"))] pub mod verification; diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index dc1d56721e88..810c079987eb 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -2,6 +2,8 @@ pub mod api_error_response; pub mod customers_error_response; pub mod error_handlers; pub mod transformers; +#[cfg(feature = "olap")] +pub mod user; pub mod utils; use std::fmt::Display; @@ -13,6 +15,8 @@ use diesel_models::errors as storage_errors; pub use redis_interface::errors::RedisError; use scheduler::errors as sch_errors; use storage_impl::errors as storage_impl_errors; +#[cfg(feature = "olap")] +pub use user::*; pub use self::{ api_error_response::ApiErrorResponse, diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs new file mode 100644 index 000000000000..b4d48365dc84 --- /dev/null +++ b/crates/router/src/core/errors/user.rs @@ -0,0 +1,78 @@ +use common_utils::errors::CustomResult; + +use crate::services::ApplicationResponse; + +pub type UserResult = CustomResult; +pub type UserResponse = CustomResult, UserErrors>; + +#[derive(Debug, thiserror::Error)] +pub enum UserErrors { + #[error("User InternalServerError")] + InternalServerError, + #[error("InvalidCredentials")] + InvalidCredentials, + #[error("UserExists")] + UserExists, + #[error("EmailParsingError")] + EmailParsingError, + #[error("NameParsingError")] + NameParsingError, + #[error("PasswordParsingError")] + PasswordParsingError, + #[error("CompanyNameParsingError")] + CompanyNameParsingError, + #[error("MerchantAccountCreationError: {0}")] + MerchantAccountCreationError(String), + #[error("InvalidEmailError")] + InvalidEmailError, + #[error("DuplicateOrganizationId")] + DuplicateOrganizationId, +} + +impl common_utils::errors::ErrorSwitch for UserErrors { + fn switch(&self) -> api_models::errors::types::ApiErrorResponse { + use api_models::errors::types::{ApiError, ApiErrorResponse as AER}; + let sub_code = "UR"; + match self { + Self::InternalServerError => { + AER::InternalServerError(ApiError::new("HE", 0, "Something Went Wrong", None)) + } + Self::InvalidCredentials => AER::Unauthorized(ApiError::new( + sub_code, + 1, + "Incorrect email or password", + None, + )), + Self::UserExists => AER::BadRequest(ApiError::new( + sub_code, + 3, + "An account already exists with this email", + None, + )), + Self::EmailParsingError => { + AER::BadRequest(ApiError::new(sub_code, 7, "Invalid Email", None)) + } + Self::NameParsingError => { + AER::BadRequest(ApiError::new(sub_code, 8, "Invalid Name", None)) + } + Self::PasswordParsingError => { + AER::BadRequest(ApiError::new(sub_code, 9, "Invalid Password", None)) + } + Self::CompanyNameParsingError => { + AER::BadRequest(ApiError::new(sub_code, 14, "Invalid Company Name", None)) + } + Self::MerchantAccountCreationError(error_message) => { + AER::InternalServerError(ApiError::new(sub_code, 15, error_message, None)) + } + Self::InvalidEmailError => { + AER::BadRequest(ApiError::new(sub_code, 16, "Invalid Email", None)) + } + Self::DuplicateOrganizationId => AER::InternalServerError(ApiError::new( + sub_code, + 21, + "An Organization with the id already exists", + None, + )), + } + } +} diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs new file mode 100644 index 000000000000..710dc9281bfa --- /dev/null +++ b/crates/router/src/core/user.rs @@ -0,0 +1,81 @@ +use api_models::user as api; +use diesel_models::enums::UserStatus; +use error_stack::IntoReport; +use masking::{ExposeInterface, Secret}; +use router_env::env; + +use super::errors::{UserErrors, UserResponse}; +use crate::{ + consts::user as consts, routes::AppState, services::ApplicationResponse, types::domain, +}; + +pub async fn connect_account( + state: AppState, + request: api::ConnectAccountRequest, +) -> UserResponse { + let find_user = state + .store + .find_user_by_email(request.email.clone().expose().expose().as_str()) + .await; + + if let Ok(found_user) = find_user { + let user_from_db: domain::UserFromStorage = found_user.into(); + + user_from_db.compare_password(request.password)?; + + let user_role = user_from_db.get_role_from_db(state.clone()).await?; + let jwt_token = user_from_db + .get_jwt_auth_token(state.clone(), user_role.org_id) + .await?; + + return Ok(ApplicationResponse::Json(api::ConnectAccountResponse { + token: Secret::new(jwt_token), + merchant_id: user_role.merchant_id, + name: user_from_db.get_name(), + email: user_from_db.get_email(), + verification_days_left: None, + user_role: user_role.role_id, + user_id: user_from_db.get_user_id().to_string(), + })); + } else if find_user + .map_err(|e| e.current_context().is_db_not_found()) + .err() + .unwrap_or(false) + { + if matches!(env::which(), env::Env::Production) { + return Err(UserErrors::InvalidCredentials).into_report(); + } + + let new_user = domain::NewUser::try_from(request)?; + let _ = new_user + .get_new_merchant() + .get_new_organization() + .insert_org_in_db(state.clone()) + .await?; + let user_from_db = new_user + .insert_user_and_merchant_in_db(state.clone()) + .await?; + let user_role = new_user + .insert_user_role_in_db( + state.clone(), + consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(), + UserStatus::Active, + ) + .await?; + let jwt_token = user_from_db + .get_jwt_auth_token(state.clone(), user_role.org_id) + .await?; + + return Ok(ApplicationResponse::Json(api::ConnectAccountResponse { + token: Secret::new(jwt_token), + merchant_id: user_role.merchant_id, + name: user_from_db.get_name(), + email: user_from_db.get_email(), + verification_days_left: None, + user_role: user_role.role_id, + user_id: user_from_db.get_user_id().to_string(), + })); + } else { + Err(UserErrors::InternalServerError.into()) + } +} diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 5cd0b6cbea5f..e106eb06a766 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -146,6 +146,7 @@ pub fn mk_app( .service(routes::Analytics::server(state.clone())) .service(routes::Routing::server(state.clone())) .service(routes::Gsm::server(state.clone())) + .service(routes::User::server(state.clone())) } #[cfg(all(feature = "olap", feature = "kms"))] diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index ac5c14200600..745433c2074b 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -23,6 +23,8 @@ pub mod payouts; pub mod refunds; #[cfg(feature = "olap")] pub mod routing; +#[cfg(feature = "olap")] +pub mod user; #[cfg(all(feature = "olap", feature = "kms"))] pub mod verification; pub mod webhooks; @@ -38,7 +40,7 @@ pub use self::app::Verify; pub use self::app::{ ApiKeys, AppState, BusinessProfile, Cache, Cards, Configs, Customers, Disputes, EphemeralKey, Files, Gsm, Health, Mandates, MerchantAccount, MerchantConnectorAccount, PaymentLink, - PaymentMethods, Payments, Refunds, Webhooks, + PaymentMethods, Payments, Refunds, User, Webhooks, }; #[cfg(feature = "stripe")] pub use super::compatibility::stripe::StripeApis; diff --git a/crates/router/src/routes/admin.rs b/crates/router/src/routes/admin.rs index 9153e9e747f6..a8eda22402c3 100644 --- a/crates/router/src/routes/admin.rs +++ b/crates/router/src/routes/admin.rs @@ -64,7 +64,10 @@ pub async fn retrieve_merchant_account( ) -> HttpResponse { let flow = Flow::MerchantsAccountRetrieve; let merchant_id = mid.into_inner(); - let payload = web::Json(admin::MerchantId { merchant_id }).into_inner(); + let payload = web::Json(admin::MerchantId { + merchant_id: merchant_id.to_owned(), + }) + .into_inner(); api::server_wrap( flow, @@ -72,7 +75,11 @@ pub async fn retrieve_merchant_account( &req, payload, |state, _, req| get_merchant_account(state, req), - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { merchant_id }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -130,7 +137,13 @@ pub async fn update_merchant_account( &req, json_payload.into_inner(), |state, _, req| merchant_account_update(state, &merchant_id, req), - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { + merchant_id: merchant_id.clone(), + }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -203,7 +216,13 @@ pub async fn payment_connector_create( &req, json_payload.into_inner(), |state, _, req| create_payment_connector(state, req, &merchant_id), - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { + merchant_id: merchant_id.clone(), + }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -236,7 +255,7 @@ pub async fn payment_connector_retrieve( let flow = Flow::MerchantConnectorsRetrieve; let (merchant_id, merchant_connector_id) = path.into_inner(); let payload = web::Json(admin::MerchantConnectorId { - merchant_id, + merchant_id: merchant_id.clone(), merchant_connector_id, }) .into_inner(); @@ -249,7 +268,11 @@ pub async fn payment_connector_retrieve( |state, _, req| { retrieve_payment_connector(state, req.merchant_id, req.merchant_connector_id) }, - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { merchant_id }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -285,9 +308,13 @@ pub async fn payment_connector_list( flow, state, &req, - merchant_id, + merchant_id.to_owned(), |state, _, merchant_id| list_payment_connectors(state, merchant_id), - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { merchant_id }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -328,7 +355,13 @@ pub async fn payment_connector_update( &req, json_payload.into_inner(), |state, _, req| update_payment_connector(state, &merchant_id, &merchant_connector_id, req), - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { + merchant_id: merchant_id.clone(), + }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -362,7 +395,7 @@ pub async fn payment_connector_delete( let (merchant_id, merchant_connector_id) = path.into_inner(); let payload = web::Json(admin::MerchantConnectorId { - merchant_id, + merchant_id: merchant_id.clone(), merchant_connector_id, }) .into_inner(); @@ -372,7 +405,11 @@ pub async fn payment_connector_delete( &req, payload, |state, _, req| delete_payment_connector(state, req.merchant_id, req.merchant_connector_id), - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { merchant_id }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -419,7 +456,13 @@ pub async fn business_profile_create( &req, payload, |state, _, req| create_business_profile(state, req, &merchant_id), - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { + merchant_id: merchant_id.clone(), + }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -431,7 +474,7 @@ pub async fn business_profile_retrieve( path: web::Path<(String, String)>, ) -> HttpResponse { let flow = Flow::BusinessProfileRetrieve; - let (_, profile_id) = path.into_inner(); + let (merchant_id, profile_id) = path.into_inner(); api::server_wrap( flow, @@ -439,7 +482,11 @@ pub async fn business_profile_retrieve( &req, profile_id, |state, _, profile_id| retrieve_business_profile(state, profile_id), - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { merchant_id }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -460,7 +507,13 @@ pub async fn business_profile_update( &req, json_payload.into_inner(), |state, _, req| update_business_profile(state, &profile_id, &merchant_id, req), - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { + merchant_id: merchant_id.clone(), + }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -498,9 +551,13 @@ pub async fn business_profiles_list( flow, state, &req, - merchant_id, + merchant_id.clone(), |state, _, merchant_id| list_business_profile(state, merchant_id), - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { merchant_id }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await diff --git a/crates/router/src/routes/api_keys.rs b/crates/router/src/routes/api_keys.rs index c2e289cd0f7e..1f71f1dc2800 100644 --- a/crates/router/src/routes/api_keys.rs +++ b/crates/router/src/routes/api_keys.rs @@ -53,7 +53,13 @@ pub async fn api_key_create( ) .await }, - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { + merchant_id: merchant_id.clone(), + }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -91,7 +97,13 @@ pub async fn api_key_retrieve( &req, (&merchant_id, &key_id), |state, _, (merchant_id, key_id)| api_keys::retrieve_api_key(state, merchant_id, key_id), - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { + merchant_id: merchant_id.clone(), + }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -173,7 +185,13 @@ pub async fn api_key_revoke( &req, (&merchant_id, &key_id), |state, _, (merchant_id, key_id)| api_keys::revoke_api_key(state, merchant_id, key_id), - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { + merchant_id: merchant_id.clone(), + }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -213,11 +231,15 @@ pub async fn api_key_list( flow, state, &req, - (limit, offset, merchant_id), + (limit, offset, merchant_id.clone()), |state, _, (limit, offset, merchant_id)| async move { api_keys::list_api_keys(state, merchant_id, limit, offset).await }, - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { merchant_id }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 67662961ed44..c34c542d1b6c 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -19,7 +19,7 @@ use super::routing as cloud_routing; #[cfg(all(feature = "olap", feature = "kms"))] use super::verification::{apple_pay_merchant_registration, retrieve_apple_pay_verified_domains}; #[cfg(feature = "olap")] -use super::{admin::*, api_keys::*, disputes::*, files::*, gsm::*}; +use super::{admin::*, api_keys::*, disputes::*, files::*, gsm::*, user::*}; use super::{cache::*, health::*, payment_link::*}; #[cfg(any(feature = "olap", feature = "oltp"))] use super::{configs::*, customers::*, mandates::*, payments::*, refunds::*}; @@ -710,3 +710,17 @@ impl Verify { ) } } + +pub struct User; + +#[cfg(feature = "olap")] +impl User { + pub fn server(state: AppState) -> Scope { + web::scope("/user") + .app_data(web::Data::new(state)) + .service(web::resource("/signin").route(web::post().to(user_connect_account))) + .service(web::resource("/signup").route(web::post().to(user_connect_account))) + .service(web::resource("/v2/signin").route(web::post().to(user_connect_account))) + .service(web::resource("/v2/signup").route(web::post().to(user_connect_account))) + } +} diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 4e6fc1870f56..ae573e871627 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -24,6 +24,7 @@ pub enum ApiIdentifier { PaymentLink, Routing, Gsm, + User, } impl From for ApiIdentifier { @@ -134,6 +135,8 @@ impl From for ApiIdentifier { | Flow::GsmRuleRetrieve | Flow::GsmRuleUpdate | Flow::GsmRuleDelete => Self::Gsm, + + Flow::UserConnectAccount => Self::User, } } } diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 5ed73df1c175..ed36721da445 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -4,7 +4,7 @@ pub mod helpers; use actix_web::{web, Responder}; use api_models::payments::HeaderPayload; use error_stack::report; -use router_env::{instrument, tracing, types, Flow}; +use router_env::{env, instrument, tracing, types, Flow}; use crate::{ self as app, @@ -118,7 +118,10 @@ pub async fn payments_create( api::AuthFlow::Merchant, ) }, - &auth::ApiKeyAuth, + match env::which() { + env::Env::Production => &auth::ApiKeyAuth, + _ => auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + }, locking_action, ) .await @@ -249,7 +252,11 @@ pub async fn payments_retrieve( HeaderPayload::default(), ) }, - &*auth_type, + auth::auth_type( + &*auth_type, + &auth::JWTAuth, + req.headers(), + ), locking_action, ) .await @@ -828,7 +835,7 @@ pub async fn payments_list( &req, payload, |state, auth, req| payments::list_payments(state, auth.merchant_account, req), - &auth::ApiKeyAuth, + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, ) .await @@ -848,7 +855,7 @@ pub async fn payments_list_by_filter( &req, payload, |state, auth, req| payments::apply_filters_on_payments(state, auth.merchant_account, req), - &auth::ApiKeyAuth, + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, ) .await @@ -868,7 +875,7 @@ pub async fn get_filters_for_payments( &req, payload, |state, auth, req| payments::get_filters_for_payments(state, auth.merchant_account, req), - &auth::ApiKeyAuth, + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, ) .await diff --git a/crates/router/src/routes/refunds.rs b/crates/router/src/routes/refunds.rs index c20f3fbf975d..d1f5cb56fe23 100644 --- a/crates/router/src/routes/refunds.rs +++ b/crates/router/src/routes/refunds.rs @@ -37,7 +37,7 @@ pub async fn refunds_create( &req, json_payload.into_inner(), |state, auth, req| refund_create_core(state, auth.merchant_account, auth.key_store, req), - &auth::ApiKeyAuth, + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, ) .await @@ -88,7 +88,7 @@ pub async fn refunds_retrieve( refund_retrieve_core, ) }, - &auth::ApiKeyAuth, + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, ) .await @@ -202,7 +202,7 @@ pub async fn refunds_list( &req, payload.into_inner(), |state, auth, req| refund_list(state, auth.merchant_account, req), - &auth::ApiKeyAuth, + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, ) .await @@ -235,7 +235,7 @@ pub async fn refunds_filter_list( &req, payload.into_inner(), |state, auth, req| refund_filter_list(state, auth.merchant_account, req), - &auth::ApiKeyAuth, + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, ) .await diff --git a/crates/router/src/routes/routing.rs b/crates/router/src/routes/routing.rs index 9252c360a9ce..b87116f47fc5 100644 --- a/crates/router/src/routes/routing.rs +++ b/crates/router/src/routes/routing.rs @@ -14,7 +14,7 @@ use router_env::{ use crate::{ core::{api_locking, routing}, routes::AppState, - services::{api as oss_api, authentication as oss_auth, authentication as auth}, + services::{api as oss_api, authentication as auth}, }; #[cfg(feature = "olap")] @@ -30,11 +30,11 @@ pub async fn routing_create_config( state, &req, json_payload.into_inner(), - |state, auth: oss_auth::AuthenticationData, payload| { + |state, auth: auth::AuthenticationData, payload| { routing::create_routing_config(state, auth.merchant_account, auth.key_store, payload) }, #[cfg(not(feature = "release"))] - auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), #[cfg(feature = "release")] &auth::JWTAuth, api_locking::LockAction::NotApplicable, @@ -55,7 +55,7 @@ pub async fn routing_link_config( state, &req, path.into_inner(), - |state, auth: oss_auth::AuthenticationData, algorithm_id| { + |state, auth: auth::AuthenticationData, algorithm_id| { routing::link_routing_config( state, auth.merchant_account, @@ -65,7 +65,7 @@ pub async fn routing_link_config( ) }, #[cfg(not(feature = "release"))] - auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), #[cfg(feature = "release")] &auth::JWTAuth, api_locking::LockAction::NotApplicable, @@ -87,11 +87,11 @@ pub async fn routing_retrieve_config( state, &req, algorithm_id, - |state, auth: oss_auth::AuthenticationData, algorithm_id| { + |state, auth: auth::AuthenticationData, algorithm_id| { routing::retrieve_routing_config(state, auth.merchant_account, algorithm_id) }, #[cfg(not(feature = "release"))] - auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), #[cfg(feature = "release")] &auth::JWTAuth, api_locking::LockAction::NotApplicable, @@ -114,7 +114,7 @@ pub async fn routing_retrieve_dictionary( state, &req, query.into_inner(), - |state, auth: oss_auth::AuthenticationData, query_params| { + |state, auth: auth::AuthenticationData, query_params| { routing::retrieve_merchant_routing_dictionary( state, auth.merchant_account, @@ -122,7 +122,7 @@ pub async fn routing_retrieve_dictionary( ) }, #[cfg(not(feature = "release"))] - auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), #[cfg(feature = "release")] &auth::JWTAuth, api_locking::LockAction::NotApplicable, @@ -138,11 +138,11 @@ pub async fn routing_retrieve_dictionary( state, &req, (), - |state, auth: oss_auth::AuthenticationData, _| { + |state, auth: auth::AuthenticationData, _| { routing::retrieve_merchant_routing_dictionary(state, auth.merchant_account) }, #[cfg(not(feature = "release"))] - auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), #[cfg(feature = "release")] &auth::JWTAuth, api_locking::LockAction::NotApplicable, @@ -168,11 +168,11 @@ pub async fn routing_unlink_config( state, &req, payload.into_inner(), - |state, auth: oss_auth::AuthenticationData, payload_req| { + |state, auth: auth::AuthenticationData, payload_req| { routing::unlink_routing_config(state, auth.merchant_account, payload_req) }, #[cfg(not(feature = "release"))] - auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), #[cfg(feature = "release")] &auth::JWTAuth, api_locking::LockAction::NotApplicable, @@ -188,11 +188,11 @@ pub async fn routing_unlink_config( state, &req, (), - |state, auth: oss_auth::AuthenticationData, _| { + |state, auth: auth::AuthenticationData, _| { routing::unlink_routing_config(state, auth.merchant_account, auth.key_store) }, #[cfg(not(feature = "release"))] - auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), #[cfg(feature = "release")] &auth::JWTAuth, api_locking::LockAction::NotApplicable, @@ -213,11 +213,11 @@ pub async fn routing_update_default_config( state, &req, json_payload.into_inner(), - |state, auth: oss_auth::AuthenticationData, updated_config| { + |state, auth: auth::AuthenticationData, updated_config| { routing::update_default_routing_config(state, auth.merchant_account, updated_config) }, #[cfg(not(feature = "release"))] - auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), #[cfg(feature = "release")] &auth::JWTAuth, api_locking::LockAction::NotApplicable, @@ -236,11 +236,11 @@ pub async fn routing_retrieve_default_config( state, &req, (), - |state, auth: oss_auth::AuthenticationData, _| { + |state, auth: auth::AuthenticationData, _| { routing::retrieve_default_routing_config(state, auth.merchant_account) }, #[cfg(not(feature = "release"))] - auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), #[cfg(feature = "release")] &auth::JWTAuth, api_locking::LockAction::NotApplicable, @@ -268,7 +268,7 @@ pub async fn routing_retrieve_linked_config( routing::retrieve_linked_routing_config(state, auth.merchant_account, query_params) }, #[cfg(not(feature = "release"))] - auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), #[cfg(feature = "release")] &auth::JWTAuth, api_locking::LockAction::NotApplicable, @@ -284,11 +284,11 @@ pub async fn routing_retrieve_linked_config( state, &req, (), - |state, auth: oss_auth::AuthenticationData, _| { + |state, auth: auth::AuthenticationData, _| { routing::retrieve_linked_routing_config(state, auth.merchant_account) }, #[cfg(not(feature = "release"))] - auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), #[cfg(feature = "release")] &auth::JWTAuth, api_locking::LockAction::NotApplicable, diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs new file mode 100644 index 000000000000..0ff11ce087b5 --- /dev/null +++ b/crates/router/src/routes/user.rs @@ -0,0 +1,31 @@ +use actix_web::{web, HttpRequest, HttpResponse}; +use api_models::user as user_api; +use router_env::Flow; + +use super::AppState; +use crate::{ + core::{api_locking, user}, + services::{ + api, + authentication::{self as auth}, + }, +}; + +pub async fn user_connect_account( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::UserConnectAccount; + let req_payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + req_payload.clone(), + |state, _, req_body| user::connect_account(state, req_body), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/services.rs b/crates/router/src/services.rs index 631e9a5c189d..21f33f0fa0b8 100644 --- a/crates/router/src/services.rs +++ b/crates/router/src/services.rs @@ -1,6 +1,8 @@ pub mod api; pub mod authentication; pub mod encryption; +#[cfg(feature = "olap")] +pub mod jwt; pub mod logger; #[cfg(feature = "kms")] diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 0a7f5189b904..da4dec2eec8a 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -9,6 +9,10 @@ use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use masking::{PeekInterface, StrongSecret}; use serde::Serialize; +#[cfg(feature = "olap")] +use super::jwt; +#[cfg(feature = "olap")] +use crate::consts; use crate::{ configs::settings, core::{ @@ -71,6 +75,37 @@ impl AuthenticationType { } } +#[derive(serde::Serialize, serde::Deserialize)] +pub struct AuthToken { + pub user_id: String, + pub merchant_id: String, + pub role_id: String, + pub exp: u64, + pub org_id: String, +} + +#[cfg(feature = "olap")] +impl AuthToken { + pub async fn new_token( + user_id: String, + merchant_id: String, + role_id: String, + settings: &settings::Settings, + org_id: String, + ) -> errors::UserResult { + let exp_duration = std::time::Duration::from_secs(consts::JWT_TOKEN_TIME_IN_SECS); + let exp = jwt::generate_exp(exp_duration)?.as_secs(); + let token_payload = Self { + user_id, + merchant_id, + role_id, + exp, + org_id, + }; + jwt::generate_jwt(&token_payload, settings).await + } +} + pub trait AuthInfo { fn get_merchant_id(&self) -> Option<&str>; } @@ -366,14 +401,58 @@ where request_headers: &HeaderMap, state: &A, ) -> RouterResult<((), AuthenticationType)> { - let mut token = get_jwt(request_headers)?; - token = strip_jwt_token(token)?; - decode_jwt::(token, state) - .await - .map(|_| ((), AuthenticationType::NoAuth)) + let payload = parse_jwt_payload::(request_headers, state).await?; + Ok(( + (), + AuthenticationType::MerchantJWT { + merchant_id: payload.merchant_id, + user_id: Some(payload.user_id), + }, + )) + } +} + +pub struct JWTAuthMerchantFromRoute { + pub merchant_id: String, +} + +#[async_trait] +impl AuthenticateAndFetch<(), A> for JWTAuthMerchantFromRoute +where + A: AppStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<((), AuthenticationType)> { + let payload = parse_jwt_payload::(request_headers, state).await?; + + // Check if token has access to merchantID that has been requested through query param + if payload.merchant_id != self.merchant_id { + return Err(report!(errors::ApiErrorResponse::InvalidJwtToken)); + } + Ok(( + (), + AuthenticationType::MerchantJWT { + merchant_id: payload.merchant_id, + user_id: Some(payload.user_id), + }, + )) } } +pub async fn parse_jwt_payload(headers: &HeaderMap, state: &A) -> RouterResult +where + T: serde::de::DeserializeOwned, + A: AppStateInfo + Sync, +{ + let token = get_jwt_from_authorization_header(headers)?; + let payload = decode_jwt(token, state).await?; + + Ok(payload) +} + #[derive(serde::Deserialize)] struct JwtAuthPayloadFetchMerchantAccount { merchant_id: String, @@ -389,9 +468,9 @@ where request_headers: &HeaderMap, state: &A, ) -> RouterResult<(AuthenticationData, AuthenticationType)> { - let mut token = get_jwt(request_headers)?; - token = strip_jwt_token(token)?; - let payload = decode_jwt::(token, state).await?; + let payload = + parse_jwt_payload::(request_headers, state) + .await?; let key_store = state .store() .get_merchant_key_store_by_merchant_id( @@ -595,14 +674,16 @@ pub fn get_header_value_by_key(key: String, headers: &HeaderMap) -> RouterResult .transpose() } -pub fn get_jwt(headers: &HeaderMap) -> RouterResult<&str> { +pub fn get_jwt_from_authorization_header(headers: &HeaderMap) -> RouterResult<&str> { headers .get(crate::headers::AUTHORIZATION) .get_required_value(crate::headers::AUTHORIZATION)? .to_str() .into_report() .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to convert JWT token to string") + .attach_printable("Failed to convert JWT token to string")? + .strip_prefix("Bearer ") + .ok_or(errors::ApiErrorResponse::InvalidJwtToken.into()) } pub fn strip_jwt_token(token: &str) -> RouterResult<&str> { diff --git a/crates/router/src/services/jwt.rs b/crates/router/src/services/jwt.rs new file mode 100644 index 000000000000..b69a21583919 --- /dev/null +++ b/crates/router/src/services/jwt.rs @@ -0,0 +1,42 @@ +use common_utils::errors::CustomResult; +use error_stack::{IntoReport, ResultExt}; +use jsonwebtoken::{encode, EncodingKey, Header}; +use masking::PeekInterface; + +use super::authentication; +use crate::{configs::settings::Settings, core::errors::UserErrors}; + +pub fn generate_exp( + exp_duration: std::time::Duration, +) -> CustomResult { + std::time::SystemTime::now() + .checked_add(exp_duration) + .ok_or(UserErrors::InternalServerError)? + .duration_since(std::time::UNIX_EPOCH) + .into_report() + .change_context(UserErrors::InternalServerError) +} + +pub async fn generate_jwt( + claims_data: &T, + settings: &Settings, +) -> CustomResult +where + T: serde::ser::Serialize, +{ + let jwt_secret = authentication::get_jwt_secret( + &settings.secrets, + #[cfg(feature = "kms")] + external_services::kms::get_kms_client(&settings.kms).await, + ) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("Failed to obtain JWT secret")?; + encode( + &Header::default(), + claims_data, + &EncodingKey::from_secret(jwt_secret.peek().as_bytes()), + ) + .into_report() + .change_context(UserErrors::InternalServerError) +} diff --git a/crates/router/src/types/domain.rs b/crates/router/src/types/domain.rs index 44123850d468..c93f96eaf09e 100644 --- a/crates/router/src/types/domain.rs +++ b/crates/router/src/types/domain.rs @@ -5,9 +5,13 @@ mod merchant_account; mod merchant_connector_account; mod merchant_key_store; pub mod types; +#[cfg(feature = "olap")] +pub mod user; pub use address::*; pub use customer::*; pub use merchant_account::*; pub use merchant_connector_account::*; pub use merchant_key_store::*; +#[cfg(feature = "olap")] +pub use user::*; diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs new file mode 100644 index 000000000000..c053b0f15448 --- /dev/null +++ b/crates/router/src/types/domain/user.rs @@ -0,0 +1,483 @@ +use std::{collections::HashSet, ops, str::FromStr}; + +use api_models::{admin as admin_api, organization as api_org, user as user_api}; +use common_utils::pii; +use diesel_models::{ + enums::UserStatus, + organization as diesel_org, + organization::Organization, + user as storage_user, + user_role::{UserRole, UserRoleNew}, +}; +use error_stack::{IntoReport, ResultExt}; +use masking::{ExposeInterface, PeekInterface, Secret}; +use once_cell::sync::Lazy; +use unicode_segmentation::UnicodeSegmentation; + +use crate::{ + consts::user as consts, + core::{ + admin, + errors::{UserErrors, UserResult}, + }, + db::StorageInterface, + routes::AppState, + services::authentication::AuthToken, + types::transformers::ForeignFrom, + utils::user::password, +}; + +#[derive(Clone)] +pub struct UserName(Secret); + +impl UserName { + pub fn new(name: Secret) -> UserResult { + let name = name.expose(); + let is_empty_or_whitespace = name.trim().is_empty(); + let is_too_long = name.graphemes(true).count() > consts::MAX_NAME_LENGTH; + + let forbidden_characters = ['/', '(', ')', '"', '<', '>', '\\', '{', '}']; + let contains_forbidden_characters = name.chars().any(|g| forbidden_characters.contains(&g)); + + if is_empty_or_whitespace || is_too_long || contains_forbidden_characters { + Err(UserErrors::NameParsingError.into()) + } else { + Ok(Self(name.into())) + } + } + + pub fn get_secret(self) -> Secret { + self.0 + } +} + +impl TryFrom for UserName { + type Error = error_stack::Report; + + fn try_from(value: pii::Email) -> UserResult { + Self::new(Secret::new( + value + .peek() + .split_once('@') + .ok_or(UserErrors::InvalidEmailError)? + .0 + .to_string(), + )) + } +} + +#[derive(Clone, Debug)] +pub struct UserEmail(pii::Email); + +static BLOCKED_EMAIL: Lazy> = Lazy::new(|| { + let blocked_emails_content = include_str!("../../utils/user/blocker_emails.txt"); + let blocked_emails: HashSet = blocked_emails_content + .lines() + .map(|s| s.trim().to_owned()) + .collect(); + blocked_emails +}); + +impl UserEmail { + pub fn new(email: Secret) -> UserResult { + let email_string = email.expose(); + let email = + pii::Email::from_str(&email_string).change_context(UserErrors::EmailParsingError)?; + + if validator::validate_email(&email_string) { + let (_username, domain) = match email_string.as_str().split_once('@') { + Some((u, d)) => (u, d), + None => return Err(UserErrors::EmailParsingError.into()), + }; + + if BLOCKED_EMAIL.contains(domain) { + return Err(UserErrors::InvalidEmailError.into()); + } + Ok(Self(email)) + } else { + Err(UserErrors::EmailParsingError.into()) + } + } + + pub fn from_pii_email(email: pii::Email) -> UserResult { + let email_string = email.peek(); + if validator::validate_email(email_string) { + let (_username, domain) = match email_string.split_once('@') { + Some((u, d)) => (u, d), + None => return Err(UserErrors::EmailParsingError.into()), + }; + if BLOCKED_EMAIL.contains(domain) { + return Err(UserErrors::InvalidEmailError.into()); + } + Ok(Self(email)) + } else { + Err(UserErrors::EmailParsingError.into()) + } + } + + pub fn into_inner(self) -> pii::Email { + self.0 + } + + pub fn get_secret(self) -> Secret { + (*self.0).clone() + } +} + +impl TryFrom for UserEmail { + type Error = error_stack::Report; + + fn try_from(value: pii::Email) -> Result { + Self::from_pii_email(value) + } +} + +impl ops::Deref for UserEmail { + type Target = Secret; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Clone)] +pub struct UserPassword(Secret); + +impl UserPassword { + pub fn new(password: Secret) -> UserResult { + let password = password.expose(); + if password.is_empty() { + Err(UserErrors::PasswordParsingError.into()) + } else { + Ok(Self(password.into())) + } + } + + pub fn get_secret(&self) -> Secret { + self.0.clone() + } +} + +#[derive(Clone)] +pub struct UserCompanyName(String); + +impl UserCompanyName { + pub fn new(company_name: String) -> UserResult { + let company_name = company_name.trim(); + let is_empty_or_whitespace = company_name.is_empty(); + let is_too_long = company_name.graphemes(true).count() > consts::MAX_COMPANY_NAME_LENGTH; + + let is_all_valid_characters = company_name + .chars() + .all(|x| x.is_alphanumeric() || x.is_ascii_whitespace() || x == '_'); + if is_empty_or_whitespace || is_too_long || !is_all_valid_characters { + Err(UserErrors::CompanyNameParsingError.into()) + } else { + Ok(Self(company_name.to_string())) + } + } + + pub fn get_secret(self) -> String { + self.0 + } +} + +#[derive(Clone)] +pub struct NewUserOrganization(diesel_org::OrganizationNew); + +impl NewUserOrganization { + pub async fn insert_org_in_db(self, state: AppState) -> UserResult { + state + .store + .insert_organization(self.0) + .await + .map_err(|e| { + if e.current_context().is_db_unique_violation() { + e.change_context(UserErrors::DuplicateOrganizationId) + } else { + e.change_context(UserErrors::InternalServerError) + } + }) + .attach_printable("Error while inserting organization") + } + + pub fn get_organization_id(&self) -> String { + self.0.org_id.clone() + } +} + +impl From for NewUserOrganization { + fn from(_value: user_api::ConnectAccountRequest) -> Self { + let new_organization = api_org::OrganizationNew::new(None); + let db_organization = ForeignFrom::foreign_from(new_organization); + Self(db_organization) + } +} + +#[derive(Clone)] +pub struct NewUserMerchant { + merchant_id: String, + company_name: Option, + new_organization: NewUserOrganization, +} + +impl NewUserMerchant { + pub fn get_company_name(&self) -> Option { + self.company_name.clone().map(UserCompanyName::get_secret) + } + + pub fn get_merchant_id(&self) -> String { + self.merchant_id.clone() + } + + pub fn get_new_organization(&self) -> NewUserOrganization { + self.new_organization.clone() + } + + pub async fn check_if_already_exists_in_db(&self, state: AppState) -> UserResult<()> { + if state + .store + .get_merchant_key_store_by_merchant_id( + self.get_merchant_id().as_str(), + &state.store.get_master_key().to_vec().into(), + ) + .await + .is_ok() + { + return Err(UserErrors::MerchantAccountCreationError(format!( + "Merchant with {} already exists", + self.get_merchant_id() + ))) + .into_report(); + } + Ok(()) + } + + pub async fn create_new_merchant_and_insert_in_db(&self, state: AppState) -> UserResult<()> { + self.check_if_already_exists_in_db(state.clone()).await?; + Box::pin(admin::create_merchant_account( + state.clone(), + admin_api::MerchantAccountCreate { + merchant_id: self.get_merchant_id(), + metadata: None, + locker_id: None, + return_url: None, + merchant_name: self.get_company_name().map(Secret::new), + webhook_details: None, + publishable_key: None, + organization_id: Some(self.new_organization.get_organization_id()), + merchant_details: None, + routing_algorithm: None, + parent_merchant_id: None, + payment_link_config: None, + sub_merchants_enabled: None, + frm_routing_algorithm: None, + intent_fulfillment_time: None, + payout_routing_algorithm: None, + primary_business_details: None, + payment_response_hash_key: None, + enable_payment_response_hash: None, + redirect_to_merchant_with_http_post: None, + }, + )) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("Error while creating a merchant")?; + Ok(()) + } +} + +impl TryFrom for NewUserMerchant { + type Error = error_stack::Report; + + fn try_from(value: user_api::ConnectAccountRequest) -> UserResult { + let merchant_id = format!("merchant_{}", common_utils::date_time::now_unix_timestamp()); + let new_organization = NewUserOrganization::from(value); + + Ok(Self { + company_name: None, + merchant_id, + new_organization, + }) + } +} + +#[derive(Clone)] +pub struct NewUser { + user_id: String, + name: UserName, + email: UserEmail, + password: UserPassword, + new_merchant: NewUserMerchant, +} + +impl NewUser { + pub fn get_user_id(&self) -> String { + self.user_id.clone() + } + + pub fn get_email(&self) -> UserEmail { + self.email.clone() + } + + pub fn get_name(&self) -> Secret { + self.name.clone().get_secret() + } + + pub fn get_new_merchant(&self) -> NewUserMerchant { + self.new_merchant.clone() + } + + pub async fn insert_user_in_db( + &self, + db: &dyn StorageInterface, + ) -> UserResult { + match db.insert_user(self.clone().try_into()?).await { + Ok(user) => Ok(user.into()), + Err(e) => { + if e.current_context().is_db_unique_violation() { + return Err(e.change_context(UserErrors::UserExists)); + } else { + return Err(e.change_context(UserErrors::InternalServerError)); + } + } + } + .attach_printable("Error while inserting user") + } + + pub async fn insert_user_and_merchant_in_db( + &self, + state: AppState, + ) -> UserResult { + let db = state.store.as_ref(); + let merchant_id = self.get_new_merchant().get_merchant_id(); + self.new_merchant + .create_new_merchant_and_insert_in_db(state.clone()) + .await?; + let created_user = self.insert_user_in_db(db).await; + if created_user.is_err() { + let _ = admin::merchant_account_delete(state, merchant_id).await; + }; + created_user + } + + pub async fn insert_user_role_in_db( + self, + state: AppState, + role_id: String, + user_status: UserStatus, + ) -> UserResult { + let now = common_utils::date_time::now(); + let user_id = self.get_user_id(); + + state + .store + .insert_user_role(UserRoleNew { + merchant_id: self.get_new_merchant().get_merchant_id(), + status: user_status, + created_by: user_id.clone(), + last_modified_by: user_id.clone(), + user_id, + role_id, + created_at: now, + last_modified_at: now, + org_id: self + .get_new_merchant() + .get_new_organization() + .get_organization_id(), + }) + .await + .change_context(UserErrors::InternalServerError) + } +} + +impl TryFrom for storage_user::UserNew { + type Error = error_stack::Report; + + fn try_from(value: NewUser) -> UserResult { + let hashed_password = password::generate_password_hash(value.password.get_secret())?; + Ok(Self { + user_id: value.get_user_id(), + name: value.get_name(), + email: value.get_email().into_inner(), + password: hashed_password, + ..Default::default() + }) + } +} + +impl TryFrom for NewUser { + type Error = error_stack::Report; + + fn try_from(value: user_api::ConnectAccountRequest) -> UserResult { + let user_id = uuid::Uuid::new_v4().to_string(); + let email = value.email.clone().try_into()?; + let name = UserName::try_from(value.email.clone())?; + let password = UserPassword::new(value.password.clone())?; + let new_merchant = NewUserMerchant::try_from(value)?; + + Ok(Self { + user_id, + name, + email, + password, + new_merchant, + }) + } +} + +pub struct UserFromStorage(pub storage_user::User); + +impl From for UserFromStorage { + fn from(value: storage_user::User) -> Self { + Self(value) + } +} + +impl UserFromStorage { + pub fn get_user_id(&self) -> &str { + self.0.user_id.as_str() + } + + pub fn compare_password(&self, candidate: Secret) -> UserResult<()> { + match password::is_correct_password(candidate, self.0.password.clone()) { + Ok(true) => Ok(()), + Ok(false) => Err(UserErrors::InvalidCredentials.into()), + Err(e) => Err(e), + } + } + + pub fn get_name(&self) -> Secret { + self.0.name.clone() + } + + pub fn get_email(&self) -> pii::Email { + self.0.email.clone() + } + + pub async fn get_jwt_auth_token(&self, state: AppState, org_id: String) -> UserResult { + let role_id = self.get_role_from_db(state.clone()).await?.role_id; + let merchant_id = state + .store + .find_user_role_by_user_id(self.get_user_id()) + .await + .change_context(UserErrors::InternalServerError)? + .merchant_id; + AuthToken::new_token( + self.0.user_id.clone(), + merchant_id, + role_id, + &state.conf, + org_id, + ) + .await + } + + pub async fn get_role_from_db(&self, state: AppState) -> UserResult { + state + .store + .find_user_role_by_user_id(self.get_user_id()) + .await + .change_context(UserErrors::InternalServerError) + } +} diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index 558044028f7a..aadb714e8ce2 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -1,6 +1,8 @@ pub mod custom_serde; pub mod db_utils; pub mod ext_traits; +#[cfg(feature = "olap")] +pub mod user; #[cfg(feature = "kv_store")] pub mod storage_partitioning; diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs new file mode 100644 index 000000000000..c72e4b9feb3c --- /dev/null +++ b/crates/router/src/utils/user.rs @@ -0,0 +1 @@ +pub mod password; diff --git a/crates/router/src/utils/user/blocker_emails.txt b/crates/router/src/utils/user/blocker_emails.txt new file mode 100644 index 000000000000..e29e1b2d86f4 --- /dev/null +++ b/crates/router/src/utils/user/blocker_emails.txt @@ -0,0 +1,2349 @@ +020.co.uk +123.com +123box.net +123india.com +123mail.cl +123mail.org +123qwe.co.uk +138mail.com +141.ro +150mail.com +150ml.com +16mail.com +1963chevrolet.com +1963pontiac.com +1netdrive.com +1st-website.com +1stpd.net +2-mail.com +20after4.com +21cn.com +24h.co.jp +24horas.com +271soundview.com +2die4.com +2mydns.com +2net.us +3000.it +3ammagazine.com +3email.com +3xl.net +444.net +4email.com +4email.net +4newyork.com +50mail.com +55mail.cc +5fm.za.com +6210.hu +6sens.com +702mail.co.za +7110.hu +8848.net +8m.com +8m.net +8x.com.br +8u8.com +8u8.hk +8u8.tw +a-topmail.at +about.com +abv.bg +acceso.or.cr +access4less.net +accessgcc.com +acmemail.net +adiga.com +adinet.com.uy +adres.nl +advalvas.be +aeiou.pt +aeneasmail.com +afrik.com +afropoets.com +aggies.com +ahaa.dk +aichi.com +aim.com +airpost.net +aiutamici.com +aklan.com +aknet.kg +alabama.usa.com +alaska.usa.com +alavatotal.com +albafind.com +albawaba.com +alburaq.net +aldeax.com +aldeax.com.ar +alex4all.com +aliyun.com +alexandria.cc +algeria.com +alice.it +allmail.net +alskens.dk +altavista.se +altbox.org +alternativagratis.com +alum.com +alunos.unipar.br +alvilag.hu +amenworld.com +america.hm +americamail.com +amnetsal.com +amorous.com +ananzi.co.za +anet.ne.jp +anfmail.com +angelfire.com +animail.net +aniverse.com +anjungcafe.com +another.com +antedoonsub.com +antwerpen.com +anunciador.net +anytimenow.com +aon.at +apexmail.com +apollo.lv +approvers.net +aprava.com +apropo.ro +arcor.de +argentina.com +arizona.usa.com +arkansas.usa.com +armmail.com +army.com +arnet.com.ar +aroma.com +arrl.net +aruba.it +asheville.com +asia-links.com +asiamail.com +assala.com +assamesemail.com +asurfer.com +atl.lv +atlas.cz +atlas.sk +atozasia.com +atreillou.com +att.net +au.ru +aubenin.com +aus-city.com +aussiemail.com.au +avasmail.com.mv +axarnet.com +ayna.com +azet.sk +babbalu.com +badgers.com +bakpaka.com +bakpaka.net +balochistan.org +baluch.com +bama-fan.com +bancora.net +bankersmail.com +barlick.net +beeebank.com +beehive.org +been-there.com +beirut.com +belizehome.com +belizemail.net +belizeweb.com +bellsouth.net +berlin.de +bestmail.us +bflomail.com +bgnmail.com +bharatmail.com +big-orange.com +bigboss.cz +bigfoot.com +bigger.com +bigmailbox.com +bigmir.net +bigstring.com +bip.net +bigpond.com +bitwiser.com +biz.by +bizhosting.com +black-sea.ro +blackburnmail.com +blackglobalnetwork.net +blink182.net +blue.devils.com +bluebottle.com +bluemail.ch +blumail.org +blvds.com +bol.com.br +bolando.com +bollywood2000.com +bollywoodz.com +bombka.dyn.pl +bonbon.net +boom.com +bootmail.com +bostonoffice.com +box.az +boxbg.com +boxemail.com +brain.com.pk +brasilia.net +bravanese.com +brazilmail.com.br +breathe.com +brestonline.com +brfree.com.br +brujula.net +btcc.org +buffaloes.com +bulgaria.com +bulldogs.com +bumerang.ro +burntmail.com +butch-femme.net +buzy.com +buzzjakkerz.com +c-box.cz +c3.hu +c4.com +cadinfo.net +calcfacil.com.br +calcware.org +california.usa.com +callnetuk.com +camaroclubsweden.com +canada-11.com +canada.com +canal21.com +canoemail.com +caramail.com +cardblvd.com +care-mail.com +care2.com +caress.com +carioca.net +cashette.com +casino.com +casinomail.com +cataloniamail.com +catalunyamail.com +cataz.com +catcha.com +catholic.org +caths.co.uk +caxess.net +cbrmail.com +cc.lv +cemelli.com +centoper.it +centralpets.com +centrum.cz +centrum.sk +centurylink.net +cercaziende.it +cgac.es +chaiyo.com +chaiyomail.com +chance2mail.com +channelonetv.com +charter.net +chattown.com +checkitmail.at +chelny.com +cheshiremail.com +chil-e.com +chillimail.com +china.com +christianmail.org +ciaoweb.it +cine.com +ciphercom.net +circlemail.com +cititrustbank1.cjb.net +citromail.hu +citynetusa.com +ciudad.com.ar +claramail.com +classicmail.co.za +cliffhanger.com +clix.pt +close2you.net +cluemail.com +clujnapoca.ro +collegeclub.com +colombia.com +colorado.usa.com +comcast.net +comfortable.com +compaqnet.fr +compuserve.com +computer.net +computermail.net +computhouse.com +conevyt.org.mx +connect4free.net +connecticut.usa.com +coolgoose.com +coolkiwi.com +coollist.com +coxinet.net +coolmail.com +coolmail.net +coolsend.com +cooltoad.com +cooperation.net +copacabana.com +copticmail.com +corporateattorneys.com +corporation.net +correios.net.br +correomagico.com +cosmo.com +cosmosurf.net +cougars.com +count.com +countrybass.com +couple.com +criticalpath.net +critterpost.com +crosspaths.net +crosswinds.net +cryingmail.com +cs.com +csucsposta.hu +cumbriamail.com +curio-city.com +custmail.com +cwazy.co.uk +cwazy.net +cww.de +cyberaccess.com.pk +cybergirls.dk +cyberguys.dk +cybernet.it +cymail.net +dabsol.net +dada.net +dadanet.it +dailypioneer.com +damuc.org.br +dansegulvet.com +darkhorsefan.net +data54.com +davegracey.com +dayzers.com +daum.net +dbmail.com +dcemail.com +dcsi.net +deacons.com +deadlymob.org +deal-maker.com +dearriba.com +degoo.com +delajaonline.org +delaware.usa.com +delfi.lv +delhimail.com +demon.deacons.com +desertonline.com +desidrivers.com +deskpilot.com +despammed.com +detik.com +devils.com +dexara.net +dhmail.net +di-ve.com +didamail.com +digitaltrue.com +direccion.com +director-general.com +diri.com +discardmail.com +discoverymail.net +disinfo.net +djmillenium.com +dmailman.com +dnsmadeeasy.com +do.net.ar +dodgeit.com +dogmail.co.uk +doityourself.com +domaindiscover.com +domainmanager.com +doneasy.com +dontexist.org +dores.com +dostmail.com +dot5hosting.com +dotcom.fr +dotnow.com +dott.it +doubt.com +dplanet.ch +dragoncon.net +dragonfans.com +dropzone.com +dserver.org +dubaiwebcity.com +dublin.ie +dustdevil.com +dynamitemail.com +dyndns.org +e-apollo.lv +e-hkma.com +e-mail.cz +e-mail.ph +e-mailanywhere.com +e-milio.com +e-tapaal.com +e-webtec.com +earthalliance.com +earthling.net +eastmail.com +eastrolog.com +easy-pages.com +easy.com +easyinfomail.co.za +easypeasy.com +echina.com +ecn.org +ecplaza.net +eircom.net +edsamail.com.ph +educacao.te.pt +edumail.co.za +eeism.com +ego.co.th +ekolay.net +elforotv.com.ar +elitemail.org +elsitio.com +eltimon.com +elvis.com +email.com.br +email.cz +email.bg +email.it +email.lu +email.lviv.ua +email.nu +email.ro +email.si +email2me.com +emailacc.com +emailaccount.com +emailaddresses.com +emailchoice.com +emailcorner.net +emailn.de +emailengine.net +emailengine.org +emailgaul.com +emailgroups.net +emailhut.net +emailpinoy.com +emailplanet.com +emailplus.org +emailuser.net +ematic.com +embarqmail.com +embroideryforums.com +eml.cc +emoka.ro +emptymail.com +enel.net +enelpunto.net +england.com +enterate.com.ar +entryweb.it +entusiastisk.com +enusmail.com +epatra.com +epix.net +epomail.com +epost.de +eprompter.com +eqqu.com +eramail.co.za +eresmas.com +eriga.lv +ertelecom.ru +esde-s.org +esfera.cl +estadao.com.br +etllao.com +euromail.net +euroseek.com +euskalmail.com +evafan.com +everyday.com.kh +everymail.net +everyone.net +execs2k.com +executivemail.co.za +expn.com +ezilon.com +ezrs.com +f-m.fm +facilmail.com +fadrasha.net +fadrasha.org +faithhighway.com +faithmail.com +familymailbox.com +familyroll.com +familysafeweb.net +fan.com +fan.net +faroweb.com +fast-email.com +fast-mail.org +fastem.com +fastemail.us +fastemailer.com +fastermail.com +fastest.cc +fastimap.com +fastmailbox.net +fastmessaging.com +fastwebmail.it +fawz.net +fea.st +federalcontractors.com +fedxmail.com +feelings.com +female.ru +fepg.net +ffanet.com +fiberia.com +filipinolinks.com +financesource.com +findmail.com +fiscal.net +flashmail.com +flipcode.com +florida.usa.com +floridagators.com +fmail.co.uk +fmailbox.com +fmgirl.com +fmguy.com +fnmail.com +footballer.com +foxmail.com +forfree.at +forsythmissouri.org +fortuncity.com +forum.dk +free.com.pe +free.fr +free.net.nz +freeaccess.nl +freegates.be +freeghana.com +freehosting.nl +freei.co.th +freeler.nl +freemail.globalsite.com.br +freemuslim.net +freenet.de +freenet.kg +freeola.net +freepgs.com +freesbee.fr +freeservers.com +freestart.hu +freesurf.ch +freesurf.fr +freesurf.nl +freeuk.com +freeuk.net +freeweb.it +freewebemail.com +freeyellow.com +frisurf.no +frontiernet.net +fsmail.net +fsnet.co.uk +ftml.net +fuelie.org +fun-greetings-jokes.com +fun.21cn.com +fusemail.com +fut.es +gala.net +galmail.co.za +gamebox.net +gamecocks.com +gawab.com +gay.com +gaymailbox.com +gaza.net +gazeta.pl +gci.net +gdi.net +geeklife.com +gemari.or.id +genxemail.com +geopia.com +georgia.usa.com +getmail.no +ggaweb.ch +giga4u.de +gjk.dk +glay.org +glendale.net +globalfree.it +globomail.com +globalpinoy.com +globalsite.com.br +globalum.com +globetrotter.net +go-bama.com +go-cavs.com +go-chargers.com +go-dawgs.com +go-gators.com +go-hogs.com +go-irish.com +go-spartans.com +go-tigers.com +go.aggies.com +go.air-force.com +go.badgers.com +go.big-orange.com +go.blue.devils.com +go.buffaloes.com +go.bulldogs.com +go.com +go.cougars.com +go.dores.com +go.gamecocks.com +go.huskies.com +go.longhorns.com +go.mustangs.com +go.rebels.com +go.ro +go.ru +go.terrapins.com +go.wildcats.com +go.wolverines.com +go.yellow-jackets.com +go2net.com +go4.it +gofree.co.uk +golfemail.com +goliadtexas.com +gomail.com.ua +gonowmail.com +gonuts4free.com +googlemail.com +goplay.com +gorontalo.net +gotmail.com +gotomy.com +govzone.com +grad.com +graffiti.net +gratisweb.com +gtechnics.com +guate.net +guessmail.com +gwalla.com +h-mail.us +haberx.com +hailmail.net +halejob.com +hamptonroads.com +handbag.com +hanmail.net +happemail.com +happycounsel.com +hawaii.com +hawaii.usa.com +hayahaya.tg +hedgeai.com +heesun.net +heremail.com +hetnet.nl +highveldmail.co.za +hildebrands.de +hingis.org +hispavista.com +hitmanrecords.com +hockeyghiaccio.com +hockeymail.com +holapuravida.com +home.no.net +home.ro +home.se +homelocator.com +homemail.co.za +homenetmail.com +homestead.com +homosexual.net +hongkong.com +hong-kong-1.com +hopthu.com +hosanna.net +hot.ee +hotbot.com +hotbox.ru +hotcoolmail.com +hotdak.com +hotfire.net +hotinbox.com +hotpop.com +hotvoice.com +hour.com +howling.com +huhmail.com +humour.com +hurra.de +hush.ai +hush.com +hushmail.com +huskies.com +hutchcity.com +i-france.com +i-p.com +i12.com +i2828.com +ibatam.com +ibest.com.br +ibizdns.com +icafe.com +ice.is +icestorm.com +icq.com +icqmail.com +icrazy.com +id.ru +idaho.usa.com +idirect.com +idncafe.com +ieg.com.br +iespalomeras.net +iespana.es +ifrance.com +ig.com.br +ignazio.it +illinois.usa.com +ilse.net +ilse.nl +imail.ru +imailbox.com +imap-mail.com +imap.cc +imapmail.org +imel.org +in-box.net +inbox.com +inbox.ge +inbox.lv +inbox.net +inbox.ru +in.com +incamail.com +indexa.fr +india.com +indiamail.com +indiana.usa.com +indiatimes.com +induquimica.org +inet.com.ua +infinito.it +infoapex.com +infohq.com +infomail.es +infomart.or.jp +infosat.net +infovia.com.ar +inicia.es +inmail.sk +inmail24.com +inoutbox.com +intelnet.net.gt +intelnett.com +interblod.com +interfree.it +interia.pl +interlap.com.ar +intermail.hu +internet-e-mail.com +internet-mail.org +internet.lu +internetegypt.com +internetemails.net +internetkeno.com +internetmailing.net +inwind.it +iobox.com +iobox.fi +iol.it +iol.pt +iowa.usa.com +ip3.com +ipermitmail.com +iqemail.com +iquebec.com +iran.com +irangate.net +iscool.net +islandmama.com +ismart.net +isonews2.com +isonfire.com +isp9.net +ispey.com +itelgua.com +itloox.com +itmom.com +ivenus.com +iwan-fals.com +iwon.com +ixp.net +japan.com +jaydemail.com +jedrzejow.pl +jetemail.net +jingjo.net +jippii.fi +jmail.co.za +jojomail.com +jovem.te.pt +joymail.com +jubii.dk +jubiipost.dk +jumpy.it +juno.com +justemail.net +justmailz.com +k.ro +kaazoo.com +kabissa.org +kaixo.com +kalluritimes.com +kalpoint.com +kansas.usa.com +katamail.com +kataweb.it +kayafmmail.co.za +keko.com.ar +kentucky.usa.com +keptprivate.com +kimo.com +kiwitown.com +klik.it +klikni.cz +kmtn.ru +koko.com +kolozsvar.ro +kombud.com +koreanmail.com +kotaksuratku.info +krunis.com +kukamail.com +kuronowish.com +kyokodate.com +kyokofukada.net +ladymail.cz +lagoon.nc +lahaonline.com +lamalla.net +lancsmail.com +land.ru +laposte.net +latinmail.com +lawyer.com +lawyersmail.com +lawyerzone.com +lebanonatlas.com +leehom.net +leonardo.it +leonlai.net +letsjam.com +letterbox.org +letterboxes.org +levele.com +lexpress.net +libero.it +liberomail.com +libertysurf.net +libre.net +lightwines.org +linkmaster.com +linuxfreemail.com +lionsfan.com.au +livedoor.com +llandudno.com +llangollen.com +lmxmail.sk +loggain.net +loggain.nu +lolnetwork.net +london.com +longhorns.com +look.com +looksmart.co.uk +looksmart.com +looksmart.com.au +loteria.net +lotonazo.com +louisiana.usa.com +louiskoo.com +loveable.com +lovemail.com +lovingjesus.com +lpemail.com +luckymail.com +luso.pt +lusoweb.pt +luukku.com +lycosmail.com +mac.com +machinecandy.com +macmail.com +mad.scientist.com +madcrazy.com +madonno.com +madrid.com +mag2.com +magicmail.co.za +magik-net.com +mail-atlas.net +mail-awu.de +mail-box.cz +mail.by +mail-center.com +mail-central.com +mail-jp.org +mail-online.dk +mail-page.com +mail-x-change.com +mail.austria.com +mail.az +mail.de +mail.be +mail.bg +mail.bulgaria.com +mail.co.za +mail.dk +mail.ee +mail.goo.ne.jp +mail.gr +mail.lawguru.com +mail.md +mail.mn +mail.org +mail.pf +mail.pt +mail.ru +mail.yahoo.co.jp +mail15.com +mail3000.com +mail333.com +mail8.com +mailandftp.com +mailandnews.com +mailas.com +mailasia.com +mailbg.com +mailblocks.com +mailbolt.com +mailbox.as +mailbox.co.za +mailbox.gr +mailbox.hu +mailbox.sk +mailc.net +mailcan.com +mailcircuit.com +mailclub.fr +mailclub.net +maildozy.com +mailfly.com +mailforce.net +mailftp.com +mailglobal.net +mailhaven.com +mailinator.com +mailingaddress.org +mailingweb.com +mailisent.com +mailite.com +mailme.dk +mailmight.com +mailmij.nl +mailnew.com +mailops.com +mailpanda.com +mailpersonal.com +mailroom.com +mailru.com +mails.de +mailsent.net +mailserver.dk +mailservice.ms +mailsnare.net +mailsurf.com +mailup.net +mailvault.com +mailworks.org +maine.usa.com +majorana.martina-franca.ta.it +maktoob.com +malayalamtelevision.net +malayalapathram.com +male.ru +manager.de +manlymail.net +mantrafreenet.com +mantramail.com +mantraonline.com +marihuana.ro +marijuana.nl +marketweighton.com +maryland.usa.com +masrawy.com +massachusetts.usa.com +mauimail.com +mbox.com.au +mcrmail.com +me.by +me.com +medicinatv.com +meetingmall.com +megamail.pt +menara.ma +merseymail.com +mesra.net +messagez.com +metacrawler.com +mexico.com +miaoweb.net +michigan.usa.com +micro2media.com +miesto.sk +mighty.co.za +milacamn.net +milmail.com +mindless.com +mindviz.com +minnesota.usa.com +mississippi.usa.com +missouri.usa.com +mixmail.com +ml1.net +ml2clan.com +mlanime.com +mm.st +mmail.com +mobimail.mn +mobsters.com +mobstop.com +modemnet.net +modomail.com +moldova.com +moldovacc.com +monarchy.com +montana.usa.com +montevideo.com.uy +moomia.com +moose-mail.com +mosaicfx.com +motormania.com +movemail.com +mr.outblaze.com +mrspender.com +mscold.com +msnzone.cn +mundo-r.com +muslimsonline.com +mustangs.com +mxs.de +myblue.cc +mycabin.com +mycity.com +mycommail.com +mycool.com +mydomain.com +myeweb.com +myfastmail.com +myfunnymail.com +mygrande.net +mykolab.com +mygamingconsoles.com +myiris.com +myjazzmail.com +mymacmail.com +mymail.dk +mymail.ph.inter.net +mymail.ro +mynet.com +mynet.com.tr +myotw.net +myopera.com +myownemail.com +mypersonalemail.com +myplace.com +myrealbox.com +myspace.com +myt.mu +myway.com +mzgchaos.de +n2.com +n2business.com +n2mail.com +n2software.com +nabble.com +name.com +nameplanet.com +nanamail.co.il +nanaseaikawa.com +nandomail.com +naseej.com +nastything.com +national-champs.com +nativeweb.net +narod.ru +nate.com +naveganas.com +naver.com +nebraska.usa.com +nemra1.com +nenter.com +nerdshack.com +nervhq.org +net.hr +net4b.pt +net4jesus.com +net4you.at +netbounce.com +netcabo.pt +netcape.net +netcourrier.com +netexecutive.com +netfirms.com +netkushi.com +netmongol.com +netpiper.com +netposta.net +netscape.com +netscape.net +netscapeonline.co.uk +netsquare.com +nettaxi.com +netti.fi +networld.com +netzero.com +netzero.net +neustreet.com +nevada.usa.com +newhampshire.usa.com +newjersey.usa.com +newmail.com +newmail.net +newmail.ok.com +newmail.ru +newmexico.usa.com +newspaperemail.com +newyork.com +newyork.usa.com +newyorkcity.com +nfmail.com +nicegal.com +nightimeuk.com +nightly.com +nightmail.com +nightmail.ru +noavar.com +noemail.com +nonomail.com +nokiamail.com +noolhar.com +northcarolina.usa.com +northdakota.usa.com +nospammail.net +nowzer.com +ny.com +nyc.com +nz11.com +nzoomail.com +o2.pl +oceanfree.net +ocsnet.net +oddpost.com +odeon.pl +odmail.com +offshorewebmail.com +ofir.dk +ohio.usa.com +oicexchange.com +ok.ru +oklahoma.usa.com +ole.com +oleco.net +olympist.net +omaninfo.com +onatoo.com +ondikoi.com +onebox.com +onenet.com.ar +onet.pl +ongc.net +oninet.pt +online.ie +online.ru +onlinewiz.com +onobox.com +open.by +openbg.com +openforyou.com +opentransfer.com +operamail.com +oplusnet.com +orange.fr +orangehome.co.uk +orange.es +orange.jo +orange.pl +orbitel.bg +orcon.net.nz +oregon.usa.com +oreka.com +organizer.net +orgio.net +orthodox.com +osite.com.br +oso.com +ourbrisbane.com +ournet.md +ourprofile.net +ourwest.com +outgun.com +ownmail.net +oxfoot.com +ozu.es +pacer.com +paginasamarillas.com +pakistanmail.com +pandawa.com +pando.com +pandora.be +paris.com +parsimail.com +parspage.com +patmail.com +pattayacitythailand.com +pc4me.us +pcpostal.com +penguinmaster.com +pennsylvania.usa.com +peoplepc.com +peopleweb.com +personal.ro +personales.com +peru.com +petml.com +phreaker.net +pigeonportal.com +pilu.com +pimagop.com +pinoymail.com +pipni.cz +pisem.net +planet-school.de +planetaccess.com +planetout.com +plasa.com +playersodds.com +playful.com +pluno.com +plusmail.com.br +pmail.net +pnetmail.co.za +pobox.ru +pobox.sk +pochtamt.ru +pochta.ru +poczta.fm +poetic.com +pogowave.com +polbox.com +pop3.ru +pop.co.th +popmail.com +poppymail.com +popsmail.com +popstar.com +portafree.com +portaldosalunos.com +portugalmail.com +portugalmail.pt +post.cz +post.expart.ne.jp +post.pl +post.sk +posta.ge +postaccesslite.com +postiloota.net +postinbox.com +postino.ch +postino.it +postmaster.co.uk +postpro.net +praize.com +press.co.jp +primposta.com +printesamargareta.ro +private.21cn.com +probemail.com +profesional.com +profession.freemail.com.br +proinbox.com +promessage.com +prontomail.com +provincial.net +publicaccounting.com +punkass.com +puppy.com.my +q.com +qatar.io +qlmail.com +qq.com +qrio.com +qsl.net +qudsmail.com +queerplaces.com +quepasa.com +quick.cz +quickwebmail.com +r-o-o-t.com +r320.hu +raakim.com +rbcmail.ru +racingseat.com +radicalz.com +radiojobbank.com +ragingbull.com +raisingadaughter.com +rallye-webmail.com +rambler.ru +ranmamail.com +ravearena.com +ravemail.co.za +razormail.com +real.ro +realemail.net +reallyfast.biz +reallyfast.info +rebels.com +recife.net +recme.net +rediffmailpro.com +redseven.de +redwhitearmy.com +relia.com +revenue.com +rexian.com +rhodeisland.usa.com +ritmes.net +rn.com +roanokemail.com +rochester-mail.com +rock.com +rocketmail.com +rockfan.com +rockinghamgateway.com +rojname.com +rol.ro +rollin.com +rome.com +romymichele.com +royal.net +rpharmacist.com +rt.nl +ru.ru +rushpost.com +russiamail.com +rxpost.net +s-mail.com +saabnet.com +sacbeemail.com +sacmail.com +safe-mail.net +safe-mailbox.com +saigonnet.vn +saint-mike.org +samilan.net +sandiego.com +sanook.com +sanriotown.com +sapibon.com +sapo.pt +saturnfans.com +sayhi.net +sbcglobal.com +scfn.net +schweiz.org +sci.fi +sciaga.pl +scrapbookscrapbook.com +seapole.com +search417.com +seark.com +sebil.com +secretservices.net +secure-jlnet.com +seductive.com +sendmail.ru +sendme.cz +sent.as +sent.at +sent.com +serga.com.ar +sermix.com +server4free.de +serverwench.com +sesmail.com +sexmagnet.com +seznam.cz +shadango.com +she.com +shuf.com +siamlocalhost.com +siamnow.net +sify.com +sinamail.com +singapore.com +singmail.com +singnet.com.sg +siraj.org +sirindia.com +sirunet.com +sister.com +sina.com +sina.cn +sinanail.com +sistersbrothers.com +sizzling.com +slamdunkfan.com +slickriffs.co.uk +slingshot.com +slo.net +slomusic.net +smartemail.co.uk +smtp.ru +snail-mail.net +sndt.net +sneakemail.com +snoopymail.com +snowboarding.com +so-simple.org +socamail.com +softhome.net +sohu.com +sol.dk +solidmail.com +soon.com +sos.lv +soundvillage.org +southcarolina.usa.com +southdakota.usa.com +space.com +spacetowns.com +spamex.com +spartapiet.com +speed-racer.com +speedpost.net +speedymail.org +spils.com +spinfinder.com +sportemail.com +spray.net +spray.no +spray.se +spymac.com +srbbs.com +srilankan.net +ssan.com +ssl-mail.com +stade.fr +stalag13.com +stampmail.com +starbuzz.com +starline.ee +starmail.com +starmail.org +starmedia.com +starspath.com +start.com.au +start.no +stribmail.com +student.com +student.ednet.ns.ca +studmail.com +sudanmail.net +suisse.org +sunbella.net +sunmail1.com +sunpoint.net +sunrise.ch +sunumail.sn +sunuweb.net +suomi24.fi +superdada.it +supereva.com +supereva.it +supermailbox.com +superposta.com +surf3.net +surfassistant.com +surfsupnet.net +surfy.net +surimail.com +surnet.cl +sverige.nu +svizzera.org +sweb.cz +swift-mail.com +swissinfo.org +swissmail.net +switzerland.org +syom.com +syriamail.com +t-mail.com +t-net.net.ve +t2mail.com +tabasheer.com +talk21.com +talkcity.com +tangmonkey.com +tatanova.com +taxcutadvice.com +techemail.com +technisamail.co.za +teenmail.co.uk +teenmail.co.za +tejary.com +telebot.com +telefonica.net +telegraf.by +teleline.es +telinco.net +telkom.net +telpage.net +telstra.com +telenet.be +telusplanet.net +tempting.com +tenchiclub.com +tennessee.usa.com +terrapins.com +texas.usa.com +texascrossroads.com +tfz.net +thai.com +thaimail.com +thaimail.net +the-fastest.net +the-quickest.com +thegame.com +theinternetemail.com +theoffice.net +thepostmaster.net +theracetrack.com +theserverbiz.com +thewatercooler.com +thewebpros.co.uk +thinkpost.net +thirdage.com +thundermail.com +tim.it +timemail.com +tin.it +tinati.net +tiscalinet.it +tjohoo.se +tkcity.com +tlcfan.com +tlen.pl +tmicha.net +todito.com +todoperros.com +tokyo.com +topchat.com +topmail.com.ar +topmail.dk +topmail.co.ie +topmail.co.in +topmail.co.nz +topmail.co.uk +topmail.co.za +topsurf.com +toquedequeda.com +torba.com +torchmail.com +totalmail.com +totalsurf.com +totonline.net +tough.com +toughguy.net +trav.se +trevas.net +tripod-mail.com +triton.net +trmailbox.com +tsamail.co.za +turbonett.com +turkey.com +tvnet.lv +twc.com +typemail.com +u2club.com +uae.ac +ubbi.com +ubbi.com.br +uboot.com +ugeek.com +uk2.net +uk2net.com +ukr.net +ukrpost.net +ukrpost.ua +uku.co.uk +ulimit.com +ummah.org +unbounded.com +unicum.de +unimail.mn +unitedemailsystems.com +universal.pt +universia.cl +universia.edu.ve +universia.es +universia.net.co +universia.net.mx +universia.pr +universia.pt +universiabrasil.net +unofree.it +uol.com.ar +uol.com.br +uole.com +uolmail.com +uomail.com +uraniomail.com +urbi.com.br +ureach.com +usanetmail.com +userbeam.com +utah.usa.com +uyuyuy.com +v-sexi.com +v3mail.com +valanides.com +vegetarisme.be +velnet.com +velocall.com +vercorreo.com +verizonmail.com +vermont.usa.com +verticalheaven.com +veryfast.biz +veryspeedy.net +vfemail.net +vietmedia.com +vip.gr +virgilio.it +virgin.net +virginia.usa.com +virtual-mail.com +visitmail.com +visto.com +vivelared.com +vjtimail.com +vnn.vn +vsnl.com +vsnl.net +vodamail.co.za +voila.fr +volkermord.com +vosforums.com +w.cn +walla.com +walla.co.il +wallet.com +wam.co.za +wanex.ge +wap.hu +wapda.com +wapicode.com +wappi.com +warpmail.net +washington.usa.com +wassup.com +waterloo.com +waumail.com +wazmail.com +wearab.net +web-mail.com.ar +web.de +web.nl +web2mail.com +webaddressbook.com +webbworks.com +webcity.ca +webdream.com +webemaillist.com +webindia123.com +webinfo.fi +webjump.com +webl-3.br.inter.net +webmail.co.yu +webmail.co.za +webmails.com +webmailv.com +webpim.cc +webspawner.com +webstation.com +websurfer.co.za +webtopmail.com +webtribe.net +webtv.net +weedmail.com +weekonline.com +weirdness.com +westvirginia.usa.com +whale-mail.com +whipmail.com +who.net +whoever.com +wildcats.com +wildmail.com +williams.net.ar +winning.com +winningteam.com +winwinhosting.com +wisconsin.usa.com +witelcom.com +witty.com +wolverines.com +wooow.it +workmail.co.za +worldcrossing.com +worldemail.com +worldmedic.com +worldonline.de +wowmail.com +wp.pl +wprost.pl +wrongmail.com +wtonetwork.com +wurtele.net +www.com +www.consulcredit.it +wyoming.usa.com +x-mail.net +xasa.com +xfreehosting.com +xmail.net +xmsg.com +xnmsn.cn +xoom.com +xtra.co.nz +xuite.net +xpectmore.com +xrea.com +xsmail.com +xzapmail.com +y7mail.com +yahala.co.il +yaho.com +yalla.com.lb +ya.com +yeah.net +ya.ru +yahoomail.com +yam.com +yamal.info +yapost.com +yawmail.com +yebox.com +yehey.com +yellow-jackets.com +yellowstone.net +yenimail.com +yepmail.net +yifan.net +yopmail.com +your-mail.com +yours.com +yourwap.com +yyhmail.com +z11.com +z6.com +zednet.co.uk +zeeman.nl +ziplip.com +zipmail.com.br +zipmax.com +zmail.pt +zmail.ru +zona-andina.net +zonai.com +zoneview.net +zonnet.nl +zoho.com +zoomshare.com +zoznam.sk +zubee.com +zuvio.com +zwallet.com +zworg.com +zybermail.com +zzn.com +126.com +139.com +163.com +188.com +189.cn +263.net +9.cn +vip.126.com +vip.163.com +vip.188.com +vip.sina.com +vip.sohu.com +vip.sohu.net +vip.tom.com +vip.qq.com +vipsohu.net +clovermail.net +mail-on.us +chewiemail.com +offcolormail.com +powdermail.com +tightmail.com +toothandmail.com +tushmail.com +openmail.cc +expressmail.dk +4xn.de +5x2.de +5x2.me +aufdrogen.de +auf-steroide.de +besser-als-du.de +brainsurfer.de +chillaxer.de +cyberkriminell.de +danneben.so +freemailen.de +freemailn.de +ist-der-mann.de +ist-der-wahnsinn.de +ist-echt.so +istecht.so +ist-genialer.de +ist-schlauer.de +ist-supersexy.de +kann.so +mag-spam.net +mega-schlau.de +muss.so +nerd4life.de +ohne-drogen-gehts.net +on-steroids.de +scheint.so +staatsterrorist.de +super-gerissen.de +unendlich-schlau.de +vip-client.de +will-keinen-spam.de +zu-geil.de +rbox.me +rbox.co +tunome.com +acatperson.com +adogperson.com +all4theskins.com +allsportsrock.com +alwaysgrilling.com +alwaysinthekitchen.com +alwayswatchingmovies.com +alwayswatchingtv.com +asylum.com +basketball-email.com +beabookworm.com +beagolfer.com +beahealthnut.com +believeinliberty.com +bestcoolcars.com +bestjobcandidate.com +besure2vote.com +bigtimecatperson.com +bigtimedogperson.com +bigtimereader.com +bigtimesportsfan.com +blackvoices.com +capsfanatic.com +capshockeyfan.com +capsred.com +car-nut.net +cat-person.com +catpeoplerule.com +chat-with-me.com +cheatasrule.com +crazy4baseball.com +crazy4homeimprovement.com +crazy4mail.com +crazyaboutfilms.net +crazycarfan.com +crazyforemail.com +crazymoviefan.com +descriptivemail.com +differentmail.com +dog-person.com +dogpeoplerule.com +easydoesit.com +expertrenovator.com +expressivemail.com +fanaticos.com +fanofbooks.com +fanofcomputers.com +fanofcooking.com +fanoftheweb.com +fieldmail.com +fleetmail.com +focusedonprofits.com +focusedonreturns.com +futboladdict.com +games.com +getintobooks.com +hail2theskins.com +hitthepuck.com +i-dig-movies.com +i-love-restaurants.com +idigcomputers.com +idigelectronics.com +idigvideos.com +ilike2helpothers.com +ilike2invest.com +ilike2workout.com +ilikeelectronics.com +ilikeworkingout.com +ilovehomeprojects.com +iloveourteam.com +iloveworkingout.com +in2autos.net +interestedinthejob.com +intomotors.com +iwatchrealitytv.com +lemondrop.com +love2exercise.com +love2workout.com +lovefantasysports.com +lovetoexercise.com +luvfishing.com +luvgolfing.com +luvsoccer.com +mail4me.com +majorgolfer.com +majorshopaholic.com +majortechie.com +mcom.com +motor-nut.com +moviefan.com +mycapitalsmail.com +mycatiscool.com +myfantasyteamrules.com +myteamisbest.com +netbusiness.com +news-fanatic.com +newspaperfan.com +onlinevideosrock.com +realbookfan.com +realhealthnut.com +realitytvaddict.net +realitytvnut.com +reallyintomusic.com +realtravelfan.com +redskinscheer.com +redskinsfamily.com +redskinsfancentral.com +redskinshog.com +redskinsrule.com +redskinsspecialteams.com +redskinsultimatefan.com +scoutmail.com +skins4life.com +stargate2.com +stargateatlantis.com +stargatefanclub.com +stargatesg1.com +stargateu.com +switched.com +t-online.de +thegamefanatic.com +total-techie.com +totalfoodnut.com +totally-into-cooking.com +totallyintobaseball.com +totallyintobasketball.com +totallyintocooking.com +totallyintofootball.com +totallyintogolf.com +totallyintohockey.com +totallyintomusic.com +totallyintoreading.com +totallyintosports.com +totallyintotravel.com +totalmoviefan.com +travel2newplaces.com +tvchannelsurfer.com +ultimateredskinsfan.com +videogamesrock.com +volunteeringisawesome.com +wayintocomputers.com +whatmail.com +when.com +wild4music.com +wildaboutelectronics.com +workingaroundthehouse.com +workingonthehouse.com +writesoon.com +xmasmail.com +arab.ir +denmark.ir +egypt.ir +icq.ir +ir.ae +iraq.ir +ire.ir +ireland.ir +irr.ir +jpg.ir +ksa.ir +kuwait.ir +london.ir +paltalk.ir +spain.ir +sweden.ir +tokyo.ir +111mail.com +123iran.com +37.com +420email.com +4degreez.com +4-music-today.com +actingbiz.com +allhiphop.com +anatomicrock.com +animeone.com +asiancutes.com +a-teens.net +ausi.com +autoindia.com +autopm.com +barriolife.com +b-boy.com +beautifulboy.com +bgay.com +bicycledata.com +bicycling.com +bigheavyworld.com +bigmailbox.net +bikerheaven.net +bikermail.com +billssite.com +blackandchristian.com +blackcity.net +blackvault.com +bmxtrix.com +boarderzone.com +boatnerd.com +bolbox.com +bongmail.com +bowl.com +butch-femme.org +byke.com +calle22.com +cannabismail.com +catlovers.com +certifiedbitches.com +championboxing.com +chatway.com +chillymail.com +classprod.com +classycouples.com +congiu.net +coolshit.com +corpusmail.com +cyberunlimited.org +cycledata.com +darkfear.com +darkforces.com +dirtythird.com +dopefiends.com +draac.com +drakmail.net +dr-dre.com +dreamstop.com +egypt.net +emailfast.com +envirocitizen.com +escapeartist.com +ezsweeps.com +famous.as +farts.com +feelingnaughty.com +firemyst.com +freeonline.com +fudge.com +funkytimes.com +gamerssolution.com +gazabo.net +glittergrrrls.com +goatrance.com +goddess.com +gohip.com +gospelcity.com +gothicgirl.com +grapemail.net +greatautos.org +guy.com +haitisurf.com +happyhippo.com +hateinthebox.com +houseofhorrors.com +hugkiss.com +hullnumber.com +idunno4recipes.com +ihatenetscape.com +intimatefire.com +irow.com +jazzemail.com +juanitabynum.com +kanoodle.com +kickboxing.com +kidrock.com +kinkyemail.com +kool-things.com +latinabarbie.com +latinogreeks.com +leesville.com +loveemail.com +lowrider.com +lucky7lotto.net +madeniggaz.net +mailbomb.com +marillion.net +megarave.com +mofa.com +motley.com +music.com +musician.net +musicsites.com +netbroadcaster.com +netfingers.com +net-surf.com +nocharge.com +operationivy.com +paidoffers.net +pcbee.com +persian.com +petrofind.com +phunkybitches.com +pikaguam.com +pinkcity.net +pitbullmail.com +planetsmeg.com +poop.com +poormail.com +potsmokersnet.com +primetap.com +project420.com +prolife.net +puertoricowow.com +puppetweb.com +rapstar.com +rapworld.com +rastamall.com +ratedx.net +ravermail.com +relapsecult.com +remixer.com +rockeros.com +romance106fm.com +singalongcenter.com +sketchyfriends.com +slayerized.com +smartstocks.com +soulja-beatz.org +specialoperations.com +speedymail.net +spells.com +superbikeclub.com +superintendents.net +surfguiden.com +sweetwishes.com +tattoodesign.com +teamster.net +teenchatnow.com +the5thquarter.com +theblackmarket.com +tombstone.ws +troamail.org +u2tours.com +vitalogy.org +whatisthis.com +wrestlezone.com +abha.cc +agadir.cc +ahsa.ws +ajman.cc +ajman.us +ajman.ws +albaha.cc +algerie.cc +alriyadh.cc +amman.cc +aqaba.cc +arar.ws +aswan.cc +baalbeck.cc +bahraini.cc +banha.cc +bizerte.cc +blida.info +buraydah.cc +cameroon.cc +dhahran.cc +dhofar.cc +djibouti.cc +dominican.cc +eritrea.cc +falasteen.cc +fujairah.cc +fujairah.us +fujairah.ws +gabes.cc +gafsa.cc +giza.cc +guinea.cc +hamra.cc +hasakah.com +hebron.tv +homs.cc +ibra.cc +irbid.ws +ismailia.cc +jadida.cc +jadida.org +jerash.cc +jizan.cc +jouf.cc +kairouan.cc +karak.cc +khaimah.cc +khartoum.cc +khobar.cc +kuwaiti.tv +kyrgyzstan.cc +latakia.cc +lebanese.cc +lubnan.cc +lubnan.ws +madinah.cc +maghreb.cc +manama.cc +mansoura.tv +marrakesh.cc +mascara.ws +meknes.cc +muscat.tv +muscat.ws +nabeul.cc +nabeul.info +nablus.cc +nador.cc +najaf.cc +omani.ws +omdurman.cc +oran.cc +oued.info +oued.org +oujda.biz +oujda.cc +pakistani.ws +palmyra.cc +palmyra.ws +portsaid.cc +qassem.cc +quds.cc +rabat.cc +rafah.cc +ramallah.cc +safat.biz +safat.info +safat.us +safat.ws +salalah.cc +salmiya.biz +sanaa.cc +seeb.cc +sfax.ws +sharm.cc +sinai.cc +siria.cc +sousse.cc +sudanese.cc +suez.cc +tabouk.cc +tajikistan.cc +tangiers.cc +tanta.cc +tayef.cc +tetouan.cc +timor.cc +tunisian.cc +urdun.cc +yanbo.cc +yemeni.cc +yunus.cc +zagazig.cc +zambia.cc +5005.lv +a.org.ua +bmx.lv +company.org.ua +coolmail.ru +dino.lv +eclub.lv +e-mail.am +fit.lv +hacker.am +human.lv +iphon.biz +latchess.com +loveis.lv +lv-inter.net +pookmail.com +sexriga.lv diff --git a/crates/router/src/utils/user/password.rs b/crates/router/src/utils/user/password.rs new file mode 100644 index 000000000000..cff17863c32d --- /dev/null +++ b/crates/router/src/utils/user/password.rs @@ -0,0 +1,43 @@ +use argon2::{ + password_hash::{ + rand_core::OsRng, Error as argon2Err, PasswordHash, PasswordHasher, PasswordVerifier, + SaltString, + }, + Argon2, +}; +use common_utils::errors::CustomResult; +use error_stack::{IntoReport, ResultExt}; +use masking::{ExposeInterface, Secret}; + +use crate::core::errors::UserErrors; + +pub fn generate_password_hash( + password: Secret, +) -> CustomResult, UserErrors> { + let salt = SaltString::generate(&mut OsRng); + + let argon2 = Argon2::default(); + let password_hash = argon2 + .hash_password(password.expose().as_bytes(), &salt) + .into_report() + .change_context(UserErrors::InternalServerError)?; + Ok(Secret::new(password_hash.to_string())) +} + +pub fn is_correct_password( + candidate: Secret, + password: Secret, +) -> CustomResult { + let password = password.expose(); + let parsed_hash = PasswordHash::new(&password) + .into_report() + .change_context(UserErrors::InternalServerError)?; + let result = Argon2::default().verify_password(candidate.expose().as_bytes(), &parsed_hash); + match result { + Ok(_) => Ok(true), + Err(argon2Err::Password) => Ok(false), + Err(e) => Err(e), + } + .into_report() + .change_context(UserErrors::InternalServerError) +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 0c9751aee440..9cd678083959 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -243,6 +243,8 @@ pub enum Flow { GsmRuleUpdate, /// Gsm Rule Delete flow GsmRuleDelete, + /// User connect account + UserConnectAccount, } /// From 8e538dbd5c189047d0a0b24fa752b9a1c67554f5 Mon Sep 17 00:00:00 2001 From: Prajjwal Kumar Date: Mon, 13 Nov 2023 14:57:34 +0530 Subject: [PATCH 003/146] feat(router): profile specific fallback derivation while routing payments (#2806) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Aprabhat19 Co-authored-by: Amisha Prabhat <55580080+Aprabhat19@users.noreply.github.com> --- crates/api_models/src/events/routing.rs | 16 ++- crates/api_models/src/routing.rs | 13 ++ crates/router/Cargo.toml | 5 +- crates/router/src/core/admin.rs | 16 ++- crates/router/src/core/payments/routing.rs | 70 +++++++-- crates/router/src/core/routing.rs | 159 ++++++++++++++++++--- crates/router/src/routes/app.rs | 10 ++ crates/router/src/routes/routing.rs | 57 ++++++++ 8 files changed, 312 insertions(+), 34 deletions(-) diff --git a/crates/api_models/src/events/routing.rs b/crates/api_models/src/events/routing.rs index 5eca01acc6fb..a09735bc5722 100644 --- a/crates/api_models/src/events/routing.rs +++ b/crates/api_models/src/events/routing.rs @@ -1,8 +1,9 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; use crate::routing::{ - LinkedRoutingConfigRetrieveResponse, MerchantRoutingAlgorithm, RoutingAlgorithmId, - RoutingConfigRequest, RoutingDictionaryRecord, RoutingKind, + LinkedRoutingConfigRetrieveResponse, MerchantRoutingAlgorithm, ProfileDefaultRoutingConfig, + RoutingAlgorithmId, RoutingConfigRequest, RoutingDictionaryRecord, RoutingKind, + RoutingPayloadWrapper, }; #[cfg(feature = "business_profile_routing")] use crate::routing::{RoutingRetrieveLinkQuery, RoutingRetrieveQuery}; @@ -37,6 +38,17 @@ impl ApiEventMetric for LinkedRoutingConfigRetrieveResponse { } } +impl ApiEventMetric for RoutingPayloadWrapper { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Routing) + } +} +impl ApiEventMetric for ProfileDefaultRoutingConfig { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Routing) + } +} + #[cfg(feature = "business_profile_routing")] impl ApiEventMetric for RoutingRetrieveQuery { fn get_api_event_type(&self) -> Option { diff --git a/crates/api_models/src/routing.rs b/crates/api_models/src/routing.rs index 425ca364191d..363df5389a79 100644 --- a/crates/api_models/src/routing.rs +++ b/crates/api_models/src/routing.rs @@ -40,6 +40,12 @@ pub struct RoutingConfigRequest { pub profile_id: Option, } +#[derive(Debug, serde::Serialize)] +pub struct ProfileDefaultRoutingConfig { + pub profile_id: String, + pub connectors: Vec, +} + #[cfg(feature = "business_profile_routing")] #[derive(Debug, serde::Deserialize, serde::Serialize)] pub struct RoutingRetrieveQuery { @@ -389,6 +395,13 @@ pub enum RoutingAlgorithmKind { Advanced, } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] + +pub struct RoutingPayloadWrapper { + pub updated_config: Vec, + pub profile_id: String, +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde( tag = "type", diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index d765a5b5c5ed..8f6906e06855 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -9,13 +9,13 @@ readme = "README.md" license.workspace = true [features] -default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts"] +default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "profile_specific_fallback_routing"] s3 = ["dep:aws-sdk-s3", "dep:aws-config"] kms = ["external_services/kms", "dep:aws-config"] email = ["external_services/email", "dep:aws-config"] basilisk = ["kms"] stripe = ["dep:serde_qs"] -release = ["kms", "stripe", "basilisk", "s3", "email", "business_profile_routing", "accounts_cache", "kv_store", "olap"] +release = ["kms", "stripe", "basilisk", "s3", "email", "business_profile_routing", "accounts_cache", "kv_store", "profile_specific_fallback_routing"] olap = ["data_models/olap", "storage_impl/olap", "scheduler/olap"] oltp = ["data_models/oltp", "storage_impl/oltp"] kv_store = ["scheduler/kv_store"] @@ -24,6 +24,7 @@ openapi = ["olap", "oltp", "payouts"] vergen = ["router_env/vergen"] backwards_compatibility = ["api_models/backwards_compatibility", "euclid/backwards_compatibility", "kgraph_utils/backwards_compatibility"] business_profile_routing=["api_models/business_profile_routing"] +profile_specific_fallback_routing = [] dummy_connector = ["api_models/dummy_connector", "euclid/dummy_connector", "kgraph_utils/dummy_connector"] connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connector_choice_mca_id", "kgraph_utils/connector_choice_mca_id"] external_access_dc = ["dummy_connector"] diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index e1e5ea744e2f..5ccd9e964866 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -916,13 +916,16 @@ pub async fn create_payment_connector( let mut default_routing_config = routing_helpers::get_merchant_default_config(&*state.store, merchant_id).await?; + let mut default_routing_config_for_profile = + routing_helpers::get_merchant_default_config(&*state.clone().store, &profile_id).await?; + let mca = state .store .insert_merchant_connector_account(merchant_connector_account, &key_store) .await .to_duplicate_response( errors::ApiErrorResponse::DuplicateMerchantConnectorAccount { - profile_id, + profile_id: profile_id.clone(), connector_name: req.connector_name.to_string(), }, )?; @@ -939,7 +942,7 @@ pub async fn create_payment_connector( }; if !default_routing_config.contains(&choice) { - default_routing_config.push(choice); + default_routing_config.push(choice.clone()); routing_helpers::update_merchant_default_config( &*state.store, merchant_id, @@ -947,6 +950,15 @@ pub async fn create_payment_connector( ) .await?; } + if !default_routing_config_for_profile.contains(&choice.clone()) { + default_routing_config_for_profile.push(choice); + routing_helpers::update_merchant_default_config( + &*state.store, + &profile_id.clone(), + default_routing_config_for_profile, + ) + .await?; + } } metrics::MCA_CREATE.add( diff --git a/crates/router/src/core/payments/routing.rs b/crates/router/src/core/payments/routing.rs index 4134ddf65ea0..3b89d4e38e4e 100644 --- a/crates/router/src/core/payments/routing.rs +++ b/crates/router/src/core/payments/routing.rs @@ -71,7 +71,10 @@ pub struct SessionRoutingPmTypeInput<'a> { routing_algorithm: &'a MerchantAccountRoutingAlgorithm, backend_input: dsl_inputs::BackendInput, allowed_connectors: FxHashMap, - #[cfg(feature = "business_profile_routing")] + #[cfg(any( + feature = "business_profile_routing", + feature = "profile_specific_fallback_routing" + ))] profile_id: Option, } static ROUTING_CACHE: StaticCache = StaticCache::new(); @@ -207,10 +210,22 @@ pub async fn perform_static_routing_v1( let algorithm_id = if let Some(id) = algorithm_ref.algorithm_id { id } else { - let fallback_config = - routing_helpers::get_merchant_default_config(&*state.clone().store, merchant_id) - .await - .change_context(errors::RoutingError::FallbackConfigFetchFailed)?; + let fallback_config = routing_helpers::get_merchant_default_config( + &*state.clone().store, + #[cfg(not(feature = "profile_specific_fallback_routing"))] + merchant_id, + #[cfg(feature = "profile_specific_fallback_routing")] + { + payment_data + .payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::RoutingError::ProfileIdMissing)? + }, + ) + .await + .change_context(errors::RoutingError::FallbackConfigFetchFailed)?; return Ok(fallback_config); }; @@ -616,10 +631,22 @@ pub async fn perform_fallback_routing( eligible_connectors: Option<&Vec>, #[cfg(feature = "business_profile_routing")] profile_id: Option, ) -> RoutingResult> { - let fallback_config = - routing_helpers::get_merchant_default_config(&*state.store, &key_store.merchant_id) - .await - .change_context(errors::RoutingError::FallbackConfigFetchFailed)?; + let fallback_config = routing_helpers::get_merchant_default_config( + &*state.store, + #[cfg(not(feature = "profile_specific_fallback_routing"))] + &key_store.merchant_id, + #[cfg(feature = "profile_specific_fallback_routing")] + { + payment_data + .payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::RoutingError::ProfileIdMissing)? + }, + ) + .await + .change_context(errors::RoutingError::FallbackConfigFetchFailed)?; let backend_input = make_dsl_input(payment_data)?; perform_kgraph_filtering( @@ -819,8 +846,11 @@ pub async fn perform_session_flow_routing( routing_algorithm: &routing_algorithm, backend_input: backend_input.clone(), allowed_connectors, - #[cfg(feature = "business_profile_routing")] - profile_id: session_input.payment_intent.clone().profile_id, + #[cfg(any( + feature = "business_profile_routing", + feature = "profile_specific_fallback_routing" + ))] + profile_id: session_input.payment_intent.profile_id.clone(), }; let maybe_choice = perform_session_routing_for_pm_type(session_pm_input).await?; @@ -880,7 +910,16 @@ async fn perform_session_routing_for_pm_type( } else { routing_helpers::get_merchant_default_config( &*session_pm_input.state.clone().store, + #[cfg(not(feature = "profile_specific_fallback_routing"))] merchant_id, + #[cfg(feature = "profile_specific_fallback_routing")] + { + session_pm_input + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::RoutingError::ProfileIdMissing)? + }, ) .await .change_context(errors::RoutingError::FallbackConfigFetchFailed)? @@ -903,7 +942,16 @@ async fn perform_session_routing_for_pm_type( if final_selection.is_empty() { let fallback = routing_helpers::get_merchant_default_config( &*session_pm_input.state.clone().store, + #[cfg(not(feature = "profile_specific_fallback_routing"))] merchant_id, + #[cfg(feature = "profile_specific_fallback_routing")] + { + session_pm_input + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::RoutingError::ProfileIdMissing)? + }, ) .await .change_context(errors::RoutingError::FallbackConfigFetchFailed)?; diff --git a/crates/router/src/core/routing.rs b/crates/router/src/core/routing.rs index 723611ed5009..4171c3385637 100644 --- a/crates/router/src/core/routing.rs +++ b/crates/router/src/core/routing.rs @@ -13,13 +13,14 @@ use diesel_models::routing_algorithm::RoutingAlgorithm; use error_stack::{IntoReport, ResultExt}; use rustc_hash::FxHashSet; -#[cfg(feature = "business_profile_routing")] -use crate::core::utils::validate_and_get_business_profile; #[cfg(feature = "business_profile_routing")] use crate::types::transformers::{ForeignInto, ForeignTryInto}; use crate::{ consts, - core::errors::{RouterResponse, StorageErrorExt}, + core::{ + errors::{RouterResponse, StorageErrorExt}, + utils as core_utils, + }, routes::AppState, types::domain, utils::{self, OptionExt, ValueExt}, @@ -111,8 +112,12 @@ pub async fn create_routing_config( }) .attach_printable("Profile_id not provided")?; - validate_and_get_business_profile(db, Some(&profile_id), &merchant_account.merchant_id) - .await?; + core_utils::validate_and_get_business_profile( + db, + Some(&profile_id), + &merchant_account.merchant_id, + ) + .await?; helpers::validate_connectors_in_routing_config( db, @@ -229,7 +234,7 @@ pub async fn link_routing_config( .await .change_context(errors::ApiErrorResponse::ResourceIdNotFound)?; - let business_profile = validate_and_get_business_profile( + let business_profile = core_utils::validate_and_get_business_profile( db, Some(&routing_algorithm.profile_id), &merchant_account.merchant_id, @@ -332,7 +337,7 @@ pub async fn retrieve_routing_config( .await .to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?; - validate_and_get_business_profile( + core_utils::validate_and_get_business_profile( db, Some(&routing_algorithm.profile_id), &merchant_account.merchant_id, @@ -401,9 +406,12 @@ pub async fn unlink_routing_config( field_name: "profile_id", }) .attach_printable("Profile_id not provided")?; - let business_profile = - validate_and_get_business_profile(db, Some(&profile_id), &merchant_account.merchant_id) - .await?; + let business_profile = core_utils::validate_and_get_business_profile( + db, + Some(&profile_id), + &merchant_account.merchant_id, + ) + .await?; match business_profile { Some(business_profile) => { let routing_algo_ref: routing_types::RoutingAlgorithmRef = business_profile @@ -622,13 +630,15 @@ pub async fn retrieve_linked_routing_config( #[cfg(feature = "business_profile_routing")] { let business_profiles = if let Some(profile_id) = query_params.profile_id { - validate_and_get_business_profile(db, Some(&profile_id), &merchant_account.merchant_id) - .await? - .map(|profile| vec![profile]) - .get_required_value("BusinessProfile") - .change_context(errors::ApiErrorResponse::BusinessProfileNotFound { - id: profile_id, - })? + core_utils::validate_and_get_business_profile( + db, + Some(&profile_id), + &merchant_account.merchant_id, + ) + .await? + .map(|profile| vec![profile]) + .get_required_value("BusinessProfile") + .change_context(errors::ApiErrorResponse::BusinessProfileNotFound { id: profile_id })? } else { db.list_business_profile_by_merchant_id(&merchant_account.merchant_id) .await @@ -711,3 +721,118 @@ pub async fn retrieve_linked_routing_config( Ok(service_api::ApplicationResponse::Json(response)) } } + +pub async fn retrieve_default_routing_config_for_profiles( + state: AppState, + merchant_account: domain::MerchantAccount, +) -> RouterResponse> { + let db = state.store.as_ref(); + + let all_profiles = db + .list_business_profile_by_merchant_id(&merchant_account.merchant_id) + .await + .to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound) + .attach_printable("error retrieving all business profiles for merchant")?; + + let retrieve_config_futures = all_profiles + .iter() + .map(|prof| helpers::get_merchant_default_config(db, &prof.profile_id)) + .collect::>(); + + let configs = futures::future::join_all(retrieve_config_futures) + .await + .into_iter() + .collect::, _>>()?; + + let default_configs = configs + .into_iter() + .zip(all_profiles.iter().map(|prof| prof.profile_id.clone())) + .map( + |(config, profile_id)| routing_types::ProfileDefaultRoutingConfig { + profile_id, + connectors: config, + }, + ) + .collect::>(); + + Ok(service_api::ApplicationResponse::Json(default_configs)) +} + +pub async fn update_default_routing_config_for_profile( + state: AppState, + merchant_account: domain::MerchantAccount, + updated_config: Vec, + profile_id: String, +) -> RouterResponse { + let db = state.store.as_ref(); + + let business_profile = core_utils::validate_and_get_business_profile( + db, + Some(&profile_id), + &merchant_account.merchant_id, + ) + .await? + .get_required_value("BusinessProfile") + .change_context(errors::ApiErrorResponse::BusinessProfileNotFound { id: profile_id })?; + let default_config = + helpers::get_merchant_default_config(db, &business_profile.profile_id).await?; + + utils::when(default_config.len() != updated_config.len(), || { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: "current config and updated config have different lengths".to_string(), + }) + .into_report() + })?; + + let existing_set = FxHashSet::from_iter(default_config.iter().map(|c| { + ( + c.connector.to_string(), + #[cfg(feature = "connector_choice_mca_id")] + c.merchant_connector_id.as_ref(), + #[cfg(not(feature = "connector_choice_mca_id"))] + c.sub_label.as_ref(), + ) + })); + + let updated_set = FxHashSet::from_iter(updated_config.iter().map(|c| { + ( + c.connector.to_string(), + #[cfg(feature = "connector_choice_mca_id")] + c.merchant_connector_id.as_ref(), + #[cfg(not(feature = "connector_choice_mca_id"))] + c.sub_label.as_ref(), + ) + })); + + let symmetric_diff = existing_set + .symmetric_difference(&updated_set) + .cloned() + .collect::>(); + + utils::when(!symmetric_diff.is_empty(), || { + let error_str = symmetric_diff + .into_iter() + .map(|(connector, ident)| format!("'{connector}:{ident:?}'")) + .collect::>() + .join(", "); + + Err(errors::ApiErrorResponse::InvalidRequestData { + message: format!("connector mismatch between old and new configs ({error_str})"), + }) + .into_report() + })?; + + helpers::update_merchant_default_config( + db, + &business_profile.profile_id, + updated_config.clone(), + ) + .await?; + + Ok(service_api::ApplicationResponse::Json( + routing_types::ProfileDefaultRoutingConfig { + profile_id: business_profile.profile_id, + connectors: updated_config, + }, + )) +} diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index c34c542d1b6c..7f5c720be607 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -324,6 +324,16 @@ impl Routing { web::resource("/{algorithm_id}/activate") .route(web::post().to(cloud_routing::routing_link_config)), ) + .service( + web::resource("/default/profile/{profile_id}").route( + web::post().to(cloud_routing::routing_update_default_config_for_profile), + ), + ) + .service( + web::resource("/default/profile").route( + web::get().to(cloud_routing::routing_retrieve_default_config_for_profiles), + ), + ) } } diff --git a/crates/router/src/routes/routing.rs b/crates/router/src/routes/routing.rs index b87116f47fc5..606111a88818 100644 --- a/crates/router/src/routes/routing.rs +++ b/crates/router/src/routes/routing.rs @@ -296,3 +296,60 @@ pub async fn routing_retrieve_linked_config( .await } } + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn routing_retrieve_default_config_for_profiles( + state: web::Data, + req: HttpRequest, +) -> impl Responder { + oss_api::server_wrap( + Flow::RoutingRetrieveDefaultConfig, + state, + &req, + (), + |state, auth: auth::AuthenticationData, _| { + routing::retrieve_default_routing_config_for_profiles(state, auth.merchant_account) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + ) + .await +} + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn routing_update_default_config_for_profile( + state: web::Data, + req: HttpRequest, + path: web::Path, + json_payload: web::Json>, +) -> impl Responder { + let routing_payload_wrapper = routing_types::RoutingPayloadWrapper { + updated_config: json_payload.into_inner(), + profile_id: path.into_inner(), + }; + oss_api::server_wrap( + Flow::RoutingUpdateDefaultConfig, + state, + &req, + routing_payload_wrapper, + |state, auth: auth::AuthenticationData, wrapper| { + routing::update_default_routing_config_for_profile( + state, + auth.merchant_account, + wrapper.updated_config, + wrapper.profile_id, + ) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + ) + .await +} From c124511052ed8911a2ccfcf648c0793b5c1ca690 Mon Sep 17 00:00:00 2001 From: harsh-sharma-juspay <125131007+harsh-sharma-juspay@users.noreply.github.com> Date: Mon, 13 Nov 2023 15:58:29 +0530 Subject: [PATCH 004/146] feat(apievent): added hs latency to api event (#2734) Co-authored-by: Sampras lopes --- crates/router/src/events/api_logs.rs | 3 +++ crates/router/src/services/api.rs | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/crates/router/src/events/api_logs.rs b/crates/router/src/events/api_logs.rs index 873102e81ec2..27a90028ba6a 100644 --- a/crates/router/src/events/api_logs.rs +++ b/crates/router/src/events/api_logs.rs @@ -38,6 +38,7 @@ pub struct ApiEvent { response: Option, #[serde(flatten)] event_type: ApiEventsType, + hs_latency: Option, } impl ApiEvent { @@ -49,6 +50,7 @@ impl ApiEvent { status_code: i64, request: serde_json::Value, response: Option, + hs_latency: Option, auth_type: AuthenticationType, event_type: ApiEventsType, http_req: &HttpRequest, @@ -72,6 +74,7 @@ impl ApiEvent { .and_then(|user_agent_value| user_agent_value.to_str().ok().map(ToOwned::to_owned)), url_path: http_req.path().to_string(), event_type, + hs_latency, } } } diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index bb0e70b4b27b..321bf909ea0c 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -830,6 +830,7 @@ where .as_millis(); let mut serialized_response = None; + let mut overhead_latency = None; let status_code = match output.as_ref() { Ok(res) => { if let ApplicationResponse::Json(data) = res { @@ -839,6 +840,19 @@ where .attach_printable("Failed to serialize json response") .change_context(errors::ApiErrorResponse::InternalServerError.switch())?, ); + } else if let ApplicationResponse::JsonWithHeaders((data, headers)) = res { + serialized_response.replace( + masking::masked_serialize(&data) + .into_report() + .attach_printable("Failed to serialize json response") + .change_context(errors::ApiErrorResponse::InternalServerError.switch())?, + ); + + if let Some((_, value)) = headers.iter().find(|(key, _)| key == X_HS_LATENCY) { + if let Ok(external_latency) = value.parse::() { + overhead_latency.replace(external_latency); + } + } } event_type = res.get_api_event_type().or(event_type); @@ -854,6 +868,7 @@ where status_code, serialized_request, serialized_response, + overhead_latency, auth_type, event_type.unwrap_or(ApiEventsType::Miscellaneous), request, From 05535871152f4a6ac24ce6b5b5390da13cc29b96 Mon Sep 17 00:00:00 2001 From: Sanchith Hegde <22217505+SanchithHegde@users.noreply.github.com> Date: Mon, 13 Nov 2023 16:50:18 +0530 Subject: [PATCH 005/146] build(deps): remove unused dependencies and features (#2854) --- Cargo.lock | 62 ------------------- crates/api_models/Cargo.toml | 3 +- crates/common_enums/Cargo.toml | 4 -- crates/common_utils/Cargo.toml | 4 +- crates/data_models/Cargo.toml | 6 +- crates/diesel_models/Cargo.toml | 6 -- crates/drainer/Cargo.toml | 4 +- crates/euclid/Cargo.toml | 4 +- crates/euclid_wasm/Cargo.toml | 19 ++---- crates/kgraph_utils/Cargo.toml | 4 +- crates/redis_interface/Cargo.toml | 2 +- crates/router/Cargo.toml | 37 +++++------ crates/router/tests/connectors/adyen.rs | 2 + .../router/tests/connectors/bankofamerica.rs | 1 + crates/router/tests/connectors/globepay.rs | 2 +- crates/router/tests/connectors/gocardless.rs | 2 +- crates/router/tests/connectors/helcim.rs | 2 +- crates/router/tests/connectors/main.rs | 4 ++ crates/router/tests/connectors/opayo.rs | 1 + crates/router/tests/connectors/payeezy.rs | 1 + crates/router/tests/connectors/powertranz.rs | 2 +- crates/router/tests/connectors/prophetpay.rs | 1 + crates/router/tests/connectors/utils.rs | 18 +++++- crates/router/tests/connectors/volt.rs | 2 +- crates/router/tests/connectors/wise.rs | 13 +++- crates/scheduler/Cargo.toml | 3 - crates/storage_impl/Cargo.toml | 16 +++-- crates/test_utils/Cargo.toml | 17 ++--- 28 files changed, 86 insertions(+), 156 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ae7afa85d7d5..222bc02212ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,30 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "actix" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f728064aca1c318585bf4bb04ffcfac9e75e508ab4e8b1bd9ba5dfe04e2cbed5" -dependencies = [ - "actix-rt", - "actix_derive", - "bitflags 1.3.2", - "bytes", - "crossbeam-channel", - "futures-core", - "futures-sink", - "futures-task", - "futures-util", - "log", - "once_cell", - "parking_lot 0.12.1", - "pin-project-lite", - "smallvec", - "tokio", - "tokio-util", -] - [[package]] name = "actix-codec" version = "0.5.1" @@ -282,17 +258,6 @@ dependencies = [ "syn 2.0.38", ] -[[package]] -name = "actix_derive" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d44b8fee1ced9671ba043476deddef739dd0959bf77030b26b738cc591737a7" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "addr2line" version = "0.21.0" @@ -418,7 +383,6 @@ dependencies = [ "serde_json", "serde_with", "strum 0.24.1", - "thiserror", "time", "url", "utoipa", @@ -1561,7 +1525,6 @@ dependencies = [ "serde", "serde_json", "strum 0.25.0", - "time", "utoipa", ] @@ -1924,7 +1887,6 @@ dependencies = [ "masking", "serde", "serde_json", - "strum 0.25.0", "thiserror", "time", ] @@ -2035,13 +1997,10 @@ name = "diesel_models" version = "0.1.0" dependencies = [ "async-bb8-diesel", - "aws-config", - "aws-sdk-s3", "common_enums", "common_utils", "diesel", "error-stack", - "external_services", "frunk", "frunk_core", "masking", @@ -3271,12 +3230,6 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3852614a3bd9ca9804678ba6be5e3b8ce76dfc902cae004e3e0c44051b6e88db" -[[package]] -name = "literally" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d2be3f5a0d4d5c983d1f8ecc2a87676a0875a14feb9eebf0675f7c3e2f3c35" - [[package]] name = "local-channel" version = "0.1.4" @@ -4593,7 +4546,6 @@ dependencies = [ name = "router" version = "0.2.0" dependencies = [ - "actix", "actix-cors", "actix-http", "actix-multipart", @@ -4635,7 +4587,6 @@ dependencies = [ "josekit", "jsonwebtoken", "kgraph_utils", - "literally", "masking", "maud", "mimalloc", @@ -4664,18 +4615,14 @@ dependencies = [ "serde_with", "serial_test", "sha-1 0.9.8", - "signal-hook", - "signal-hook-tokio", "sqlx", "storage_impl", "strum 0.24.1", "tera", "test_utils", - "thirtyfour", "thiserror", "time", "tokio", - "toml 0.7.4", "unicode-segmentation", "url", "utoipa", @@ -4962,7 +4909,6 @@ dependencies = [ "router_env", "serde", "serde_json", - "signal-hook-tokio", "storage_impl", "strum 0.24.1", "thiserror", @@ -5496,7 +5442,6 @@ dependencies = [ "diesel_models", "dyn-clone", "error-stack", - "external_services", "futures", "http", "masking", @@ -5730,27 +5675,20 @@ dependencies = [ name = "test_utils" version = "0.1.0" dependencies = [ - "actix-http", - "actix-web", - "api_models", "async-trait", - "awc", "base64 0.21.4", "clap", - "derive_deref", "masking", "rand 0.8.5", "reqwest", "serde", "serde_json", - "serde_path_to_error", "serde_urlencoded", "serial_test", "thirtyfour", "time", "tokio", "toml 0.7.4", - "uuid", ] [[package]] diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index d15fdeabf387..ac624c899c6d 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -14,7 +14,7 @@ connector_choice_bcompat = [] errors = ["dep:actix-web", "dep:reqwest"] backwards_compatibility = ["connector_choice_bcompat"] connector_choice_mca_id = ["euclid/connector_choice_mca_id"] -dummy_connector = ["common_enums/dummy_connector", "euclid/dummy_connector"] +dummy_connector = ["euclid/dummy_connector"] detailed_errors = [] payouts = [] @@ -30,7 +30,6 @@ strum = { version = "0.24.1", features = ["derive"] } time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } url = { version = "2.4.0", features = ["serde"] } utoipa = { version = "3.3.0", features = ["preserve_order"] } -thiserror = "1.0.40" # First party crates cards = { version = "0.1.0", path = "../cards" } diff --git a/crates/common_enums/Cargo.toml b/crates/common_enums/Cargo.toml index db37d27ab0f1..88628825ca64 100644 --- a/crates/common_enums/Cargo.toml +++ b/crates/common_enums/Cargo.toml @@ -7,15 +7,11 @@ rust-version.workspace = true readme = "README.md" license.workspace = true -[features] -dummy_connector = [] - [dependencies] diesel = { version = "2.1.0", features = ["postgres"] } serde = { version = "1.0.160", features = ["derive"] } serde_json = "1.0.96" strum = { version = "0.25", features = ["derive"] } -time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } utoipa = { version = "3.3.0", features = ["preserve_order"] } # First party crates diff --git a/crates/common_utils/Cargo.toml b/crates/common_utils/Cargo.toml index 62bd747da1b0..3619c93d772c 100644 --- a/crates/common_utils/Cargo.toml +++ b/crates/common_utils/Cargo.toml @@ -23,6 +23,7 @@ http = "0.2.9" md5 = "0.7.0" nanoid = "0.4.0" once_cell = "1.18.0" +phonenumber = "0.3.3" quick-xml = { version = "0.28.2", features = ["serialize"] } rand = "0.8.5" regex = "1.8.4" @@ -37,12 +38,11 @@ strum = { version = "0.24.1", features = ["derive"] } thiserror = "1.0.40" time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"], optional = true } -phonenumber = "0.3.3" # First party crates +common_enums = { version = "0.1.0", path = "../common_enums" } masking = { version = "0.1.0", path = "../masking" } router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"], optional = true } -common_enums = { version = "0.1.0", path = "../common_enums" } [target.'cfg(not(target_os = "windows"))'.dependencies] signal-hook-tokio = { version = "0.3.1", features = ["futures-v0_3"], optional = true } diff --git a/crates/data_models/Cargo.toml b/crates/data_models/Cargo.toml index c7c872771689..57ae1ec1ec87 100644 --- a/crates/data_models/Cargo.toml +++ b/crates/data_models/Cargo.toml @@ -8,16 +8,15 @@ readme = "README.md" license.workspace = true [features] -default = ["olap", "oltp"] -oltp = [] +default = ["olap"] olap = [] [dependencies] # First party deps api_models = { version = "0.1.0", path = "../api_models" } -masking = { version = "0.1.0", path = "../masking" } common_enums = { version = "0.1.0", path = "../common_enums" } common_utils = { version = "0.1.0", path = "../common_utils" } +masking = { version = "0.1.0", path = "../masking" } # Third party deps @@ -25,6 +24,5 @@ async-trait = "0.1.68" error-stack = "0.3.1" serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.96" -strum = { version = "0.25", features = [ "derive" ] } thiserror = "1.0.40" time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } diff --git a/crates/diesel_models/Cargo.toml b/crates/diesel_models/Cargo.toml index 1a0bdfe5674e..9521c690366f 100644 --- a/crates/diesel_models/Cargo.toml +++ b/crates/diesel_models/Cargo.toml @@ -9,15 +9,10 @@ license.workspace = true [features] default = ["kv_store"] -email = ["external_services/email", "dep:aws-config"] -kms = ["external_services/kms", "dep:aws-config"] kv_store = [] -s3 = ["dep:aws-sdk-s3", "dep:aws-config"] [dependencies] async-bb8-diesel = "0.1.0" -aws-config = { version = "0.55.3", optional = true } -aws-sdk-s3 = { version = "0.28.0", optional = true } diesel = { version = "2.1.0", features = ["postgres", "serde_json", "time", "64-column-tables"] } error-stack = "0.3.1" frunk = "0.4.1" @@ -31,7 +26,6 @@ time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } # First party crates common_enums = { path = "../common_enums" } common_utils = { version = "0.1.0", path = "../common_utils" } -external_services = { version = "0.1.0", path = "../external_services" } masking = { version = "0.1.0", path = "../masking" } router_derive = { version = "0.1.0", path = "../router_derive" } router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"] } diff --git a/crates/drainer/Cargo.toml b/crates/drainer/Cargo.toml index 3bf056a69b38..56bebdce6b86 100644 --- a/crates/drainer/Cargo.toml +++ b/crates/drainer/Cargo.toml @@ -8,7 +8,7 @@ readme = "README.md" license.workspace = true [features] -release = ["kms","vergen"] +release = ["kms", "vergen"] kms = ["external_services/kms"] vergen = ["router_env/vergen"] @@ -28,11 +28,11 @@ tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] } # First Party Crates common_utils = { version = "0.1.0", path = "../common_utils", features = ["signals"] } +diesel_models = { version = "0.1.0", path = "../diesel_models", features = ["kv_store"] } external_services = { version = "0.1.0", path = "../external_services" } masking = { version = "0.1.0", path = "../masking" } redis_interface = { version = "0.1.0", path = "../redis_interface" } router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"] } -diesel_models = { version = "0.1.0", path = "../diesel_models", features = ["kv_store"] } [build-dependencies] router_env = { version = "0.1.0", path = "../router_env", default-features = false } diff --git a/crates/euclid/Cargo.toml b/crates/euclid/Cargo.toml index f0e24b1ff63c..859795964145 100644 --- a/crates/euclid/Cargo.toml +++ b/crates/euclid/Cargo.toml @@ -6,6 +6,7 @@ edition.workspace = true rust-version.workspace = true [dependencies] +erased-serde = "0.3.28" frunk = "0.4.1" frunk_core = "0.4.1" nom = { version = "7.1.3", features = ["alloc"], optional = true } @@ -13,7 +14,6 @@ once_cell = "1.18.0" rustc-hash = "1.1.0" serde = { version = "1.0.163", features = ["derive", "rc"] } serde_json = "1.0.96" -erased-serde = "0.3.28" strum = { version = "0.25", features = ["derive"] } thiserror = "1.0.43" @@ -24,10 +24,8 @@ euclid_macros = { version = "0.1.0", path = "../euclid_macros" } [features] ast_parser = ["dep:nom"] valued_jit = [] -connector_choice_bcompat = [] connector_choice_mca_id = [] dummy_connector = [] -backwards_compatibility = ["connector_choice_bcompat"] [dev-dependencies] criterion = "0.5" diff --git a/crates/euclid_wasm/Cargo.toml b/crates/euclid_wasm/Cargo.toml index 90489eb78bf6..4fc8cd970f40 100644 --- a/crates/euclid_wasm/Cargo.toml +++ b/crates/euclid_wasm/Cargo.toml @@ -10,28 +10,21 @@ rust-version.workspace = true crate-type = ["cdylib"] [features] -default = ["connector_choice_bcompat", "payouts"] -connector_choice_bcompat = [ - "euclid/connector_choice_bcompat", - "api_models/connector_choice_bcompat", - "kgraph_utils/backwards_compatibility" -] -connector_choice_mca_id = [ - "api_models/connector_choice_mca_id", - "euclid/connector_choice_mca_id", - "kgraph_utils/connector_choice_mca_id" -] +default = ["connector_choice_bcompat"] +connector_choice_bcompat = ["api_models/connector_choice_bcompat"] +connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connector_choice_mca_id", "kgraph_utils/connector_choice_mca_id"] dummy_connector = ["kgraph_utils/dummy_connector"] -payouts = [] [dependencies] api_models = { version = "0.1.0", path = "../api_models", package = "api_models" } euclid = { path = "../euclid", features = [] } kgraph_utils = { version = "0.1.0", path = "../kgraph_utils" } + +# Third party crates getrandom = { version = "0.2.10", features = ["js"] } once_cell = "1.18.0" +ron-parser = "0.1.4" serde = { version = "1.0", features = [] } serde-wasm-bindgen = "0.5" strum = { version = "0.25", features = ["derive"] } wasm-bindgen = { version = "0.2.86" } -ron-parser = "0.1.4" diff --git a/crates/kgraph_utils/Cargo.toml b/crates/kgraph_utils/Cargo.toml index fa90b3974c20..cd0adf0bc8af 100644 --- a/crates/kgraph_utils/Cargo.toml +++ b/crates/kgraph_utils/Cargo.toml @@ -7,14 +7,14 @@ rust-version.workspace = true [features] dummy_connector = ["api_models/dummy_connector", "euclid/dummy_connector"] -backwards_compatibility = ["euclid/backwards_compatibility", "euclid/backwards_compatibility"] connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connector_choice_mca_id"] [dependencies] api_models = { version = "0.1.0", path = "../api_models", package = "api_models" } euclid = { version = "0.1.0", path = "../euclid" } -masking = { version = "0.1.0", path = "../masking/"} +masking = { version = "0.1.0", path = "../masking/" } +# Third party crates serde = "1.0.163" serde_json = "1.0.96" thiserror = "1.0.43" diff --git a/crates/redis_interface/Cargo.toml b/crates/redis_interface/Cargo.toml index 8066787dcae2..9d3ae724d432 100644 --- a/crates/redis_interface/Cargo.toml +++ b/crates/redis_interface/Cargo.toml @@ -9,7 +9,7 @@ license.workspace = true [dependencies] error-stack = "0.3.1" -fred = { version = "6.3.0", features = ["metrics", "partial-tracing","subscriber-client"] } +fred = { version = "6.3.0", features = ["metrics", "partial-tracing", "subscriber-client"] } futures = "0.3" serde = { version = "1.0.163", features = ["derive"] } thiserror = "1.0.40" diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 8f6906e06855..5e8cb7a72979 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -17,24 +17,22 @@ basilisk = ["kms"] stripe = ["dep:serde_qs"] release = ["kms", "stripe", "basilisk", "s3", "email", "business_profile_routing", "accounts_cache", "kv_store", "profile_specific_fallback_routing"] olap = ["data_models/olap", "storage_impl/olap", "scheduler/olap"] -oltp = ["data_models/oltp", "storage_impl/oltp"] +oltp = ["storage_impl/oltp"] kv_store = ["scheduler/kv_store"] accounts_cache = [] openapi = ["olap", "oltp", "payouts"] vergen = ["router_env/vergen"] -backwards_compatibility = ["api_models/backwards_compatibility", "euclid/backwards_compatibility", "kgraph_utils/backwards_compatibility"] -business_profile_routing=["api_models/business_profile_routing"] +backwards_compatibility = ["api_models/backwards_compatibility"] +business_profile_routing = ["api_models/business_profile_routing"] profile_specific_fallback_routing = [] dummy_connector = ["api_models/dummy_connector", "euclid/dummy_connector", "kgraph_utils/dummy_connector"] connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connector_choice_mca_id", "kgraph_utils/connector_choice_mca_id"] external_access_dc = ["dummy_connector"] detailed_errors = ["api_models/detailed_errors", "error-stack/serde"] payouts = [] -api_locking = [] [dependencies] -actix = "0.13.0" actix-cors = "0.6.4" actix-multipart = "0.6.0" actix-rt = "2.8.0" @@ -52,6 +50,7 @@ bytes = "1.4.0" clap = { version = "4.3.2", default-features = false, features = ["std", "derive", "help", "usage"] } config = { version = "0.13.3", features = ["toml"] } diesel = { version = "2.1.0", features = ["postgres"] } +digest = "0.9" dyn-clone = "1.0.11" encoding_rs = "0.8.32" error-stack = "0.3.1" @@ -63,13 +62,13 @@ image = "0.23.14" infer = "0.13.0" josekit = "0.8.3" jsonwebtoken = "8.3.0" -literally = "0.1.3" maud = { version = "0.25", features = ["actix-web"] } mimalloc = { version = "0.1", optional = true } mime = "0.3.17" nanoid = "0.4.0" num_cpus = "1.15.0" once_cell = "1.18.0" +openssl = "0.10.55" qrcode = "0.12.0" rand = "0.8.5" rand_chacha = "0.3.1" @@ -84,44 +83,38 @@ serde_path_to_error = "0.1.11" serde_qs = { version = "0.12.0", optional = true } serde_urlencoded = "0.7.1" serde_with = "3.0.0" -signal-hook = "0.3.15" -strum = { version = "0.24.1", features = ["derive"] } +sha-1 = { version = "0.9" } sqlx = { version = "0.6.3", features = ["postgres", "runtime-actix", "runtime-actix-native-tls", "time", "bigdecimal"] } +strum = { version = "0.24.1", features = ["derive"] } +tera = "1.19.1" thiserror = "1.0.40" time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] } -tera = "1.19.1" unicode-segmentation = "1.10.1" url = { version = "2.4.0", features = ["serde"] } utoipa = { version = "3.3.0", features = ["preserve_order", "time"] } utoipa-swagger-ui = { version = "3.1.3", features = ["actix-web"] } uuid = { version = "1.3.3", features = ["serde", "v4"] } validator = "0.16.0" -openssl = "0.10.55" x509-parser = "0.15.0" -sha-1 = { version = "0.9"} -digest = "0.9" # First party crates api_models = { version = "0.1.0", path = "../api_models", features = ["errors"] } cards = { version = "0.1.0", path = "../cards" } +common_enums = { version = "0.1.0", path = "../common_enums" } common_utils = { version = "0.1.0", path = "../common_utils", features = ["signals", "async_ext", "logs"] } -common_enums = { version = "0.1.0", path = "../common_enums"} -external_services = { version = "0.1.0", path = "../external_services" } +data_models = { version = "0.1.0", path = "../data_models", default-features = false } +diesel_models = { version = "0.1.0", path = "../diesel_models", features = ["kv_store"] } euclid = { version = "0.1.0", path = "../euclid", features = ["valued_jit"] } +external_services = { version = "0.1.0", path = "../external_services" } +kgraph_utils = { version = "0.1.0", path = "../kgraph_utils" } masking = { version = "0.1.0", path = "../masking" } redis_interface = { version = "0.1.0", path = "../redis_interface" } router_derive = { version = "0.1.0", path = "../router_derive" } router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"] } -diesel_models = { version = "0.1.0", path = "../diesel_models", features = ["kv_store"] } -scheduler = { version = "0.1.0", path = "../scheduler", default-features = false} -data_models = { version = "0.1.0", path = "../data_models", default-features = false } -kgraph_utils = { version = "0.1.0", path = "../kgraph_utils" } +scheduler = { version = "0.1.0", path = "../scheduler", default-features = false } storage_impl = { version = "0.1.0", path = "../storage_impl", default-features = false } -[target.'cfg(not(target_os = "windows"))'.dependencies] -signal-hook-tokio = { version = "0.3.1", features = ["futures-v0_3"] } - [build-dependencies] router_env = { version = "0.1.0", path = "../router_env", default-features = false } @@ -131,10 +124,8 @@ awc = { version = "3.1.1", features = ["rustls"] } derive_deref = "1.1.1" rand = "0.8.5" serial_test = "2.0.0" -thirtyfour = "0.31.0" time = { version = "0.3.21", features = ["macros"] } tokio = "1.28.2" -toml = "0.7.4" wiremock = "0.5" # First party dev-dependencies diff --git a/crates/router/tests/connectors/adyen.rs b/crates/router/tests/connectors/adyen.rs index dca7bbfc9b44..4b2cbcb7c4a9 100644 --- a/crates/router/tests/connectors/adyen.rs +++ b/crates/router/tests/connectors/adyen.rs @@ -23,6 +23,7 @@ impl utils::Connector for AdyenTest { } } + #[cfg(feature = "payouts")] fn get_payout_data(&self) -> Option { use router::connector::Adyen; Some(types::api::PayoutConnectorData { @@ -68,6 +69,7 @@ impl AdyenTest { }) } + #[cfg(feature = "payouts")] fn get_payout_info(payout_type: enums::PayoutType) -> Option { Some(PaymentInfo { country: Some(api_models::enums::CountryAlpha2::NL), diff --git a/crates/router/tests/connectors/bankofamerica.rs b/crates/router/tests/connectors/bankofamerica.rs index ce264cbccc86..766078fa19c0 100644 --- a/crates/router/tests/connectors/bankofamerica.rs +++ b/crates/router/tests/connectors/bankofamerica.rs @@ -12,6 +12,7 @@ impl utils::Connector for BankofamericaTest { use router::connector::Bankofamerica; types::api::ConnectorData { connector: Box::new(&Bankofamerica), + // Remove `dummy_connector` feature gate from module in `main.rs` when updating this to use actual connector variant connector_name: types::Connector::DummyConnector1, get_token: types::api::GetToken::Connector, merchant_connector_id: None, diff --git a/crates/router/tests/connectors/globepay.rs b/crates/router/tests/connectors/globepay.rs index 210f12b23d83..fcf61dd6b33d 100644 --- a/crates/router/tests/connectors/globepay.rs +++ b/crates/router/tests/connectors/globepay.rs @@ -14,7 +14,7 @@ impl utils::Connector for GlobepayTest { use router::connector::Globepay; types::api::ConnectorData { connector: Box::new(&Globepay), - connector_name: types::Connector::DummyConnector1, + connector_name: types::Connector::Globepay, get_token: types::api::GetToken::Connector, merchant_connector_id: None, } diff --git a/crates/router/tests/connectors/gocardless.rs b/crates/router/tests/connectors/gocardless.rs index 6b6bd6d86175..f19e90941b2e 100644 --- a/crates/router/tests/connectors/gocardless.rs +++ b/crates/router/tests/connectors/gocardless.rs @@ -12,7 +12,7 @@ impl utils::Connector for GocardlessTest { use router::connector::Gocardless; types::api::ConnectorData { connector: Box::new(&Gocardless), - connector_name: types::Connector::DummyConnector1, + connector_name: types::Connector::Gocardless, get_token: types::api::GetToken::Connector, merchant_connector_id: None, } diff --git a/crates/router/tests/connectors/helcim.rs b/crates/router/tests/connectors/helcim.rs index 0bac1e702360..c9a891988f3b 100644 --- a/crates/router/tests/connectors/helcim.rs +++ b/crates/router/tests/connectors/helcim.rs @@ -12,7 +12,7 @@ impl utils::Connector for HelcimTest { use router::connector::Helcim; types::api::ConnectorData { connector: Box::new(&Helcim), - connector_name: types::Connector::DummyConnector1, + connector_name: types::Connector::Helcim, get_token: types::api::GetToken::Connector, merchant_connector_id: None, } diff --git a/crates/router/tests/connectors/main.rs b/crates/router/tests/connectors/main.rs index 03b6181b8a89..fc474818b505 100644 --- a/crates/router/tests/connectors/main.rs +++ b/crates/router/tests/connectors/main.rs @@ -11,6 +11,7 @@ mod adyen; mod airwallex; mod authorizedotnet; mod bambora; +#[cfg(feature = "dummy_connector")] mod bankofamerica; mod bitpay; mod bluesnap; @@ -36,13 +37,16 @@ mod nexinets; mod nmi; mod noon; mod nuvei; +#[cfg(feature = "dummy_connector")] mod opayo; mod opennode; +#[cfg(feature = "dummy_connector")] mod payeezy; mod payme; mod paypal; mod payu; mod powertranz; +#[cfg(feature = "dummy_connector")] mod prophetpay; mod rapyd; mod shift4; diff --git a/crates/router/tests/connectors/opayo.rs b/crates/router/tests/connectors/opayo.rs index 6d76133d342e..97d744d1e9db 100644 --- a/crates/router/tests/connectors/opayo.rs +++ b/crates/router/tests/connectors/opayo.rs @@ -16,6 +16,7 @@ impl utils::Connector for OpayoTest { use router::connector::Opayo; types::api::ConnectorData { connector: Box::new(&Opayo), + // Remove `dummy_connector` feature gate from module in `main.rs` when updating this to use actual connector variant connector_name: types::Connector::DummyConnector1, get_token: types::api::GetToken::Connector, merchant_connector_id: None, diff --git a/crates/router/tests/connectors/payeezy.rs b/crates/router/tests/connectors/payeezy.rs index 81d69503b4a9..1176ad7322bf 100644 --- a/crates/router/tests/connectors/payeezy.rs +++ b/crates/router/tests/connectors/payeezy.rs @@ -22,6 +22,7 @@ impl utils::Connector for PayeezyTest { use router::connector::Payeezy; types::api::ConnectorData { connector: Box::new(&Payeezy), + // Remove `dummy_connector` feature gate from module in `main.rs` when updating this to use actual connector variant connector_name: types::Connector::DummyConnector1, get_token: types::api::GetToken::Connector, merchant_connector_id: None, diff --git a/crates/router/tests/connectors/powertranz.rs b/crates/router/tests/connectors/powertranz.rs index cc0028ef3c91..eca3f86b5690 100644 --- a/crates/router/tests/connectors/powertranz.rs +++ b/crates/router/tests/connectors/powertranz.rs @@ -14,7 +14,7 @@ impl utils::Connector for PowertranzTest { use router::connector::Powertranz; types::api::ConnectorData { connector: Box::new(&Powertranz), - connector_name: types::Connector::DummyConnector1, + connector_name: types::Connector::Powertranz, get_token: types::api::GetToken::Connector, merchant_connector_id: None, } diff --git a/crates/router/tests/connectors/prophetpay.rs b/crates/router/tests/connectors/prophetpay.rs index 2e4c6d7e380e..09e4ea422531 100644 --- a/crates/router/tests/connectors/prophetpay.rs +++ b/crates/router/tests/connectors/prophetpay.rs @@ -12,6 +12,7 @@ impl utils::Connector for ProphetpayTest { use router::connector::Prophetpay; types::api::ConnectorData { connector: Box::new(&Prophetpay), + // Remove `dummy_connector` feature gate from module in `main.rs` when updating this to use actual connector variant connector_name: types::Connector::DummyConnector1, get_token: types::api::GetToken::Connector, merchant_connector_id: None, diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 1cb3b48f72d5..1f450a19e776 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -4,9 +4,11 @@ use async_trait::async_trait; use common_utils::pii::Email; use error_stack::Report; use masking::Secret; +#[cfg(feature = "payouts")] +use router::core::utils as core_utils; use router::{ configs::settings::Settings, - core::{errors, errors::ConnectorError, payments, utils as core_utils}, + core::{errors, errors::ConnectorError, payments}, db::StorageImpl, routes, services, types::{self, api, storage::enums, AccessToken, PaymentAddress, RouterData}, @@ -17,15 +19,21 @@ use wiremock::{Mock, MockServer}; pub trait Connector { fn get_data(&self) -> types::api::ConnectorData; + fn get_auth_token(&self) -> types::ConnectorAuthType; + fn get_name(&self) -> String; + fn get_connector_meta(&self) -> Option { None } + /// interval in seconds to be followed when making the subsequent request whenever needed fn get_request_interval(&self) -> u64 { 5 } + + #[cfg(feature = "payouts")] fn get_payout_data(&self) -> Option { None } @@ -423,6 +431,7 @@ pub trait ConnectorActions: Connector { Err(errors::ConnectorError::ProcessingStepFailed(None).into()) } + #[cfg(feature = "payouts")] fn get_payout_request( &self, connector_payout_id: Option, @@ -534,6 +543,7 @@ pub trait ConnectorActions: Connector { } } + #[cfg(feature = "payouts")] async fn verify_payout_eligibility( &self, payout_type: enums::PayoutType, @@ -572,6 +582,7 @@ pub trait ConnectorActions: Connector { Ok(res.response.unwrap()) } + #[cfg(feature = "payouts")] async fn fulfill_payout( &self, connector_payout_id: Option, @@ -611,6 +622,7 @@ pub trait ConnectorActions: Connector { Ok(res.response.unwrap()) } + #[cfg(feature = "payouts")] async fn create_payout( &self, connector_customer: Option, @@ -651,6 +663,7 @@ pub trait ConnectorActions: Connector { Ok(res.response.unwrap()) } + #[cfg(feature = "payouts")] async fn cancel_payout( &self, connector_payout_id: String, @@ -691,6 +704,7 @@ pub trait ConnectorActions: Connector { Ok(res.response.unwrap()) } + #[cfg(feature = "payouts")] async fn create_and_fulfill_payout( &self, connector_customer: Option, @@ -714,6 +728,7 @@ pub trait ConnectorActions: Connector { Ok(fulfill_res) } + #[cfg(feature = "payouts")] async fn create_and_cancel_payout( &self, connector_customer: Option, @@ -737,6 +752,7 @@ pub trait ConnectorActions: Connector { Ok(cancel_res) } + #[cfg(feature = "payouts")] async fn create_payout_recipient( &self, payout_type: enums::PayoutType, diff --git a/crates/router/tests/connectors/volt.rs b/crates/router/tests/connectors/volt.rs index 1c62c47ee03c..0df21640c777 100644 --- a/crates/router/tests/connectors/volt.rs +++ b/crates/router/tests/connectors/volt.rs @@ -12,7 +12,7 @@ impl utils::Connector for VoltTest { use router::connector::Volt; types::api::ConnectorData { connector: Box::new(&Volt), - connector_name: types::Connector::DummyConnector1, + connector_name: types::Connector::Volt, get_token: types::api::GetToken::Connector, merchant_connector_id: None, } diff --git a/crates/router/tests/connectors/wise.rs b/crates/router/tests/connectors/wise.rs index 753ed4f4ed66..fb65397e1a22 100644 --- a/crates/router/tests/connectors/wise.rs +++ b/crates/router/tests/connectors/wise.rs @@ -1,10 +1,16 @@ +#[cfg(feature = "payouts")] use api_models::payments::{Address, AddressDetails}; +#[cfg(feature = "payouts")] use masking::Secret; -use router::types::{self, api, storage::enums, PaymentAddress}; +use router::types; +#[cfg(feature = "payouts")] +use router::types::{api, storage::enums, PaymentAddress}; +#[cfg(feature = "payouts")] +use crate::utils::PaymentInfo; use crate::{ connector_auth, - utils::{self, ConnectorActions, PaymentInfo}, + utils::{self, ConnectorActions}, }; struct WiseTest; @@ -20,6 +26,7 @@ impl utils::Connector for WiseTest { } } + #[cfg(feature = "payouts")] fn get_payout_data(&self) -> Option { use router::connector::Wise; Some(types::api::PayoutConnectorData { @@ -44,6 +51,7 @@ impl utils::Connector for WiseTest { } impl WiseTest { + #[cfg(feature = "payouts")] fn get_payout_info() -> Option { Some(PaymentInfo { country: Some(api_models::enums::CountryAlpha2::NL), @@ -75,6 +83,7 @@ impl WiseTest { } } +#[cfg(feature = "payouts")] static CONNECTOR: WiseTest = WiseTest {}; /******************** Payouts test cases ********************/ diff --git a/crates/scheduler/Cargo.toml b/crates/scheduler/Cargo.toml index 7ce61d9f59f4..e0b68c709e8d 100644 --- a/crates/scheduler/Cargo.toml +++ b/crates/scheduler/Cargo.toml @@ -32,9 +32,6 @@ redis_interface = { version = "0.1.0", path = "../redis_interface" } router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"] } storage_impl = { version = "0.1.0", path = "../storage_impl", default-features = false } -[target.'cfg(not(target_os = "windows"))'.dependencies] -signal-hook-tokio = { version = "0.3.1", features = ["futures-v0_3"] } - # [[bin]] # name = "scheduler" # path = "src/bin/scheduler.rs" diff --git a/crates/storage_impl/Cargo.toml b/crates/storage_impl/Cargo.toml index 8fb59d213364..31115e91589f 100644 --- a/crates/storage_impl/Cargo.toml +++ b/crates/storage_impl/Cargo.toml @@ -9,22 +9,20 @@ license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -kms = ["external_services/kms"] default = ["olap", "oltp"] -oltp = ["data_models/oltp"] +oltp = [] olap = ["data_models/olap"] [dependencies] # First Party dependencies -common_utils = { version = "0.1.0", path = "../common_utils" } api_models = { version = "0.1.0", path = "../api_models" } -diesel_models = { version = "0.1.0", path = "../diesel_models" } +common_utils = { version = "0.1.0", path = "../common_utils" } data_models = { version = "0.1.0", path = "../data_models", default-features = false } +diesel_models = { version = "0.1.0", path = "../diesel_models" } masking = { version = "0.1.0", path = "../masking" } redis_interface = { version = "0.1.0", path = "../redis_interface" } -router_env = { version = "0.1.0", path = "../router_env" } -external_services = { version = "0.1.0", path = "../external_services" } router_derive = { version = "0.1.0", path = "../router_derive" } +router_env = { version = "0.1.0", path = "../router_env" } # Third party crates actix-web = "4.3.1" @@ -34,16 +32,16 @@ bb8 = "0.8.1" bytes = "1.4.0" config = { version = "0.13.3", features = ["toml"] } crc32fast = "1.3.2" -futures = "0.3.28" diesel = { version = "2.1.0", default-features = false, features = ["postgres"] } dyn-clone = "1.0.12" error-stack = "0.3.1" +futures = "0.3.28" http = "0.2.9" mime = "0.3.17" moka = { version = "0.11.3", features = ["future"] } once_cell = "1.18.0" ring = "0.16.20" -thiserror = "1.0.40" -tokio = { version = "1.28.2", features = ["rt-multi-thread"] } serde = { version = "1.0.185", features = ["derive"] } serde_json = "1.0.105" +thiserror = "1.0.40" +tokio = { version = "1.28.2", features = ["rt-multi-thread"] } diff --git a/crates/test_utils/Cargo.toml b/crates/test_utils/Cargo.toml index 44c835b21623..957a51171da7 100644 --- a/crates/test_utils/Cargo.toml +++ b/crates/test_utils/Cargo.toml @@ -9,30 +9,23 @@ license.workspace = true [features] default = ["dummy_connector", "payouts"] -dummy_connector = ["api_models/dummy_connector"] +dummy_connector = [] payouts = [] [dependencies] async-trait = "0.1.68" -actix-web = "4.3.1" base64 = "0.21.2" clap = { version = "4.3.2", default-features = false, features = ["std", "derive", "help", "usage"] } +rand = "0.8.5" +reqwest = { version = "0.11.18", features = ["native-tls"] } serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.96" -serde_path_to_error = "0.1.11" -toml = "0.7.4" -serial_test = "2.0.0" serde_urlencoded = "0.7.1" -actix-http = "3.3.1" -awc = { version = "3.1.1", features = ["rustls"] } -derive_deref = "1.1.1" -rand = "0.8.5" -reqwest = { version = "0.11.18", features = ["native-tls"] } +serial_test = "2.0.0" thirtyfour = "0.31.0" time = { version = "0.3.21", features = ["macros"] } tokio = "1.28.2" -uuid = { version = "1.3.3", features = ["serde", "v4"] } +toml = "0.7.4" # First party crates -api_models = { version = "0.1.0", path = "../api_models", features = ["errors"] } masking = { version = "0.1.0", path = "../masking" } From fc92ec770a98875fdc9737611ae20dff2ae13a83 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 14:32:02 +0000 Subject: [PATCH 006/146] chore(version): v1.77.0 --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5926cfe86ab..61cb4839fd7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.77.0 (2023-11-13) + +### Features + +- **apievent:** Added hs latency to api event ([#2734](https://github.com/juspay/hyperswitch/pull/2734)) ([`c124511`](https://github.com/juspay/hyperswitch/commit/c124511052ed8911a2ccfcf648c0793b5c1ca690)) +- **router:** + - Add new JWT authentication variants and use them ([#2835](https://github.com/juspay/hyperswitch/pull/2835)) ([`f88eee7`](https://github.com/juspay/hyperswitch/commit/f88eee7362be2cc3e8e8dc2bb7bfd263892ff01e)) + - Profile specific fallback derivation while routing payments ([#2806](https://github.com/juspay/hyperswitch/pull/2806)) ([`8e538db`](https://github.com/juspay/hyperswitch/commit/8e538dbd5c189047d0a0b24fa752b9a1c67554f5)) + +### Build System / Dependencies + +- **deps:** Remove unused dependencies and features ([#2854](https://github.com/juspay/hyperswitch/pull/2854)) ([`0553587`](https://github.com/juspay/hyperswitch/commit/05535871152f4a6ac24ce6b5b5390da13cc29b96)) + +**Full Changelog:** [`v1.76.0...v1.77.0`](https://github.com/juspay/hyperswitch/compare/v1.76.0...v1.77.0) + +- - - + + ## 1.76.0 (2023-11-12) ### Features From d2968c94978a57422fa46a8195d906736a95b864 Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Mon, 13 Nov 2023 22:19:37 +0530 Subject: [PATCH 007/146] feat(router): add automatic retries and step up 3ds flow (#2834) --- crates/router/Cargo.toml | 4 +- crates/router/src/core/payments.rs | 31 +- crates/router/src/core/payments/retry.rs | 579 +++++++++++++++++++++++ crates/router/src/routes/metrics.rs | 8 + 4 files changed, 619 insertions(+), 3 deletions(-) create mode 100644 crates/router/src/core/payments/retry.rs diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 5e8cb7a72979..4d9c315a10b0 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -9,7 +9,7 @@ readme = "README.md" license.workspace = true [features] -default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "profile_specific_fallback_routing"] +default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "profile_specific_fallback_routing", "retry"] s3 = ["dep:aws-sdk-s3", "dep:aws-config"] kms = ["external_services/kms", "dep:aws-config"] email = ["external_services/email", "dep:aws-config"] @@ -30,7 +30,7 @@ connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connect external_access_dc = ["dummy_connector"] detailed_errors = ["api_models/detailed_errors", "error-stack/serde"] payouts = [] - +retry = [] [dependencies] actix-cors = "0.6.4" diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index a114b20380bf..5c8089271bd9 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -3,6 +3,8 @@ pub mod customers; pub mod flows; pub mod helpers; pub mod operations; +#[cfg(feature = "retry")] +pub mod retry; pub mod routing; pub mod tokenization; pub mod transformers; @@ -231,7 +233,7 @@ where state, &merchant_account, &key_store, - connector_data, + connector_data.clone(), &operation, &mut payment_data, &customer, @@ -242,6 +244,33 @@ where ) .await?; + #[cfg(feature = "retry")] + let mut router_data = router_data; + #[cfg(feature = "retry")] + { + use crate::core::payments::retry::{self, GsmValidation}; + let config_bool = + retry::config_should_call_gsm(&*state.store, &merchant_account.merchant_id) + .await; + + if config_bool && router_data.should_call_gsm() { + router_data = retry::do_gsm_actions( + state, + &mut payment_data, + connectors, + connector_data, + router_data, + &merchant_account, + &key_store, + &operation, + &customer, + &validate_result, + schedule_time, + ) + .await?; + }; + } + let operation = Box::new(PaymentResponse); let db = &*state.store; connector_http_status_code = router_data.connector_http_status_code; diff --git a/crates/router/src/core/payments/retry.rs b/crates/router/src/core/payments/retry.rs new file mode 100644 index 000000000000..f58e9ea298f7 --- /dev/null +++ b/crates/router/src/core/payments/retry.rs @@ -0,0 +1,579 @@ +use std::{str::FromStr, vec::IntoIter}; + +use diesel_models::enums as storage_enums; +use error_stack::{IntoReport, ResultExt}; +use router_env::{ + logger, + tracing::{self, instrument}, +}; + +use crate::{ + core::{ + errors::{self, RouterResult, StorageErrorExt}, + payment_methods::PaymentMethodRetrieve, + payments::{ + self, + flows::{ConstructFlowSpecificData, Feature}, + operations, + }, + }, + db::StorageInterface, + routes, + routes::{app, metrics}, + services::{self, RedirectForm}, + types, + types::{api, domain, storage}, + utils, +}; + +#[instrument(skip_all)] +#[allow(clippy::too_many_arguments)] +pub async fn do_gsm_actions( + state: &app::AppState, + payment_data: &mut payments::PaymentData, + mut connectors: IntoIter, + original_connector_data: api::ConnectorData, + mut router_data: types::RouterData, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + operation: &operations::BoxedOperation<'_, F, ApiRequest, Ctx>, + customer: &Option, + validate_result: &operations::ValidateResult<'_>, + schedule_time: Option, +) -> RouterResult> +where + F: Clone + Send + Sync, + FData: Send + Sync, + payments::PaymentResponse: operations::Operation, + + payments::PaymentData: ConstructFlowSpecificData, + types::RouterData: Feature, + dyn api::Connector: services::api::ConnectorIntegration, + Ctx: PaymentMethodRetrieve, +{ + let mut retries = None; + + metrics::AUTO_RETRY_ELIGIBLE_REQUEST_COUNT.add(&metrics::CONTEXT, 1, &[]); + + let mut initial_gsm = get_gsm(state, &router_data).await; + + //Check if step-up to threeDS is possible and merchant has enabled + let step_up_possible = initial_gsm + .clone() + .map(|gsm| gsm.step_up_possible) + .unwrap_or(false); + let is_no_three_ds_payment = matches!( + payment_data.payment_attempt.authentication_type, + Some(storage_enums::AuthenticationType::NoThreeDs) + ); + let should_step_up = if step_up_possible && is_no_three_ds_payment { + is_step_up_enabled_for_merchant_connector( + state, + &merchant_account.merchant_id, + original_connector_data.connector_name, + ) + .await + } else { + false + }; + + if should_step_up { + router_data = do_retry( + &state.clone(), + original_connector_data, + operation, + customer, + merchant_account, + key_store, + payment_data, + router_data, + validate_result, + schedule_time, + true, + ) + .await?; + } + // Step up is not applicable so proceed with auto retries flow + else { + loop { + // Use initial_gsm for first time alone + let gsm = match initial_gsm.as_ref() { + Some(gsm) => Some(gsm.clone()), + None => get_gsm(state, &router_data).await, + }; + + match get_gsm_decision(gsm) { + api_models::gsm::GsmDecision::Retry => { + retries = get_retries(state, retries, &merchant_account.merchant_id).await; + + if retries.is_none() || retries == Some(0) { + metrics::AUTO_RETRY_EXHAUSTED_COUNT.add(&metrics::CONTEXT, 1, &[]); + logger::info!("retries exhausted for auto_retry payment"); + break; + } + + if connectors.len() == 0 { + logger::info!("connectors exhausted for auto_retry payment"); + metrics::AUTO_RETRY_EXHAUSTED_COUNT.add(&metrics::CONTEXT, 1, &[]); + break; + } + + let connector = super::get_connector_data(&mut connectors)?; + + router_data = do_retry( + &state.clone(), + connector, + operation, + customer, + merchant_account, + key_store, + payment_data, + router_data, + validate_result, + schedule_time, + //this is an auto retry payment, but not step-up + false, + ) + .await?; + + retries = retries.map(|i| i - 1); + } + api_models::gsm::GsmDecision::Requeue => { + Err(errors::ApiErrorResponse::NotImplemented { + message: errors::api_error_response::NotImplementedMessage::Reason( + "Requeue not implemented".to_string(), + ), + }) + .into_report()? + } + api_models::gsm::GsmDecision::DoDefault => break, + } + initial_gsm = None; + } + } + Ok(router_data) +} + +#[instrument(skip_all)] +pub async fn is_step_up_enabled_for_merchant_connector( + state: &app::AppState, + merchant_id: &str, + connector_name: types::Connector, +) -> bool { + let key = format!("step_up_enabled_{merchant_id}"); + let db = &*state.store; + db.find_config_by_key_unwrap_or(key.as_str(), Some("[]".to_string())) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .and_then(|step_up_config| { + serde_json::from_str::>(&step_up_config.config) + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Step-up config parsing failed") + }) + .map_err(|err| { + logger::error!(step_up_config_error=?err); + }) + .ok() + .map(|connectors_enabled| connectors_enabled.contains(&connector_name)) + .unwrap_or(false) +} + +#[instrument(skip_all)] +pub async fn get_retries( + state: &app::AppState, + retries: Option, + merchant_id: &str, +) -> Option { + match retries { + Some(retries) => Some(retries), + None => { + let key = format!("max_auto_retries_enabled_{merchant_id}"); + let db = &*state.store; + db.find_config_by_key(key.as_str()) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .and_then(|retries_config| { + retries_config + .config + .parse::() + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Retries config parsing failed") + }) + .map_err(|err| { + logger::error!(retries_error=?err); + None:: + }) + .ok() + } + } +} + +#[instrument(skip_all)] +pub async fn get_gsm( + state: &app::AppState, + router_data: &types::RouterData, +) -> Option { + let error_response = router_data.response.as_ref().err(); + let error_code = error_response.map(|err| err.code.to_owned()); + let error_message = error_response.map(|err| err.message.to_owned()); + let get_gsm = || async { + let connector_name = router_data.connector.to_string(); + let flow = get_flow_name::()?; + state.store.find_gsm_rule( + connector_name.clone(), + flow.clone(), + "sub_flow".to_string(), + error_code.clone().unwrap_or_default(), // TODO: make changes in connector to get a mandatory code in case of success or error response + error_message.clone().unwrap_or_default(), + ) + .await + .map_err(|err| { + if err.current_context().is_db_not_found() { + logger::warn!( + "GSM miss for connector - {}, flow - {}, error_code - {:?}, error_message - {:?}", + connector_name, + flow, + error_code, + error_message + ); + metrics::AUTO_RETRY_GSM_MISS_COUNT.add(&metrics::CONTEXT, 1, &[]); + } else { + metrics::AUTO_RETRY_GSM_FETCH_FAILURE_COUNT.add(&metrics::CONTEXT, 1, &[]); + }; + err.change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to fetch decision from gsm") + }) + }; + get_gsm() + .await + .map_err(|err| { + // warn log should suffice here because we are not propagating this error + logger::warn!(get_gsm_decision_fetch_error=?err, "error fetching gsm decision"); + err + }) + .ok() +} + +#[instrument(skip_all)] +pub fn get_gsm_decision( + option_gsm: Option, +) -> api_models::gsm::GsmDecision { + let option_gsm_decision = option_gsm + .and_then(|gsm| { + api_models::gsm::GsmDecision::from_str(gsm.decision.as_str()) + .into_report() + .map_err(|err| { + let api_error = err.change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("gsm decision parsing failed"); + logger::warn!(get_gsm_decision_parse_error=?api_error, "error fetching gsm decision"); + api_error + }) + .ok() + }); + + if option_gsm_decision.is_some() { + metrics::AUTO_RETRY_GSM_MATCH_COUNT.add(&metrics::CONTEXT, 1, &[]); + } + option_gsm_decision.unwrap_or_default() +} + +#[inline] +fn get_flow_name() -> RouterResult { + Ok(std::any::type_name::() + .to_string() + .rsplit("::") + .next() + .ok_or(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Flow stringify failed")? + .to_string()) +} + +#[allow(clippy::too_many_arguments)] +#[instrument(skip_all)] +pub async fn do_retry( + state: &routes::AppState, + connector: api::ConnectorData, + operation: &operations::BoxedOperation<'_, F, ApiRequest, Ctx>, + customer: &Option, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + payment_data: &mut payments::PaymentData, + router_data: types::RouterData, + validate_result: &operations::ValidateResult<'_>, + schedule_time: Option, + is_step_up: bool, +) -> RouterResult> +where + F: Clone + Send + Sync, + FData: Send + Sync, + payments::PaymentResponse: operations::Operation, + + payments::PaymentData: ConstructFlowSpecificData, + types::RouterData: Feature, + dyn api::Connector: services::api::ConnectorIntegration, + Ctx: PaymentMethodRetrieve, +{ + metrics::AUTO_RETRY_PAYMENT_COUNT.add(&metrics::CONTEXT, 1, &[]); + + modify_trackers( + state, + connector.connector_name.to_string(), + payment_data, + merchant_account.storage_scheme, + router_data, + is_step_up, + ) + .await?; + + payments::call_connector_service( + state, + merchant_account, + key_store, + connector, + operation, + payment_data, + customer, + payments::CallConnectorAction::Trigger, + validate_result, + schedule_time, + api::HeaderPayload::default(), + ) + .await +} + +#[instrument(skip_all)] +pub async fn modify_trackers( + state: &routes::AppState, + connector: String, + payment_data: &mut payments::PaymentData, + storage_scheme: storage_enums::MerchantStorageScheme, + router_data: types::RouterData, + is_step_up: bool, +) -> RouterResult<()> +where + F: Clone + Send, + FData: Send, +{ + let new_attempt_count = payment_data.payment_intent.attempt_count + 1; + let new_payment_attempt = make_new_payment_attempt( + connector, + payment_data.payment_attempt.clone(), + new_attempt_count, + is_step_up, + ); + + let db = &*state.store; + + match router_data.response { + Ok(types::PaymentsResponseData::TransactionResponse { + resource_id, + connector_metadata, + redirection_data, + .. + }) => { + let encoded_data = payment_data.payment_attempt.encoded_data.clone(); + + let authentication_data = redirection_data + .map(|data| utils::Encode::::encode_to_value(&data)) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not parse the connector response")?; + + db.update_payment_attempt_with_attempt_id( + payment_data.payment_attempt.clone(), + storage::PaymentAttemptUpdate::ResponseUpdate { + status: router_data.status, + connector: None, + connector_transaction_id: match resource_id { + types::ResponseId::NoResponseId => None, + types::ResponseId::ConnectorTransactionId(id) + | types::ResponseId::EncodedData(id) => Some(id), + }, + connector_response_reference_id: payment_data + .payment_attempt + .connector_response_reference_id + .clone(), + authentication_type: None, + payment_method_id: Some(router_data.payment_method_id), + mandate_id: payment_data + .mandate_id + .clone() + .map(|mandate| mandate.mandate_id), + connector_metadata, + payment_token: None, + error_code: None, + error_message: None, + error_reason: None, + amount_capturable: if router_data.status.is_terminal_status() { + Some(0) + } else { + None + }, + updated_by: storage_scheme.to_string(), + authentication_data, + encoded_data, + }, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + } + Ok(_) => { + logger::error!("unexpected response: this response was not expected in Retry flow"); + return Ok(()); + } + Err(error_response) => { + db.update_payment_attempt_with_attempt_id( + payment_data.payment_attempt.clone(), + storage::PaymentAttemptUpdate::ErrorUpdate { + connector: None, + error_code: Some(Some(error_response.code)), + error_message: Some(Some(error_response.message)), + status: storage_enums::AttemptStatus::Failure, + error_reason: Some(error_response.reason), + amount_capturable: Some(0), + updated_by: storage_scheme.to_string(), + }, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + } + } + + let payment_attempt = db + .insert_payment_attempt(new_payment_attempt, storage_scheme) + .await + .to_duplicate_response(errors::ApiErrorResponse::DuplicatePayment { + payment_id: payment_data.payment_intent.payment_id.clone(), + })?; + + // update payment_attempt, connector_response and payment_intent in payment_data + payment_data.payment_attempt = payment_attempt; + + payment_data.payment_intent = db + .update_payment_intent( + payment_data.payment_intent.clone(), + storage::PaymentIntentUpdate::PaymentAttemptAndAttemptCountUpdate { + active_attempt_id: payment_data.payment_attempt.attempt_id.clone(), + attempt_count: new_attempt_count, + updated_by: storage_scheme.to_string(), + }, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + Ok(()) +} + +#[instrument(skip_all)] +pub fn make_new_payment_attempt( + connector: String, + old_payment_attempt: storage::PaymentAttempt, + new_attempt_count: i16, + is_step_up: bool, +) -> storage::PaymentAttemptNew { + let created_at @ modified_at @ last_synced = Some(common_utils::date_time::now()); + storage::PaymentAttemptNew { + connector: Some(connector), + attempt_id: utils::get_payment_attempt_id( + &old_payment_attempt.payment_id, + new_attempt_count, + ), + payment_id: old_payment_attempt.payment_id, + merchant_id: old_payment_attempt.merchant_id, + status: old_payment_attempt.status, + amount: old_payment_attempt.amount, + currency: old_payment_attempt.currency, + save_to_locker: old_payment_attempt.save_to_locker, + + offer_amount: old_payment_attempt.offer_amount, + surcharge_amount: old_payment_attempt.surcharge_amount, + tax_amount: old_payment_attempt.tax_amount, + payment_method_id: old_payment_attempt.payment_method_id, + payment_method: old_payment_attempt.payment_method, + payment_method_type: old_payment_attempt.payment_method_type, + capture_method: old_payment_attempt.capture_method, + capture_on: old_payment_attempt.capture_on, + confirm: old_payment_attempt.confirm, + authentication_type: if is_step_up { + Some(storage_enums::AuthenticationType::ThreeDs) + } else { + old_payment_attempt.authentication_type + }, + + amount_to_capture: old_payment_attempt.amount_to_capture, + mandate_id: old_payment_attempt.mandate_id, + browser_info: old_payment_attempt.browser_info, + payment_token: old_payment_attempt.payment_token, + + created_at, + modified_at, + last_synced, + ..storage::PaymentAttemptNew::default() + } +} + +pub async fn config_should_call_gsm(db: &dyn StorageInterface, merchant_id: &String) -> bool { + let config = db + .find_config_by_key_unwrap_or( + format!("should_call_gsm_{}", merchant_id).as_str(), + Some("false".to_string()), + ) + .await; + match config { + Ok(conf) => conf.config == "true", + Err(err) => { + logger::error!("{err}"); + false + } + } +} + +pub trait GsmValidation { + // TODO : move this function to appropriate place later. + fn should_call_gsm(&self) -> bool; +} + +impl + GsmValidation + for types::RouterData +{ + #[inline(always)] + fn should_call_gsm(&self) -> bool { + if self.response.is_err() { + true + } else { + match self.status { + storage_enums::AttemptStatus::Started + | storage_enums::AttemptStatus::AuthenticationPending + | storage_enums::AttemptStatus::AuthenticationSuccessful + | storage_enums::AttemptStatus::Authorized + | storage_enums::AttemptStatus::Charged + | storage_enums::AttemptStatus::Authorizing + | storage_enums::AttemptStatus::CodInitiated + | storage_enums::AttemptStatus::Voided + | storage_enums::AttemptStatus::VoidInitiated + | storage_enums::AttemptStatus::CaptureInitiated + | storage_enums::AttemptStatus::RouterDeclined + | storage_enums::AttemptStatus::VoidFailed + | storage_enums::AttemptStatus::AutoRefunded + | storage_enums::AttemptStatus::CaptureFailed + | storage_enums::AttemptStatus::PartialCharged + | storage_enums::AttemptStatus::Pending + | storage_enums::AttemptStatus::PaymentMethodAwaited + | storage_enums::AttemptStatus::ConfirmationAwaited + | storage_enums::AttemptStatus::Unresolved + | storage_enums::AttemptStatus::DeviceDataCollectionPending => false, + + storage_enums::AttemptStatus::AuthenticationFailed + | storage_enums::AttemptStatus::AuthorizationFailed + | storage_enums::AttemptStatus::Failure => true, + } + } + } +} diff --git a/crates/router/src/routes/metrics.rs b/crates/router/src/routes/metrics.rs index 34d818eaa392..a8e6f9d2a892 100644 --- a/crates/router/src/routes/metrics.rs +++ b/crates/router/src/routes/metrics.rs @@ -102,5 +102,13 @@ counter_metric!(APPLE_PAY_SIMPLIFIED_FLOW_SUCCESSFUL_PAYMENT, GLOBAL_METER); counter_metric!(APPLE_PAY_MANUAL_FLOW_FAILED_PAYMENT, GLOBAL_METER); counter_metric!(APPLE_PAY_SIMPLIFIED_FLOW_FAILED_PAYMENT, GLOBAL_METER); +// Metrics for Auto Retries +counter_metric!(AUTO_RETRY_ELIGIBLE_REQUEST_COUNT, GLOBAL_METER); +counter_metric!(AUTO_RETRY_GSM_MISS_COUNT, GLOBAL_METER); +counter_metric!(AUTO_RETRY_GSM_FETCH_FAILURE_COUNT, GLOBAL_METER); +counter_metric!(AUTO_RETRY_GSM_MATCH_COUNT, GLOBAL_METER); +counter_metric!(AUTO_RETRY_EXHAUSTED_COUNT, GLOBAL_METER); +counter_metric!(AUTO_RETRY_PAYMENT_COUNT, GLOBAL_METER); + pub mod request; pub mod utils; From 856c7af77e17599ca0d4d119744ac582e9c3c971 Mon Sep 17 00:00:00 2001 From: Kashif <46213975+kashif-m@users.noreply.github.com> Date: Tue, 14 Nov 2023 15:22:50 +0530 Subject: [PATCH 008/146] feat: Payment link status page UI (#2740) Co-authored-by: Kashif Co-authored-by: Sahkal Poddar Co-authored-by: Sahkal Poddar --- .../src/core/payment_link/payment_link.html | 685 ++++++++++-------- 1 file changed, 364 insertions(+), 321 deletions(-) diff --git a/crates/router/src/core/payment_link/payment_link.html b/crates/router/src/core/payment_link/payment_link.html index 67410cac8418..e02bc16e7197 100644 --- a/crates/router/src/core/payment_link/payment_link.html +++ b/crates/router/src/core/payment_link/payment_link.html @@ -1,6 +1,8 @@ + + {{ hyperloader_sdk_link }} @@ -545,22 +599,144 @@ rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700;800" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
-
+ - -
+ @@ -817,15 +870,22 @@ }; var widgets = null; + var unifiedCheckout = null; const pub_key = window.__PAYMENT_DETAILS.pub_key; const hyper = Hyper(pub_key); + function mountUnifiedCheckout(id) { + if (unifiedCheckout !== null) { + unifiedCheckout.mount(id); + } + } + async function initialize() { const paymentDetails = window.__PAYMENT_DETAILS; var client_secret = paymentDetails.client_secret; const appearance = { variables: { - colorPrimary: paymentDetails.sdk_theme, + colorPrimary: paymentDetails.sdk_theme || "rgb(0, 109, 249)", fontFamily: "Work Sans, sans-serif", fontSizeBase: "16px", colorText: "rgb(51, 65, 85)", @@ -856,11 +916,8 @@ }, }; - const unifiedCheckout = widgets.create( - "payment", - unifiedCheckoutOptions - ); - unifiedCheckout.mount("#unified-checkout"); + unifiedCheckout = widgets.create("payment", unifiedCheckoutOptions); + mountUnifiedCheckout("#unified-checkout"); // Handle button press callback var paymentElement = widgets.getElement("payment"); @@ -890,6 +947,9 @@ } else { showMessage("An unexpected error occurred."); } + + // Re-initialize SDK + mountUnifiedCheckout("#unified-checkout"); } else { const { paymentIntent } = await hyper.retrievePaymentIntent( paymentDetails.client_secret @@ -906,13 +966,19 @@ // Fetches the payment status after payment submission async function checkStatus() { - const clientSecret = new URLSearchParams(window.location.search).get( - "payment_intent_client_secret" - ); + const paymentDetails = window.__PAYMENT_DETAILS; const res = { showSdk: true, }; + let clientSecret = new URLSearchParams(window.location.search).get( + "payment_intent_client_secret" + ); + + if (!clientSecret) { + clientSecret = paymentDetails.client_secret; + } + if (!clientSecret) { return res; } @@ -921,7 +987,10 @@ clientSecret ); - if (!paymentIntent || !paymentIntent.status) { + if ( + !paymentIntent || + paymentIntent.status === "requires_confirmation" + ) { return res; } @@ -950,101 +1019,68 @@ show("#payment-message"); addText("#payment-message", msg); } + function showStatus(paymentDetails) { const status = paymentDetails.status; let statusDetails = { imageSource: "", - message: "", + message: null, status: status, amountText: "", items: [], }; + // Payment details + var paymentId = createItem("Ref Id", paymentDetails.payment_id); + // @ts-ignore + statusDetails.items.push(paymentId); + + // Status specific information switch (status) { case "succeeded": - statusDetails.imageSource = - "http://www.clipartbest.com/cliparts/4ib/oRa/4iboRa7RT.png"; - statusDetails.message = "Payment successful"; - statusDetails.status = "Succeeded"; + statusDetails.imageSource = "https://i.imgur.com/5BOmYVl.img"; + statusDetails.message = + "We have successfully received your payment"; + statusDetails.status = "Paid successfully"; statusDetails.amountText = new Date( paymentDetails.created ).toTimeString(); - - // Payment details - var amountNode = createItem( - "AMOUNT PAID", - paymentDetails.currency + " " + paymentDetails.amount - ); - var paymentId = createItem("PAYMENT ID", paymentDetails.payment_id); - // @ts-ignore - statusDetails.items.push(amountNode, paymentId); break; case "processing": - statusDetails.imageSource = - "http://www.clipartbest.com/cliparts/4ib/oRa/4iboRa7RT.png"; - statusDetails.message = "Payment in progress"; - statusDetails.status = "Processing"; - // Payment details - var amountNode = createItem( - "AMOUNT PAID", - paymentDetails.currency + " " + paymentDetails.amount - ); - var paymentId = createItem("PAYMENT ID", paymentDetails.payment_id); - // @ts-ignore - statusDetails.items.push(amountNode, paymentId); + statusDetails.imageSource = "https://i.imgur.com/Yb79Qt4.png"; + statusDetails.message = + "Sorry! Your payment is taking longer than expected. Please check back again in sometime."; + statusDetails.status = "Payment Pending"; break; case "failed": - statusDetails.imageSource = ""; - statusDetails.message = "Payment failed"; - statusDetails.status = "Failed"; - // Payment details - var amountNode = createItem( - "AMOUNT PAID", - paymentDetails.currency + " " + paymentDetails.amount + statusDetails.imageSource = "https://i.imgur.com/UD8CEuY.png"; + statusDetails.status = "Payment Failed!"; + var errorCodeNode = createItem( + "Error code", + paymentDetails.error_code + ); + var errorMessageNode = createItem( + "Error message", + paymentDetails.error_message ); - var paymentId = createItem("PAYMENT ID", paymentDetails.payment_id); // @ts-ignore - statusDetails.items.push(amountNode, paymentId); + statusDetails.items.push(errorMessageNode, errorCodeNode); break; case "cancelled": - statusDetails.imageSource = ""; - statusDetails.message = "Payment cancelled"; - statusDetails.status = "Cancelled"; - // Payment details - var amountNode = createItem( - "AMOUNT PAID", - paymentDetails.currency + " " + paymentDetails.amount - ); - var paymentId = createItem("PAYMENT ID", paymentDetails.payment_id); - // @ts-ignore - statusDetails.items.push(amountNode, paymentId); + statusDetails.imageSource = "https://i.imgur.com/UD8CEuY.png"; + statusDetails.status = "Payment Cancelled"; break; case "requires_merchant_action": - statusDetails.imageSource = ""; - statusDetails.message = "Payment under review"; - statusDetails.status = "Under review"; - // Payment details - var amountNode = createItem( - "AMOUNT PAID", - paymentDetails.currency + " " + paymentDetails.amount - ); - var paymentId = createItem("PAYMENT ID", paymentDetails.payment_id); - var paymentId = createItem( - "MESSAGE", - "Your payment is under review by the merchant." - ); - // @ts-ignore - statusDetails.items.push(amountNode, paymentId); + statusDetails.imageSource = "https://i.imgur.com/Yb79Qt4.png"; + statusDetails.status = "Payment under review"; break; default: - statusDetails.imageSource = - "http://www.clipartbest.com/cliparts/4ib/oRa/4iboRa7RT.png"; - statusDetails.message = "Something went wrong"; + statusDetails.imageSource = "https://i.imgur.com/UD8CEuY.png"; statusDetails.status = "Something went wrong"; // Error details if (typeof paymentDetails.error === "object") { @@ -1062,36 +1098,52 @@ break; } - // Append status - var statusTextNode = document.getElementById("status-text"); - if (statusTextNode !== null) { - statusTextNode.innerText = statusDetails.message; - } - - // Append image - var statusImageNode = document.getElementById("status-img"); - if (statusImageNode !== null) { - statusImageNode.src = statusDetails.imageSource; - } - - // Append status details - var statusDateNode = document.getElementById("status-date"); - if (statusDateNode !== null) { - statusDateNode.innerText = statusDetails.amountText; - } + // Form header items + var amountNode = document.createElement("div"); + amountNode.className = "hyper-checkout-status-amount"; + amountNode.innerText = + paymentDetails.currency + " " + paymentDetails.amount; + var merchantLogoNode = document.createElement("img"); + merchantLogoNode.className = "hyper-checkout-status-merchant-logo"; + merchantLogoNode.src = ""; + merchantLogoNode.alt = ""; + + // Form content items + var statusImageNode = document.createElement("img"); + statusImageNode.className = "hyper-checkout-status-image"; + statusImageNode.src = statusDetails.imageSource; + var statusTextNode = document.createElement("div"); + statusTextNode.className = "hyper-checkout-status-text"; + statusTextNode.innerText = statusDetails.status; + var statusMessageNode = document.createElement("div"); + statusMessageNode.className = "hyper-checkout-status-message"; + statusMessageNode.innerText = statusDetails.message; + var statusDetailsNode = document.createElement("div"); + statusDetailsNode.className = "hyper-checkout-status-details"; // Append items - var statusItemNode = document.getElementById( - "hyper-checkout-status-items" + statusDetails.items.map((item) => statusDetailsNode?.append(item)); + const statusHeaderNode = document.getElementById( + "hyper-checkout-status-header" ); - if (statusItemNode !== null) { - statusDetails.items.map((item) => statusItemNode?.append(item)); + if (statusHeaderNode !== null) { + statusHeaderNode.append(amountNode, merchantLogoNode); + } + const statusContentNode = document.getElementById( + "hyper-checkout-status-content" + ); + if (statusContentNode !== null) { + statusContentNode.append(statusImageNode, statusTextNode); + if (statusDetails.message !== null) { + statusContentNode.append(statusMessageNode); + } + statusContentNode.append(statusDetailsNode); } } function createItem(heading, value) { var itemNode = document.createElement("div"); - itemNode.className = "hyper-checkout-item"; + itemNode.className = "hyper-checkout-status-item"; var headerNode = document.createElement("div"); headerNode.className = "hyper-checkout-item-header"; headerNode.innerText = heading; @@ -1238,8 +1290,7 @@ // Product price var priceNode = document.createElement("div"); priceNode.className = "hyper-checkout-card-item-price"; - priceNode.innerText = - paymentDetails.currency + " " + item.amount; + priceNode.innerText = paymentDetails.currency + " " + item.amount; // Append items nameAndQuantityWrapperNode.append(productNameNode, quantityNode); itemWrapperNode.append( @@ -1336,16 +1387,6 @@ show("#hyper-checkout-cart"); } - function hideCartInMobileView() { - window.history.back(); - hide("#hyper-checkout-cart"); - } - - function viewCartInMobileView() { - show("#hyper-checkout-cart"); - window.history.pushState("view-cart", ""); - } - function renderSDKHeader() { const paymentDetails = window.__PAYMENT_DETAILS; @@ -1389,6 +1430,8 @@ show("#hyper-checkout-sdk"); show("#hyper-checkout-details"); } else { + hide("#hyper-checkout-sdk"); + hide("#hyper-checkout-details"); show("#hyper-checkout-status"); show("#hyper-footer"); } From cafea45982d7b520fe68fde967984ce88f68c6c0 Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Tue, 14 Nov 2023 16:11:38 +0530 Subject: [PATCH 009/146] fix: handle session and confirm flow discrepancy in surcharge details (#2696) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- Cargo.lock | 5 +- crates/api_models/Cargo.toml | 1 - crates/api_models/src/payment_methods.rs | 83 +++++++++++- crates/api_models/src/payments.rs | 48 +++++++ .../src/payments/payment_attempt.rs | 8 +- crates/diesel_models/src/payment_attempt.rs | 24 +++- crates/redis_interface/src/commands.rs | 5 +- .../router/src/core/payment_methods/cards.rs | 2 + crates/router/src/core/payments.rs | 39 ++++-- .../payments/operations/payment_confirm.rs | 128 ++++++++++++++---- .../payments/operations/payment_create.rs | 28 +++- .../payments/operations/payment_response.rs | 2 + .../payments/operations/payment_update.rs | 16 ++- crates/router/src/core/payments/retry.rs | 2 + crates/router/src/core/utils.rs | 74 +++++++++- crates/router/src/services/api.rs | 6 +- crates/router/src/types.rs | 22 ++- crates/router/src/types/api.rs | 25 ++++ .../src/payments/payment_attempt.rs | 32 +++-- crates/storage_impl/src/redis/kv_store.rs | 4 +- 20 files changed, 477 insertions(+), 77 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 222bc02212ec..1574933810b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -381,7 +381,6 @@ dependencies = [ "router_derive", "serde", "serde_json", - "serde_with", "strum 0.24.1", "time", "url", @@ -1659,9 +1658,9 @@ dependencies = [ [[package]] name = "crc-catalog" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc16" diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index ac624c899c6d..ce882e913282 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -25,7 +25,6 @@ mime = "0.3.17" reqwest = { version = "0.11.18", optional = true } serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.96" -serde_with = "3.0.0" strum = { version = "0.24.1", features = ["derive"] } time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } url = { version = "2.4.0", features = ["serde"] } diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 289f652981eb..755acbf7f425 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -6,7 +6,6 @@ use common_utils::{ types::Percentage, }; use serde::de; -use serde_with::serde_as; use utoipa::ToSchema; #[cfg(feature = "payouts")] @@ -15,7 +14,7 @@ use crate::{ admin, customers::CustomerId, enums as api_enums, - payments::{self, BankCodeResponse}, + payments::{self, BankCodeResponse, RequestSurchargeDetails}, }; #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] @@ -342,15 +341,85 @@ pub struct SurchargeDetailsResponse { pub final_amount: i64, } -#[serde_as] -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +impl SurchargeDetailsResponse { + pub fn is_request_surcharge_matching( + &self, + request_surcharge_details: RequestSurchargeDetails, + ) -> bool { + request_surcharge_details.surcharge_amount == self.surcharge_amount + && request_surcharge_details.tax_amount.unwrap_or(0) == self.tax_on_surcharge_amount + } +} + +#[derive(Clone, Debug)] pub struct SurchargeMetadata { - #[serde_as(as = "HashMap<_, _>")] - pub surcharge_results: HashMap, + surcharge_results: HashMap< + ( + common_enums::PaymentMethod, + common_enums::PaymentMethodType, + Option, + ), + SurchargeDetailsResponse, + >, + pub payment_attempt_id: String, } impl SurchargeMetadata { - pub fn get_key_for_surcharge_details_hash_map( + pub fn new(payment_attempt_id: String) -> Self { + Self { + surcharge_results: HashMap::new(), + payment_attempt_id, + } + } + pub fn is_empty_result(&self) -> bool { + self.surcharge_results.is_empty() + } + pub fn get_surcharge_results_size(&self) -> usize { + self.surcharge_results.len() + } + pub fn insert_surcharge_details( + &mut self, + payment_method: &common_enums::PaymentMethod, + payment_method_type: &common_enums::PaymentMethodType, + card_network: Option<&common_enums::CardNetwork>, + surcharge_details: SurchargeDetailsResponse, + ) { + let key = ( + payment_method.to_owned(), + payment_method_type.to_owned(), + card_network.cloned(), + ); + self.surcharge_results.insert(key, surcharge_details); + } + pub fn get_surcharge_details( + &self, + payment_method: &common_enums::PaymentMethod, + payment_method_type: &common_enums::PaymentMethodType, + card_network: Option<&common_enums::CardNetwork>, + ) -> Option<&SurchargeDetailsResponse> { + let key = &( + payment_method.to_owned(), + payment_method_type.to_owned(), + card_network.cloned(), + ); + self.surcharge_results.get(key) + } + pub fn get_surcharge_metadata_redis_key(payment_attempt_id: &str) -> String { + format!("surcharge_metadata_{}", payment_attempt_id) + } + pub fn get_individual_surcharge_key_value_pairs( + &self, + ) -> Vec<(String, SurchargeDetailsResponse)> { + self.surcharge_results + .iter() + .map(|((pm, pmt, card_network), surcharge_details)| { + let key = + Self::get_surcharge_details_redis_hashset_key(pm, pmt, card_network.as_ref()); + (key, surcharge_details.to_owned()) + }) + .collect() + } + pub fn get_surcharge_details_redis_hashset_key( payment_method: &common_enums::PaymentMethod, payment_method_type: &common_enums::PaymentMethodType, card_network: Option<&common_enums::CardNetwork>, diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 22579ed6d6ea..cf0259f26951 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -16,6 +16,7 @@ use crate::{ admin, disputes, enums::{self as api_enums}, ephemeral_key::EphemeralKeyCreateResponse, + payment_methods::{Surcharge, SurchargeDetailsResponse}, refunds, }; @@ -319,6 +320,23 @@ pub struct RequestSurchargeDetails { pub tax_amount: Option, } +impl RequestSurchargeDetails { + pub fn is_surcharge_zero(&self) -> bool { + self.surcharge_amount == 0 && self.tax_amount.unwrap_or(0) == 0 + } + pub fn get_surcharge_details_object(&self, original_amount: i64) -> SurchargeDetailsResponse { + let surcharge_amount = self.surcharge_amount; + let tax_on_surcharge_amount = self.tax_amount.unwrap_or(0); + SurchargeDetailsResponse { + surcharge: Surcharge::Fixed(self.surcharge_amount), + tax_on_surcharge: None, + surcharge_amount, + tax_on_surcharge_amount, + final_amount: original_amount + surcharge_amount + tax_on_surcharge_amount, + } + } +} + #[derive(Default, Debug, Clone, Copy)] pub struct HeaderPayload { pub payment_confirm_source: Option, @@ -810,6 +828,36 @@ pub enum PaymentMethodData { GiftCard(Box), } +impl PaymentMethodData { + pub fn get_payment_method_type_if_session_token_type( + &self, + ) -> Option { + match self { + Self::Wallet(wallet) => match wallet { + WalletData::ApplePay(_) => Some(api_enums::PaymentMethodType::ApplePay), + WalletData::GooglePay(_) => Some(api_enums::PaymentMethodType::GooglePay), + WalletData::PaypalSdk(_) => Some(api_enums::PaymentMethodType::Paypal), + _ => None, + }, + Self::PayLater(pay_later) => match pay_later { + PayLaterData::KlarnaSdk { .. } => Some(api_enums::PaymentMethodType::Klarna), + _ => None, + }, + Self::Card(_) + | Self::CardRedirect(_) + | Self::BankRedirect(_) + | Self::BankDebit(_) + | Self::BankTransfer(_) + | Self::Crypto(_) + | Self::MandatePayment + | Self::Reward + | Self::Upi(_) + | Self::Voucher(_) + | Self::GiftCard(_) => None, + } + } +} + pub trait GetPaymentMethodType { fn get_payment_method_type(&self) -> api_enums::PaymentMethodType; } diff --git a/crates/data_models/src/payments/payment_attempt.rs b/crates/data_models/src/payments/payment_attempt.rs index cdd41ea9db2d..88fc7b3b524a 100644 --- a/crates/data_models/src/payments/payment_attempt.rs +++ b/crates/data_models/src/payments/payment_attempt.rs @@ -224,6 +224,8 @@ pub enum PaymentAttemptUpdate { business_sub_label: Option, amount_to_capture: Option, capture_method: Option, + surcharge_amount: Option, + tax_amount: Option, updated_by: String, }, UpdateTrackers { @@ -231,6 +233,8 @@ pub enum PaymentAttemptUpdate { connector: Option, straight_through_algorithm: Option, amount_capturable: Option, + surcharge_amount: Option, + tax_amount: Option, updated_by: String, merchant_connector_id: Option, }, @@ -255,8 +259,6 @@ pub enum PaymentAttemptUpdate { error_code: Option>, error_message: Option>, amount_capturable: Option, - surcharge_amount: Option, - tax_amount: Option, updated_by: String, merchant_connector_id: Option, }, @@ -285,6 +287,8 @@ pub enum PaymentAttemptUpdate { error_reason: Option>, connector_response_reference_id: Option, amount_capturable: Option, + surcharge_amount: Option, + tax_amount: Option, updated_by: String, authentication_data: Option, encoded_data: Option, diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index ce388fea10eb..cd976b9e19db 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -141,6 +141,8 @@ pub enum PaymentAttemptUpdate { business_sub_label: Option, amount_to_capture: Option, capture_method: Option, + surcharge_amount: Option, + tax_amount: Option, updated_by: String, }, UpdateTrackers { @@ -148,6 +150,8 @@ pub enum PaymentAttemptUpdate { connector: Option, straight_through_algorithm: Option, amount_capturable: Option, + surcharge_amount: Option, + tax_amount: Option, updated_by: String, merchant_connector_id: Option, }, @@ -172,8 +176,6 @@ pub enum PaymentAttemptUpdate { error_code: Option>, error_message: Option>, amount_capturable: Option, - surcharge_amount: Option, - tax_amount: Option, updated_by: String, merchant_connector_id: Option, }, @@ -202,6 +204,8 @@ pub enum PaymentAttemptUpdate { error_reason: Option>, connector_response_reference_id: Option, amount_capturable: Option, + surcharge_amount: Option, + tax_amount: Option, updated_by: String, authentication_data: Option, encoded_data: Option, @@ -370,6 +374,8 @@ impl From for PaymentAttemptUpdateInternal { business_sub_label, amount_to_capture, capture_method, + surcharge_amount, + tax_amount, updated_by, } => Self { amount: Some(amount), @@ -386,6 +392,8 @@ impl From for PaymentAttemptUpdateInternal { business_sub_label, amount_to_capture, capture_method, + surcharge_amount, + tax_amount, updated_by, ..Default::default() }, @@ -415,8 +423,6 @@ impl From for PaymentAttemptUpdateInternal { error_code, error_message, amount_capturable, - surcharge_amount, - tax_amount, updated_by, merchant_connector_id, } => Self { @@ -437,8 +443,6 @@ impl From for PaymentAttemptUpdateInternal { error_code, error_message, amount_capturable, - surcharge_amount, - tax_amount, updated_by, merchant_connector_id, ..Default::default() @@ -479,6 +483,8 @@ impl From for PaymentAttemptUpdateInternal { error_reason, connector_response_reference_id, amount_capturable, + surcharge_amount, + tax_amount, updated_by, authentication_data, encoded_data, @@ -498,6 +504,8 @@ impl From for PaymentAttemptUpdateInternal { connector_response_reference_id, amount_capturable, updated_by, + surcharge_amount, + tax_amount, authentication_data, encoded_data, ..Default::default() @@ -531,6 +539,8 @@ impl From for PaymentAttemptUpdateInternal { connector, straight_through_algorithm, amount_capturable, + surcharge_amount, + tax_amount, updated_by, merchant_connector_id, } => Self { @@ -538,6 +548,8 @@ impl From for PaymentAttemptUpdateInternal { connector, straight_through_algorithm, amount_capturable, + surcharge_amount, + tax_amount, updated_by, merchant_connector_id, ..Default::default() diff --git a/crates/redis_interface/src/commands.rs b/crates/redis_interface/src/commands.rs index d53fd1625fe4..ca85d19d38b0 100644 --- a/crates/redis_interface/src/commands.rs +++ b/crates/redis_interface/src/commands.rs @@ -248,7 +248,7 @@ impl super::RedisConnectionPool { &self, key: &str, values: V, - ttl: Option, + ttl: Option, ) -> CustomResult<(), errors::RedisError> where V: TryInto + Debug + Send + Sync, @@ -260,11 +260,10 @@ impl super::RedisConnectionPool { .await .into_report() .change_context(errors::RedisError::SetHashFailed); - // setting expiry for the key output .async_and_then(|_| { - self.set_expiry(key, ttl.unwrap_or(self.config.default_hash_ttl).into()) + self.set_expiry(key, ttl.unwrap_or(self.config.default_hash_ttl.into())) }) .await } diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 234323f0179a..6b3cf11f5891 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -1052,6 +1052,8 @@ pub async fn list_payment_methods( amount_capturable: None, updated_by: merchant_account.storage_scheme.to_string(), merchant_connector_id: None, + surcharge_amount: None, + tax_amount: None, }; state diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 5c8089271bd9..e7408cecf163 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -14,7 +14,7 @@ use std::{fmt::Debug, marker::PhantomData, ops::Deref, time::Instant, vec::IntoI use api_models::{ enums, - payment_methods::{SurchargeDetailsResponse, SurchargeMetadata}, + payment_methods::{Surcharge, SurchargeDetailsResponse}, payments::HeaderPayload, }; use common_utils::{ext_traits::AsyncExt, pii}; @@ -290,6 +290,8 @@ where } api::ConnectorCallType::SessionMultiple(connectors) => { + let session_surcharge_data = + get_session_surcharge_data(&payment_data.payment_attempt); call_multiple_connectors_service( state, &merchant_account, @@ -298,7 +300,7 @@ where &operation, payment_data, &customer, - None, + session_surcharge_data, ) .await? } @@ -353,6 +355,21 @@ pub fn get_connector_data( .attach_printable("Connector not found in connectors iterator") } +pub fn get_session_surcharge_data( + payment_attempt: &data_models::payments::payment_attempt::PaymentAttempt, +) -> Option { + payment_attempt.surcharge_amount.map(|surcharge_amount| { + let tax_on_surcharge_amount = payment_attempt.tax_amount.unwrap_or(0); + let final_amount = payment_attempt.amount + surcharge_amount + tax_on_surcharge_amount; + api::SessionSurchargeDetails::PreDetermined(SurchargeDetailsResponse { + surcharge: Surcharge::Fixed(surcharge_amount), + tax_on_surcharge: None, + surcharge_amount, + tax_on_surcharge_amount, + final_amount, + }) + }) +} #[allow(clippy::too_many_arguments)] pub async fn payments_core( state: AppState, @@ -920,7 +937,7 @@ pub async fn call_multiple_connectors_service( _operation: &Op, mut payment_data: PaymentData, customer: &Option, - session_surcharge_metadata: Option, + session_surcharge_details: Option, ) -> RouterResult> where Op: Debug, @@ -957,18 +974,16 @@ where ) .await?; - payment_data.surcharge_details = session_surcharge_metadata - .as_ref() - .and_then(|surcharge_metadata| { - surcharge_metadata.surcharge_results.get( - &SurchargeMetadata::get_key_for_surcharge_details_hash_map( + payment_data.surcharge_details = + session_surcharge_details + .as_ref() + .and_then(|session_surcharge_details| { + session_surcharge_details.fetch_surcharge_details( &session_connector_data.payment_method_type.into(), &session_connector_data.payment_method_type, None, - ), - ) - }) - .cloned(); + ) + }); let router_data = payment_data .construct_router_data( diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 21f7db3d0b41..96cd4f5c622f 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -1,10 +1,14 @@ use std::marker::PhantomData; -use api_models::{enums::FrmSuggestion, payment_methods}; +use api_models::{ + enums::FrmSuggestion, + payment_methods::{self, SurchargeDetailsResponse}, +}; use async_trait::async_trait; use common_utils::ext_traits::{AsyncExt, Encode}; use error_stack::ResultExt; use futures::FutureExt; +use redis_interface::errors::RedisError; use router_derive::PaymentOperation; use router_env::{instrument, tracing}; @@ -14,6 +18,7 @@ use crate::{ errors::{self, CustomResult, RouterResult, StorageErrorExt}, payment_methods::PaymentMethodRetrieve, payments::{self, helpers, operations, CustomerDetails, PaymentAddress, PaymentData}, + utils::get_individual_surcharge_detail_from_redis, }, db::StorageInterface, routes::AppState, @@ -305,19 +310,17 @@ impl sm.mandate_type = payment_attempt.mandate_details.clone().or(sm.mandate_type); sm }); + Self::validate_request_surcharge_details_with_session_surcharge_details( + state, + &payment_attempt, + request, + ) + .await?; - // populate payment_data.surcharge_details from request - let surcharge_details = request.surcharge_details.map(|surcharge_details| { - payment_methods::SurchargeDetailsResponse { - surcharge: payment_methods::Surcharge::Fixed(surcharge_details.surcharge_amount), - tax_on_surcharge: None, - surcharge_amount: surcharge_details.surcharge_amount, - tax_on_surcharge_amount: surcharge_details.tax_amount.unwrap_or(0), - final_amount: payment_attempt.amount - + surcharge_details.surcharge_amount - + surcharge_details.tax_amount.unwrap_or(0), - } - }); + let surcharge_details = Self::get_surcharge_details_from_payment_request_or_payment_attempt( + request, + &payment_attempt, + ); Ok(( Box::new(self), @@ -529,14 +532,6 @@ impl .take(); let order_details = payment_data.payment_intent.order_details.clone(); let metadata = payment_data.payment_intent.metadata.clone(); - let surcharge_amount = payment_data - .surcharge_details - .as_ref() - .map(|surcharge_details| surcharge_details.surcharge_amount); - let tax_amount = payment_data - .surcharge_details - .as_ref() - .map(|surcharge_details| surcharge_details.tax_on_surcharge_amount); let authorized_amount = payment_data .surcharge_details .as_ref() @@ -562,8 +557,6 @@ impl error_code, error_message, amount_capturable: Some(authorized_amount), - surcharge_amount, - tax_amount, updated_by: storage_scheme.to_string(), merchant_connector_id, }, @@ -672,3 +665,92 @@ impl ValidateRequest RouterResult<()> { + match ( + request.surcharge_details, + request.payment_method_data.as_ref(), + ) { + (Some(request_surcharge_details), Some(payment_method_data)) => { + if let Some(payment_method_type) = + payment_method_data.get_payment_method_type_if_session_token_type() + { + let invalid_surcharge_details_error = Err(errors::ApiErrorResponse::InvalidRequestData { + message: "surcharge_details sent in session token flow doesn't match with the one sent in confirm request".into(), + }.into()); + if let Some(attempt_surcharge_amount) = payment_attempt.surcharge_amount { + // payment_attempt.surcharge_amount will be Some if some surcharge was sent in payment create + // if surcharge was sent in payment create call, the same would have been sent to the connector during session call + // So verify the same + if request_surcharge_details.surcharge_amount != attempt_surcharge_amount + || request_surcharge_details.tax_amount != payment_attempt.tax_amount + { + return invalid_surcharge_details_error; + } + } else { + // if not sent in payment create + // verify that any calculated surcharge sent in session flow is same as the one sent in confirm + return match get_individual_surcharge_detail_from_redis( + state, + &payment_method_type.into(), + &payment_method_type, + None, + &payment_attempt.attempt_id, + ) + .await + { + Ok(surcharge_details) => utils::when( + !surcharge_details + .is_request_surcharge_matching(request_surcharge_details), + || invalid_surcharge_details_error, + ), + Err(err) if err.current_context() == &RedisError::NotFound => { + utils::when(!request_surcharge_details.is_surcharge_zero(), || { + invalid_surcharge_details_error + }) + } + Err(err) => Err(err) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to fetch redis value"), + }; + } + } + Ok(()) + } + (Some(_request_surcharge_details), None) => { + Err(errors::ApiErrorResponse::MissingRequiredField { + field_name: "payment_method_data", + } + .into()) + } + _ => Ok(()), + } + } + + fn get_surcharge_details_from_payment_request_or_payment_attempt( + payment_request: &api::PaymentsRequest, + payment_attempt: &storage::PaymentAttempt, + ) -> Option { + payment_request + .surcharge_details + .map(|surcharge_details| { + surcharge_details.get_surcharge_details_object(payment_attempt.amount) + }) // if not passed in confirm request, look inside payment_attempt + .or(payment_attempt + .surcharge_amount + .map(|surcharge_amount| SurchargeDetailsResponse { + surcharge: payment_methods::Surcharge::Fixed(surcharge_amount), + tax_on_surcharge: None, + surcharge_amount, + tax_on_surcharge_amount: payment_attempt.tax_amount.unwrap_or(0), + final_amount: payment_attempt.amount + + surcharge_amount + + payment_attempt.tax_amount.unwrap_or(0), + })) + } +} diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 97bb84371306..fad7212c61d3 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use api_models::enums::FrmSuggestion; +use api_models::{enums::FrmSuggestion, payment_methods}; use async_trait::async_trait; use common_utils::ext_traits::{AsyncExt, Encode, ValueExt}; use data_models::{mandates::MandateData, payments::payment_attempt::PaymentAttempt}; @@ -267,6 +267,19 @@ impl // The operation merges mandate data from both request and payment_attempt let setup_mandate: Option = setup_mandate.map(Into::into); + // populate payment_data.surcharge_details from request + let surcharge_details = request.surcharge_details.map(|surcharge_details| { + payment_methods::SurchargeDetailsResponse { + surcharge: payment_methods::Surcharge::Fixed(surcharge_details.surcharge_amount), + tax_on_surcharge: None, + surcharge_amount: surcharge_details.surcharge_amount, + tax_on_surcharge_amount: surcharge_details.tax_amount.unwrap_or(0), + final_amount: payment_attempt.amount + + surcharge_details.surcharge_amount + + surcharge_details.tax_amount.unwrap_or(0), + } + }); + Ok(( operation, PaymentData { @@ -299,7 +312,7 @@ impl ephemeral_key, multiple_capture_data: None, redirect_response: None, - surcharge_details: None, + surcharge_details, frm_message: None, payment_link_data, }, @@ -421,6 +434,15 @@ impl let authorized_amount = payment_data.payment_attempt.amount; let merchant_connector_id = payment_data.payment_attempt.merchant_connector_id.clone(); + let surcharge_amount = payment_data + .surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.surcharge_amount); + let tax_amount = payment_data + .surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.tax_on_surcharge_amount); + payment_data.payment_attempt = db .update_payment_attempt_with_attempt_id( payment_data.payment_attempt, @@ -432,6 +454,8 @@ impl true => Some(authorized_amount), false => None, }, + surcharge_amount, + tax_amount, updated_by: storage_scheme.to_string(), merchant_connector_id, }, diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 77c344949660..d6346a512ef1 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -466,6 +466,8 @@ async fn payment_response_update_tracker( } else { None }, + surcharge_amount: router_data.request.get_surcharge_amount(), + tax_amount: router_data.request.get_tax_on_surcharge_amount(), updated_by: storage_scheme.to_string(), authentication_data, encoded_data, diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 0a49c830b732..26bda6d6bee6 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -304,6 +304,10 @@ impl // The operation merges mandate data from both request and payment_attempt let setup_mandate = setup_mandate.map(Into::into); + let surcharge_details = request.surcharge_details.map(|request_surcharge_details| { + request_surcharge_details.get_surcharge_details_object(payment_attempt.amount) + }); + Ok(( next_operation, PaymentData { @@ -336,7 +340,7 @@ impl ephemeral_key: None, multiple_capture_data: None, redirect_response: None, - surcharge_details: None, + surcharge_details, frm_message: None, payment_link_data: None, }, @@ -467,6 +471,14 @@ impl let payment_experience = payment_data.payment_attempt.payment_experience; let amount_to_capture = payment_data.payment_attempt.amount_to_capture; let capture_method = payment_data.payment_attempt.capture_method; + let surcharge_amount = payment_data + .surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.surcharge_amount); + let tax_amount = payment_data + .surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.tax_on_surcharge_amount); payment_data.payment_attempt = db .update_payment_attempt_with_attempt_id( payment_data.payment_attempt, @@ -483,6 +495,8 @@ impl business_sub_label, amount_to_capture, capture_method, + surcharge_amount, + tax_amount, updated_by: storage_scheme.to_string(), }, storage_scheme, diff --git a/crates/router/src/core/payments/retry.rs b/crates/router/src/core/payments/retry.rs index f58e9ea298f7..376b9048c856 100644 --- a/crates/router/src/core/payments/retry.rs +++ b/crates/router/src/core/payments/retry.rs @@ -412,6 +412,8 @@ where } else { None }, + surcharge_amount: None, + tax_amount: None, updated_by: storage_scheme.to_string(), authentication_data, encoded_data, diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 1eb9029ae398..fb3dc3e7d281 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -1,10 +1,18 @@ use std::{marker::PhantomData, str::FromStr}; -use api_models::enums::{DisputeStage, DisputeStatus}; +use api_models::{ + enums::{DisputeStage, DisputeStatus}, + payment_methods::{SurchargeDetailsResponse, SurchargeMetadata}, +}; #[cfg(feature = "payouts")] use common_utils::{crypto::Encryptable, pii::Email}; -use common_utils::{errors::CustomResult, ext_traits::AsyncExt}; +use common_utils::{ + errors::CustomResult, + ext_traits::{AsyncExt, Encode}, +}; use error_stack::{report, IntoReport, ResultExt}; +use euclid::enums as euclid_enums; +use redis_interface::errors::RedisError; use router_env::{instrument, tracing}; use uuid::Uuid; @@ -1073,3 +1081,65 @@ pub fn get_flow_name() -> RouterResult { .attach_printable("Flow stringify failed")? .to_string()) } + +pub async fn persist_individual_surcharge_details_in_redis( + state: &AppState, + merchant_account: &domain::MerchantAccount, + surcharge_metadata: &SurchargeMetadata, +) -> RouterResult<()> { + if !surcharge_metadata.is_empty_result() { + let redis_conn = state + .store + .get_redis_conn() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection")?; + let redis_key = SurchargeMetadata::get_surcharge_metadata_redis_key( + &surcharge_metadata.payment_attempt_id, + ); + + let mut value_list = Vec::with_capacity(surcharge_metadata.get_surcharge_results_size()); + for (key, value) in surcharge_metadata + .get_individual_surcharge_key_value_pairs() + .into_iter() + { + value_list.push(( + key, + Encode::::encode_to_string_of_json(&value) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to encode to string of json")?, + )); + } + let intent_fulfillment_time = merchant_account + .intent_fulfillment_time + .unwrap_or(consts::DEFAULT_FULFILLMENT_TIME); + redis_conn + .set_hash_fields(&redis_key, value_list, Some(intent_fulfillment_time)) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to write to redis")?; + } + Ok(()) +} + +pub async fn get_individual_surcharge_detail_from_redis( + state: &AppState, + payment_method: &euclid_enums::PaymentMethod, + payment_method_type: &euclid_enums::PaymentMethodType, + card_network: Option, + payment_attempt_id: &str, +) -> CustomResult { + let redis_conn = state + .store + .get_redis_conn() + .attach_printable("Failed to get redis connection")?; + let redis_key = SurchargeMetadata::get_surcharge_metadata_redis_key(payment_attempt_id); + let value_key = SurchargeMetadata::get_surcharge_details_redis_hashset_key( + payment_method, + payment_method_type, + card_network.as_ref(), + ); + + redis_conn + .get_hash_field_and_deserialize(&redis_key, &value_key, "SurchargeDetailsResponse") + .await +} diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 321bf909ea0c..0a8b84ffd11c 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -98,11 +98,7 @@ pub trait ConnectorValidation: ConnectorCommon { } fn validate_if_surcharge_implemented(&self) -> CustomResult<(), errors::ConnectorError> { - Err(errors::ConnectorError::NotImplemented(format!( - "Surcharge not implemented for {}", - self.id() - )) - .into()) + Err(errors::ConnectorError::NotImplemented(format!("Surcharge for {}", self.id())).into()) } } diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index f2e86a4bf335..7e9725d1a3b7 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -547,11 +547,31 @@ pub trait Capturable { fn get_capture_amount(&self) -> Option { Some(0) } + fn get_surcharge_amount(&self) -> Option { + None + } + fn get_tax_on_surcharge_amount(&self) -> Option { + None + } } impl Capturable for PaymentsAuthorizeData { fn get_capture_amount(&self) -> Option { - Some(self.amount) + let final_amount = self + .surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.final_amount); + final_amount.or(Some(self.amount)) + } + fn get_surcharge_amount(&self) -> Option { + self.surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.surcharge_amount) + } + fn get_tax_on_surcharge_amount(&self) -> Option { + self.surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.tax_on_surcharge_amount) } } diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index e815740cac48..67d2d37f4fea 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -16,6 +16,7 @@ pub mod webhooks; use std::{fmt::Debug, str::FromStr}; +use api_models::payment_methods::{SurchargeDetailsResponse, SurchargeMetadata}; use error_stack::{report, IntoReport, ResultExt}; pub use self::{ @@ -214,6 +215,30 @@ pub struct SessionConnectorData { pub business_sub_label: Option, } +/// Session Surcharge type +pub enum SessionSurchargeDetails { + /// Surcharge is calculated by hyperswitch + Calculated(SurchargeMetadata), + /// Surcharge is sent by merchant + PreDetermined(SurchargeDetailsResponse), +} + +impl SessionSurchargeDetails { + pub fn fetch_surcharge_details( + &self, + payment_method: &enums::PaymentMethod, + payment_method_type: &enums::PaymentMethodType, + card_network: Option<&enums::CardNetwork>, + ) -> Option { + match self { + Self::Calculated(surcharge_metadata) => surcharge_metadata + .get_surcharge_details(payment_method, payment_method_type, card_network) + .cloned(), + Self::PreDetermined(surcharge_details) => Some(surcharge_details.clone()), + } + } +} + pub enum ConnectorChoice { SessionMultiple(Vec), StraightThrough(serde_json::Value), diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index 21002917df83..d34230e2cb49 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -1138,6 +1138,8 @@ impl DataModelExt for PaymentAttemptUpdate { business_sub_label, amount_to_capture, capture_method, + surcharge_amount, + tax_amount, updated_by, } => DieselPaymentAttemptUpdate::Update { amount, @@ -1152,6 +1154,8 @@ impl DataModelExt for PaymentAttemptUpdate { business_sub_label, amount_to_capture, capture_method, + surcharge_amount, + tax_amount, updated_by, }, Self::UpdateTrackers { @@ -1160,12 +1164,16 @@ impl DataModelExt for PaymentAttemptUpdate { straight_through_algorithm, amount_capturable, updated_by, + surcharge_amount, + tax_amount, merchant_connector_id, } => DieselPaymentAttemptUpdate::UpdateTrackers { payment_token, connector, straight_through_algorithm, amount_capturable, + surcharge_amount, + tax_amount, updated_by, merchant_connector_id, }, @@ -1193,8 +1201,6 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, - surcharge_amount, - tax_amount, updated_by, merchant_connector_id: connector_id, } => DieselPaymentAttemptUpdate::ConfirmUpdate { @@ -1214,8 +1220,6 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, - surcharge_amount, - tax_amount, updated_by, merchant_connector_id: connector_id, }, @@ -1243,6 +1247,8 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, + surcharge_amount, + tax_amount, authentication_data, encoded_data, } => DieselPaymentAttemptUpdate::ResponseUpdate { @@ -1260,6 +1266,8 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, + surcharge_amount, + tax_amount, authentication_data, encoded_data, }, @@ -1379,6 +1387,8 @@ impl DataModelExt for PaymentAttemptUpdate { business_sub_label, amount_to_capture, capture_method, + surcharge_amount, + tax_amount, updated_by, } => Self::Update { amount, @@ -1393,6 +1403,8 @@ impl DataModelExt for PaymentAttemptUpdate { business_sub_label, amount_to_capture, capture_method, + surcharge_amount, + tax_amount, updated_by, }, DieselPaymentAttemptUpdate::UpdateTrackers { @@ -1401,12 +1413,16 @@ impl DataModelExt for PaymentAttemptUpdate { straight_through_algorithm, amount_capturable, updated_by, + surcharge_amount, + tax_amount, merchant_connector_id: connector_id, } => Self::UpdateTrackers { payment_token, connector, straight_through_algorithm, amount_capturable, + surcharge_amount, + tax_amount, updated_by, merchant_connector_id: connector_id, }, @@ -1434,8 +1450,6 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, - surcharge_amount, - tax_amount, updated_by, merchant_connector_id: connector_id, } => Self::ConfirmUpdate { @@ -1455,8 +1469,6 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, - surcharge_amount, - tax_amount, updated_by, merchant_connector_id: connector_id, }, @@ -1484,6 +1496,8 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, + surcharge_amount, + tax_amount, authentication_data, encoded_data, } => Self::ResponseUpdate { @@ -1501,6 +1515,8 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, + surcharge_amount, + tax_amount, authentication_data, encoded_data, }, diff --git a/crates/storage_impl/src/redis/kv_store.rs b/crates/storage_impl/src/redis/kv_store.rs index 0c615d74f89a..3eadd8b83ade 100644 --- a/crates/storage_impl/src/redis/kv_store.rs +++ b/crates/storage_impl/src/redis/kv_store.rs @@ -111,7 +111,9 @@ where KvOperation::Hset(value, sql) => { logger::debug!(kv_operation= %operation, value = ?value); - redis_conn.set_hash_fields(key, value, Some(ttl)).await?; + redis_conn + .set_hash_fields(key, value, Some(ttl.into())) + .await?; store .push_to_drainer_stream::(sql, partition_key) From 496245d990e123b626089e70c848856ace295fb5 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Nov 2023 14:32:13 +0000 Subject: [PATCH 010/146] chore(version): v1.78.0 --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61cb4839fd7b..e5da650def02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.78.0 (2023-11-14) + +### Features + +- **router:** Add automatic retries and step up 3ds flow ([#2834](https://github.com/juspay/hyperswitch/pull/2834)) ([`d2968c9`](https://github.com/juspay/hyperswitch/commit/d2968c94978a57422fa46a8195d906736a95b864)) +- Payment link status page UI ([#2740](https://github.com/juspay/hyperswitch/pull/2740)) ([`856c7af`](https://github.com/juspay/hyperswitch/commit/856c7af77e17599ca0d4d119744ac582e9c3c971)) + +### Bug Fixes + +- Handle session and confirm flow discrepancy in surcharge details ([#2696](https://github.com/juspay/hyperswitch/pull/2696)) ([`cafea45`](https://github.com/juspay/hyperswitch/commit/cafea45982d7b520fe68fde967984ce88f68c6c0)) + +**Full Changelog:** [`v1.77.0...v1.78.0`](https://github.com/juspay/hyperswitch/compare/v1.77.0...v1.78.0) + +- - - + + ## 1.77.0 (2023-11-13) ### Features From d634fdeac349b92e3619234580299a6c6c38e6d4 Mon Sep 17 00:00:00 2001 From: Arun Raj M Date: Thu, 16 Nov 2023 10:27:34 +0530 Subject: [PATCH 011/146] feat: change async-bb8 fork and tokio spawn for concurrent database calls (#2774) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: akshay-97 Co-authored-by: akshay.s Co-authored-by: Kartikeya Hegde --- Cargo.lock | 820 ++++++++++++++---- crates/common_utils/src/types.rs | 4 +- crates/diesel_models/Cargo.toml | 2 +- crates/drainer/Cargo.toml | 2 +- crates/router/Cargo.toml | 3 +- crates/router/src/bin/router.rs | 4 +- crates/router/src/bin/scheduler.rs | 7 +- .../router/src/core/payment_methods/cards.rs | 13 +- crates/router/src/core/payments.rs | 33 +- .../src/core/payments/flows/approve_flow.rs | 7 +- .../src/core/payments/flows/authorize_flow.rs | 15 +- .../src/core/payments/flows/cancel_flow.rs | 7 +- .../src/core/payments/flows/capture_flow.rs | 7 +- .../payments/flows/complete_authorize_flow.rs | 4 +- .../src/core/payments/flows/psync_flow.rs | 7 +- .../src/core/payments/flows/reject_flow.rs | 7 +- .../src/core/payments/flows/session_flow.rs | 7 +- .../core/payments/flows/setup_mandate_flow.rs | 12 +- crates/router/src/core/payments/operations.rs | 4 +- .../payments/operations/payment_approve.rs | 3 +- .../payments/operations/payment_cancel.rs | 27 +- .../payments/operations/payment_capture.rs | 4 +- .../operations/payment_complete_authorize.rs | 2 +- .../payments/operations/payment_confirm.rs | 393 ++++++--- .../payments/operations/payment_create.rs | 10 +- .../operations/payment_method_validate.rs | 5 +- .../payments/operations/payment_reject.rs | 9 +- .../payments/operations/payment_response.rs | 172 ++-- .../payments/operations/payment_session.rs | 5 +- .../core/payments/operations/payment_start.rs | 2 +- .../payments/operations/payment_status.rs | 4 +- .../payments/operations/payment_update.rs | 11 +- crates/router/src/core/webhooks.rs | 79 +- crates/router/src/db/address.rs | 4 +- crates/router/src/db/refund.rs | 20 +- crates/router/src/db/reverse_lookup.rs | 6 +- crates/router/src/lib.rs | 2 +- crates/router/src/routes/admin.rs | 16 +- crates/router/src/routes/api_keys.rs | 4 +- crates/router/src/routes/app.rs | 103 ++- crates/router/src/routes/customers.rs | 12 +- crates/router/src/routes/disputes.rs | 16 +- crates/router/src/routes/files.rs | 12 +- crates/router/src/routes/payment_link.rs | 4 +- crates/router/src/routes/payment_methods.rs | 28 +- crates/router/src/routes/payments.rs | 36 +- crates/router/src/routes/payouts.rs | 20 +- crates/router/src/routes/refunds.rs | 12 +- crates/router/src/routes/verification.rs | 4 +- crates/router/src/routes/webhooks.rs | 4 +- crates/router/src/types/domain/customer.rs | 2 +- crates/router/src/utils.rs | 17 +- crates/router/src/workflows/payment_sync.rs | 18 +- crates/router/src/workflows/refund_router.rs | 2 +- crates/router/tests/cache.rs | 12 +- crates/router/tests/connectors/utils.rs | 18 +- crates/router/tests/customers.rs | 4 +- crates/router/tests/integration_demo.rs | 6 +- crates/router/tests/payments.rs | 76 +- crates/router/tests/payments2.rs | 8 +- crates/router/tests/payouts.rs | 2 +- crates/router/tests/refunds.rs | 6 +- crates/router/tests/services.rs | 18 +- crates/router/tests/utils.rs | 4 +- crates/storage_impl/Cargo.toml | 2 +- crates/storage_impl/src/lib.rs | 2 +- crates/storage_impl/src/lookup.rs | 6 +- .../src/payments/payment_attempt.rs | 26 +- .../src/payments/payment_intent.rs | 6 +- 69 files changed, 1512 insertions(+), 717 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1574933810b3..a03340093c88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,12 +9,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617a8268e3537fe1d8c9ead925fca49ef6400927ee7bc26750e90ecee14ce4b8" dependencies = [ "bitflags 1.3.2", - "bytes", + "bytes 1.5.0", "futures-core", "futures-sink", "memchr", "pin-project-lite", - "tokio", + "tokio 1.32.0", "tokio-util", "tracing", ] @@ -31,7 +31,7 @@ dependencies = [ "futures-util", "log", "once_cell", - "smallvec", + "smallvec 1.11.1", ] [[package]] @@ -48,7 +48,7 @@ dependencies = [ "base64 0.21.4", "bitflags 1.3.2", "brotli", - "bytes", + "bytes 1.5.0", "bytestring", "derive_more", "encoding_rs", @@ -66,8 +66,8 @@ dependencies = [ "pin-project-lite", "rand 0.8.5", "sha1", - "smallvec", - "tokio", + "smallvec 1.11.1", + "tokio 1.32.0", "tokio-util", "tracing", "zstd", @@ -92,7 +92,7 @@ dependencies = [ "actix-multipart-derive", "actix-utils", "actix-web", - "bytes", + "bytes 1.5.0", "derive_more", "futures-core", "futures-util", @@ -105,7 +105,7 @@ dependencies = [ "serde_json", "serde_plain", "tempfile", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -142,7 +142,7 @@ checksum = "28f32d40287d3f402ae0028a9d54bef51af15c8769492826a69d28f81893151d" dependencies = [ "actix-macros", "futures-core", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -156,9 +156,9 @@ dependencies = [ "actix-utils", "futures-core", "futures-util", - "mio", + "mio 0.8.8", "socket2 0.5.4", - "tokio", + "tokio 1.32.0", "tracing", ] @@ -188,7 +188,7 @@ dependencies = [ "pin-project-lite", "rustls 0.21.7", "rustls-webpki", - "tokio", + "tokio 1.32.0", "tokio-rustls", "tokio-util", "tracing", @@ -221,9 +221,9 @@ dependencies = [ "actix-utils", "actix-web-codegen", "ahash 0.7.6", - "bytes", + "bytes 1.5.0", "bytestring", - "cfg-if", + "cfg-if 1.0.0", "cookie", "derive_more", "encoding_rs", @@ -240,7 +240,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "smallvec", + "smallvec 1.11.1", "socket2 0.4.9", "time", "url", @@ -296,7 +296,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "getrandom 0.2.10", "once_cell", "version_check", @@ -475,14 +475,14 @@ dependencies = [ [[package]] name = "async-bb8-diesel" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "779f1fa3defe66bf147fe5c811b23a02cfcaa528a25293e0b20d1911eac1fb05" +source = "git+https://github.com/jarnura/async-bb8-diesel?rev=53b4ab901aab7635c8215fd1c2d542c8db443094#53b4ab901aab7635c8215fd1c2d542c8db443094" dependencies = [ "async-trait", "bb8", "diesel", "thiserror", - "tokio", + "tokio 1.32.0", + "tracing", ] [[package]] @@ -506,7 +506,7 @@ dependencies = [ "futures-core", "memchr", "pin-project-lite", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -517,7 +517,7 @@ checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" dependencies = [ "async-lock", "autocfg", - "cfg-if", + "cfg-if 1.0.0", "concurrent-queue", "futures-lite", "log", @@ -606,8 +606,8 @@ dependencies = [ "actix-utils", "ahash 0.7.6", "base64 0.21.4", - "bytes", - "cfg-if", + "bytes 1.5.0", + "cfg-if 1.0.0", "cookie", "derive_more", "futures-core", @@ -624,7 +624,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -644,14 +644,14 @@ dependencies = [ "aws-smithy-json", "aws-smithy-types", "aws-types", - "bytes", + "bytes 1.5.0", "fastrand 1.9.0", "hex", "http", "hyper", "ring", "time", - "tokio", + "tokio 1.32.0", "tower", "tracing", "zeroize", @@ -666,7 +666,7 @@ dependencies = [ "aws-smithy-async", "aws-smithy-types", "fastrand 1.9.0", - "tokio", + "tokio 1.32.0", "tracing", "zeroize", ] @@ -695,7 +695,7 @@ dependencies = [ "aws-smithy-http", "aws-smithy-types", "aws-types", - "bytes", + "bytes 1.5.0", "http", "http-body", "lazy_static", @@ -721,7 +721,7 @@ dependencies = [ "aws-smithy-json", "aws-smithy-types", "aws-types", - "bytes", + "bytes 1.5.0", "http", "regex", "tokio-stream", @@ -750,7 +750,7 @@ dependencies = [ "aws-smithy-types", "aws-smithy-xml", "aws-types", - "bytes", + "bytes 1.5.0", "http", "http-body", "once_cell", @@ -779,7 +779,7 @@ dependencies = [ "aws-smithy-json", "aws-smithy-types", "aws-types", - "bytes", + "bytes 1.5.0", "http", "regex", "tokio-stream", @@ -804,7 +804,7 @@ dependencies = [ "aws-smithy-json", "aws-smithy-types", "aws-types", - "bytes", + "bytes 1.5.0", "http", "regex", "tokio-stream", @@ -831,7 +831,7 @@ dependencies = [ "aws-smithy-types", "aws-smithy-xml", "aws-types", - "bytes", + "bytes 1.5.0", "http", "regex", "tower", @@ -861,7 +861,7 @@ checksum = "9d2ce6f507be68e968a33485ced670111d1cbad161ddbbab1e313c03d37d8f4c" dependencies = [ "aws-smithy-eventstream", "aws-smithy-http", - "bytes", + "bytes 1.5.0", "form_urlencoded", "hex", "hmac", @@ -882,7 +882,7 @@ checksum = "13bda3996044c202d75b91afeb11a9afae9db9a721c6a7a427410018e286b880" dependencies = [ "futures-util", "pin-project-lite", - "tokio", + "tokio 1.32.0", "tokio-stream", ] @@ -894,7 +894,7 @@ checksum = "07ed8b96d95402f3f6b8b57eb4e0e45ee365f78b1a924faf20ff6e97abf1eae6" dependencies = [ "aws-smithy-http", "aws-smithy-types", - "bytes", + "bytes 1.5.0", "crc32c", "crc32fast", "hex", @@ -917,7 +917,7 @@ dependencies = [ "aws-smithy-http", "aws-smithy-http-tower", "aws-smithy-types", - "bytes", + "bytes 1.5.0", "fastrand 1.9.0", "http", "http-body", @@ -926,7 +926,7 @@ dependencies = [ "lazy_static", "pin-project-lite", "rustls 0.20.9", - "tokio", + "tokio 1.32.0", "tower", "tracing", ] @@ -938,7 +938,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460c8da5110835e3d9a717c61f5556b20d03c32a1dec57f8fc559b360f733bb8" dependencies = [ "aws-smithy-types", - "bytes", + "bytes 1.5.0", "crc32fast", ] @@ -950,7 +950,7 @@ checksum = "2b3b693869133551f135e1f2c77cb0b8277d9e3e17feaf2213f735857c4f0d28" dependencies = [ "aws-smithy-eventstream", "aws-smithy-types", - "bytes", + "bytes 1.5.0", "bytes-utils", "futures-core", "http", @@ -960,7 +960,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "pin-utils", - "tokio", + "tokio 1.32.0", "tokio-util", "tracing", ] @@ -973,7 +973,7 @@ checksum = "3ae4f6c5798a247fac98a867698197d9ac22643596dc3777f0c76b91917616b9" dependencies = [ "aws-smithy-http", "aws-smithy-types", - "bytes", + "bytes 1.5.0", "http", "http-body", "pin-project-lite", @@ -1034,7 +1034,7 @@ dependencies = [ "aws-smithy-http", "aws-smithy-types", "http", - "rustc_version", + "rustc_version 0.4.0", "tracing", ] @@ -1047,7 +1047,7 @@ dependencies = [ "async-trait", "axum-core", "bitflags 1.3.2", - "bytes", + "bytes 1.5.0", "futures-util", "http", "http-body", @@ -1073,7 +1073,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" dependencies = [ "async-trait", - "bytes", + "bytes 1.5.0", "futures-util", "http", "http-body", @@ -1091,7 +1091,7 @@ checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", - "cfg-if", + "cfg-if 1.0.0", "libc", "miniz_oxide 0.7.1", "object", @@ -1136,7 +1136,7 @@ dependencies = [ "futures-channel", "futures-util", "parking_lot 0.12.1", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -1204,7 +1204,7 @@ dependencies = [ "arrayref", "arrayvec", "cc", - "cfg-if", + "cfg-if 1.0.0", "constant_time_eq", "digest 0.10.7", ] @@ -1282,6 +1282,16 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "bytes" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" +dependencies = [ + "byteorder", + "iovec", +] + [[package]] name = "bytes" version = "1.5.0" @@ -1294,7 +1304,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e47d3a8076e283f3acd27400535992edb3ba4b5bb72f8891ad8fbe7932a7d4b9" dependencies = [ - "bytes", + "bytes 1.5.0", "either", ] @@ -1304,7 +1314,7 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "238e4886760d98c4f899360c834fa93e62cf7f721ac3c2da375cbdf4b8679aae" dependencies = [ - "bytes", + "bytes 1.5.0", ] [[package]] @@ -1347,7 +1357,7 @@ checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" dependencies = [ "camino", "cargo-platform", - "semver", + "semver 1.0.19", "serde", "serde_json", ] @@ -1360,7 +1370,7 @@ checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" dependencies = [ "camino", "cargo-platform", - "semver", + "semver 1.0.19", "serde", "serde_json", "thiserror", @@ -1393,6 +1403,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + [[package]] name = "cfg-if" version = "1.0.0" @@ -1509,6 +1525,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -1532,12 +1557,12 @@ name = "common_utils" version = "0.1.0" dependencies = [ "async-trait", - "bytes", + "bytes 1.5.0", "common_enums", "diesel", "error-stack", "fake", - "futures", + "futures 0.3.28", "hex", "http", "masking", @@ -1562,7 +1587,7 @@ dependencies = [ "test-case", "thiserror", "time", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -1571,7 +1596,7 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" dependencies = [ - "crossbeam-utils", + "crossbeam-utils 0.8.16", ] [[package]] @@ -1674,7 +1699,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8f48d60e5b4d2c53d5c2b1d8a58c849a70ae5e5509b08a48d047e3b65714a74" dependencies = [ - "rustc_version", + "rustc_version 0.4.0", ] [[package]] @@ -1683,7 +1708,7 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -1728,8 +1753,19 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" dependencies = [ - "cfg-if", - "crossbeam-utils", + "cfg-if 1.0.0", + "crossbeam-utils 0.8.16", +] + +[[package]] +name = "crossbeam-deque" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20ff29ded3204c5106278a81a38f4b482636ed4fa1e6cfbeef193291beb29ed" +dependencies = [ + "crossbeam-epoch 0.8.2", + "crossbeam-utils 0.7.2", + "maybe-uninit", ] [[package]] @@ -1738,9 +1774,24 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" dependencies = [ - "cfg-if", - "crossbeam-epoch", - "crossbeam-utils", + "cfg-if 1.0.0", + "crossbeam-epoch 0.9.15", + "crossbeam-utils 0.8.16", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" +dependencies = [ + "autocfg", + "cfg-if 0.1.10", + "crossbeam-utils 0.7.2", + "lazy_static", + "maybe-uninit", + "memoffset 0.5.6", + "scopeguard", ] [[package]] @@ -1750,20 +1801,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" dependencies = [ "autocfg", - "cfg-if", - "crossbeam-utils", - "memoffset", + "cfg-if 1.0.0", + "crossbeam-utils 0.8.16", + "memoffset 0.9.0", "scopeguard", ] +[[package]] +name = "crossbeam-queue" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570" +dependencies = [ + "cfg-if 0.1.10", + "crossbeam-utils 0.7.2", + "maybe-uninit", +] + [[package]] name = "crossbeam-queue" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" dependencies = [ - "cfg-if", - "crossbeam-utils", + "cfg-if 1.0.0", + "crossbeam-utils 0.8.16", +] + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg", + "cfg-if 0.1.10", + "lazy_static", ] [[package]] @@ -1772,7 +1845,7 @@ version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -1861,9 +1934,9 @@ version = "5.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "hashbrown 0.14.1", - "lock_api", + "lock_api 0.4.10", "once_cell", "parking_lot_core 0.9.8", ] @@ -1900,7 +1973,7 @@ dependencies = [ "deadpool-runtime", "num_cpus", "retain_mut", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -1953,7 +2026,7 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "rustc_version", + "rustc_version 0.4.0", "syn 1.0.109", ] @@ -2064,7 +2137,7 @@ checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" dependencies = [ "libc", "redox_users", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2111,7 +2184,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "thiserror", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -2132,7 +2205,7 @@ version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -2187,7 +2260,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f00447f331c7f726db5b8532ebc9163519eed03c6d7c8b73c90b3ff5646ac85" dependencies = [ "anyhow", - "rustc_version", + "rustc_version 0.4.0", "serde", ] @@ -2261,7 +2334,7 @@ dependencies = [ "router_env", "serde", "thiserror", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -2291,7 +2364,7 @@ dependencies = [ "serde", "serde_json", "time", - "tokio", + "tokio 1.32.0", "url", "webdriver", ] @@ -2375,19 +2448,19 @@ dependencies = [ "arc-swap", "arcstr", "async-trait", - "bytes", + "bytes 1.5.0", "bytes-utils", - "cfg-if", + "cfg-if 1.0.0", "float-cmp", - "futures", + "futures 0.3.28", "lazy_static", "log", "parking_lot 0.12.1", "rand 0.8.5", "redis-protocol", - "semver", + "semver 1.0.19", "sha-1 0.10.1", - "tokio", + "tokio 1.32.0", "tokio-stream", "tokio-util", "tracing", @@ -2447,6 +2520,28 @@ dependencies = [ "syn 2.0.38", ] +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags 1.3.2", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + +[[package]] +name = "futures" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" + [[package]] name = "futures" version = "0.3.28" @@ -2496,7 +2591,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a604f7a68fbf8103337523b1fadc8ade7361ee3f112f7c680ad179651616aed5" dependencies = [ "futures-core", - "lock_api", + "lock_api 0.4.10", "parking_lot 0.11.2", ] @@ -2594,7 +2689,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "libc", "wasi 0.9.0+wasi-snapshot-preview1", ] @@ -2605,7 +2700,7 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", @@ -2677,7 +2772,7 @@ version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" dependencies = [ - "bytes", + "bytes 1.5.0", "fnv", "futures-core", "futures-sink", @@ -2685,7 +2780,7 @@ dependencies = [ "http", "indexmap 1.9.3", "slab", - "tokio", + "tokio 1.32.0", "tokio-util", "tracing", ] @@ -2769,7 +2864,7 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ - "bytes", + "bytes 1.5.0", "fnv", "itoa", ] @@ -2780,7 +2875,7 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ - "bytes", + "bytes 1.5.0", "http", "pin-project-lite", ] @@ -2833,7 +2928,7 @@ version = "0.14.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" dependencies = [ - "bytes", + "bytes 1.5.0", "futures-channel", "futures-core", "futures-util", @@ -2845,7 +2940,7 @@ dependencies = [ "itoa", "pin-project-lite", "socket2 0.4.9", - "tokio", + "tokio 1.32.0", "tower-service", "tracing", "want", @@ -2862,7 +2957,7 @@ dependencies = [ "log", "rustls 0.20.9", "rustls-native-certs", - "tokio", + "tokio 1.32.0", "tokio-rustls", ] @@ -2874,7 +2969,7 @@ checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ "hyper", "pin-project-lite", - "tokio", + "tokio 1.32.0", "tokio-io-timeout", ] @@ -2884,10 +2979,10 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ - "bytes", + "bytes 1.5.0", "hyper", "native-tls", - "tokio", + "tokio 1.32.0", "tokio-native-tls", ] @@ -3015,7 +3110,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -3029,6 +3124,15 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + [[package]] name = "ipnet" version = "2.8.0" @@ -3140,6 +3244,16 @@ dependencies = [ "simple_asn1", ] +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + [[package]] name = "kgraph_utils" version = "0.1.0" @@ -3246,6 +3360,15 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" +[[package]] +name = "lock_api" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" +dependencies = [ + "scopeguard", +] + [[package]] name = "lock_api" version = "0.4.10" @@ -3293,7 +3416,7 @@ dependencies = [ name = "masking" version = "0.1.0" dependencies = [ - "bytes", + "bytes 1.5.0", "diesel", "serde", "serde_json", @@ -3340,13 +3463,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + [[package]] name = "md-5" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "digest 0.10.7", ] @@ -3362,6 +3491,15 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "memoffset" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa" +dependencies = [ + "autocfg", +] + [[package]] name = "memoffset" version = "0.9.0" @@ -3430,6 +3568,25 @@ dependencies = [ "adler", ] +[[package]] +name = "mio" +version = "0.6.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" +dependencies = [ + "cfg-if 0.1.10", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log", + "miow", + "net2", + "slab", + "winapi 0.2.8", +] + [[package]] name = "mio" version = "0.8.8" @@ -3442,6 +3599,29 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "mio-uds" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" +dependencies = [ + "iovec", + "libc", + "mio 0.6.23", +] + +[[package]] +name = "miow" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", +] + [[package]] name = "moka" version = "0.11.3" @@ -3451,16 +3631,16 @@ dependencies = [ "async-io", "async-lock", "crossbeam-channel", - "crossbeam-epoch", - "crossbeam-utils", + "crossbeam-epoch 0.9.15", + "crossbeam-utils 0.8.16", "futures-util", "once_cell", "parking_lot 0.12.1", "quanta", - "rustc_version", + "rustc_version 0.4.0", "scheduled-thread-pool", "skeptic", - "smallvec", + "smallvec 1.11.1", "tagptr", "thiserror", "triomphe", @@ -3494,6 +3674,17 @@ dependencies = [ "tempfile", ] +[[package]] +name = "net2" +version = "0.2.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b13b648036a2339d06de780866fbdfda0dde886de7b3af2ddeba8b14f4ee34ac" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "winapi 0.3.9", +] + [[package]] name = "nom" version = "7.1.3" @@ -3511,7 +3702,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ "overload", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -3626,7 +3817,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" dependencies = [ "bitflags 2.4.0", - "cfg-if", + "cfg-if 1.0.0", "foreign-types", "libc", "once_cell", @@ -3680,14 +3871,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8af72d59a4484654ea8eb183fea5ae4eb6a41d7ac3e3bae5f4d2a282a3a7d3ca" dependencies = [ "async-trait", - "futures", + "futures 0.3.28", "futures-util", "http", "opentelemetry", "opentelemetry-proto", "prost", "thiserror", - "tokio", + "tokio 1.32.0", "tonic", ] @@ -3697,7 +3888,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "045f8eea8c0fa19f7d48e7bc3128a39c2e5c533d5c61298c548dfefc1064474c" dependencies = [ - "futures", + "futures 0.3.28", "futures-util", "opentelemetry", "prost", @@ -3738,7 +3929,7 @@ dependencies = [ "percent-encoding", "rand 0.8.5", "thiserror", - "tokio", + "tokio 1.32.0", "tokio-stream", ] @@ -3770,6 +3961,17 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e52c774a4c39359c1d1c52e43f73dd91a75a614652c825408eec30c95a9b2067" +[[package]] +name = "parking_lot" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252" +dependencies = [ + "lock_api 0.3.4", + "parking_lot_core 0.6.3", + "rustc_version 0.2.3", +] + [[package]] name = "parking_lot" version = "0.11.2" @@ -3777,7 +3979,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", - "lock_api", + "lock_api 0.4.10", "parking_lot_core 0.8.6", ] @@ -3787,22 +3989,37 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ - "lock_api", + "lock_api 0.4.10", "parking_lot_core 0.9.8", ] +[[package]] +name = "parking_lot_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66b810a62be75176a80873726630147a5ca780cd33921e0b5709033e66b0a" +dependencies = [ + "cfg-if 0.1.10", + "cloudabi", + "libc", + "redox_syscall 0.1.57", + "rustc_version 0.2.3", + "smallvec 0.6.14", + "winapi 0.3.9", +] + [[package]] name = "parking_lot_core" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "instant", "libc", "redox_syscall 0.2.16", - "smallvec", - "winapi", + "smallvec 1.11.1", + "winapi 0.3.9", ] [[package]] @@ -3811,10 +4028,10 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "libc", "redox_syscall 0.3.5", - "smallvec", + "smallvec 1.11.1", "windows-targets", ] @@ -4061,7 +4278,7 @@ checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" dependencies = [ "autocfg", "bitflags 1.3.2", - "cfg-if", + "cfg-if 1.0.0", "concurrent-queue", "libc", "log", @@ -4143,7 +4360,7 @@ version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" dependencies = [ - "bytes", + "bytes 1.5.0", "prost-derive", ] @@ -4187,14 +4404,14 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a17e662a7a8291a865152364c20c7abc5e60486ab2001e8ec10b24862de0b9ab" dependencies = [ - "crossbeam-utils", + "crossbeam-utils 0.8.16", "libc", "mach2", "once_cell", "raw-cpuid", "wasi 0.11.0+wasi-snapshot-preview1", "web-sys", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -4338,8 +4555,8 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" dependencies = [ - "crossbeam-deque", - "crossbeam-utils", + "crossbeam-deque 0.8.3", + "crossbeam-utils 0.8.16", ] [[package]] @@ -4348,7 +4565,7 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c31deddf734dc0a39d3112e73490e88b61a05e83e074d211f348404cee4d2c6" dependencies = [ - "bytes", + "bytes 1.5.0", "bytes-utils", "cookie-factory", "crc16", @@ -4363,13 +4580,19 @@ dependencies = [ "common_utils", "error-stack", "fred", - "futures", + "futures 0.3.28", "router_env", "serde", "thiserror", - "tokio", + "tokio 1.32.0", ] +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + [[package]] name = "redox_syscall" version = "0.2.16" @@ -4463,7 +4686,7 @@ checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" dependencies = [ "async-compression", "base64 0.21.4", - "bytes", + "bytes 1.5.0", "encoding_rs", "futures-core", "futures-util", @@ -4485,7 +4708,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "system-configuration", - "tokio", + "tokio 1.32.0", "tokio-native-tls", "tokio-util", "tower-service", @@ -4514,7 +4737,7 @@ dependencies = [ "spin", "untrusted", "web-sys", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -4561,7 +4784,7 @@ dependencies = [ "bb8", "bigdecimal", "blake3", - "bytes", + "bytes 1.5.0", "cards", "clap", "common_enums", @@ -4577,7 +4800,7 @@ dependencies = [ "error-stack", "euclid", "external_services", - "futures", + "futures 0.3.28", "hex", "http", "hyper", @@ -4621,7 +4844,8 @@ dependencies = [ "test_utils", "thiserror", "time", - "tokio", + "tokio 1.32.0", + "tracing-futures", "unicode-segmentation", "url", "utoipa", @@ -4664,7 +4888,7 @@ dependencies = [ "serde_path_to_error", "strum 0.24.1", "time", - "tokio", + "tokio 1.32.0", "tracing", "tracing-actix-web", "tracing-appender", @@ -4724,7 +4948,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "ordered-multimap", ] @@ -4740,13 +4964,22 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + [[package]] name = "rustc_version" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver", + "semver 1.0.19", ] [[package]] @@ -4900,7 +5133,7 @@ dependencies = [ "diesel_models", "error-stack", "external_services", - "futures", + "futures 0.3.28", "masking", "once_cell", "rand 0.8.5", @@ -4912,7 +5145,7 @@ dependencies = [ "strum 0.24.1", "thiserror", "time", - "tokio", + "tokio 1.32.0", "uuid", ] @@ -4961,6 +5194,15 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + [[package]] name = "semver" version = "1.0.19" @@ -4970,6 +5212,12 @@ dependencies = [ "serde", ] +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + [[package]] name = "serde" version = "1.0.188" @@ -5122,7 +5370,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e56dd856803e253c8f298af3f4d7eb0ae5e23a737252cd90bb4f3b435033b2d" dependencies = [ "dashmap", - "futures", + "futures 0.3.28", "lazy_static", "log", "parking_lot 0.12.1", @@ -5147,7 +5395,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" dependencies = [ "block-buffer 0.9.0", - "cfg-if", + "cfg-if 1.0.0", "cpufeatures", "digest 0.9.0", "opaque-debug", @@ -5159,7 +5407,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "cpufeatures", "digest 0.10.7", ] @@ -5170,7 +5418,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "cpufeatures", "digest 0.10.7", ] @@ -5181,7 +5429,7 @@ version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "cpufeatures", "digest 0.10.7", ] @@ -5232,7 +5480,7 @@ dependencies = [ "futures-core", "libc", "signal-hook", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -5286,6 +5534,15 @@ dependencies = [ "deunicode", ] +[[package]] +name = "smallvec" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0" +dependencies = [ + "maybe-uninit", +] + [[package]] name = "smallvec" version = "1.11.1" @@ -5299,7 +5556,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -5351,9 +5608,9 @@ dependencies = [ "bigdecimal", "bitflags 1.3.2", "byteorder", - "bytes", + "bytes 1.5.0", "crc", - "crossbeam-queue", + "crossbeam-queue 0.3.8", "dirs", "dotenvy", "either", @@ -5381,7 +5638,7 @@ dependencies = [ "serde_json", "sha1", "sha2", - "smallvec", + "smallvec 1.11.1", "sqlformat", "sqlx-rt", "stringprep", @@ -5419,7 +5676,7 @@ checksum = "804d3f245f894e61b1e6263c84b23ca675d96753b5abfd5cc8597d86806e8024" dependencies = [ "native-tls", "once_cell", - "tokio", + "tokio 1.32.0", "tokio-native-tls", ] @@ -5432,7 +5689,7 @@ dependencies = [ "async-bb8-diesel", "async-trait", "bb8", - "bytes", + "bytes 1.5.0", "common_utils", "config", "crc32fast", @@ -5441,7 +5698,7 @@ dependencies = [ "diesel_models", "dyn-clone", "error-stack", - "futures", + "futures 0.3.28", "http", "masking", "mime", @@ -5454,7 +5711,7 @@ dependencies = [ "serde", "serde_json", "thiserror", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -5606,7 +5863,7 @@ version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "fastrand 2.0.1", "redox_syscall 0.3.5", "rustix 0.38.17", @@ -5650,7 +5907,7 @@ version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54c25e2cb8f5fcd7318157634e8838aa6f7e4715c96637f969fabaccd1ef5462" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "proc-macro-error", "proc-macro2", "quote", @@ -5686,7 +5943,7 @@ dependencies = [ "serial_test", "thirtyfour", "time", - "tokio", + "tokio 1.32.0", "toml 0.7.4", ] @@ -5701,7 +5958,7 @@ dependencies = [ "chrono", "cookie", "fantoccini", - "futures", + "futures 0.3.28", "http", "log", "parking_lot 0.12.1", @@ -5711,7 +5968,7 @@ dependencies = [ "stringmatch", "thirtyfour-macros", "thiserror", - "tokio", + "tokio 1.32.0", "url", "urlparse", ] @@ -5754,7 +6011,7 @@ version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "once_cell", ] @@ -5821,6 +6078,30 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokio" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a09c0b5bb588872ab2f09afa13ee6e9dac11e10a0ec9e8e3ba39a5a5d530af6" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "mio 0.6.23", + "num_cpus", + "tokio-codec", + "tokio-current-thread", + "tokio-executor", + "tokio-fs", + "tokio-io", + "tokio-reactor", + "tokio-sync", + "tokio-tcp", + "tokio-threadpool", + "tokio-timer", + "tokio-udp", + "tokio-uds", +] + [[package]] name = "tokio" version = "1.32.0" @@ -5828,9 +6109,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" dependencies = [ "backtrace", - "bytes", + "bytes 1.5.0", "libc", - "mio", + "mio 0.8.8", "num_cpus", "parking_lot 0.12.1", "pin-project-lite", @@ -5840,6 +6121,59 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "tokio-codec" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25b2998660ba0e70d18684de5d06b70b70a3a747469af9dea7618cc59e75976b" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "tokio-io", +] + +[[package]] +name = "tokio-current-thread" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1de0e32a83f131e002238d7ccde18211c0a5397f60cbfffcb112868c2e0e20e" +dependencies = [ + "futures 0.1.31", + "tokio-executor", +] + +[[package]] +name = "tokio-executor" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb2d1b8f4548dbf5e1f7818512e9c406860678f29c300cdf0ebac72d1a3a1671" +dependencies = [ + "crossbeam-utils 0.7.2", + "futures 0.1.31", +] + +[[package]] +name = "tokio-fs" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297a1206e0ca6302a0eed35b700d292b275256f596e2f3fea7729d5e629b6ff4" +dependencies = [ + "futures 0.1.31", + "tokio-io", + "tokio-threadpool", +] + +[[package]] +name = "tokio-io" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57fc868aae093479e3131e3d165c93b1c7474109d13c90ec0dda2a1bbfff0674" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "log", +] + [[package]] name = "tokio-io-timeout" version = "1.2.0" @@ -5847,7 +6181,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" dependencies = [ "pin-project-lite", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -5868,7 +6202,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ "native-tls", - "tokio", + "tokio 1.32.0", +] + +[[package]] +name = "tokio-reactor" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09bc590ec4ba8ba87652da2068d150dcada2cfa2e07faae270a5e0409aa51351" +dependencies = [ + "crossbeam-utils 0.7.2", + "futures 0.1.31", + "lazy_static", + "log", + "mio 0.6.23", + "num_cpus", + "parking_lot 0.9.0", + "slab", + "tokio-executor", + "tokio-io", + "tokio-sync", ] [[package]] @@ -5878,7 +6231,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" dependencies = [ "rustls 0.20.9", - "tokio", + "tokio 1.32.0", "webpki", ] @@ -5890,7 +6243,93 @@ checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", "pin-project-lite", - "tokio", + "tokio 1.32.0", +] + +[[package]] +name = "tokio-sync" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfe50152bc8164fcc456dab7891fa9bf8beaf01c5ee7e1dd43a397c3cf87dee" +dependencies = [ + "fnv", + "futures 0.1.31", +] + +[[package]] +name = "tokio-tcp" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98df18ed66e3b72e742f185882a9e201892407957e45fbff8da17ae7a7c51f72" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "iovec", + "mio 0.6.23", + "tokio-io", + "tokio-reactor", +] + +[[package]] +name = "tokio-threadpool" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df720b6581784c118f0eb4310796b12b1d242a7eb95f716a8367855325c25f89" +dependencies = [ + "crossbeam-deque 0.7.4", + "crossbeam-queue 0.2.3", + "crossbeam-utils 0.7.2", + "futures 0.1.31", + "lazy_static", + "log", + "num_cpus", + "slab", + "tokio-executor", +] + +[[package]] +name = "tokio-timer" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93044f2d313c95ff1cb7809ce9a7a05735b012288a888b62d4434fd58c94f296" +dependencies = [ + "crossbeam-utils 0.7.2", + "futures 0.1.31", + "slab", + "tokio-executor", +] + +[[package]] +name = "tokio-udp" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2a0b10e610b39c38b031a2fcab08e4b82f16ece36504988dcbd81dbba650d82" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "log", + "mio 0.6.23", + "tokio-codec", + "tokio-io", + "tokio-reactor", +] + +[[package]] +name = "tokio-uds" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab57a4ac4111c8c9dbcf70779f6fc8bc35ae4b2454809febac840ad19bd7e4e0" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "iovec", + "libc", + "log", + "mio 0.6.23", + "mio-uds", + "tokio-codec", + "tokio-io", + "tokio-reactor", ] [[package]] @@ -5899,11 +6338,11 @@ version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" dependencies = [ - "bytes", + "bytes 1.5.0", "futures-core", "futures-sink", "pin-project-lite", - "tokio", + "tokio 1.32.0", "tracing", ] @@ -5960,7 +6399,7 @@ dependencies = [ "async-trait", "axum", "base64 0.13.1", - "bytes", + "bytes 1.5.0", "futures-core", "futures-util", "h2", @@ -5972,7 +6411,7 @@ dependencies = [ "pin-project", "prost", "prost-derive", - "tokio", + "tokio 1.32.0", "tokio-stream", "tokio-util", "tower", @@ -5995,7 +6434,7 @@ dependencies = [ "pin-project-lite", "rand 0.8.5", "slab", - "tokio", + "tokio 1.32.0", "tokio-util", "tower-layer", "tower-service", @@ -6020,7 +6459,7 @@ version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "log", "pin-project-lite", "tracing-attributes", @@ -6080,6 +6519,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" dependencies = [ "pin-project", + "tokio 0.1.22", "tracing", ] @@ -6131,7 +6571,7 @@ dependencies = [ "serde", "serde_json", "sharded-slab", - "smallvec", + "smallvec 1.11.1", "thread_local", "tracing", "tracing-core", @@ -6389,7 +6829,7 @@ checksum = "8b3c89c2c7e50f33e4d35527e5bf9c11d6d132226dbbd1753f0fbe9f19ef88c6" dependencies = [ "anyhow", "git2", - "rustc_version", + "rustc_version 0.4.0", "rustversion", "time", ] @@ -6458,7 +6898,7 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "wasm-bindgen-macro", ] @@ -6483,7 +6923,7 @@ version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "js-sys", "wasm-bindgen", "web-sys", @@ -6535,7 +6975,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9973cb72c8587d5ad5efdb91e663d36177dc37725e6c90ca86c626b0cc45c93f" dependencies = [ "base64 0.13.1", - "bytes", + "bytes 1.5.0", "cookie", "http", "log", @@ -6582,6 +7022,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + [[package]] name = "winapi" version = "0.3.9" @@ -6592,6 +7038,12 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -6604,7 +7056,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -6703,7 +7155,7 @@ version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "windows-sys", ] @@ -6717,7 +7169,7 @@ dependencies = [ "async-trait", "base64 0.21.4", "deadpool", - "futures", + "futures 0.3.28", "futures-timer", "http-types", "hyper", @@ -6726,7 +7178,17 @@ dependencies = [ "regex", "serde", "serde_json", - "tokio", + "tokio 1.32.0", +] + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", ] [[package]] @@ -6775,7 +7237,7 @@ checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" dependencies = [ "byteorder", "crc32fast", - "crossbeam-utils", + "crossbeam-utils 0.8.16", "flate2", ] diff --git a/crates/common_utils/src/types.rs b/crates/common_utils/src/types.rs index b28bffe0dc90..111f0f43c0f2 100644 --- a/crates/common_utils/src/types.rs +++ b/crates/common_utils/src/types.rs @@ -77,7 +77,9 @@ impl Percentage { if value.contains('.') { // if string has '.' then take the decimal part and verify precision length match value.split('.').last() { - Some(decimal_part) => decimal_part.trim_end_matches('0').len() <= PRECISION.into(), + Some(decimal_part) => { + decimal_part.trim_end_matches('0').len() <= >::into(PRECISION) + } // will never be None None => false, } diff --git a/crates/diesel_models/Cargo.toml b/crates/diesel_models/Cargo.toml index 9521c690366f..ccef0bf4e742 100644 --- a/crates/diesel_models/Cargo.toml +++ b/crates/diesel_models/Cargo.toml @@ -12,7 +12,7 @@ default = ["kv_store"] kv_store = [] [dependencies] -async-bb8-diesel = "0.1.0" +async-bb8-diesel = { git = "https://github.com/jarnura/async-bb8-diesel", rev = "53b4ab901aab7635c8215fd1c2d542c8db443094" } diesel = { version = "2.1.0", features = ["postgres", "serde_json", "time", "64-column-tables"] } error-stack = "0.3.1" frunk = "0.4.1" diff --git a/crates/drainer/Cargo.toml b/crates/drainer/Cargo.toml index 56bebdce6b86..668e8b0574fe 100644 --- a/crates/drainer/Cargo.toml +++ b/crates/drainer/Cargo.toml @@ -13,7 +13,7 @@ kms = ["external_services/kms"] vergen = ["router_env/vergen"] [dependencies] -async-bb8-diesel = "0.1.0" +async-bb8-diesel = { git = "https://github.com/jarnura/async-bb8-diesel", rev = "53b4ab901aab7635c8215fd1c2d542c8db443094" } bb8 = "0.8" clap = { version = "4.3.2", default-features = false, features = ["std", "derive", "help", "usage"] } config = { version = "0.13.3", features = ["toml"] } diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 4d9c315a10b0..01595dc18cd5 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -37,8 +37,8 @@ actix-cors = "0.6.4" actix-multipart = "0.6.0" actix-rt = "2.8.0" actix-web = "4.3.1" +async-bb8-diesel = { git = "https://github.com/jarnura/async-bb8-diesel", rev = "53b4ab901aab7635c8215fd1c2d542c8db443094" } argon2 = { version = "0.5.0", features = ["std"] } -async-bb8-diesel = "0.1.0" async-trait = "0.1.68" aws-config = { version = "0.55.3", optional = true } aws-sdk-s3 = { version = "0.28.0", optional = true } @@ -97,6 +97,7 @@ utoipa-swagger-ui = { version = "3.1.3", features = ["actix-web"] } uuid = { version = "1.3.3", features = ["serde", "v4"] } validator = "0.16.0" x509-parser = "0.15.0" +tracing-futures = { version = "0.2.5", features = ["tokio"] } # First party crates api_models = { version = "0.1.0", path = "../api_models", features = ["errors"] } diff --git a/crates/router/src/bin/router.rs b/crates/router/src/bin/router.rs index cb3a8d83b031..beb2869f998c 100644 --- a/crates/router/src/bin/router.rs +++ b/crates/router/src/bin/router.rs @@ -4,7 +4,7 @@ use router::{ logger, }; -#[actix_web::main] +#[tokio::main] async fn main() -> ApplicationResult<()> { // get commandline config before initializing config let cmd_line = ::parse(); @@ -43,7 +43,7 @@ async fn main() -> ApplicationResult<()> { logger::info!("Application started [{:?}] [{:?}]", conf.server, conf.log); #[allow(clippy::expect_used)] - let server = router::start_server(conf) + let server = Box::pin(router::start_server(conf)) .await .expect("Failed to create the server"); let _ = server.await; diff --git a/crates/router/src/bin/scheduler.rs b/crates/router/src/bin/scheduler.rs index 09f23bc3b2f3..4c19408582bc 100644 --- a/crates/router/src/bin/scheduler.rs +++ b/crates/router/src/bin/scheduler.rs @@ -40,7 +40,12 @@ async fn main() -> CustomResult<(), ProcessTrackerError> { ); // channel for listening to redis disconnect events let (redis_shutdown_signal_tx, redis_shutdown_signal_rx) = oneshot::channel(); - let state = routes::AppState::new(conf, redis_shutdown_signal_tx, api_client).await; + let state = Box::pin(routes::AppState::new( + conf, + redis_shutdown_signal_tx, + api_client, + )) + .await; // channel to shutdown scheduler gracefully let (tx, rx) = mpsc::channel(1); tokio::spawn(router::receiver_for_error( diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 6b3cf11f5891..38ab03ddcb77 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -1944,7 +1944,14 @@ pub async fn do_list_customer_pm_fetch_customer_if_not_passed( ) -> errors::RouterResponse { let db = state.store.as_ref(); if let Some(customer_id) = customer_id { - list_customer_payment_method(&state, merchant_account, key_store, None, customer_id).await + Box::pin(list_customer_payment_method( + &state, + merchant_account, + key_store, + None, + customer_id, + )) + .await } else { let cloned_secret = req.and_then(|r| r.client_secret.as_ref().cloned()); let payment_intent = helpers::verify_payment_intent_time_and_client_secret( @@ -1957,13 +1964,13 @@ pub async fn do_list_customer_pm_fetch_customer_if_not_passed( .as_ref() .and_then(|intent| intent.customer_id.to_owned()) .ok_or(errors::ApiErrorResponse::CustomerNotFound)?; - list_customer_payment_method( + Box::pin(list_customer_payment_method( &state, merchant_account, key_store, payment_intent, &customer_id, - ) + )) .await } } diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index e7408cecf163..7e19b0b60571 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -193,7 +193,7 @@ where ) .await?; let operation = Box::new(PaymentResponse); - let db = &*state.store; + connector_http_status_code = router_data.connector_http_status_code; external_latency = router_data.external_latency; //add connector http status code metrics @@ -201,7 +201,7 @@ where operation .to_post_update_tracker()? .update_tracker( - db, + state, &validate_result.payment_id, payment_data, router_data, @@ -272,7 +272,6 @@ where } let operation = Box::new(PaymentResponse); - let db = &*state.store; connector_http_status_code = router_data.connector_http_status_code; external_latency = router_data.external_latency; //add connector http status code metrics @@ -280,7 +279,7 @@ where operation .to_post_update_tracker()? .update_tracker( - db, + state, &validate_result.payment_id, payment_data, router_data, @@ -323,7 +322,7 @@ where (_, payment_data) = operation .to_update_tracker()? .update_trackers( - &*state.store, + state, payment_data.clone(), customer.clone(), validate_result.storage_scheme, @@ -582,7 +581,14 @@ impl PaymentRedirectFlow for PaymentRedirectCom }), ..Default::default() }; - payments_core::( + Box::pin(payments_core::< + api::CompleteAuthorize, + api::PaymentsResponse, + _, + _, + _, + Ctx, + >( state.clone(), merchant_account, merchant_key_store, @@ -592,7 +598,7 @@ impl PaymentRedirectFlow for PaymentRedirectCom connector_action, None, HeaderPayload::default(), - ) + )) .await } @@ -678,7 +684,14 @@ impl PaymentRedirectFlow for PaymentRedirectSyn expand_attempts: None, expand_captures: None, }; - payments_core::( + Box::pin(payments_core::< + api::PSync, + api::PaymentsResponse, + _, + _, + _, + Ctx, + >( state.clone(), merchant_account, merchant_key_store, @@ -688,7 +701,7 @@ impl PaymentRedirectFlow for PaymentRedirectSyn connector_action, None, HeaderPayload::default(), - ) + )) .await } fn generate_response( @@ -889,7 +902,7 @@ where (_, *payment_data) = operation .to_update_tracker()? .update_trackers( - &*state.store, + state, payment_data.clone(), customer.clone(), merchant_account.storage_scheme, diff --git a/crates/router/src/core/payments/flows/approve_flow.rs b/crates/router/src/core/payments/flows/approve_flow.rs index 24f7e05e7b9d..14b710de914a 100644 --- a/crates/router/src/core/payments/flows/approve_flow.rs +++ b/crates/router/src/core/payments/flows/approve_flow.rs @@ -25,7 +25,10 @@ impl customer: &Option, merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult { - transformers::construct_payment_router_data::( + Box::pin(transformers::construct_payment_router_data::< + api::Approve, + types::PaymentsApproveData, + >( state, self.clone(), connector_id, @@ -33,7 +36,7 @@ impl key_store, customer, merchant_connector_account, - ) + )) .await } } diff --git a/crates/router/src/core/payments/flows/authorize_flow.rs b/crates/router/src/core/payments/flows/authorize_flow.rs index e27fe54c0ed0..04bd7f0b4338 100644 --- a/crates/router/src/core/payments/flows/authorize_flow.rs +++ b/crates/router/src/core/payments/flows/authorize_flow.rs @@ -39,7 +39,10 @@ impl types::PaymentsResponseData, >, > { - transformers::construct_payment_router_data::( + Box::pin(transformers::construct_payment_router_data::< + api::Authorize, + types::PaymentsAuthorizeData, + >( state, self.clone(), connector_id, @@ -47,7 +50,7 @@ impl key_store, customer, merchant_connector_account, - ) + )) .await } } @@ -96,7 +99,7 @@ impl Feature for types::PaymentsAu metrics::PAYMENT_COUNT.add(&metrics::CONTEXT, 1, &[]); // Metrics if resp.request.setup_mandate_details.clone().is_some() { - let payment_method_id = tokenization::save_payment_method( + let payment_method_id = Box::pin(tokenization::save_payment_method( state, connector, resp.to_owned(), @@ -104,7 +107,7 @@ impl Feature for types::PaymentsAu merchant_account, self.request.payment_method_type, key_store, - ) + )) .await?; Ok(mandate::mandate_procedure( state, @@ -127,7 +130,7 @@ impl Feature for types::PaymentsAu tokio::spawn(async move { logger::info!("Starting async call to save_payment_method in locker"); - let result = tokenization::save_payment_method( + let result = Box::pin(tokenization::save_payment_method( &state, &connector, response, @@ -135,7 +138,7 @@ impl Feature for types::PaymentsAu &merchant_account, self.request.payment_method_type, &key_store, - ) + )) .await; if let Err(err) = result { diff --git a/crates/router/src/core/payments/flows/cancel_flow.rs b/crates/router/src/core/payments/flows/cancel_flow.rs index 3a3ac1b5b0bb..5918380ee0b2 100644 --- a/crates/router/src/core/payments/flows/cancel_flow.rs +++ b/crates/router/src/core/payments/flows/cancel_flow.rs @@ -24,7 +24,10 @@ impl ConstructFlowSpecificData, merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult { - transformers::construct_payment_router_data::( + Box::pin(transformers::construct_payment_router_data::< + api::Void, + types::PaymentsCancelData, + >( state, self.clone(), connector_id, @@ -32,7 +35,7 @@ impl ConstructFlowSpecificData, merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult { - transformers::construct_payment_router_data::( + Box::pin(transformers::construct_payment_router_data::< + api::Capture, + types::PaymentsCaptureData, + >( state, self.clone(), connector_id, @@ -33,7 +36,7 @@ impl key_store, customer, merchant_connector_account, - ) + )) .await } } diff --git a/crates/router/src/core/payments/flows/complete_authorize_flow.rs b/crates/router/src/core/payments/flows/complete_authorize_flow.rs index 6fbbb01e1a64..44d8728fd4d2 100644 --- a/crates/router/src/core/payments/flows/complete_authorize_flow.rs +++ b/crates/router/src/core/payments/flows/complete_authorize_flow.rs @@ -35,7 +35,7 @@ impl types::PaymentsResponseData, >, > { - transformers::construct_payment_router_data::< + Box::pin(transformers::construct_payment_router_data::< api::CompleteAuthorize, types::CompleteAuthorizeData, >( @@ -46,7 +46,7 @@ impl key_store, customer, merchant_connector_account, - ) + )) .await } } diff --git a/crates/router/src/core/payments/flows/psync_flow.rs b/crates/router/src/core/payments/flows/psync_flow.rs index 36d418a3ae8c..cb7a764985d1 100644 --- a/crates/router/src/core/payments/flows/psync_flow.rs +++ b/crates/router/src/core/payments/flows/psync_flow.rs @@ -28,7 +28,10 @@ impl ConstructFlowSpecificData RouterResult< types::RouterData, > { - transformers::construct_payment_router_data::( + Box::pin(transformers::construct_payment_router_data::< + api::PSync, + types::PaymentsSyncData, + >( state, self.clone(), connector_id, @@ -36,7 +39,7 @@ impl ConstructFlowSpecificData, merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult { - transformers::construct_payment_router_data::( + Box::pin(transformers::construct_payment_router_data::< + api::Reject, + types::PaymentsRejectData, + >( state, self.clone(), connector_id, @@ -32,7 +35,7 @@ impl ConstructFlowSpecificData, merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult { - transformers::construct_payment_router_data::( + Box::pin(transformers::construct_payment_router_data::< + api::Session, + types::PaymentsSessionData, + >( state, self.clone(), connector_id, @@ -40,7 +43,7 @@ impl key_store, customer, merchant_connector_account, - ) + )) .await } } diff --git a/crates/router/src/core/payments/flows/setup_mandate_flow.rs b/crates/router/src/core/payments/flows/setup_mandate_flow.rs index dae9ed0bf833..0c03c8ce123b 100644 --- a/crates/router/src/core/payments/flows/setup_mandate_flow.rs +++ b/crates/router/src/core/payments/flows/setup_mandate_flow.rs @@ -31,7 +31,7 @@ impl customer: &Option, merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult { - transformers::construct_payment_router_data::< + Box::pin(transformers::construct_payment_router_data::< api::SetupMandate, types::SetupMandateRequestData, >( @@ -42,7 +42,7 @@ impl key_store, customer, merchant_connector_account, - ) + )) .await } } @@ -75,7 +75,7 @@ impl Feature for types::Setup .await .to_setup_mandate_failed_response()?; - let pm_id = tokenization::save_payment_method( + let pm_id = Box::pin(tokenization::save_payment_method( state, connector, resp.to_owned(), @@ -83,7 +83,7 @@ impl Feature for types::Setup merchant_account, self.request.payment_method_type, key_store, - ) + )) .await?; mandate::mandate_procedure( @@ -208,7 +208,7 @@ impl types::SetupMandateRouterData { .to_setup_mandate_failed_response()?; let payment_method_type = self.request.payment_method_type; - let pm_id = tokenization::save_payment_method( + let pm_id = Box::pin(tokenization::save_payment_method( state, connector, resp.to_owned(), @@ -216,7 +216,7 @@ impl types::SetupMandateRouterData { merchant_account, payment_method_type, key_store, - ) + )) .await?; Ok(mandate::mandate_procedure( diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index ad747ac2792a..f65e65459e00 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -154,7 +154,7 @@ pub trait Domain: Send + Sync { pub trait UpdateTracker: Send { async fn update_trackers<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_data: D, customer: Option, storage_scheme: enums::MerchantStorageScheme, @@ -171,7 +171,7 @@ pub trait UpdateTracker: Send { pub trait PostUpdateTracker: Send { async fn update_tracker<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_id: &api::PaymentIdType, payment_data: D, response: types::RouterData, diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index d5d0d2d01765..538e65e4b22e 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -336,7 +336,7 @@ impl #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, mut payment_data: PaymentData, _customer: Option, storage_scheme: storage_enums::MerchantStorageScheme, @@ -356,6 +356,7 @@ impl updated_by: storage_scheme.to_string(), }; payment_data.payment_intent = db + .store .update_payment_intent( payment_data.payment_intent, intent_status_update, diff --git a/crates/router/src/core/payments/operations/payment_cancel.rs b/crates/router/src/core/payments/operations/payment_cancel.rs index f734afef7826..535edf736ca6 100644 --- a/crates/router/src/core/payments/operations/payment_cancel.rs +++ b/crates/router/src/core/payments/operations/payment_cancel.rs @@ -14,7 +14,6 @@ use crate::{ payment_methods::PaymentMethodRetrieve, payments::{helpers, operations, CustomerDetails, PaymentAddress, PaymentData}, }, - db::StorageInterface, routes::AppState, services, types::{ @@ -178,7 +177,7 @@ impl #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, mut payment_data: PaymentData, _customer: Option, storage_scheme: enums::MerchantStorageScheme, @@ -207,6 +206,7 @@ impl if let Some(payment_intent_update) = intent_status_update { payment_data.payment_intent = db + .store .update_payment_intent( payment_data.payment_intent, payment_intent_update, @@ -216,17 +216,18 @@ impl .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; } - db.update_payment_attempt_with_attempt_id( - payment_data.payment_attempt.clone(), - storage::PaymentAttemptUpdate::VoidUpdate { - status: attempt_status_update, - cancellation_reason, - updated_by: storage_scheme.to_string(), - }, - storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + db.store + .update_payment_attempt_with_attempt_id( + payment_data.payment_attempt.clone(), + storage::PaymentAttemptUpdate::VoidUpdate { + status: attempt_status_update, + cancellation_reason, + updated_by: storage_scheme.to_string(), + }, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; Ok((Box::new(self), payment_data)) } } diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index 6e794b1ba618..ff51a2c49d77 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -13,7 +13,6 @@ use crate::{ payment_methods::PaymentMethodRetrieve, payments::{self, helpers, operations, types::MultipleCaptureData}, }, - db::StorageInterface, routes::AppState, services, types::{ @@ -222,7 +221,7 @@ impl #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, mut payment_data: payments::PaymentData, _customer: Option, storage_scheme: enums::MerchantStorageScheme, @@ -239,6 +238,7 @@ impl { payment_data.payment_attempt = match &payment_data.multiple_capture_data { Some(multiple_capture_data) => db + .store .update_payment_attempt_with_attempt_id( payment_data.payment_attempt, storage::PaymentAttemptUpdate::MultipleCaptureCountUpdate { diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index 038d34ea290f..c648d95a4950 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -326,7 +326,7 @@ impl #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - _db: &dyn StorageInterface, + _state: &'b AppState, payment_data: PaymentData, _customer: Option, _storage_scheme: storage_enums::MerchantStorageScheme, diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 96cd4f5c622f..88462e7f8563 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -11,6 +11,7 @@ use futures::FutureExt; use redis_interface::errors::RedisError; use router_derive::PaymentOperation; use router_env::{instrument, tracing}; +use tracing_futures::Instrument; use super::{BoxedOperation, Domain, GetTracker, Operation, UpdateTracker, ValidateRequest}; use crate::{ @@ -65,20 +66,46 @@ impl // Stage 1 - let payment_intent_fut = db - .find_payment_intent_by_payment_id_merchant_id(&payment_id, merchant_id, storage_scheme) - .map(|x| x.change_context(errors::ApiErrorResponse::PaymentNotFound)); + let store = state.clone().store; + let m_merchant_id = merchant_id.clone(); + let payment_intent_fut = tokio::spawn( + async move { + store + .find_payment_intent_by_payment_id_merchant_id( + &payment_id, + m_merchant_id.as_str(), + storage_scheme, + ) + .map(|x| x.change_context(errors::ApiErrorResponse::PaymentNotFound)) + .await + } + .in_current_span(), + ); - let mandate_details_fut = helpers::get_token_pm_type_mandate_details( - state, - request, - mandate_type.clone(), - merchant_account, - key_store, + let m_state = state.clone(); + let m_mandate_type = mandate_type.clone(); + let m_merchant_account = merchant_account.clone(); + let m_request = request.clone(); + let m_key_store = key_store.clone(); + + let mandate_details_fut = tokio::spawn( + async move { + helpers::get_token_pm_type_mandate_details( + &m_state, + &m_request, + m_mandate_type, + &m_merchant_account, + &m_key_store, + ) + .await + } + .in_current_span(), ); - let (mut payment_intent, mandate_details) = - futures::try_join!(payment_intent_fut, mandate_details_fut)?; + let (mut payment_intent, mandate_details) = tokio::try_join!( + utils::flatten_join_error(payment_intent_fut), + utils::flatten_join_error(mandate_details_fut) + )?; helpers::validate_customer_access(&payment_intent, auth_flow, request)?; @@ -112,76 +139,122 @@ impl // Stage 2 let attempt_id = payment_intent.active_attempt.get_id(); - let payment_attempt_fut = db - .find_payment_attempt_by_payment_id_merchant_id_attempt_id( - payment_intent.payment_id.as_str(), - merchant_id, - attempt_id.as_str(), - storage_scheme, - ) - .map(|x| x.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)); - - let shipping_address_fut = helpers::create_or_find_address_for_payment_by_request( - db, - request.shipping.as_ref(), - payment_intent.shipping_address_id.as_deref(), - merchant_id, - payment_intent - .customer_id - .as_ref() - .or(customer_details.customer_id.as_ref()), - key_store, - &payment_intent.payment_id, - merchant_account.storage_scheme, + let store = state.clone().store; + let m_payment_id = payment_intent.payment_id.clone(); + let m_merchant_id = merchant_id.clone(); + + let payment_attempt_fut = tokio::spawn( + async move { + store + .find_payment_attempt_by_payment_id_merchant_id_attempt_id( + m_payment_id.as_str(), + m_merchant_id.as_str(), + attempt_id.as_str(), + storage_scheme, + ) + .map(|x| x.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)) + .await + } + .in_current_span(), ); - let billing_address_fut = helpers::create_or_find_address_for_payment_by_request( - db, - request.billing.as_ref(), - payment_intent.billing_address_id.as_deref(), - merchant_id, - payment_intent - .customer_id - .as_ref() - .or(customer_details.customer_id.as_ref()), - key_store, - &payment_intent.payment_id, - merchant_account.storage_scheme, + let m_merchant_id = merchant_id.clone(); + let m_request_shipping = request.shipping.clone(); + let m_payment_intent_shipping_address_id = payment_intent.shipping_address_id.clone(); + let m_payment_intent_payment_id = payment_intent.payment_id.clone(); + let m_customer_details_customer_id = customer_details.customer_id.clone(); + let m_payment_intent_customer_id = payment_intent.customer_id.clone(); + let store = state.clone().store; + let m_key_store = key_store.clone(); + + let shipping_address_fut = tokio::spawn( + async move { + helpers::create_or_find_address_for_payment_by_request( + store.as_ref(), + m_request_shipping.as_ref(), + m_payment_intent_shipping_address_id.as_deref(), + m_merchant_id.as_str(), + m_payment_intent_customer_id + .as_ref() + .or(m_customer_details_customer_id.as_ref()), + &m_key_store, + m_payment_intent_payment_id.as_ref(), + storage_scheme, + ) + .await + } + .in_current_span(), ); - let config_update_fut = request - .merchant_connector_details - .to_owned() - .async_map(|mcd| async { - helpers::insert_merchant_connector_creds_to_config( - db, - merchant_account.merchant_id.as_str(), - mcd, + let m_merchant_id = merchant_id.clone(); + let m_request_billing = request.billing.clone(); + let m_customer_details_customer_id = customer_details.customer_id.clone(); + let m_payment_intent_customer_id = payment_intent.customer_id.clone(); + let m_payment_intent_billing_address_id = payment_intent.billing_address_id.clone(); + let m_payment_intent_payment_id = payment_intent.payment_id.clone(); + let store = state.clone().store; + let m_key_store = key_store.clone(); + + let billing_address_fut = tokio::spawn( + async move { + helpers::create_or_find_address_for_payment_by_request( + store.as_ref(), + m_request_billing.as_ref(), + m_payment_intent_billing_address_id.as_deref(), + m_merchant_id.as_ref(), + m_payment_intent_customer_id + .as_ref() + .or(m_customer_details_customer_id.as_ref()), + &m_key_store, + m_payment_intent_payment_id.as_ref(), + storage_scheme, ) .await - }) - .map(|x| x.transpose()); + } + .in_current_span(), + ); + + let m_merchant_id = merchant_id.clone(); + let store = state.clone().store; + let m_request_merchant_connector_details = request.merchant_connector_details.clone(); + + let config_update_fut = tokio::spawn( + async move { + m_request_merchant_connector_details + .async_map(|mcd| async { + helpers::insert_merchant_connector_creds_to_config( + store.as_ref(), + m_merchant_id.as_str(), + mcd, + ) + .await + }) + .map(|x| x.transpose()) + .await + } + .in_current_span(), + ); let (mut payment_attempt, shipping_address, billing_address) = match payment_intent.status { api_models::enums::IntentStatus::RequiresCustomerAction | api_models::enums::IntentStatus::RequiresMerchantAction | api_models::enums::IntentStatus::RequiresPaymentMethod | api_models::enums::IntentStatus::RequiresConfirmation => { - let (payment_attempt, shipping_address, billing_address, _) = futures::try_join!( - payment_attempt_fut, - shipping_address_fut, - billing_address_fut, - config_update_fut + let (payment_attempt, shipping_address, billing_address, _) = tokio::try_join!( + utils::flatten_join_error(payment_attempt_fut), + utils::flatten_join_error(shipping_address_fut), + utils::flatten_join_error(billing_address_fut), + utils::flatten_join_error(config_update_fut) )?; (payment_attempt, shipping_address, billing_address) } _ => { - let (mut payment_attempt, shipping_address, billing_address, _) = futures::try_join!( - payment_attempt_fut, - shipping_address_fut, - billing_address_fut, - config_update_fut + let (mut payment_attempt, shipping_address, billing_address, _) = tokio::try_join!( + utils::flatten_join_error(payment_attempt_fut), + utils::flatten_join_error(shipping_address_fut), + utils::flatten_join_error(billing_address_fut), + utils::flatten_join_error(config_update_fut) )?; let attempt_type = helpers::get_attempt_type( @@ -193,11 +266,10 @@ impl (payment_intent, payment_attempt) = attempt_type .modify_payment_intent_and_payment_attempt( - // 3 request, payment_intent, payment_attempt, - db, + &*state.store, storage_scheme, ) .await?; @@ -445,7 +517,7 @@ impl #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - db: &dyn StorageInterface, + state: &'b AppState, mut payment_data: PaymentData, customer: Option, storage_scheme: storage_enums::MerchantStorageScheme, @@ -501,7 +573,7 @@ impl .payment_method_data .as_ref() .async_map(|payment_method_data| async { - helpers::get_additional_payment_data(payment_method_data, db).await + helpers::get_additional_payment_data(payment_method_data, &*state.store).await }) .await .as_ref() @@ -537,76 +609,131 @@ impl .as_ref() .map(|surcharge_details| surcharge_details.final_amount) .unwrap_or(payment_data.payment_attempt.amount); - let payment_attempt_fut = db - .update_payment_attempt_with_attempt_id( - payment_data.payment_attempt, - storage::PaymentAttemptUpdate::ConfirmUpdate { - amount: payment_data.amount.into(), - currency: payment_data.currency, - status: attempt_status, - payment_method, - authentication_type, - browser_info, - connector, - payment_token, - payment_method_data: additional_pm_data, - payment_method_type, - payment_experience, - business_sub_label, - straight_through_algorithm, - error_code, - error_message, - amount_capturable: Some(authorized_amount), - updated_by: storage_scheme.to_string(), - merchant_connector_id, - }, - storage_scheme, - ) - .map(|x| x.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)); - - let payment_intent_fut = db - .update_payment_intent( - payment_data.payment_intent, - storage::PaymentIntentUpdate::Update { - amount: payment_data.amount.into(), - currency: payment_data.currency, - setup_future_usage, - status: intent_status, - customer_id, - shipping_address_id: shipping_address, - billing_address_id: billing_address, - return_url, - business_country, - business_label, - description, - statement_descriptor_name, - statement_descriptor_suffix, - order_details, - metadata, - payment_confirm_source: header_payload.payment_confirm_source, - updated_by: storage_scheme.to_string(), - }, - storage_scheme, - ) - .map(|x| x.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)); - let customer_fut = Box::pin(async { - if let Some((updated_customer, customer)) = updated_customer.zip(customer) { - db.update_customer_by_customer_id_merchant_id( - customer.customer_id.to_owned(), - customer.merchant_id.to_owned(), - updated_customer, - key_store, + let m_payment_data_payment_attempt = payment_data.payment_attempt.clone(); + let m_browser_info = browser_info.clone(); + let m_connector = connector.clone(); + let m_payment_token = payment_token.clone(); + let m_additional_pm_data = additional_pm_data.clone(); + let m_business_sub_label = business_sub_label.clone(); + let m_straight_through_algorithm = straight_through_algorithm.clone(); + let m_error_code = error_code.clone(); + let m_error_message = error_message.clone(); + let m_db = state.clone().store; + + let payment_attempt_fut = tokio::spawn( + async move { + m_db.update_payment_attempt_with_attempt_id( + m_payment_data_payment_attempt, + storage::PaymentAttemptUpdate::ConfirmUpdate { + amount: payment_data.amount.into(), + currency: payment_data.currency, + status: attempt_status, + payment_method, + authentication_type, + browser_info: m_browser_info, + connector: m_connector, + payment_token: m_payment_token, + payment_method_data: m_additional_pm_data, + payment_method_type, + payment_experience, + business_sub_label: m_business_sub_label, + straight_through_algorithm: m_straight_through_algorithm, + error_code: m_error_code, + error_message: m_error_message, + amount_capturable: Some(authorized_amount), + updated_by: storage_scheme.to_string(), + merchant_connector_id, + }, + storage_scheme, + ) + .map(|x| x.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)) + .await + } + .in_current_span(), + ); + + let m_payment_data_payment_intent = payment_data.payment_intent.clone(); + let m_customer_id = customer_id.clone(); + let m_shipping_address_id = shipping_address.clone(); + let m_billing_address_id = billing_address.clone(); + let m_return_url = return_url.clone(); + let m_business_label = business_label.clone(); + let m_description = description.clone(); + let m_statement_descriptor_name = statement_descriptor_name.clone(); + let m_statement_descriptor_suffix = statement_descriptor_suffix.clone(); + let m_order_details = order_details.clone(); + let m_metadata = metadata.clone(); + let m_db = state.clone().store; + let m_storage_scheme = storage_scheme.to_string(); + + let payment_intent_fut = tokio::spawn( + async move { + m_db.update_payment_intent( + m_payment_data_payment_intent, + storage::PaymentIntentUpdate::Update { + amount: payment_data.amount.into(), + currency: payment_data.currency, + setup_future_usage, + status: intent_status, + customer_id: m_customer_id, + shipping_address_id: m_shipping_address_id, + billing_address_id: m_billing_address_id, + return_url: m_return_url, + business_country, + business_label: m_business_label, + description: m_description, + statement_descriptor_name: m_statement_descriptor_name, + statement_descriptor_suffix: m_statement_descriptor_suffix, + order_details: m_order_details, + metadata: m_metadata, + payment_confirm_source: header_payload.payment_confirm_source, + updated_by: m_storage_scheme, + }, + storage_scheme, ) + .map(|x| x.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)) .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to update CustomerConnector in customer")?; + } + .in_current_span(), + ); + + let customer_fut = + if let Some((updated_customer, customer)) = updated_customer.zip(customer) { + let m_customer_customer_id = customer.customer_id.to_owned(); + let m_customer_merchant_id = customer.merchant_id.to_owned(); + let m_key_store = key_store.clone(); + let m_updated_customer = updated_customer.clone(); + let m_db = state.clone().store; + tokio::spawn( + async move { + m_db.update_customer_by_customer_id_merchant_id( + m_customer_customer_id, + m_customer_merchant_id, + m_updated_customer, + &m_key_store, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update CustomerConnector in customer")?; + + Ok::<_, error_stack::Report>(()) + } + .in_current_span(), + ) + } else { + tokio::spawn( + async move { Ok::<_, error_stack::Report>(()) } + .in_current_span(), + ) }; - Ok::<_, error_stack::Report>(()) - }); - let (payment_intent, payment_attempt, _) = - futures::try_join!(payment_intent_fut, payment_attempt_fut, customer_fut)?; + let (payment_intent, payment_attempt, _) = tokio::try_join!( + utils::flatten_join_error(payment_intent_fut), + utils::flatten_join_error(payment_attempt_fut), + utils::flatten_join_error(customer_fut) + )?; + payment_data.payment_intent = payment_intent; payment_data.payment_attempt = payment_attempt; diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index fad7212c61d3..974f5e6ab5b6 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -16,7 +16,7 @@ use crate::{ errors::{self, CustomResult, RouterResult, StorageErrorExt}, payment_methods::PaymentMethodRetrieve, payments::{self, helpers, operations, CustomerDetails, PaymentAddress, PaymentData}, - utils::{self as core_utils}, + utils as core_utils, }, db::StorageInterface, routes::AppState, @@ -394,7 +394,7 @@ impl #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - db: &dyn StorageInterface, + state: &'b AppState, mut payment_data: PaymentData, _customer: Option, storage_scheme: enums::MerchantStorageScheme, @@ -443,7 +443,8 @@ impl .as_ref() .map(|surcharge_details| surcharge_details.tax_on_surcharge_amount); - payment_data.payment_attempt = db + payment_data.payment_attempt = state + .store .update_payment_attempt_with_attempt_id( payment_data.payment_attempt, storage::PaymentAttemptUpdate::UpdateTrackers { @@ -466,7 +467,8 @@ impl let customer_id = payment_data.payment_intent.customer_id.clone(); - payment_data.payment_intent = db + payment_data.payment_intent = state + .store .update_payment_intent( payment_data.payment_intent, storage::PaymentIntentUpdate::ReturnUrlUpdate { diff --git a/crates/router/src/core/payments/operations/payment_method_validate.rs b/crates/router/src/core/payments/operations/payment_method_validate.rs index 7e4fe0951b03..62f12cfbc90c 100644 --- a/crates/router/src/core/payments/operations/payment_method_validate.rs +++ b/crates/router/src/core/payments/operations/payment_method_validate.rs @@ -205,7 +205,7 @@ impl UpdateTracker, api: #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - db: &dyn StorageInterface, + state: &'b AppState, mut payment_data: PaymentData, _customer: Option, storage_scheme: storage_enums::MerchantStorageScheme, @@ -225,7 +225,8 @@ impl UpdateTracker, api: let customer_id = payment_data.payment_intent.customer_id.clone(); - payment_data.payment_intent = db + payment_data.payment_intent = state + .store .update_payment_intent( payment_data.payment_intent, storage::PaymentIntentUpdate::ReturnUrlUpdate { diff --git a/crates/router/src/core/payments/operations/payment_reject.rs b/crates/router/src/core/payments/operations/payment_reject.rs index a6c2561aaeed..16d264c001ec 100644 --- a/crates/router/src/core/payments/operations/payment_reject.rs +++ b/crates/router/src/core/payments/operations/payment_reject.rs @@ -13,7 +13,6 @@ use crate::{ payment_methods::PaymentMethodRetrieve, payments::{helpers, operations, CustomerDetails, PaymentAddress, PaymentData}, }, - db::StorageInterface, routes::AppState, services, types::{ @@ -164,7 +163,7 @@ impl #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - db: &dyn StorageInterface, + state: &'b AppState, mut payment_data: PaymentData, _customer: Option, storage_scheme: enums::MerchantStorageScheme, @@ -201,7 +200,8 @@ impl updated_by: storage_scheme.to_string(), }; - payment_data.payment_intent = db + payment_data.payment_intent = state + .store .update_payment_intent( payment_data.payment_intent, intent_status_update, @@ -210,7 +210,8 @@ impl .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; - payment_data.payment_attempt = db + payment_data.payment_attempt = state + .store .update_payment_attempt_with_attempt_id( payment_data.payment_attempt.clone(), attempt_status_update, diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index d6346a512ef1..b55b0c46f6ad 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -1,10 +1,13 @@ use std::collections::HashMap; use async_trait::async_trait; +use data_models::payments::payment_attempt::PaymentAttempt; use error_stack::ResultExt; use futures::FutureExt; use router_derive; use router_env::{instrument, tracing}; +use storage_impl::DataModelExt; +use tracing_futures::Instrument; use super::{Operation, PostUpdateTracker}; use crate::{ @@ -15,8 +18,7 @@ use crate::{ payments::{types::MultipleCaptureData, PaymentData}, utils as core_utils, }, - db::StorageInterface, - routes::metrics, + routes::{metrics, AppState}, services::RedirectForm, types::{ self, api, @@ -43,7 +45,7 @@ impl PostUpdateTracker, types::PaymentsAuthorizeData { async fn update_tracker<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_id: &api::PaymentIdType, mut payment_data: PaymentData, router_data: types::RouterData< @@ -60,13 +62,13 @@ impl PostUpdateTracker, types::PaymentsAuthorizeData .mandate_id .or_else(|| router_data.request.mandate_id.clone()); - payment_data = payment_response_update_tracker( + payment_data = Box::pin(payment_response_update_tracker( db, payment_id, payment_data, router_data, storage_scheme, - ) + )) .await?; Ok(payment_data) @@ -77,7 +79,7 @@ impl PostUpdateTracker, types::PaymentsAuthorizeData impl PostUpdateTracker, types::PaymentsSyncData> for PaymentResponse { async fn update_tracker<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_id: &api::PaymentIdType, payment_data: PaymentData, router_data: types::RouterData, @@ -86,8 +88,14 @@ impl PostUpdateTracker, types::PaymentsSyncData> for where F: 'b + Send, { - payment_response_update_tracker(db, payment_id, payment_data, router_data, storage_scheme) - .await + Box::pin(payment_response_update_tracker( + db, + payment_id, + payment_data, + router_data, + storage_scheme, + )) + .await } } @@ -97,7 +105,7 @@ impl PostUpdateTracker, types::PaymentsSessionData> { async fn update_tracker<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_id: &api::PaymentIdType, mut payment_data: PaymentData, router_data: types::RouterData, @@ -106,13 +114,13 @@ impl PostUpdateTracker, types::PaymentsSessionData> where F: 'b + Send, { - payment_data = payment_response_update_tracker( + payment_data = Box::pin(payment_response_update_tracker( db, payment_id, payment_data, router_data, storage_scheme, - ) + )) .await?; Ok(payment_data) @@ -125,7 +133,7 @@ impl PostUpdateTracker, types::PaymentsCaptureData> { async fn update_tracker<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_id: &api::PaymentIdType, mut payment_data: PaymentData, router_data: types::RouterData, @@ -134,13 +142,13 @@ impl PostUpdateTracker, types::PaymentsCaptureData> where F: 'b + Send, { - payment_data = payment_response_update_tracker( + payment_data = Box::pin(payment_response_update_tracker( db, payment_id, payment_data, router_data, storage_scheme, - ) + )) .await?; Ok(payment_data) @@ -151,7 +159,7 @@ impl PostUpdateTracker, types::PaymentsCaptureData> impl PostUpdateTracker, types::PaymentsCancelData> for PaymentResponse { async fn update_tracker<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_id: &api::PaymentIdType, mut payment_data: PaymentData, router_data: types::RouterData, @@ -161,13 +169,13 @@ impl PostUpdateTracker, types::PaymentsCancelData> f where F: 'b + Send, { - payment_data = payment_response_update_tracker( + payment_data = Box::pin(payment_response_update_tracker( db, payment_id, payment_data, router_data, storage_scheme, - ) + )) .await?; Ok(payment_data) @@ -180,7 +188,7 @@ impl PostUpdateTracker, types::PaymentsApproveData> { async fn update_tracker<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_id: &api::PaymentIdType, mut payment_data: PaymentData, router_data: types::RouterData, @@ -190,13 +198,13 @@ impl PostUpdateTracker, types::PaymentsApproveData> where F: 'b + Send, { - payment_data = payment_response_update_tracker( + payment_data = Box::pin(payment_response_update_tracker( db, payment_id, payment_data, router_data, storage_scheme, - ) + )) .await?; Ok(payment_data) @@ -207,7 +215,7 @@ impl PostUpdateTracker, types::PaymentsApproveData> impl PostUpdateTracker, types::PaymentsRejectData> for PaymentResponse { async fn update_tracker<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_id: &api::PaymentIdType, mut payment_data: PaymentData, router_data: types::RouterData, @@ -217,13 +225,13 @@ impl PostUpdateTracker, types::PaymentsRejectData> f where F: 'b + Send, { - payment_data = payment_response_update_tracker( + payment_data = Box::pin(payment_response_update_tracker( db, payment_id, payment_data, router_data, storage_scheme, - ) + )) .await?; Ok(payment_data) @@ -236,7 +244,7 @@ impl PostUpdateTracker, types::SetupMandateRequestDa { async fn update_tracker<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_id: &api::PaymentIdType, mut payment_data: PaymentData, router_data: types::RouterData< @@ -255,13 +263,13 @@ impl PostUpdateTracker, types::SetupMandateRequestDa // .map(api_models::payments::MandateIds::new) }); - payment_data = payment_response_update_tracker( + payment_data = Box::pin(payment_response_update_tracker( db, payment_id, payment_data, router_data, storage_scheme, - ) + )) .await?; Ok(payment_data) @@ -274,7 +282,7 @@ impl PostUpdateTracker, types::CompleteAuthorizeData { async fn update_tracker<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_id: &api::PaymentIdType, payment_data: PaymentData, response: types::RouterData, @@ -283,14 +291,20 @@ impl PostUpdateTracker, types::CompleteAuthorizeData where F: 'b + Send, { - payment_response_update_tracker(db, payment_id, payment_data, response, storage_scheme) - .await + Box::pin(payment_response_update_tracker( + db, + payment_id, + payment_data, + response, + storage_scheme, + )) + .await } } #[instrument(skip_all)] async fn payment_response_update_tracker( - db: &dyn StorageInterface, + state: &AppState, _payment_id: &api::PaymentIdType, mut payment_data: PaymentData, router_data: types::RouterData, @@ -524,7 +538,8 @@ async fn payment_response_update_tracker( payment_data.multiple_capture_data = match capture_update { Some((mut multiple_capture_data, capture_updates)) => { for (capture, capture_update) in capture_updates { - let updated_capture = db + let updated_capture = state + .store .update_capture_with_capture_id(capture, capture_update, storage_scheme) .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; @@ -548,17 +563,43 @@ async fn payment_response_update_tracker( let payment_attempt = payment_data.payment_attempt.clone(); - payment_data.payment_attempt = match payment_attempt_update { - Some(payment_attempt_update) => db - .update_payment_attempt_with_attempt_id( - payment_attempt, - payment_attempt_update, - storage_scheme, + let m_db = state.clone().store; + let m_payment_attempt_update = payment_attempt_update.clone(); + let m_payment_attempt = payment_attempt.clone(); + + let payment_attempt = payment_attempt_update + .map(|payment_attempt_update| { + PaymentAttempt::from_storage_model( + payment_attempt_update + .to_storage_model() + .apply_changeset(payment_attempt.clone().to_storage_model()), ) + }) + .unwrap_or_else(|| payment_attempt); + + let payment_attempt_fut = tokio::spawn( + async move { + Box::pin(async move { + Ok::<_, error_stack::Report>( + match m_payment_attempt_update { + Some(payment_attempt_update) => m_db + .update_payment_attempt_with_attempt_id( + m_payment_attempt, + payment_attempt_update, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?, + None => m_payment_attempt, + }, + ) + }) .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?, - None => payment_attempt, - }; + } + .in_current_span(), + ); + + payment_data.payment_attempt = payment_attempt; let amount_captured = get_total_amount_captured( router_data.request, @@ -566,6 +607,7 @@ async fn payment_response_update_tracker( router_data.status, &payment_data, ); + let payment_intent_update = match &router_data.response { Err(_) => storage::PaymentIntentUpdate::PGStatusUpdate { status: payment_data @@ -583,25 +625,47 @@ async fn payment_response_update_tracker( }, }; - let payment_intent_fut = db - .update_payment_intent( - payment_data.payment_intent.clone(), - payment_intent_update, - storage_scheme, - ) - .map(|x| x.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)); + let m_db = state.clone().store; + let m_payment_data_payment_intent = payment_data.payment_intent.clone(); + let m_payment_intent_update = payment_intent_update.clone(); + let payment_intent_fut = tokio::spawn( + async move { + m_db.update_payment_intent( + m_payment_data_payment_intent, + m_payment_intent_update, + storage_scheme, + ) + .map(|x| x.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)) + .await + } + .in_current_span(), + ); // When connector requires redirection for mandate creation it can update the connector mandate_id during Psync - let mandate_update_fut = mandate::update_connector_mandate_id( - db, - router_data.merchant_id, - payment_data.mandate_id.clone(), - router_data.response.clone(), + let m_db = state.clone().store; + let m_router_data_merchant_id = router_data.merchant_id.clone(); + let m_payment_data_mandate_id = payment_data.mandate_id.clone(); + let m_router_data_response = router_data.response.clone(); + let mandate_update_fut = tokio::spawn( + async move { + mandate::update_connector_mandate_id( + m_db.as_ref(), + m_router_data_merchant_id, + m_payment_data_mandate_id, + m_router_data_response, + ) + .await + } + .in_current_span(), ); - let (payment_intent, _) = futures::try_join!(payment_intent_fut, mandate_update_fut)?; - payment_data.payment_intent = payment_intent; + let (payment_intent, _, _) = futures::try_join!( + utils::flatten_join_error(payment_intent_fut), + utils::flatten_join_error(mandate_update_fut), + utils::flatten_join_error(payment_attempt_fut) + )?; + payment_data.payment_intent = payment_intent; Ok(payment_data) } diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index 52677ab3cc8d..3abde60c2e9b 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -200,7 +200,7 @@ impl #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - db: &dyn StorageInterface, + state: &'b AppState, mut payment_data: PaymentData, _customer: Option, storage_scheme: storage_enums::MerchantStorageScheme, @@ -217,7 +217,8 @@ impl { let metadata = payment_data.payment_intent.metadata.clone(); payment_data.payment_intent = match metadata { - Some(metadata) => db + Some(metadata) => state + .store .update_payment_intent( payment_data.payment_intent, storage::PaymentIntentUpdate::MetadataUpdate { diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index 5578f6b3dc15..17f39d5150bb 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -174,7 +174,7 @@ impl #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - _db: &dyn StorageInterface, + _state: &'b AppState, payment_data: PaymentData, _customer: Option, _storage_scheme: storage_enums::MerchantStorageScheme, diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index 83e7131b2675..fb58aeb34e07 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -132,7 +132,7 @@ impl { async fn update_trackers<'b>( &'b self, - _db: &dyn StorageInterface, + _state: &'b AppState, payment_data: PaymentData, _customer: Option, _storage_scheme: enums::MerchantStorageScheme, @@ -157,7 +157,7 @@ impl { async fn update_trackers<'b>( &'b self, - _db: &dyn StorageInterface, + _state: &'b AppState, payment_data: PaymentData, _customer: Option, _storage_scheme: enums::MerchantStorageScheme, diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 26bda6d6bee6..53a768f26810 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -422,7 +422,7 @@ impl #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - db: &dyn StorageInterface, + state: &'b AppState, mut payment_data: PaymentData, customer: Option, storage_scheme: storage_enums::MerchantStorageScheme, @@ -456,7 +456,7 @@ impl .payment_method_data .as_ref() .async_map(|payment_method_data| async { - helpers::get_additional_payment_data(payment_method_data, db).await + helpers::get_additional_payment_data(payment_method_data, &*state.store).await }) .await .as_ref() @@ -471,6 +471,7 @@ impl let payment_experience = payment_data.payment_attempt.payment_experience; let amount_to_capture = payment_data.payment_attempt.amount_to_capture; let capture_method = payment_data.payment_attempt.capture_method; + let surcharge_amount = payment_data .surcharge_details .as_ref() @@ -479,7 +480,8 @@ impl .surcharge_details .as_ref() .map(|surcharge_details| surcharge_details.tax_on_surcharge_amount); - payment_data.payment_attempt = db + payment_data.payment_attempt = state + .store .update_payment_attempt_with_attempt_id( payment_data.payment_attempt, storage::PaymentAttemptUpdate::Update { @@ -540,7 +542,8 @@ impl let order_details = payment_data.payment_intent.order_details.clone(); let metadata = payment_data.payment_intent.metadata.clone(); - payment_data.payment_intent = db + payment_data.payment_intent = state + .store .update_payment_intent( payment_data.payment_intent, storage::PaymentIntentUpdate::Update { diff --git a/crates/router/src/core/webhooks.rs b/crates/router/src/core/webhooks.rs index ba4d7f6549e7..db53a3b56a15 100644 --- a/crates/router/src/core/webhooks.rs +++ b/crates/router/src/core/webhooks.rs @@ -79,29 +79,35 @@ pub async fn payments_incoming_webhook_flow< .perform_locking_action(&state, merchant_account.merchant_id.to_string()) .await?; - let response = - payments::payments_core::( - state.clone(), - merchant_account.clone(), - key_store, - payments::operations::PaymentStatus, - api::PaymentsRetrieveRequest { - resource_id: id, - merchant_id: Some(merchant_account.merchant_id.clone()), - force_sync: true, - connector: None, - param: None, - merchant_connector_details: None, - client_secret: None, - expand_attempts: None, - expand_captures: None, - }, - services::AuthFlow::Merchant, - consume_or_trigger_flow, - None, - HeaderPayload::default(), - ) - .await; + let response = Box::pin(payments::payments_core::< + api::PSync, + api::PaymentsResponse, + _, + _, + _, + Ctx, + >( + state.clone(), + merchant_account.clone(), + key_store, + payments::operations::PaymentStatus, + api::PaymentsRetrieveRequest { + resource_id: id, + merchant_id: Some(merchant_account.merchant_id.clone()), + force_sync: true, + connector: None, + param: None, + merchant_connector_details: None, + client_secret: None, + expand_attempts: None, + expand_captures: None, + }, + services::AuthFlow::Merchant, + consume_or_trigger_flow, + None, + HeaderPayload::default(), + )) + .await; lock_action .free_lock_action(&state, merchant_account.merchant_id.to_owned()) @@ -572,7 +578,14 @@ async fn bank_transfer_webhook_flow( + Box::pin(payments::payments_core::< + api::Authorize, + api::PaymentsResponse, + _, + _, + _, + Ctx, + >( state.clone(), merchant_account.to_owned(), key_store, @@ -582,7 +595,7 @@ async fn bank_transfer_webhook_flow RouterResponse { - let (application_response, _webhooks_response_tracker) = webhooks_core::( + let (application_response, _webhooks_response_tracker) = Box::pin(webhooks_core::( state, req, merchant_account, key_store, connector_name_or_mca_id, body, - ) + )) .await?; Ok(application_response) @@ -1089,18 +1102,18 @@ pub async fn webhooks_core payments_incoming_webhook_flow::( + api::WebhookFlow::Payment => Box::pin(payments_incoming_webhook_flow::( state.clone(), merchant_account, business_profile, key_store, webhook_details, source_verified, - ) + )) .await .attach_printable("Incoming webhook flow for payments failed")?, - api::WebhookFlow::Refund => refunds_incoming_webhook_flow::( + api::WebhookFlow::Refund => Box::pin(refunds_incoming_webhook_flow::( state.clone(), merchant_account, business_profile, @@ -1109,7 +1122,7 @@ pub async fn webhooks_core bank_transfer_webhook_flow::( + api::WebhookFlow::BankTransfer => Box::pin(bank_transfer_webhook_flow::( state.clone(), merchant_account, business_profile, key_store, webhook_details, source_verified, - ) + )) .await .attach_printable("Incoming bank-transfer webhook flow failed")?, diff --git a/crates/router/src/db/address.rs b/crates/router/src/db/address.rs index 9244fc022d9e..689d1f9c7891 100644 --- a/crates/router/src/db/address.rs +++ b/crates/router/src/db/address.rs @@ -339,7 +339,7 @@ mod storage { MerchantStorageScheme::RedisKv => { let key = format!("mid_{}_pid_{}", merchant_id, payment_id); let field = format!("add_{}", address_id); - db_utils::try_redis_get_else_try_database_get( + Box::pin(db_utils::try_redis_get_else_try_database_get( async { kv_wrapper( self, @@ -350,7 +350,7 @@ mod storage { .try_into_hget() }, database_call, - ) + )) .await } }?; diff --git a/crates/router/src/db/refund.rs b/crates/router/src/db/refund.rs index c9b9f8ac55f5..8ac8bd106eff 100644 --- a/crates/router/src/db/refund.rs +++ b/crates/router/src/db/refund.rs @@ -310,7 +310,7 @@ mod storage { .await?; let key = &lookup.pk_id; - db_utils::try_redis_get_else_try_database_get( + Box::pin(db_utils::try_redis_get_else_try_database_get( async { kv_wrapper( self, @@ -321,7 +321,7 @@ mod storage { .try_into_hget() }, database_call, - ) + )) .await } } @@ -490,7 +490,7 @@ mod storage { let pattern = db_utils::generate_hscan_pattern_for_refund(&lookup.sk_id); - db_utils::try_redis_get_else_try_database_get( + Box::pin(db_utils::try_redis_get_else_try_database_get( async { kv_wrapper( self, @@ -501,7 +501,7 @@ mod storage { .try_into_scan() }, database_call, - ) + )) .await } } @@ -581,7 +581,7 @@ mod storage { .await?; let key = &lookup.pk_id; - db_utils::try_redis_get_else_try_database_get( + Box::pin(db_utils::try_redis_get_else_try_database_get( async { kv_wrapper( self, @@ -592,7 +592,7 @@ mod storage { .try_into_hget() }, database_call, - ) + )) .await } } @@ -626,7 +626,7 @@ mod storage { .await?; let key = &lookup.pk_id; - db_utils::try_redis_get_else_try_database_get( + Box::pin(db_utils::try_redis_get_else_try_database_get( async { kv_wrapper( self, @@ -637,7 +637,7 @@ mod storage { .try_into_hget() }, database_call, - ) + )) .await } } @@ -664,7 +664,7 @@ mod storage { enums::MerchantStorageScheme::PostgresOnly => database_call().await, enums::MerchantStorageScheme::RedisKv => { let key = format!("mid_{merchant_id}_pid_{payment_id}"); - db_utils::try_redis_get_else_try_database_get( + Box::pin(db_utils::try_redis_get_else_try_database_get( async { kv_wrapper( self, @@ -675,7 +675,7 @@ mod storage { .try_into_scan() }, database_call, - ) + )) .await } } diff --git a/crates/router/src/db/reverse_lookup.rs b/crates/router/src/db/reverse_lookup.rs index 4a4056032b18..445e171fa277 100644 --- a/crates/router/src/db/reverse_lookup.rs +++ b/crates/router/src/db/reverse_lookup.rs @@ -150,7 +150,11 @@ mod storage { .try_into_get() }; - db_utils::try_redis_get_else_try_database_get(redis_fut, database_call).await + Box::pin(db_utils::try_redis_get_else_try_database_get( + redis_fut, + database_call, + )) + .await } } } diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index e106eb06a766..a3ed0b35c785 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -189,7 +189,7 @@ pub async fn start_server(conf: settings::Settings) -> ApplicationResult errors::ApplicationError::ApiClientError(error.current_context().clone()) })?, ); - let state = routes::AppState::new(conf, tx, api_client).await; + let state = Box::pin(routes::AppState::new(conf, tx, api_client)).await; let request_body_limit = server.request_body_limit; let server = actix_web::HttpServer::new(move || mk_app(state.clone(), request_body_limit)) .bind((server.host.as_str(), server.port))? diff --git a/crates/router/src/routes/admin.rs b/crates/router/src/routes/admin.rs index a8eda22402c3..eef8cacc5f92 100644 --- a/crates/router/src/routes/admin.rs +++ b/crates/router/src/routes/admin.rs @@ -30,7 +30,7 @@ pub async fn merchant_account_create( json_payload: web::Json, ) -> HttpResponse { let flow = Flow::MerchantsAccountCreate; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -38,7 +38,7 @@ pub async fn merchant_account_create( |state, _, req| create_merchant_account(state, req), &auth::AdminApiAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Merchant Account - Retrieve @@ -131,7 +131,7 @@ pub async fn update_merchant_account( ) -> HttpResponse { let flow = Flow::MerchantsAccountUpdate; let merchant_id = mid.into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -145,7 +145,7 @@ pub async fn update_merchant_account( req.headers(), ), api_locking::LockAction::NotApplicable, - ) + )) .await } @@ -210,7 +210,7 @@ pub async fn payment_connector_create( ) -> HttpResponse { let flow = Flow::MerchantConnectorsCreate; let merchant_id = path.into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -224,7 +224,7 @@ pub async fn payment_connector_create( req.headers(), ), api_locking::LockAction::NotApplicable, - ) + )) .await } /// Merchant Connector - Retrieve @@ -450,7 +450,7 @@ pub async fn business_profile_create( let payload = json_payload.into_inner(); let merchant_id = path.into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -464,7 +464,7 @@ pub async fn business_profile_create( req.headers(), ), api_locking::LockAction::NotApplicable, - ) + )) .await } #[instrument(skip_all, fields(flow = ?Flow::BusinessProfileRetrieve))] diff --git a/crates/router/src/routes/api_keys.rs b/crates/router/src/routes/api_keys.rs index 1f71f1dc2800..7299aa696390 100644 --- a/crates/router/src/routes/api_keys.rs +++ b/crates/router/src/routes/api_keys.rs @@ -36,7 +36,7 @@ pub async fn api_key_create( let payload = json_payload.into_inner(); let merchant_id = path.into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -61,7 +61,7 @@ pub async fn api_key_create( req.headers(), ), api_locking::LockAction::NotApplicable, - ) + )) .await } /// API Key - Retrieve diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 7f5c720be607..15b6df733489 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -112,56 +112,59 @@ impl AppState { shut_down_signal: oneshot::Sender<()>, api_client: Box, ) -> Self { - #[cfg(feature = "kms")] - let kms_client = kms::get_kms_client(&conf.kms).await; - let testable = storage_impl == StorageImpl::PostgresqlTest; - let store: Box = match storage_impl { - StorageImpl::Postgresql | StorageImpl::PostgresqlTest => Box::new( + Box::pin(async move { + #[cfg(feature = "kms")] + let kms_client = kms::get_kms_client(&conf.kms).await; + let testable = storage_impl == StorageImpl::PostgresqlTest; + let store: Box = match storage_impl { + StorageImpl::Postgresql | StorageImpl::PostgresqlTest => Box::new( + #[allow(clippy::expect_used)] + get_store(&conf, shut_down_signal, testable) + .await + .expect("Failed to create store"), + ), #[allow(clippy::expect_used)] - get_store(&conf, shut_down_signal, testable) - .await - .expect("Failed to create store"), - ), - #[allow(clippy::expect_used)] - StorageImpl::Mock => Box::new( - MockDb::new(&conf.redis) - .await - .expect("Failed to create mock store"), - ), - }; + StorageImpl::Mock => Box::new( + MockDb::new(&conf.redis) + .await + .expect("Failed to create mock store"), + ), + }; + + #[cfg(feature = "olap")] + let pool = crate::analytics::AnalyticsProvider::from_conf( + &conf.analytics, + #[cfg(feature = "kms")] + kms_client, + ) + .await; - #[cfg(feature = "olap")] - let pool = crate::analytics::AnalyticsProvider::from_conf( - &conf.analytics, #[cfg(feature = "kms")] - kms_client, - ) - .await; - - #[cfg(feature = "kms")] - #[allow(clippy::expect_used)] - let kms_secrets = settings::ActiveKmsSecrets { - jwekey: conf.jwekey.clone().into(), - } - .decrypt_inner(kms_client) - .await - .expect("Failed while performing KMS decryption"); - - #[cfg(feature = "email")] - let email_client = Arc::new(AwsSes::new(&conf.email).await); - Self { - flow_name: String::from("default"), - store, - conf: Arc::new(conf), + #[allow(clippy::expect_used)] + let kms_secrets = settings::ActiveKmsSecrets { + jwekey: conf.jwekey.clone().into(), + } + .decrypt_inner(kms_client) + .await + .expect("Failed while performing KMS decryption"); + #[cfg(feature = "email")] - email_client, - #[cfg(feature = "kms")] - kms_secrets: Arc::new(kms_secrets), - api_client, - event_handler: Box::::default(), - #[cfg(feature = "olap")] - pool, - } + let email_client = Arc::new(AwsSes::new(&conf.email).await); + Self { + flow_name: String::from("default"), + store, + conf: Arc::new(conf), + #[cfg(feature = "email")] + email_client, + #[cfg(feature = "kms")] + kms_secrets: Arc::new(kms_secrets), + api_client, + event_handler: Box::::default(), + #[cfg(feature = "olap")] + pool, + } + }) + .await } pub async fn new( @@ -169,7 +172,13 @@ impl AppState { shut_down_signal: oneshot::Sender<()>, api_client: Box, ) -> Self { - Self::with_storage(conf, StorageImpl::Postgresql, shut_down_signal, api_client).await + Box::pin(Self::with_storage( + conf, + StorageImpl::Postgresql, + shut_down_signal, + api_client, + )) + .await } } diff --git a/crates/router/src/routes/customers.rs b/crates/router/src/routes/customers.rs index ff2ffc2a3fe3..cfc37cbdbb2a 100644 --- a/crates/router/src/routes/customers.rs +++ b/crates/router/src/routes/customers.rs @@ -30,7 +30,7 @@ pub async fn customers_create( json_payload: web::Json, ) -> HttpResponse { let flow = Flow::CustomersCreate; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -38,7 +38,7 @@ pub async fn customers_create( |state, auth, req| create_customer(state, auth.merchant_account, auth.key_store, req), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Retrieve Customer @@ -142,7 +142,7 @@ pub async fn customers_update( let flow = Flow::CustomersUpdate; let customer_id = path.into_inner(); json_payload.customer_id = customer_id; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -150,7 +150,7 @@ pub async fn customers_update( |state, auth, req| update_customer(state, auth.merchant_account, req, auth.key_store), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Delete Customer @@ -179,7 +179,7 @@ pub async fn customers_delete( customer_id: path.into_inner(), }) .into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -187,7 +187,7 @@ pub async fn customers_delete( |state, auth, req| delete_customer(state, auth.merchant_account, req, auth.key_store), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } #[instrument(skip_all, fields(flow = ?Flow::CustomersGetMandates))] diff --git a/crates/router/src/routes/disputes.rs b/crates/router/src/routes/disputes.rs index d570a5319687..aaeb118645db 100644 --- a/crates/router/src/routes/disputes.rs +++ b/crates/router/src/routes/disputes.rs @@ -117,7 +117,7 @@ pub async fn accept_dispute( let dispute_id = dispute_types::DisputeId { dispute_id: path.into_inner(), }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -127,7 +127,7 @@ pub async fn accept_dispute( }, auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, - ) + )) .await } /// Disputes - Submit Dispute Evidence @@ -150,7 +150,7 @@ pub async fn submit_dispute_evidence( json_payload: web::Json, ) -> HttpResponse { let flow = Flow::DisputesEvidenceSubmit; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -160,7 +160,7 @@ pub async fn submit_dispute_evidence( }, auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, - ) + )) .await } /// Disputes - Attach Evidence to Dispute @@ -191,7 +191,7 @@ pub async fn attach_dispute_evidence( Ok(valid_request) => valid_request, Err(err) => return api::log_and_return_error_response(err), }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -201,7 +201,7 @@ pub async fn attach_dispute_evidence( }, auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, - ) + )) .await } /// Diputes - Retrieve Dispute @@ -229,7 +229,7 @@ pub async fn retrieve_dispute_evidence( let dispute_id = dispute_types::DisputeId { dispute_id: path.into_inner(), }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -237,6 +237,6 @@ pub async fn retrieve_dispute_evidence( |state, auth, req| disputes::retrieve_dispute_evidence(state, auth.merchant_account, req), auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, - ) + )) .await } diff --git a/crates/router/src/routes/files.rs b/crates/router/src/routes/files.rs index 4a327ba0807d..bde221ebc161 100644 --- a/crates/router/src/routes/files.rs +++ b/crates/router/src/routes/files.rs @@ -39,7 +39,7 @@ pub async fn files_create( Ok(valid_request) => valid_request, Err(err) => return api::log_and_return_error_response(err), }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -47,7 +47,7 @@ pub async fn files_create( |state, auth, req| files_create_core(state, auth.merchant_account, auth.key_store, req), auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, - ) + )) .await } /// Files - Delete @@ -77,7 +77,7 @@ pub async fn files_delete( let file_id = files::FileId { file_id: path.into_inner(), }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -85,7 +85,7 @@ pub async fn files_delete( |state, auth, req| files_delete_core(state, auth.merchant_account, req), auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, - ) + )) .await } /// Files - Retrieve @@ -115,7 +115,7 @@ pub async fn files_retrieve( let file_id = files::FileId { file_id: path.into_inner(), }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -123,6 +123,6 @@ pub async fn files_retrieve( |state, auth, req| files_retrieve_core(state, auth.merchant_account, auth.key_store, req), auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, - ) + )) .await } diff --git a/crates/router/src/routes/payment_link.rs b/crates/router/src/routes/payment_link.rs index b664ee4429d4..7d6bf1a05f09 100644 --- a/crates/router/src/routes/payment_link.rs +++ b/crates/router/src/routes/payment_link.rs @@ -62,7 +62,7 @@ pub async fn initiate_payment_link( payment_id, merchant_id: merchant_id.clone(), }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -77,6 +77,6 @@ pub async fn initiate_payment_link( }, &crate::services::authentication::MerchantIdAuth(merchant_id), api_locking::LockAction::NotApplicable, - ) + )) .await } diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index faaf757fd7e7..83d4c7f96611 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -34,7 +34,7 @@ pub async fn create_payment_method_api( json_payload: web::Json, ) -> HttpResponse { let flow = Flow::PaymentMethodsCreate; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -44,7 +44,7 @@ pub async fn create_payment_method_api( }, &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// List payment methods for a Merchant @@ -84,7 +84,7 @@ pub async fn list_payment_method_api( Err(e) => return api::log_and_return_error_response(e), }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -94,7 +94,7 @@ pub async fn list_payment_method_api( }, &*auth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// List payment methods for a Customer @@ -135,7 +135,7 @@ pub async fn list_customer_payment_method_api( Err(e) => return api::log_and_return_error_response(e), }; let customer_id = customer_id.into_inner().0; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -151,7 +151,7 @@ pub async fn list_customer_payment_method_api( }, &*auth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// List payment methods for a Customer @@ -191,7 +191,7 @@ pub async fn list_customer_payment_method_api_client( Ok((auth, _auth_flow)) => (auth, _auth_flow), Err(e) => return api::log_and_return_error_response(e), }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -207,7 +207,7 @@ pub async fn list_customer_payment_method_api_client( }, &*auth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Payment Method - Retrieve @@ -239,7 +239,7 @@ pub async fn payment_method_retrieve_api( }) .into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -247,7 +247,7 @@ pub async fn payment_method_retrieve_api( |state, _auth, pm| cards::retrieve_payment_method(state, pm), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Payment Method - Update @@ -278,7 +278,7 @@ pub async fn payment_method_update_api( let flow = Flow::PaymentMethodsUpdate; let payment_method_id = path.into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -294,7 +294,7 @@ pub async fn payment_method_update_api( }, &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Payment Method - Delete @@ -324,7 +324,7 @@ pub async fn payment_method_delete_api( let pm = PaymentMethodId { payment_method_id: payment_method_id.into_inner().0, }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -332,7 +332,7 @@ pub async fn payment_method_delete_api( |state, auth, req| cards::delete_payment_method(state, auth.merchant_account, req), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } #[cfg(test)] diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index ed36721da445..b05fae65338a 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -102,7 +102,7 @@ pub async fn payments_create( let locking_action = payload.get_locking_input(flow.clone()); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -123,7 +123,7 @@ pub async fn payments_create( _ => auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), }, locking_action, - ) + )) .await } // /// Payments - Redirect @@ -160,7 +160,7 @@ pub async fn payments_start( let locking_action = payload.get_locking_input(flow.clone()); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -187,7 +187,7 @@ pub async fn payments_start( }, &auth::MerchantIdAuth(merchant_id), locking_action, - ) + )) .await } /// Payments - Retrieve @@ -234,7 +234,7 @@ pub async fn payments_retrieve( let locking_action = payload.get_locking_input(flow.clone()); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -258,7 +258,7 @@ pub async fn payments_retrieve( req.headers(), ), locking_action, - ) + )) .await } /// Payments - Retrieve with gateway credentials @@ -300,7 +300,7 @@ pub async fn payments_retrieve_with_gateway_creds( let locking_action = payload.get_locking_input(flow.clone()); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -320,7 +320,7 @@ pub async fn payments_retrieve_with_gateway_creds( }, &*auth_type, locking_action, - ) + )) .await } /// Payments - Update @@ -367,7 +367,7 @@ pub async fn payments_update( let locking_action = payload.get_locking_input(flow.clone()); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -385,7 +385,7 @@ pub async fn payments_update( }, &*auth_type, locking_action, - ) + )) .await } /// Payments - Confirm @@ -443,7 +443,7 @@ pub async fn payments_confirm( let locking_action = payload.get_locking_input(flow.clone()); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -461,7 +461,7 @@ pub async fn payments_confirm( }, &*auth_type, locking_action, - ) + )) .await } /// Payments - Capture @@ -498,7 +498,7 @@ pub async fn payments_capture( let locking_action = payload.get_locking_input(flow.clone()); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -525,7 +525,7 @@ pub async fn payments_capture( }, &auth::ApiKeyAuth, locking_action, - ) + )) .await } /// Payments - Session token @@ -554,7 +554,7 @@ pub async fn payments_connector_session( let locking_action = payload.get_locking_input(flow.clone()); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -581,7 +581,7 @@ pub async fn payments_connector_session( }, &auth::PublishableKeyAuth, locking_action, - ) + )) .await } // /// Payments - Redirect response @@ -772,7 +772,7 @@ pub async fn payments_cancel( let payment_id = path.into_inner(); payload.payment_id = payment_id; let locking_action = payload.get_locking_input(flow.clone()); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -792,7 +792,7 @@ pub async fn payments_cancel( }, &auth::ApiKeyAuth, locking_action, - ) + )) .await } /// Payments - List diff --git a/crates/router/src/routes/payouts.rs b/crates/router/src/routes/payouts.rs index 15cf59aaf32d..cc47263a0c56 100644 --- a/crates/router/src/routes/payouts.rs +++ b/crates/router/src/routes/payouts.rs @@ -33,7 +33,7 @@ pub async fn payouts_create( json_payload: web::Json, ) -> HttpResponse { let flow = Flow::PayoutsCreate; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -41,7 +41,7 @@ pub async fn payouts_create( |state, auth, req| payouts_create_core(state, auth.merchant_account, auth.key_store, req), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Payouts - Retrieve @@ -72,7 +72,7 @@ pub async fn payouts_retrieve( force_sync: query_params.force_sync, }; let flow = Flow::PayoutsRetrieve; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -80,7 +80,7 @@ pub async fn payouts_retrieve( |state, auth, req| payouts_retrieve_core(state, auth.merchant_account, auth.key_store, req), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Payouts - Update @@ -111,7 +111,7 @@ pub async fn payouts_update( let payout_id = path.into_inner(); let mut payout_update_payload = json_payload.into_inner(); payout_update_payload.payout_id = Some(payout_id); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -119,7 +119,7 @@ pub async fn payouts_update( |state, auth, req| payouts_update_core(state, auth.merchant_account, auth.key_store, req), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Payouts - Cancel @@ -150,7 +150,7 @@ pub async fn payouts_cancel( let mut payload = json_payload.into_inner(); payload.payout_id = path.into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -158,7 +158,7 @@ pub async fn payouts_cancel( |state, auth, req| payouts_cancel_core(state, auth.merchant_account, auth.key_store, req), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Payouts - Fulfill @@ -189,7 +189,7 @@ pub async fn payouts_fulfill( let mut payload = json_payload.into_inner(); payload.payout_id = path.into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -197,7 +197,7 @@ pub async fn payouts_fulfill( |state, auth, req| payouts_fulfill_core(state, auth.merchant_account, auth.key_store, req), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } #[instrument(skip_all, fields(flow = ?Flow::PayoutsAccounts))] diff --git a/crates/router/src/routes/refunds.rs b/crates/router/src/routes/refunds.rs index d1f5cb56fe23..d370af6b8d7a 100644 --- a/crates/router/src/routes/refunds.rs +++ b/crates/router/src/routes/refunds.rs @@ -31,7 +31,7 @@ pub async fn refunds_create( json_payload: web::Json, ) -> HttpResponse { let flow = Flow::RefundsCreate; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -39,7 +39,7 @@ pub async fn refunds_create( |state, auth, req| refund_create_core(state, auth.merchant_account, auth.key_store, req), auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, - ) + )) .await } /// Refunds - Retrieve (GET) @@ -74,7 +74,7 @@ pub async fn refunds_retrieve( }; let flow = Flow::RefundsRetrieve; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -90,7 +90,7 @@ pub async fn refunds_retrieve( }, auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, - ) + )) .await } /// Refunds - Retrieve (POST) @@ -115,7 +115,7 @@ pub async fn refunds_retrieve_with_body( json_payload: web::Json, ) -> HttpResponse { let flow = Flow::RefundsRetrieve; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -131,7 +131,7 @@ pub async fn refunds_retrieve_with_body( }, &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Refunds - Update diff --git a/crates/router/src/routes/verification.rs b/crates/router/src/routes/verification.rs index 2ad061848c92..d0525bb272e8 100644 --- a/crates/router/src/routes/verification.rs +++ b/crates/router/src/routes/verification.rs @@ -18,7 +18,7 @@ pub async fn apple_pay_merchant_registration( let flow = Flow::Verification; let merchant_id = path.into_inner(); let kms_conf = &state.clone().conf.kms; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -34,7 +34,7 @@ pub async fn apple_pay_merchant_registration( }, auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, - ) + )) .await } diff --git a/crates/router/src/routes/webhooks.rs b/crates/router/src/routes/webhooks.rs index 5c90e46bb90b..63f2328ec6ce 100644 --- a/crates/router/src/routes/webhooks.rs +++ b/crates/router/src/routes/webhooks.rs @@ -21,7 +21,7 @@ pub async fn receive_incoming_webhook( let flow = Flow::IncomingWebhookReceive; let (merchant_id, connector_id_or_name) = path.into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -38,6 +38,6 @@ pub async fn receive_incoming_webhook( }, &auth::MerchantIdAuth(merchant_id), api_locking::LockAction::NotApplicable, - ) + )) .await } diff --git a/crates/router/src/types/domain/customer.rs b/crates/router/src/types/domain/customer.rs index 3810523b413f..fe575851dc49 100644 --- a/crates/router/src/types/domain/customer.rs +++ b/crates/router/src/types/domain/customer.rs @@ -99,7 +99,7 @@ impl super::behaviour::Conversion for Customer { } } -#[derive(Debug)] +#[derive(Clone, Debug)] pub enum CustomerUpdate { Update { name: crypto::OptionalEncryptableName, diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index aadb714e8ce2..4933b4d700d3 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -753,9 +753,11 @@ where if let services::ApplicationResponse::JsonWithHeaders((payments_response_json, _)) = payments_response { + let m_state = state.clone(); + Box::pin( webhooks_core::create_event_and_trigger_appropriate_outgoing_webhook( - state.clone(), + m_state, merchant_account, business_profile, event_type, @@ -772,3 +774,16 @@ where Ok(()) } + +type Handle = tokio::task::JoinHandle>; + +pub async fn flatten_join_error(handle: Handle) -> RouterResult { + match handle.await { + Ok(Ok(t)) => Ok(t), + Ok(Err(err)) => Err(err), + Err(err) => Err(err) + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Join Error"), + } +} diff --git a/crates/router/src/workflows/payment_sync.rs b/crates/router/src/workflows/payment_sync.rs index f41b300c5127..00e7357d896f 100644 --- a/crates/router/src/workflows/payment_sync.rs +++ b/crates/router/src/workflows/payment_sync.rs @@ -61,7 +61,13 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow { .await?; let (mut payment_data, _, customer, _, _) = - payment_flows::payments_operation_core::( + Box::pin(payment_flows::payments_operation_core::< + api::PSync, + _, + _, + _, + Oss, + >( state, merchant_account.clone(), key_store, @@ -71,7 +77,7 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow { services::AuthFlow::Client, None, api::HeaderPayload::default(), - ) + )) .await?; let terminal_status = [ @@ -169,7 +175,11 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow { // Trigger the outgoing webhook to notify the merchant about failed payment let operation = operations::PaymentStatus; - utils::trigger_payments_webhook::<_, api_models::payments::PaymentsRequest, _>( + Box::pin(utils::trigger_payments_webhook::< + _, + api_models::payments::PaymentsRequest, + _, + >( merchant_account, business_profile, payment_data, @@ -177,7 +187,7 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow { customer, state, operation, - ) + )) .await .map_err(|error| logger::warn!(payments_outgoing_webhook_error=?error)) .ok(); diff --git a/crates/router/src/workflows/refund_router.rs b/crates/router/src/workflows/refund_router.rs index 8ca3551cfc0f..934c208f9115 100644 --- a/crates/router/src/workflows/refund_router.rs +++ b/crates/router/src/workflows/refund_router.rs @@ -13,7 +13,7 @@ impl ProcessTrackerWorkflow for RefundWorkflowRouter { state: &'a AppState, process: storage::ProcessTracker, ) -> Result<(), errors::ProcessTrackerError> { - Ok(refund_flow::start_refund_workflow(state, &process).await?) + Ok(Box::pin(refund_flow::start_refund_workflow(state, &process)).await?) } async fn error_handler<'a>( diff --git a/crates/router/tests/cache.rs b/crates/router/tests/cache.rs index e1fd3a0f0279..4de45c7132a8 100644 --- a/crates/router/tests/cache.rs +++ b/crates/router/tests/cache.rs @@ -7,10 +7,14 @@ mod utils; #[actix_web::test] async fn invalidate_existing_cache_success() { // Arrange - utils::setup().await; + Box::pin(utils::setup()).await; let (tx, _) = tokio::sync::oneshot::channel(); - let state = - routes::AppState::new(Settings::default(), tx, Box::new(services::MockApiClient)).await; + let state = Box::pin(routes::AppState::new( + Settings::default(), + tx, + Box::new(services::MockApiClient), + )) + .await; let cache_key = "cacheKey".to_string(); let cache_key_value = "val".to_string(); @@ -53,7 +57,7 @@ async fn invalidate_existing_cache_success() { #[actix_web::test] async fn invalidate_non_existing_cache_success() { // Arrange - utils::setup().await; + Box::pin(utils::setup()).await; let cache_key = "cacheKey".to_string(); let api_key = ("api-key", "test_admin"); let client = awc::Client::default(); diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 1f450a19e776..67a0625968fb 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -80,7 +80,7 @@ pub trait ConnectorActions: Connector { ) .await; integration.execute_pretasks(&mut request, &state).await?; - call_connector(request, integration).await + Box::pin(call_connector(request, integration)).await } async fn create_connector_customer( @@ -104,7 +104,7 @@ pub trait ConnectorActions: Connector { ) .await; integration.execute_pretasks(&mut request, &state).await?; - call_connector(request, integration).await + Box::pin(call_connector(request, integration)).await } async fn create_connector_pm_token( @@ -128,7 +128,7 @@ pub trait ConnectorActions: Connector { ) .await; integration.execute_pretasks(&mut request, &state).await?; - call_connector(request, integration).await + Box::pin(call_connector(request, integration)).await } /// For initiating payments when `CaptureMethod` is set to `Automatic` @@ -156,7 +156,7 @@ pub trait ConnectorActions: Connector { ) .await; integration.execute_pretasks(&mut request, &state).await?; - call_connector(request, integration).await + Box::pin(call_connector(request, integration)).await } async fn sync_payment( @@ -169,7 +169,7 @@ pub trait ConnectorActions: Connector { payment_data.unwrap_or_else(|| PaymentSyncType::default().0), payment_info, ); - call_connector(request, integration).await + Box::pin(call_connector(request, integration)).await } /// will retry the psync till the given status matches or retry max 3 times @@ -207,7 +207,7 @@ pub trait ConnectorActions: Connector { }, payment_info, ); - call_connector(request, integration).await + Box::pin(call_connector(request, integration)).await } async fn authorize_and_capture_payment( @@ -243,7 +243,7 @@ pub trait ConnectorActions: Connector { }, payment_info, ); - call_connector(request, integration).await + Box::pin(call_connector(request, integration)).await } async fn authorize_and_void_payment( @@ -280,7 +280,7 @@ pub trait ConnectorActions: Connector { }, payment_info, ); - call_connector(request, integration).await + Box::pin(call_connector(request, integration)).await } async fn capture_payment_and_refund( @@ -400,7 +400,7 @@ pub trait ConnectorActions: Connector { }), payment_info, ); - call_connector(request, integration).await + Box::pin(call_connector(request, integration)).await } /// will retry the rsync till the given status matches or retry max 3 times diff --git a/crates/router/tests/customers.rs b/crates/router/tests/customers.rs index aa17635388fd..065f98fe6609 100644 --- a/crates/router/tests/customers.rs +++ b/crates/router/tests/customers.rs @@ -10,7 +10,7 @@ mod utils; #[ignore] // verify the API-KEY/merchant id has stripe as first choice async fn customer_success() { - utils::setup().await; + Box::pin(utils::setup()).await; let customer_id = format!("customer_{}", uuid::Uuid::new_v4()); let api_key = ("API-KEY", "MySecretApiKey"); @@ -79,7 +79,7 @@ async fn customer_success() { #[ignore] // verify the API-KEY/merchant id has stripe as first choice async fn customer_failure() { - utils::setup().await; + Box::pin(utils::setup()).await; let customer_id = format!("customer_{}", uuid::Uuid::new_v4()); let api_key = ("api-key", "MySecretApiKey"); diff --git a/crates/router/tests/integration_demo.rs b/crates/router/tests/integration_demo.rs index 16e7ead0a383..5bdf9a5f525e 100644 --- a/crates/router/tests/integration_demo.rs +++ b/crates/router/tests/integration_demo.rs @@ -10,7 +10,7 @@ use utils::{mk_service, ApiKey, AppClient, MerchantId, PaymentId, Status}; /// 1) Create Merchant account #[actix_web::test] async fn create_merchant_account() { - let server = mk_service().await; + let server = Box::pin(mk_service()).await; let client = AppClient::guest(); let admin_client = client.admin("test_admin"); @@ -59,7 +59,7 @@ async fn create_merchant_account() { #[actix_web::test] async fn partial_refund() { let authentication = ConnectorAuthentication::new(); - let server = mk_service().await; + let server = Box::pin(mk_service()).await; let client = AppClient::guest(); let admin_client = client.admin("test_admin"); @@ -125,7 +125,7 @@ async fn partial_refund() { #[actix_web::test] async fn exceed_refund() { let authentication = ConnectorAuthentication::new(); - let server = mk_service().await; + let server = Box::pin(mk_service()).await; let client = AppClient::guest(); let admin_client = client.admin("test_admin"); diff --git a/crates/router/tests/payments.rs b/crates/router/tests/payments.rs index d2d6c48507e5..9d48aaddd451 100644 --- a/crates/router/tests/payments.rs +++ b/crates/router/tests/payments.rs @@ -24,7 +24,7 @@ use uuid::Uuid; #[ignore] // verify the API-KEY/merchant id has stripe as first choice async fn payments_create_stripe() { - utils::setup().await; + Box::pin(utils::setup()).await; let payment_id = format!("test_{}", uuid::Uuid::new_v4()); let api_key = ("API-KEY", "MySecretApiKey"); @@ -93,7 +93,7 @@ async fn payments_create_stripe() { #[ignore] // verify the API-KEY/merchant id has adyen as first choice async fn payments_create_adyen() { - utils::setup().await; + Box::pin(utils::setup()).await; let payment_id = format!("test_{}", uuid::Uuid::new_v4()); let api_key = ("API-KEY", "321"); @@ -162,7 +162,7 @@ async fn payments_create_adyen() { // verify the API-KEY/merchant id has stripe as first choice #[ignore] async fn payments_create_fail() { - utils::setup().await; + Box::pin(utils::setup()).await; let payment_id = format!("test_{}", uuid::Uuid::new_v4()); let api_key = ("API-KEY", "MySecretApiKey"); @@ -221,7 +221,7 @@ async fn payments_create_fail() { #[actix_web::test] #[ignore] async fn payments_todo() { - utils::setup().await; + Box::pin(utils::setup()).await; let client = awc::Client::default(); let mut response; @@ -360,20 +360,26 @@ async fn payments_create_core() { }; let expected_response = services::ApplicationResponse::JsonWithHeaders((expected_response, vec![])); - let actual_response = - payments::payments_core::( - state, - merchant_account, - key_store, - payments::PaymentCreate, - req, - services::AuthFlow::Merchant, - payments::CallConnectorAction::Trigger, - None, - api::HeaderPayload::default(), - ) - .await - .unwrap(); + let actual_response = Box::pin(payments::payments_core::< + api::Authorize, + api::PaymentsResponse, + _, + _, + _, + Oss, + >( + state, + merchant_account, + key_store, + payments::PaymentCreate, + req, + services::AuthFlow::Merchant, + payments::CallConnectorAction::Trigger, + None, + api::HeaderPayload::default(), + )) + .await + .unwrap(); assert_eq!(expected_response, actual_response); } @@ -531,19 +537,25 @@ async fn payments_create_core_adyen_no_redirect() { }, vec![], )); - let actual_response = - payments::payments_core::( - state, - merchant_account, - key_store, - payments::PaymentCreate, - req, - services::AuthFlow::Merchant, - payments::CallConnectorAction::Trigger, - None, - api::HeaderPayload::default(), - ) - .await - .unwrap(); + let actual_response = Box::pin(payments::payments_core::< + api::Authorize, + api::PaymentsResponse, + _, + _, + _, + Oss, + >( + state, + merchant_account, + key_store, + payments::PaymentCreate, + req, + services::AuthFlow::Merchant, + payments::CallConnectorAction::Trigger, + None, + api::HeaderPayload::default(), + )) + .await + .unwrap(); assert_eq!(expected_response, actual_response); } diff --git a/crates/router/tests/payments2.rs b/crates/router/tests/payments2.rs index ed8827a910be..5d4ca844061f 100644 --- a/crates/router/tests/payments2.rs +++ b/crates/router/tests/payments2.rs @@ -120,7 +120,7 @@ async fn payments_create_core() { }; let expected_response = services::ApplicationResponse::JsonWithHeaders((expected_response, vec![])); - let actual_response = router::core::payments::payments_core::< + let actual_response = Box::pin(router::core::payments::payments_core::< api::Authorize, api::PaymentsResponse, _, @@ -137,7 +137,7 @@ async fn payments_create_core() { payments::CallConnectorAction::Trigger, None, api::HeaderPayload::default(), - ) + )) .await .unwrap(); assert_eq!(expected_response, actual_response); @@ -299,7 +299,7 @@ async fn payments_create_core_adyen_no_redirect() { }, vec![], )); - let actual_response = router::core::payments::payments_core::< + let actual_response = Box::pin(router::core::payments::payments_core::< api::Authorize, api::PaymentsResponse, _, @@ -316,7 +316,7 @@ async fn payments_create_core_adyen_no_redirect() { payments::CallConnectorAction::Trigger, None, api::HeaderPayload::default(), - ) + )) .await .unwrap(); assert_eq!(expected_response, actual_response); diff --git a/crates/router/tests/payouts.rs b/crates/router/tests/payouts.rs index 566930cd4e31..ab0bc891a7cc 100644 --- a/crates/router/tests/payouts.rs +++ b/crates/router/tests/payouts.rs @@ -4,7 +4,7 @@ mod utils; #[actix_web::test] async fn payouts_todo() { - utils::setup().await; + Box::pin(utils::setup()).await; let client = awc::Client::default(); let mut response; diff --git a/crates/router/tests/refunds.rs b/crates/router/tests/refunds.rs index c9e08d223503..6b9dfd5ed4a2 100644 --- a/crates/router/tests/refunds.rs +++ b/crates/router/tests/refunds.rs @@ -11,7 +11,7 @@ mod utils; #[actix_web::test] // verify the API-KEY/merchant id has stripe as first choice async fn refund_create_fail_stripe() { - let app = mk_service().await; + let app = Box::pin(mk_service()).await; let client = AppClient::guest(); let user_client = client.user("321"); @@ -25,7 +25,7 @@ async fn refund_create_fail_stripe() { #[actix_web::test] // verify the API-KEY/merchant id has adyen as first choice async fn refund_create_fail_adyen() { - let app = mk_service().await; + let app = Box::pin(mk_service()).await; let client = AppClient::guest(); let user_client = client.user("321"); @@ -39,7 +39,7 @@ async fn refund_create_fail_adyen() { #[actix_web::test] #[ignore] async fn refunds_todo() { - utils::setup().await; + Box::pin(utils::setup()).await; let client = awc::Client::default(); let mut response; diff --git a/crates/router/tests/services.rs b/crates/router/tests/services.rs index 64f1c3d8ee1b..eff7fe7f8738 100644 --- a/crates/router/tests/services.rs +++ b/crates/router/tests/services.rs @@ -10,8 +10,12 @@ async fn get_redis_conn_failure() { // Arrange utils::setup().await; let (tx, _) = tokio::sync::oneshot::channel(); - let state = - routes::AppState::new(Settings::default(), tx, Box::new(services::MockApiClient)).await; + let state = Box::pin(routes::AppState::new( + Settings::default(), + tx, + Box::new(services::MockApiClient), + )) + .await; let _ = state.store.get_redis_conn().map(|conn| { conn.is_redis_available @@ -28,10 +32,14 @@ async fn get_redis_conn_failure() { #[tokio::test] async fn get_redis_conn_success() { // Arrange - utils::setup().await; + Box::pin(utils::setup()).await; let (tx, _) = tokio::sync::oneshot::channel(); - let state = - routes::AppState::new(Settings::default(), tx, Box::new(services::MockApiClient)).await; + let state = Box::pin(routes::AppState::new( + Settings::default(), + tx, + Box::new(services::MockApiClient), + )) + .await; // Act let result = state.store.get_redis_conn(); diff --git a/crates/router/tests/utils.rs b/crates/router/tests/utils.rs index 274c011df7a0..6cddbc043662 100644 --- a/crates/router/tests/utils.rs +++ b/crates/router/tests/utils.rs @@ -20,7 +20,7 @@ static SERVER: OnceCell = OnceCell::const_new(); async fn spawn_server() -> bool { let conf = Settings::new().expect("invalid settings"); - let server = router::start_server(conf) + let server = Box::pin(router::start_server(conf)) .await .expect("failed to create server"); @@ -29,7 +29,7 @@ async fn spawn_server() -> bool { } pub async fn setup() { - SERVER.get_or_init(spawn_server).await; + Box::pin(SERVER.get_or_init(spawn_server)).await; } const STRIPE_MOCK: &str = "http://localhost:12111/"; diff --git a/crates/storage_impl/Cargo.toml b/crates/storage_impl/Cargo.toml index 31115e91589f..77589cc7d782 100644 --- a/crates/storage_impl/Cargo.toml +++ b/crates/storage_impl/Cargo.toml @@ -26,7 +26,7 @@ router_env = { version = "0.1.0", path = "../router_env" } # Third party crates actix-web = "4.3.1" -async-bb8-diesel = "0.1.0" +async-bb8-diesel = { git = "https://github.com/jarnura/async-bb8-diesel", rev = "53b4ab901aab7635c8215fd1c2d542c8db443094" } async-trait = "0.1.72" bb8 = "0.8.1" bytes = "1.4.0" diff --git a/crates/storage_impl/src/lib.rs b/crates/storage_impl/src/lib.rs index 00d8703940c7..dc0dea4bb59c 100644 --- a/crates/storage_impl/src/lib.rs +++ b/crates/storage_impl/src/lib.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use data_models::errors::{StorageError, StorageResult}; -use diesel_models::{self as store}; +use diesel_models as store; use error_stack::ResultExt; use masking::StrongSecret; use redis::{kv_store::RedisConnInterface, RedisStore}; diff --git a/crates/storage_impl/src/lookup.rs b/crates/storage_impl/src/lookup.rs index dbfd77a8d6a0..bd045fedd379 100644 --- a/crates/storage_impl/src/lookup.rs +++ b/crates/storage_impl/src/lookup.rs @@ -135,7 +135,11 @@ impl ReverseLookupInterface for KVRouterStore { .try_into_get() }; - try_redis_get_else_try_database_get(redis_fut, database_call).await + Box::pin(try_redis_get_else_try_database_get( + redis_fut, + database_call, + )) + .await } } } diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index d34230e2cb49..3d00e2f2bf7a 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -557,12 +557,12 @@ impl PaymentAttemptInterface for KVRouterStore { .await?; let key = &lookup.pk_id; - try_redis_get_else_try_database_get( + Box::pin(try_redis_get_else_try_database_get( async { kv_wrapper(self, KvOperation::::HGet(&lookup.sk_id), key).await?.try_into_hget() }, || async {self.router_store.find_payment_attempt_by_connector_transaction_id_payment_id_merchant_id(connector_transaction_id, payment_id, merchant_id, storage_scheme).await}, - ) + )) .await } } @@ -607,7 +607,11 @@ impl PaymentAttemptInterface for KVRouterStore { )) }) }; - try_redis_get_else_try_database_get(redis_fut, database_call).await + Box::pin(try_redis_get_else_try_database_get( + redis_fut, + database_call, + )) + .await } } } @@ -635,7 +639,7 @@ impl PaymentAttemptInterface for KVRouterStore { .await?; let key = &lookup.pk_id; - try_redis_get_else_try_database_get( + Box::pin(try_redis_get_else_try_database_get( async { kv_wrapper( self, @@ -654,7 +658,7 @@ impl PaymentAttemptInterface for KVRouterStore { ) .await }, - ) + )) .await } } @@ -682,7 +686,7 @@ impl PaymentAttemptInterface for KVRouterStore { MerchantStorageScheme::RedisKv => { let key = format!("mid_{merchant_id}_pid_{payment_id}"); let field = format!("pa_{attempt_id}"); - try_redis_get_else_try_database_get( + Box::pin(try_redis_get_else_try_database_get( async { kv_wrapper(self, KvOperation::::HGet(&field), key) .await? @@ -698,7 +702,7 @@ impl PaymentAttemptInterface for KVRouterStore { ) .await }, - ) + )) .await } } @@ -726,7 +730,7 @@ impl PaymentAttemptInterface for KVRouterStore { .get_lookup_by_lookup_id(&lookup_id, storage_scheme) .await?; let key = &lookup.pk_id; - try_redis_get_else_try_database_get( + Box::pin(try_redis_get_else_try_database_get( async { kv_wrapper( self, @@ -745,7 +749,7 @@ impl PaymentAttemptInterface for KVRouterStore { ) .await }, - ) + )) .await } } @@ -774,7 +778,7 @@ impl PaymentAttemptInterface for KVRouterStore { .await?; let key = &lookup.pk_id; - try_redis_get_else_try_database_get( + Box::pin(try_redis_get_else_try_database_get( async { kv_wrapper( self, @@ -793,7 +797,7 @@ impl PaymentAttemptInterface for KVRouterStore { ) .await }, - ) + )) .await } } diff --git a/crates/storage_impl/src/payments/payment_intent.rs b/crates/storage_impl/src/payments/payment_intent.rs index 2dc5cdd1c026..c3b3d22ffe35 100644 --- a/crates/storage_impl/src/payments/payment_intent.rs +++ b/crates/storage_impl/src/payments/payment_intent.rs @@ -39,7 +39,7 @@ use crate::connection; use crate::{ diesel_error_to_data_error, redis::kv_store::{kv_wrapper, KvOperation}, - utils::{pg_connection_read, pg_connection_write}, + utils::{self, pg_connection_read, pg_connection_write}, DataModelExt, DatabaseStore, KVRouterStore, }; @@ -206,7 +206,7 @@ impl PaymentIntentInterface for KVRouterStore { MerchantStorageScheme::RedisKv => { let key = format!("mid_{merchant_id}_pid_{payment_id}"); let field = format!("pi_{payment_id}"); - crate::utils::try_redis_get_else_try_database_get( + Box::pin(utils::try_redis_get_else_try_database_get( async { kv_wrapper::( self, @@ -217,7 +217,7 @@ impl PaymentIntentInterface for KVRouterStore { .try_into_hget() }, database_call, - ) + )) .await } } From b1290234ba13de2dd8cc4210f63bae514c2988b4 Mon Sep 17 00:00:00 2001 From: SamraatBansal <55536657+SamraatBansal@users.noreply.github.com> Date: Thu, 16 Nov 2023 14:58:45 +0530 Subject: [PATCH 012/146] fix(connector): [noon] add validate psync reference (#2886) --- crates/router/src/connector/noon.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/router/src/connector/noon.rs b/crates/router/src/connector/noon.rs index 866f8f4c58fa..0ea73efd94bd 100644 --- a/crates/router/src/connector/noon.rs +++ b/crates/router/src/connector/noon.rs @@ -154,6 +154,14 @@ impl ConnectorValidation for Noon { ), } } + + fn validate_psync_reference_id( + &self, + _data: &types::PaymentsSyncRouterData, + ) -> CustomResult<(), errors::ConnectorError> { + // since we can make psync call with our reference_id, having connector_transaction_id is not an mandatory criteria + Ok(()) + } } impl ConnectorIntegration From 5956242588ef7bdbaa1804a952d48dc47c6e15f1 Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Thu, 16 Nov 2023 15:07:53 +0530 Subject: [PATCH 013/146] fix: paypal postman collection changes for surcharge feature (#2884) --- .../Payments - Confirm/request.json | 10 ++++++++++ .../Payments - Create/event.test.js | 4 ++-- .../Payments - Create/request.json | 10 ---------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Confirm/request.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Confirm/request.json index c60989439784..8559af25e82c 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Confirm/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Confirm/request.json @@ -42,6 +42,16 @@ "surcharge_details": { "surcharge_amount": 5, "tax_amount": 5 + }, + "payment_method": "card", + "payment_method_data": { + "card": { + "card_number": "4012000033330026", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } } } }, diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/event.test.js b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/event.test.js index fe83ca7852a5..b6d04374f6b3 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/event.test.js +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/event.test.js @@ -63,9 +63,9 @@ if (jsonData?.client_secret) { // Response body should have value "requires_confirmation" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments - Content check if value for 'status' matches 'requires_capture'", + "[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'", function () { - pm.expect(jsonData.status).to.eql("requires_confirmation"); + pm.expect(jsonData.status).to.eql("requires_payment_method"); }, ); } diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/request.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/request.json index 8cf69c5039f6..f7d813c34efd 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/request.json @@ -31,16 +31,6 @@ "description": "Its my first payment request", "authentication_type": "no_three_ds", "return_url": "https://duck.com", - "payment_method": "card", - "payment_method_data": { - "card": { - "card_number": "4012000033330026", - "card_exp_month": "10", - "card_exp_year": "25", - "card_holder_name": "joseph Doe", - "card_cvc": "123" - } - }, "billing": { "address": { "line1": "1467", From d4d2c2c7076a46996aa0aa74d1df827169f73155 Mon Sep 17 00:00:00 2001 From: Kashif <46213975+kashif-m@users.noreply.github.com> Date: Thu, 16 Nov 2023 15:26:04 +0530 Subject: [PATCH 014/146] fix(payment_link): render SDK for status requires_payment_method (#2887) Co-authored-by: Kashif --- .../src/core/payment_link/payment_link.html | 131 +++++++++++------- 1 file changed, 84 insertions(+), 47 deletions(-) diff --git a/crates/router/src/core/payment_link/payment_link.html b/crates/router/src/core/payment_link/payment_link.html index e02bc16e7197..abacf0998f67 100644 --- a/crates/router/src/core/payment_link/payment_link.html +++ b/crates/router/src/core/payment_link/payment_link.html @@ -871,8 +871,8 @@ var widgets = null; var unifiedCheckout = null; - const pub_key = window.__PAYMENT_DETAILS.pub_key; - const hyper = Hyper(pub_key); + var pub_key = window.__PAYMENT_DETAILS.pub_key; + var hyper = Hyper(pub_key); function mountUnifiedCheckout(id) { if (unifiedCheckout !== null) { @@ -881,9 +881,9 @@ } async function initialize() { - const paymentDetails = window.__PAYMENT_DETAILS; + var paymentDetails = window.__PAYMENT_DETAILS; var client_secret = paymentDetails.client_secret; - const appearance = { + var appearance = { variables: { colorPrimary: paymentDetails.sdk_theme || "rgb(0, 109, 249)", fontFamily: "Work Sans, sans-serif", @@ -902,7 +902,7 @@ clientSecret: client_secret, }); - const unifiedCheckoutOptions = { + var unifiedCheckoutOptions = { layout: "tabs", sdkHandleConfirmPayment: true, branding: "never", @@ -930,16 +930,16 @@ initialize(); async function handleSubmit(e) { - const paymentDetails = window.__PAYMENT_DETAILS; - const { error, data, status } = await hyper.confirmPayment({ + var paymentDetails = window.__PAYMENT_DETAILS; + var { error, data, status } = await hyper.confirmPayment({ widgets, confirmParams: { // Make sure to change this to your payment completion page return_url: paymentDetails.return_url, }, }); - // This point will only be reached if there is an immediate error occurring while confirming the payment. Otherwise, your customer will be redirected to your `return_url`. - // For some payment flows such as Sofort, iDEAL, your customer will be redirected to an intermediate page to complete authorization of the payment, and then redirected to the `return_url`. + // This point will only be reached if there is an immediate error occurring while confirming the payment. Otherwise, your customer will be redirected to your 'return_url'. + // For some payment flows such as Sofort, iDEAL, your customer will be redirected to an intermediate page to complete authorization of the payment, and then redirected to the 'return_url'. if (error) { if (error.type === "validation_error") { @@ -951,7 +951,7 @@ // Re-initialize SDK mountUnifiedCheckout("#unified-checkout"); } else { - const { paymentIntent } = await hyper.retrievePaymentIntent( + var { paymentIntent } = await hyper.retrievePaymentIntent( paymentDetails.client_secret ); if (paymentIntent && paymentIntent.status) { @@ -966,8 +966,8 @@ // Fetches the payment status after payment submission async function checkStatus() { - const paymentDetails = window.__PAYMENT_DETAILS; - const res = { + var paymentDetails = window.__PAYMENT_DETAILS; + var res = { showSdk: true, }; @@ -975,25 +975,50 @@ "payment_intent_client_secret" ); + // If clientSecret is not found in URL params, try to fetch from window context if (!clientSecret) { clientSecret = paymentDetails.client_secret; } + // If clientSecret is not present, show status if (!clientSecret) { + res.showSdk = false; + showStatus( + Object.assign({}, paymentDetails, { + status: "", + error: { + code: "NO_CLIENT_SECRET", + message: "client_secret not found", + }, + }) + ); return res; } - const { paymentIntent } = await hyper.retrievePaymentIntent( - clientSecret - ); - - if ( - !paymentIntent || - paymentIntent.status === "requires_confirmation" - ) { + var { paymentIntent } = await hyper.retrievePaymentIntent(clientSecret); + + // If paymentIntent was not found, show status + if (!paymentIntent) { + res.showSdk = false; + showStatus( + Object.assign({}, paymentDetails, { + status: "", + error: { + code: "NOT_FOUND", + message: "PaymentIntent was not found", + }, + }) + ); return res; } + // Show SDK only if paymentIntent status has not been initiated + switch (paymentIntent.status) { + case "requires_confirmation": + case "requires_payment_method": + return res; + } + showStatus(paymentIntent); res.showSdk = false; @@ -1021,7 +1046,7 @@ } function showStatus(paymentDetails) { - const status = paymentDetails.status; + var status = paymentDetails.status; let statusDetails = { imageSource: "", message: null, @@ -1038,7 +1063,7 @@ // Status specific information switch (status) { case "succeeded": - statusDetails.imageSource = "https://i.imgur.com/5BOmYVl.img"; + statusDetails.imageSource = "https://i.imgur.com/5BOmYVl.png"; statusDetails.message = "We have successfully received your payment"; statusDetails.status = "Paid successfully"; @@ -1079,17 +1104,28 @@ statusDetails.status = "Payment under review"; break; + case "requires_capture": + statusDetails.imageSource = "https://i.imgur.com/Yb79Qt4.png"; + statusDetails.status = "Payment Pending"; + break; + + case "partially_captured": + statusDetails.imageSource = "https://i.imgur.com/Yb79Qt4.png"; + statusDetails.message = "Partial payment was captured."; + statusDetails.status = "Partial Payment Pending"; + break; + default: statusDetails.imageSource = "https://i.imgur.com/UD8CEuY.png"; statusDetails.status = "Something went wrong"; // Error details if (typeof paymentDetails.error === "object") { var errorCodeNode = createItem( - "ERROR CODE", + "Error Code", paymentDetails.error.code ); var errorMessageNode = createItem( - "ERROR MESSAGE", + "Error Message", paymentDetails.error.message ); // @ts-ignore @@ -1105,7 +1141,7 @@ paymentDetails.currency + " " + paymentDetails.amount; var merchantLogoNode = document.createElement("img"); merchantLogoNode.className = "hyper-checkout-status-merchant-logo"; - merchantLogoNode.src = ""; + merchantLogoNode.src = window.__PAYMENT_DETAILS.merchant_logo; merchantLogoNode.alt = ""; // Form content items @@ -1123,13 +1159,13 @@ // Append items statusDetails.items.map((item) => statusDetailsNode?.append(item)); - const statusHeaderNode = document.getElementById( + var statusHeaderNode = document.getElementById( "hyper-checkout-status-header" ); if (statusHeaderNode !== null) { statusHeaderNode.append(amountNode, merchantLogoNode); } - const statusContentNode = document.getElementById( + var statusContentNode = document.getElementById( "hyper-checkout-status-content" ); if (statusContentNode !== null) { @@ -1171,7 +1207,7 @@ } function renderPaymentDetails() { - const paymentDetails = window.__PAYMENT_DETAILS; + var paymentDetails = window.__PAYMENT_DETAILS; // Create price node var priceNode = document.createElement("div"); @@ -1221,14 +1257,14 @@ } function renderCart() { - const paymentDetails = window.__PAYMENT_DETAILS; - const orderDetails = paymentDetails.order_details; + var paymentDetails = window.__PAYMENT_DETAILS; + var orderDetails = paymentDetails.order_details; var cartNode = document.getElementById("hyper-checkout-cart"); var cartItemsNode = document.getElementById( "hyper-checkout-cart-items" ); - const MAX_ITEMS_VISIBLE_AFTER_COLLAPSE = + var MAX_ITEMS_VISIBLE_AFTER_COLLAPSE = paymentDetails.max_items_visible_after_collapse; // Cart items @@ -1247,7 +1283,7 @@ } // Expand / collapse button - const totalItems = orderDetails.length; + var totalItems = orderDetails.length; if (totalItems > MAX_ITEMS_VISIBLE_AFTER_COLLAPSE) { var expandButtonNode = document.createElement("div"); expandButtonNode.className = "hyper-checkout-cart-button"; @@ -1255,9 +1291,9 @@ var buttonImageNode = document.createElement("img"); var buttonTextNode = document.createElement("span"); buttonTextNode.id = "hyper-checkout-cart-button-text"; - const hiddenItemsCount = + var hiddenItemsCount = orderDetails.length - MAX_ITEMS_VISIBLE_AFTER_COLLAPSE; - buttonTextNode.innerText = `Show More (${hiddenItemsCount})`; + buttonTextNode.innerText = "Show More (" + hiddenItemsCount + ")"; expandButtonNode.append(buttonTextNode, buttonImageNode); cartNode.append(expandButtonNode); } @@ -1307,9 +1343,9 @@ } function handleCartView() { - const paymentDetails = window.__PAYMENT_DETAILS; - const orderDetails = paymentDetails.order_details; - const MAX_ITEMS_VISIBLE_AFTER_COLLAPSE = + var paymentDetails = window.__PAYMENT_DETAILS; + var orderDetails = paymentDetails.order_details; + var MAX_ITEMS_VISIBLE_AFTER_COLLAPSE = paymentDetails.max_items_visible_after_collapse; var itemsHTMLCollection = document.getElementsByClassName( "hyper-checkout-cart-item" @@ -1347,7 +1383,7 @@ cartItemsNode.style.maxHeight = "354px"; cartItemsNode.style.height = "354px"; cartItemsNode.scrollTo({ top: 0, behavior: "smooth" }); - setTimeout(() => { + setTimeout(function () { cartItems.map((item, index) => { if (index < MAX_ITEMS_VISIBLE_AFTER_COLLAPSE) { return; @@ -1361,34 +1397,35 @@ cartItemsNode.removeChild(item); }); }, 300); - setTimeout(() => { - const hiddenItemsCount = + setTimeout(function () { + var hiddenItemsCount = orderDetails.length - MAX_ITEMS_VISIBLE_AFTER_COLLAPSE; - cartButtonTextNode.innerText = `Show More (${hiddenItemsCount})`; + cartButtonTextNode.innerText = + "Show More (" + hiddenItemsCount + ")"; }, 250); } } function hideCartInMobileView() { window.history.back(); - const cartNode = document.getElementById("hyper-checkout-cart"); + var cartNode = document.getElementById("hyper-checkout-cart"); cartNode.style.animation = "slide-to-right 0.3s linear"; cartNode.style.right = "-582px"; - setTimeout(() => { + setTimeout(function () { hide("#hyper-checkout-cart"); }, 300); } function viewCartInMobileView() { window.history.pushState("view-cart", ""); - const cartNode = document.getElementById("hyper-checkout-cart"); + var cartNode = document.getElementById("hyper-checkout-cart"); cartNode.style.animation = "slide-from-right 0.3s linear"; cartNode.style.right = "0px"; show("#hyper-checkout-cart"); } function renderSDKHeader() { - const paymentDetails = window.__PAYMENT_DETAILS; + var paymentDetails = window.__PAYMENT_DETAILS; // SDK headers' items var sdkHeaderItemNode = document.createElement("div"); @@ -1443,8 +1480,8 @@ } window.addEventListener("resize", (event) => { - const currentHeight = window.innerHeight; - const currentWidth = window.innerWidth; + var currentHeight = window.innerHeight; + var currentWidth = window.innerWidth; if (currentWidth <= 1200 && window.state.prevWidth > 1200) { hide("#hyper-checkout-cart"); } else if (currentWidth > 1200 && window.state.prevWidth <= 1200) { From 5c313656a129362b0e905e5fbf349dbbec57199c Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 16 Nov 2023 11:43:26 +0000 Subject: [PATCH 015/146] test(postman): update postman collection files --- postman/collection-json/paypal.postman_collection.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/postman/collection-json/paypal.postman_collection.json b/postman/collection-json/paypal.postman_collection.json index 4849a27fe051..d9deae47f9af 100644 --- a/postman/collection-json/paypal.postman_collection.json +++ b/postman/collection-json/paypal.postman_collection.json @@ -747,9 +747,9 @@ "// Response body should have value \"requires_confirmation\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", " },", " );", "}", @@ -808,7 +808,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"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://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"paypal\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"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://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"paypal\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -990,7 +990,7 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"surcharge_details\":{\"surcharge_amount\":5,\"tax_amount\":5}}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"surcharge_details\":{\"surcharge_amount\":5,\"tax_amount\":5},\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}}}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", From bd55e57a5ba337faa18d7ac7944db678b8720bd7 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 16 Nov 2023 11:43:26 +0000 Subject: [PATCH 016/146] chore(version): v1.79.0 --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5da650def02..bffafeb53bd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,27 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.79.0 (2023-11-16) + +### Features + +- Change async-bb8 fork and tokio spawn for concurrent database calls ([#2774](https://github.com/juspay/hyperswitch/pull/2774)) ([`d634fde`](https://github.com/juspay/hyperswitch/commit/d634fdeac349b92e3619234580299a6c6c38e6d4)) + +### Bug Fixes + +- **connector:** [noon] add validate psync reference ([#2886](https://github.com/juspay/hyperswitch/pull/2886)) ([`b129023`](https://github.com/juspay/hyperswitch/commit/b1290234ba13de2dd8cc4210f63bae514c2988b4)) +- **payment_link:** Render SDK for status requires_payment_method ([#2887](https://github.com/juspay/hyperswitch/pull/2887)) ([`d4d2c2c`](https://github.com/juspay/hyperswitch/commit/d4d2c2c7076a46996aa0aa74d1df827169f73155)) +- Paypal postman collection changes for surcharge feature ([#2884](https://github.com/juspay/hyperswitch/pull/2884)) ([`5956242`](https://github.com/juspay/hyperswitch/commit/5956242588ef7bdbaa1804a952d48dc47c6e15f1)) + +### Testing + +- **postman:** Update postman collection files ([`5c31365`](https://github.com/juspay/hyperswitch/commit/5c313656a129362b0e905e5fbf349dbbec57199c)) + +**Full Changelog:** [`v1.78.0...v1.79.0`](https://github.com/juspay/hyperswitch/compare/v1.78.0...v1.79.0) + +- - - + + ## 1.78.0 (2023-11-14) ### Features From f248fe2889c9cb68af4464ab0db1735224ab5c8d Mon Sep 17 00:00:00 2001 From: Arun Raj M Date: Thu, 16 Nov 2023 15:42:10 +0530 Subject: [PATCH 017/146] feat: spawn webhooks and async scheduling in background (#2780) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: akshay-97 Co-authored-by: akshay.s Co-authored-by: Kartikeya Hegde --- .../payments/operations/payment_confirm.rs | 22 +++++++++- crates/router/src/utils.rs | 40 ++++++++++++------- 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 88462e7f8563..afb7f110ed5d 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -493,7 +493,27 @@ impl Domain, ) -> CustomResult<(), errors::ApiErrorResponse> { - helpers::add_domain_task_to_pt(self, state, payment_attempt, requeue, schedule_time).await + // This spawns this futures in a background thread, the exception inside this future won't affect + // the current thread and the lifecycle of spawn thread is not handled by runtime. + // So when server shutdown won't wait for this thread's completion. + let m_payment_attempt = payment_attempt.clone(); + let m_state = state.clone(); + let m_self = *self; + tokio::spawn( + async move { + helpers::add_domain_task_to_pt( + &m_self, + m_state.as_ref(), + &m_payment_attempt, + requeue, + schedule_time, + ) + .await + } + .in_current_span(), + ); + + Ok(()) } async fn get_connector<'a>( diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index 4933b4d700d3..83586e51d66a 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -24,6 +24,7 @@ use nanoid::nanoid; use qrcode; use serde::de::DeserializeOwned; use serde_json::Value; +use tracing_futures::Instrument; use uuid::Uuid; pub use self::ext_traits::{OptionExt, ValidateCall}; @@ -754,21 +755,30 @@ where payments_response { let m_state = state.clone(); - - Box::pin( - webhooks_core::create_event_and_trigger_appropriate_outgoing_webhook( - m_state, - merchant_account, - business_profile, - event_type, - diesel_models::enums::EventClass::Payments, - None, - payment_id, - diesel_models::enums::EventObjectType::PaymentDetails, - webhooks::OutgoingWebhookContent::PaymentDetails(payments_response_json), - ), - ) - .await?; + // This spawns this futures in a background thread, the exception inside this future won't affect + // the current thread and the lifecycle of spawn thread is not handled by runtime. + // So when server shutdown won't wait for this thread's completion. + tokio::spawn( + async move { + Box::pin( + webhooks_core::create_event_and_trigger_appropriate_outgoing_webhook( + m_state, + merchant_account, + business_profile, + event_type, + diesel_models::enums::EventClass::Payments, + None, + payment_id, + diesel_models::enums::EventObjectType::PaymentDetails, + webhooks::OutgoingWebhookContent::PaymentDetails( + payments_response_json, + ), + ), + ) + .await + } + .in_current_span(), + ); } } From 62c9ccae6ab0d128c54962675b88739ad7797fe6 Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Thu, 16 Nov 2023 18:48:42 +0530 Subject: [PATCH 018/146] refactor(router): add openapi spec support for gsm apis (#2871) --- crates/api_models/src/events/gsm.rs | 6 + crates/api_models/src/gsm.rs | 32 +- crates/router/src/core/gsm.rs | 14 +- crates/router/src/openapi.rs | 13 +- crates/router/src/routes/gsm.rs | 68 +++++ crates/router/src/types.rs | 2 - crates/router/src/types/transformers.rs | 16 + openapi/openapi_spec.json | 382 ++++++++++++++++++++++++ 8 files changed, 515 insertions(+), 18 deletions(-) diff --git a/crates/api_models/src/events/gsm.rs b/crates/api_models/src/events/gsm.rs index d984ae1ff698..a653cc291d6c 100644 --- a/crates/api_models/src/events/gsm.rs +++ b/crates/api_models/src/events/gsm.rs @@ -31,3 +31,9 @@ impl ApiEventMetric for gsm::GsmDeleteResponse { Some(ApiEventsType::Gsm) } } + +impl ApiEventMetric for gsm::GsmResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Gsm) + } +} diff --git a/crates/api_models/src/gsm.rs b/crates/api_models/src/gsm.rs index 6bd8fd99dd93..254981b1f8f7 100644 --- a/crates/api_models/src/gsm.rs +++ b/crates/api_models/src/gsm.rs @@ -1,8 +1,10 @@ -use crate::enums; +use utoipa::ToSchema; -#[derive(Debug, serde::Deserialize, serde::Serialize)] +use crate::enums::Connector; + +#[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] pub struct GsmCreateRequest { - pub connector: enums::Connector, + pub connector: Connector, pub flow: String, pub sub_flow: String, pub code: String, @@ -13,9 +15,9 @@ pub struct GsmCreateRequest { pub step_up_possible: bool, } -#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] pub struct GsmRetrieveRequest { - pub connector: enums::Connector, + pub connector: Connector, pub flow: String, pub sub_flow: String, pub code: String, @@ -33,6 +35,7 @@ pub struct GsmRetrieveRequest { serde::Serialize, serde::Deserialize, strum::EnumString, + ToSchema, )] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] @@ -43,7 +46,7 @@ pub enum GsmDecision { DoDefault, } -#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] pub struct GsmUpdateRequest { pub connector: String, pub flow: String, @@ -56,7 +59,7 @@ pub struct GsmUpdateRequest { pub step_up_possible: Option, } -#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] pub struct GsmDeleteRequest { pub connector: String, pub flow: String, @@ -65,7 +68,7 @@ pub struct GsmDeleteRequest { pub message: String, } -#[derive(Debug, serde::Serialize)] +#[derive(Debug, serde::Serialize, ToSchema)] pub struct GsmDeleteResponse { pub gsm_rule_delete: bool, pub connector: String, @@ -73,3 +76,16 @@ pub struct GsmDeleteResponse { pub sub_flow: String, pub code: String, } + +#[derive(serde::Serialize, Debug, ToSchema)] +pub struct GsmResponse { + pub connector: String, + pub flow: String, + pub sub_flow: String, + pub code: String, + pub message: String, + pub status: String, + pub router_error: Option, + pub decision: String, + pub step_up_possible: bool, +} diff --git a/crates/router/src/core/gsm.rs b/crates/router/src/core/gsm.rs index d25860674570..ed72275a73ab 100644 --- a/crates/router/src/core/gsm.rs +++ b/crates/router/src/core/gsm.rs @@ -10,7 +10,7 @@ use crate::{ }, db::gsm::GsmInterface, services, - types::{self, transformers::ForeignInto}, + types::transformers::ForeignInto, AppState, }; @@ -18,21 +18,21 @@ use crate::{ pub async fn create_gsm_rule( state: AppState, gsm_rule: gsm_api_types::GsmCreateRequest, -) -> RouterResponse { +) -> RouterResponse { let db = state.store.as_ref(); GsmInterface::add_gsm_rule(db, gsm_rule.foreign_into()) .await .to_duplicate_response(errors::ApiErrorResponse::GenericDuplicateError { message: "GSM with given key already exists in our records".to_string(), }) - .map(services::ApplicationResponse::Json) + .map(|gsm| services::ApplicationResponse::Json(gsm.foreign_into())) } #[instrument(skip_all)] pub async fn retrieve_gsm_rule( state: AppState, gsm_request: gsm_api_types::GsmRetrieveRequest, -) -> RouterResponse { +) -> RouterResponse { let db = state.store.as_ref(); let gsm_api_types::GsmRetrieveRequest { connector, @@ -46,14 +46,14 @@ pub async fn retrieve_gsm_rule( .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { message: "GSM with given key does not exist in our records".to_string(), }) - .map(services::ApplicationResponse::Json) + .map(|gsm| services::ApplicationResponse::Json(gsm.foreign_into())) } #[instrument(skip_all)] pub async fn update_gsm_rule( state: AppState, gsm_request: gsm_api_types::GsmUpdateRequest, -) -> RouterResponse { +) -> RouterResponse { let db = state.store.as_ref(); let gsm_api_types::GsmUpdateRequest { connector, @@ -85,7 +85,7 @@ pub async fn update_gsm_rule( message: "GSM with given key does not exist in our records".to_string(), }) .attach_printable("Failed while updating Gsm rule") - .map(services::ApplicationResponse::Json) + .map(|gsm| services::ApplicationResponse::Json(gsm.foreign_into())) } #[instrument(skip_all)] diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index dbcd8cbe4ce2..095e1f45f93f 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -114,7 +114,11 @@ Never share your secret api keys. Keep them guarded and secure. crate::routes::payouts::payouts_fulfill, crate::routes::payouts::payouts_retrieve, crate::routes::payouts::payouts_update, - crate::routes::payment_link::payment_link_retrieve + crate::routes::payment_link::payment_link_retrieve, + crate::routes::gsm::create_gsm_rule, + crate::routes::gsm::get_gsm_rule, + crate::routes::gsm::update_gsm_rule, + crate::routes::gsm::delete_gsm_rule, ), components(schemas( crate::types::api::refunds::RefundRequest, @@ -184,6 +188,13 @@ Never share your secret api keys. Keep them guarded and secure. api_models::admin::PaymentLinkColorSchema, api_models::disputes::DisputeResponse, api_models::disputes::DisputeResponsePaymentsRetrieve, + api_models::gsm::GsmCreateRequest, + api_models::gsm::GsmRetrieveRequest, + api_models::gsm::GsmUpdateRequest, + api_models::gsm::GsmDeleteRequest, + api_models::gsm::GsmDeleteResponse, + api_models::gsm::GsmResponse, + api_models::gsm::GsmDecision, api_models::payments::AddressDetails, api_models::payments::BankDebitData, api_models::payments::AliPayQr, diff --git a/crates/router/src/routes/gsm.rs b/crates/router/src/routes/gsm.rs index 02d943792dba..ff70635959fc 100644 --- a/crates/router/src/routes/gsm.rs +++ b/crates/router/src/routes/gsm.rs @@ -8,6 +8,23 @@ use crate::{ services::{api, authentication as auth}, }; +/// Gsm - Create +/// +/// To create a Gsm Rule +#[utoipa::path( + post, + path = "/gsm", + request_body( + content = GsmCreateRequest, + ), + responses( + (status = 200, description = "Gsm created", body = GsmResponse), + (status = 400, description = "Missing Mandatory fields") + ), + tag = "Gsm", + operation_id = "Create Gsm Rule", + security(("admin_api_key" = [])), +)] #[instrument(skip_all, fields(flow = ?Flow::GsmRuleCreate))] pub async fn create_gsm_rule( state: web::Data, @@ -29,6 +46,23 @@ pub async fn create_gsm_rule( .await } +/// Gsm - Get +/// +/// To get a Gsm Rule +#[utoipa::path( + post, + path = "/gsm/get", + request_body( + content = GsmRetrieveRequest, + ), + responses( + (status = 200, description = "Gsm retrieved", body = GsmResponse), + (status = 400, description = "Missing Mandatory fields") + ), + tag = "Gsm", + operation_id = "Retrieve Gsm Rule", + security(("admin_api_key" = [])), +)] #[instrument(skip_all, fields(flow = ?Flow::GsmRuleRetrieve))] pub async fn get_gsm_rule( state: web::Data, @@ -49,6 +83,23 @@ pub async fn get_gsm_rule( .await } +/// Gsm - Update +/// +/// To update a Gsm Rule +#[utoipa::path( + post, + path = "/gsm/update", + request_body( + content = GsmUpdateRequest, + ), + responses( + (status = 200, description = "Gsm updated", body = GsmResponse), + (status = 400, description = "Missing Mandatory fields") + ), + tag = "Gsm", + operation_id = "Update Gsm Rule", + security(("admin_api_key" = [])), +)] #[instrument(skip_all, fields(flow = ?Flow::GsmRuleUpdate))] pub async fn update_gsm_rule( state: web::Data, @@ -70,6 +121,23 @@ pub async fn update_gsm_rule( .await } +/// Gsm - Delete +/// +/// To delete a Gsm Rule +#[utoipa::path( + post, + path = "/gsm/delete", + request_body( + content = GsmDeleteRequest, + ), + responses( + (status = 200, description = "Gsm deleted", body = GsmDeleteResponse), + (status = 400, description = "Missing Mandatory fields") + ), + tag = "Gsm", + operation_id = "Delete Gsm Rule", + security(("admin_api_key" = [])), +)] #[instrument(skip_all, fields(flow = ?Flow::GsmRuleDelete))] pub async fn delete_gsm_rule( state: web::Data, diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 7e9725d1a3b7..7cf8f6b71fa5 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -1213,5 +1213,3 @@ impl } } } - -pub type GsmResponse = storage::GatewayStatusMap; diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 1cd016de18e6..69ce2df974f6 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -1047,3 +1047,19 @@ impl ForeignFrom for storage::GatewayStatusMapp } } } + +impl ForeignFrom for gsm_api_types::GsmResponse { + fn foreign_from(value: storage::GatewayStatusMap) -> Self { + Self { + connector: value.connector.to_string(), + flow: value.flow, + sub_flow: value.sub_flow, + code: value.code, + message: value.message, + decision: value.decision.to_string(), + status: value.status, + router_error: value.router_error, + step_up_possible: value.step_up_possible, + } + } +} diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 23f8f1b3628b..c576c6c8a99c 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -745,6 +745,166 @@ ] } }, + "/gsm": { + "post": { + "tags": [ + "Gsm" + ], + "summary": "Gsm - Create", + "description": "Gsm - Create\n\nTo create a Gsm Rule", + "operationId": "Create Gsm Rule", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GsmCreateRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Gsm created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GsmResponse" + } + } + } + }, + "400": { + "description": "Missing Mandatory fields" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + } + }, + "/gsm/delete": { + "post": { + "tags": [ + "Gsm" + ], + "summary": "Gsm - Delete", + "description": "Gsm - Delete\n\nTo delete a Gsm Rule", + "operationId": "Delete Gsm Rule", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GsmDeleteRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Gsm deleted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GsmDeleteResponse" + } + } + } + }, + "400": { + "description": "Missing Mandatory fields" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + } + }, + "/gsm/get": { + "post": { + "tags": [ + "Gsm" + ], + "summary": "Gsm - Get", + "description": "Gsm - Get\n\nTo get a Gsm Rule", + "operationId": "Retrieve Gsm Rule", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GsmRetrieveRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Gsm retrieved", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GsmResponse" + } + } + } + }, + "400": { + "description": "Missing Mandatory fields" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + } + }, + "/gsm/update": { + "post": { + "tags": [ + "Gsm" + ], + "summary": "Gsm - Update", + "description": "Gsm - Update\n\nTo update a Gsm Rule", + "operationId": "Update Gsm Rule", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GsmUpdateRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Gsm updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GsmResponse" + } + } + } + }, + "400": { + "description": "Missing Mandatory fields" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + } + }, "/mandates/revoke/{mandate_id}": { "post": { "tags": [ @@ -5713,6 +5873,228 @@ } } }, + "GsmCreateRequest": { + "type": "object", + "required": [ + "connector", + "flow", + "sub_flow", + "code", + "message", + "status", + "decision", + "step_up_possible" + ], + "properties": { + "connector": { + "$ref": "#/components/schemas/Connector" + }, + "flow": { + "type": "string" + }, + "sub_flow": { + "type": "string" + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "string" + }, + "router_error": { + "type": "string", + "nullable": true + }, + "decision": { + "$ref": "#/components/schemas/GsmDecision" + }, + "step_up_possible": { + "type": "boolean" + } + } + }, + "GsmDecision": { + "type": "string", + "enum": [ + "retry", + "requeue", + "do_default" + ] + }, + "GsmDeleteRequest": { + "type": "object", + "required": [ + "connector", + "flow", + "sub_flow", + "code", + "message" + ], + "properties": { + "connector": { + "type": "string" + }, + "flow": { + "type": "string" + }, + "sub_flow": { + "type": "string" + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "GsmDeleteResponse": { + "type": "object", + "required": [ + "gsm_rule_delete", + "connector", + "flow", + "sub_flow", + "code" + ], + "properties": { + "gsm_rule_delete": { + "type": "boolean" + }, + "connector": { + "type": "string" + }, + "flow": { + "type": "string" + }, + "sub_flow": { + "type": "string" + }, + "code": { + "type": "string" + } + } + }, + "GsmResponse": { + "type": "object", + "required": [ + "connector", + "flow", + "sub_flow", + "code", + "message", + "status", + "decision", + "step_up_possible" + ], + "properties": { + "connector": { + "type": "string" + }, + "flow": { + "type": "string" + }, + "sub_flow": { + "type": "string" + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "string" + }, + "router_error": { + "type": "string", + "nullable": true + }, + "decision": { + "type": "string" + }, + "step_up_possible": { + "type": "boolean" + } + } + }, + "GsmRetrieveRequest": { + "type": "object", + "required": [ + "connector", + "flow", + "sub_flow", + "code", + "message" + ], + "properties": { + "connector": { + "$ref": "#/components/schemas/Connector" + }, + "flow": { + "type": "string" + }, + "sub_flow": { + "type": "string" + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "GsmUpdateRequest": { + "type": "object", + "required": [ + "connector", + "flow", + "sub_flow", + "code", + "message" + ], + "properties": { + "connector": { + "type": "string" + }, + "flow": { + "type": "string" + }, + "sub_flow": { + "type": "string" + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "string", + "nullable": true + }, + "router_error": { + "type": "string", + "nullable": true + }, + "decision": { + "allOf": [ + { + "$ref": "#/components/schemas/GsmDecision" + } + ], + "nullable": true + }, + "step_up_possible": { + "type": "boolean", + "nullable": true + } + } + }, "IndomaretVoucherData": { "type": "object", "required": [ From b8b20c412df0485bf395f9aa21e6e34e90d97acd Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Thu, 16 Nov 2023 19:02:54 +0530 Subject: [PATCH 019/146] feat(router): add api to migrate card from basilisk to rust (#2853) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- config/config.example.toml | 2 + config/development.toml | 2 + config/docker_compose.toml | 2 + crates/api_models/src/enums.rs | 6 + crates/api_models/src/events.rs | 1 + .../api_models/src/events/locker_migration.rs | 9 ++ crates/api_models/src/lib.rs | 1 + crates/api_models/src/locker_migration.rs | 8 ++ crates/common_utils/src/events.rs | 1 + crates/router/src/configs/defaults.rs | 1 + crates/router/src/configs/kms.rs | 2 + crates/router/src/configs/settings.rs | 2 + crates/router/src/core.rs | 1 + crates/router/src/core/locker_migration.rs | 131 ++++++++++++++++++ .../router/src/core/payment_methods/cards.rs | 31 +++-- .../src/core/payment_methods/transformers.rs | 28 +++- crates/router/src/core/payouts/helpers.rs | 12 +- crates/router/src/lib.rs | 1 + crates/router/src/routes.rs | 5 +- crates/router/src/routes/app.rs | 15 +- crates/router/src/routes/lock_utils.rs | 2 + crates/router/src/routes/locker_migration.rs | 27 ++++ crates/router_env/src/logger/types.rs | 2 + loadtest/config/development.toml | 2 + 24 files changed, 274 insertions(+), 20 deletions(-) create mode 100644 crates/api_models/src/events/locker_migration.rs create mode 100644 crates/api_models/src/locker_migration.rs create mode 100644 crates/router/src/core/locker_migration.rs create mode 100644 crates/router/src/routes/locker_migration.rs diff --git a/config/config.example.toml b/config/config.example.toml index f0083bb48b19..40590128a5d4 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -112,6 +112,7 @@ kms_encrypted_recon_admin_api_key = "" # Base64-encoded (KMS encrypted) ciph # like card details [locker] host = "" # Locker host +host_rs = "" # Rust Locker host mock_locker = true # Emulate a locker locally using Postgres basilisk_host = "" # Basilisk host locker_signing_key_id = "1" # Key_id to sign basilisk hs locker @@ -130,6 +131,7 @@ locker_encryption_key2 = "" # public key 2 in pem format, corresponding private locker_decryption_key1 = "" # private key 1 in pem format, corresponding public key in basilisk locker_decryption_key2 = "" # private key 2 in pem format, corresponding public key in basilisk vault_encryption_key = "" # public key in pem format, corresponding private key in basilisk-hs +rust_locker_encryption_key = "" # public key in pem format, corresponding private key in rust locker vault_private_key = "" # private key in pem format, corresponding public key in basilisk-hs diff --git a/config/development.toml b/config/development.toml index 63c1f045d94f..d3cad47a23fc 100644 --- a/config/development.toml +++ b/config/development.toml @@ -48,6 +48,7 @@ applepay_endpoint = "DOMAIN SPECIFIC ENDPOINT" [locker] host = "" +host_rs = "" mock_locker = true basilisk_host = "" @@ -59,6 +60,7 @@ locker_encryption_key2 = "" locker_decryption_key1 = "" locker_decryption_key2 = "" vault_encryption_key = "" +rust_locker_encryption_key = "" vault_private_key = "" tunnel_private_key = "" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index ddda7e7021a4..39e8fad0fcaa 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -44,6 +44,7 @@ recon_admin_api_key = "recon_test_admin" [locker] host = "" +host_rs = "" mock_locker = true basilisk_host = "" @@ -55,6 +56,7 @@ locker_encryption_key2 = "" locker_decryption_key1 = "" locker_decryption_key2 = "" vault_encryption_key = "" +rust_locker_encryption_key = "" vault_private_key = "" [redis] diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index b27e71b9e8f5..100ab21e4099 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -562,3 +562,9 @@ pub enum RetryAction { /// Denotes that the payment is requeued Requeue, } + +#[derive(Clone, Copy)] +pub enum LockerChoice { + Basilisk, + Tartarus, +} diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index ad07340615b4..0ce7638b5ed1 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -1,5 +1,6 @@ pub mod customer; pub mod gsm; +mod locker_migration; pub mod payment; #[cfg(feature = "payouts")] pub mod payouts; diff --git a/crates/api_models/src/events/locker_migration.rs b/crates/api_models/src/events/locker_migration.rs new file mode 100644 index 000000000000..db76a8f760db --- /dev/null +++ b/crates/api_models/src/events/locker_migration.rs @@ -0,0 +1,9 @@ +use common_utils::events::ApiEventMetric; + +use crate::locker_migration::MigrateCardResponse; + +impl ApiEventMetric for MigrateCardResponse { + fn get_api_event_type(&self) -> Option { + Some(common_utils::events::ApiEventsType::RustLocker) + } +} diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index bcc3913ea824..40faa6b3e81d 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -13,6 +13,7 @@ pub mod errors; pub mod events; pub mod files; pub mod gsm; +pub mod locker_migration; pub mod mandates; pub mod organization; pub mod payment_methods; diff --git a/crates/api_models/src/locker_migration.rs b/crates/api_models/src/locker_migration.rs new file mode 100644 index 000000000000..6e2881cd463e --- /dev/null +++ b/crates/api_models/src/locker_migration.rs @@ -0,0 +1,8 @@ +#[derive(Debug, Clone, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MigrateCardResponse { + pub status_message: String, + pub status_code: String, + pub customers_moved: usize, + pub cards_moved: usize, +} diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs index 753f1deeb676..14b8d4de1c36 100644 --- a/crates/common_utils/src/events.rs +++ b/crates/common_utils/src/events.rs @@ -44,6 +44,7 @@ pub enum ApiEventsType { Gsm, // TODO: This has to be removed once the corresponding apiEventTypes are created Miscellaneous, + RustLocker, } impl ApiEventMetric for serde_json::Value {} diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index 8d58037343e0..18a70a8100aa 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -48,6 +48,7 @@ impl Default for super::settings::Locker { fn default() -> Self { Self { host: "localhost".into(), + host_rs: "localhost".into(), mock_locker: true, basilisk_host: "localhost".into(), locker_signing_key_id: "1".into(), diff --git a/crates/router/src/configs/kms.rs b/crates/router/src/configs/kms.rs index 317ad0608b49..205169fa291b 100644 --- a/crates/router/src/configs/kms.rs +++ b/crates/router/src/configs/kms.rs @@ -18,6 +18,7 @@ impl KmsDecrypt for settings::Jwekey { self.locker_decryption_key1, self.locker_decryption_key2, self.vault_encryption_key, + self.rust_locker_encryption_key, self.vault_private_key, self.tunnel_private_key, ) = tokio::try_join!( @@ -26,6 +27,7 @@ impl KmsDecrypt for settings::Jwekey { kms_client.decrypt(self.locker_decryption_key1), kms_client.decrypt(self.locker_decryption_key2), kms_client.decrypt(self.vault_encryption_key), + kms_client.decrypt(self.rust_locker_encryption_key), kms_client.decrypt(self.vault_private_key), kms_client.decrypt(self.tunnel_private_key), )?; diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index c5b71c6f7341..0007e636926c 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -420,6 +420,7 @@ pub struct Secrets { #[serde(default)] pub struct Locker { pub host: String, + pub host_rs: String, pub mock_locker: bool, pub basilisk_host: String, pub locker_signing_key_id: String, @@ -448,6 +449,7 @@ pub struct Jwekey { pub locker_decryption_key1: String, pub locker_decryption_key2: String, pub vault_encryption_key: String, + pub rust_locker_encryption_key: String, pub vault_private_key: String, pub tunnel_private_key: String, } diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index b7023fe5ae46..8cc85eef60d6 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -9,6 +9,7 @@ pub mod disputes; pub mod errors; pub mod files; pub mod gsm; +pub mod locker_migration; pub mod mandate; pub mod metrics; pub mod payment_link; diff --git a/crates/router/src/core/locker_migration.rs b/crates/router/src/core/locker_migration.rs new file mode 100644 index 000000000000..aa82b4a3a636 --- /dev/null +++ b/crates/router/src/core/locker_migration.rs @@ -0,0 +1,131 @@ +use api_models::{enums as api_enums, locker_migration::MigrateCardResponse}; +use common_utils::errors::CustomResult; +use diesel_models::PaymentMethod; +use error_stack::{FutureExt, ResultExt}; +use futures::TryFutureExt; + +use super::{errors::StorageErrorExt, payment_methods::cards}; +use crate::{ + errors, + routes::AppState, + services::{self, logger}, + types::{api, domain}, +}; + +pub async fn rust_locker_migration( + state: AppState, + merchant_id: &str, +) -> CustomResult, errors::ApiErrorResponse> { + let db = state.store.as_ref(); + + let key_store = state + .store + .get_merchant_key_store_by_merchant_id( + merchant_id, + &state.store.get_master_key().to_vec().into(), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + let merchant_account = db + .find_merchant_account_by_merchant_id(merchant_id, &key_store) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound) + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + let domain_customers = db + .list_customers_by_merchant_id(merchant_id, &key_store) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + let mut customers_moved = 0; + let mut cards_moved = 0; + + for customer in domain_customers { + let result = db + .find_payment_method_by_customer_id_merchant_id_list(&customer.customer_id, merchant_id) + .change_context(errors::ApiErrorResponse::InternalServerError) + .and_then(|pm| { + call_to_locker( + &state, + pm, + &customer.customer_id, + merchant_id, + &merchant_account, + ) + }) + .await?; + + customers_moved += 1; + cards_moved += result; + } + + Ok(services::api::ApplicationResponse::Json( + MigrateCardResponse { + status_code: "200".to_string(), + status_message: "Card migration completed".to_string(), + customers_moved, + cards_moved, + }, + )) +} + +pub async fn call_to_locker( + state: &AppState, + payment_methods: Vec, + customer_id: &String, + merchant_id: &str, + merchant_account: &domain::MerchantAccount, +) -> CustomResult { + let mut cards_moved = 0; + + for pm in payment_methods { + let card = + cards::get_card_from_locker(state, customer_id, merchant_id, &pm.payment_method_id) + .await?; + + let card_details = api::CardDetail { + card_number: card.card_number, + card_exp_month: card.card_exp_month, + card_exp_year: card.card_exp_year, + card_holder_name: card.name_on_card, + nick_name: card.nick_name.map(masking::Secret::new), + }; + + let pm_create = api::PaymentMethodCreate { + payment_method: pm.payment_method, + payment_method_type: pm.payment_method_type, + payment_method_issuer: pm.payment_method_issuer, + payment_method_issuer_code: pm.payment_method_issuer_code, + card: Some(card_details.clone()), + metadata: pm.metadata, + customer_id: Some(pm.customer_id), + card_network: card.card_brand, + }; + + let (_add_card_rs_resp, _is_duplicate) = cards::add_card_hs( + state, + pm_create, + card_details, + customer_id.to_string(), + merchant_account, + api_enums::LockerChoice::Tartarus, + Some(&pm.payment_method_id), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable(format!( + "Card migration failed for merchant_id: {merchant_id}, customer_id: {customer_id}, payment_method_id: {} ", + pm.payment_method_id + ))?; + + cards_moved += 1; + + logger::info!( + "Card migrated for merchant_id: {merchant_id}, customer_id: {customer_id}, payment_method_id: {} ", + pm.payment_method_id + ); + } + + Ok(cards_moved) +} diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 38ab03ddcb77..f9c666cbb954 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -214,12 +214,20 @@ pub async fn add_card_to_locker( metrics::STORED_TO_LOCKER.add(&metrics::CONTEXT, 1, &[]); request::record_operation_time( async { - add_card_hs(state, req, card, customer_id, merchant_account) - .await - .map_err(|error| { - metrics::CARD_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); - error - }) + add_card_hs( + state, + req, + card, + customer_id, + merchant_account, + api_enums::LockerChoice::Basilisk, + None, + ) + .await + .map_err(|error| { + metrics::CARD_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); + error + }) }, &metrics::CARD_ADD_TIME, &[], @@ -282,10 +290,13 @@ pub async fn add_card_hs( card: api::CardDetail, customer_id: String, merchant_account: &domain::MerchantAccount, + locker_choice: api_enums::LockerChoice, + card_reference: Option<&str>, ) -> errors::CustomResult<(api::PaymentMethodResponse, bool), errors::VaultError> { let payload = payment_methods::StoreLockerReq::LockerCard(payment_methods::StoreCardReq { merchant_id: &merchant_account.merchant_id, merchant_customer_id: customer_id.to_owned(), + card_reference: card_reference.map(str::to_string), card: payment_methods::Card { card_number: card.card_number.to_owned(), name_on_card: card.card_holder_name.to_owned(), @@ -296,7 +307,8 @@ pub async fn add_card_hs( nick_name: card.nick_name.as_ref().map(masking::Secret::peek).cloned(), }, }); - let store_card_payload = call_to_locker_hs(state, &payload, &customer_id).await?; + let store_card_payload = + call_to_locker_hs(state, &payload, &customer_id, locker_choice).await?; let payment_method_resp = payment_methods::mk_add_card_response_hs( card, @@ -394,6 +406,7 @@ pub async fn call_to_locker_hs<'a>( state: &routes::AppState, payload: &payment_methods::StoreLockerReq<'a>, customer_id: &str, + locker_choice: api_enums::LockerChoice, ) -> errors::CustomResult { let locker = &state.conf.locker; #[cfg(not(feature = "kms"))] @@ -402,7 +415,9 @@ pub async fn call_to_locker_hs<'a>( let jwekey = &state.kms_secrets; let db = &*state.store; let stored_card_response = if !locker.mock_locker { - let request = payment_methods::mk_add_locker_request_hs(jwekey, locker, payload).await?; + let request = + payment_methods::mk_add_locker_request_hs(jwekey, locker, payload, locker_choice) + .await?; let response = services::call_connector_api(state, request) .await .change_context(errors::VaultError::SaveCardFailed); diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index 086133ec78a5..63a0479375e8 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -1,5 +1,6 @@ use std::str::FromStr; +use api_models::enums as api_enums; use common_utils::{ext_traits::StringExt, pii::Email}; use error_stack::ResultExt; use josekit::jwe; @@ -26,6 +27,8 @@ pub enum StoreLockerReq<'a> { pub struct StoreCardReq<'a> { pub merchant_id: &'a str, pub merchant_customer_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub card_reference: Option, pub card: Card, } @@ -224,6 +227,7 @@ pub async fn mk_basilisk_req( #[cfg(feature = "kms")] jwekey: &settings::ActiveKmsSecrets, #[cfg(not(feature = "kms"))] jwekey: &settings::Jwekey, jws: &str, + locker_choice: api_enums::LockerChoice, ) -> CustomResult { let jws_payload: Vec<&str> = jws.split('.').collect(); @@ -241,10 +245,18 @@ pub async fn mk_basilisk_req( .change_context(errors::VaultError::SaveCardFailed)?; #[cfg(feature = "kms")] - let public_key = jwekey.jwekey.peek().vault_encryption_key.as_bytes(); + let public_key = match locker_choice { + api_enums::LockerChoice::Basilisk => jwekey.jwekey.peek().vault_encryption_key.as_bytes(), + api_enums::LockerChoice::Tartarus => { + jwekey.jwekey.peek().rust_locker_encryption_key.as_bytes() + } + }; #[cfg(not(feature = "kms"))] - let public_key = jwekey.vault_encryption_key.as_bytes(); + let public_key = match locker_choice { + api_enums::LockerChoice::Basilisk => jwekey.vault_encryption_key.as_bytes(), + api_enums::LockerChoice::Tartarus => jwekey.rust_locker_encryption_key.as_bytes(), + }; let jwe_encrypted = encryption::encrypt_jwe(&payload, public_key) .await @@ -272,6 +284,7 @@ pub async fn mk_add_locker_request_hs<'a>( #[cfg(feature = "kms")] jwekey: &settings::ActiveKmsSecrets, locker: &settings::Locker, payload: &StoreLockerReq<'a>, + locker_choice: api_enums::LockerChoice, ) -> CustomResult { let payload = utils::Encode::>::encode_to_vec(&payload) .change_context(errors::VaultError::RequestEncodingFailed)?; @@ -286,11 +299,14 @@ pub async fn mk_add_locker_request_hs<'a>( .await .change_context(errors::VaultError::RequestEncodingFailed)?; - let jwe_payload = mk_basilisk_req(jwekey, &jws).await?; + let jwe_payload = mk_basilisk_req(jwekey, &jws, locker_choice).await?; let body = utils::Encode::::encode_to_value(&jwe_payload) .change_context(errors::VaultError::RequestEncodingFailed)?; - let mut url = locker.host.to_owned(); + let mut url = match locker_choice { + api_enums::LockerChoice::Basilisk => locker.host.to_owned(), + api_enums::LockerChoice::Tartarus => locker.host_rs.to_owned(), + }; url.push_str("/cards/add"); let mut request = services::Request::new(services::Method::Post, &url); request.add_header(headers::CONTENT_TYPE, "application/json".into()); @@ -432,7 +448,7 @@ pub async fn mk_get_card_request_hs( .await .change_context(errors::VaultError::RequestEncodingFailed)?; - let jwe_payload = mk_basilisk_req(jwekey, &jws).await?; + let jwe_payload = mk_basilisk_req(jwekey, &jws, api_enums::LockerChoice::Basilisk).await?; let body = utils::Encode::::encode_to_value(&jwe_payload) .change_context(errors::VaultError::RequestEncodingFailed)?; @@ -512,7 +528,7 @@ pub async fn mk_delete_card_request_hs( .await .change_context(errors::VaultError::RequestEncodingFailed)?; - let jwe_payload = mk_basilisk_req(jwekey, &jws).await?; + let jwe_payload = mk_basilisk_req(jwekey, &jws, api_enums::LockerChoice::Basilisk).await?; let body = utils::Encode::::encode_to_value(&jwe_payload) .change_context(errors::VaultError::RequestEncodingFailed)?; diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index 39079ea36cd6..c1e00b9b8000 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -152,6 +152,7 @@ pub async fn save_payout_data_to_locker( card_isin: None, nick_name: None, }, + card_reference: None, }); ( payload, @@ -195,9 +196,14 @@ pub async fn save_payout_data_to_locker( } }; // Store payout method in locker - let stored_resp = cards::call_to_locker_hs(state, &locker_req, &payout_attempt.customer_id) - .await - .change_context(errors::ApiErrorResponse::InternalServerError)?; + let stored_resp = cards::call_to_locker_hs( + state, + &locker_req, + &payout_attempt.customer_id, + api_enums::LockerChoice::Basilisk, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; // Store card_reference in payouts table let db = &*state.store; diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index a3ed0b35c785..58d77d9e02f4 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -145,6 +145,7 @@ pub fn mk_app( .service(routes::Disputes::server(state.clone())) .service(routes::Analytics::server(state.clone())) .service(routes::Routing::server(state.clone())) + .service(routes::LockerMigrate::server(state.clone())) .service(routes::Gsm::server(state.clone())) .service(routes::User::server(state.clone())) } diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 745433c2074b..5166e326fb91 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -29,6 +29,7 @@ pub mod user; pub mod verification; pub mod webhooks; +pub mod locker_migration; #[cfg(feature = "dummy_connector")] pub use self::app::DummyConnector; #[cfg(feature = "payouts")] @@ -39,8 +40,8 @@ pub use self::app::Routing; pub use self::app::Verify; pub use self::app::{ ApiKeys, AppState, BusinessProfile, Cache, Cards, Configs, Customers, Disputes, EphemeralKey, - Files, Gsm, Health, Mandates, MerchantAccount, MerchantConnectorAccount, PaymentLink, - PaymentMethods, Payments, Refunds, User, Webhooks, + Files, Gsm, Health, LockerMigrate, Mandates, MerchantAccount, MerchantConnectorAccount, + PaymentLink, PaymentMethods, Payments, Refunds, User, Webhooks, }; #[cfg(feature = "stripe")] pub use super::compatibility::stripe::StripeApis; diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 15b6df733489..070f1eb29bf8 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -19,7 +19,7 @@ use super::routing as cloud_routing; #[cfg(all(feature = "olap", feature = "kms"))] use super::verification::{apple_pay_merchant_registration, retrieve_apple_pay_verified_domains}; #[cfg(feature = "olap")] -use super::{admin::*, api_keys::*, disputes::*, files::*, gsm::*, user::*}; +use super::{admin::*, api_keys::*, disputes::*, files::*, gsm::*, locker_migration, user::*}; use super::{cache::*, health::*, payment_link::*}; #[cfg(any(feature = "olap", feature = "oltp"))] use super::{configs::*, customers::*, mandates::*, payments::*, refunds::*}; @@ -743,3 +743,16 @@ impl User { .service(web::resource("/v2/signup").route(web::post().to(user_connect_account))) } } + +pub struct LockerMigrate; + +#[cfg(feature = "olap")] +impl LockerMigrate { + pub fn server(state: AppState) -> Scope { + web::scope("locker_migration/{merchant_id}") + .app_data(web::Data::new(state)) + .service( + web::resource("").route(web::post().to(locker_migration::rust_locker_migration)), + ) + } +} diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index ae573e871627..c093523d455a 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -23,6 +23,7 @@ pub enum ApiIdentifier { ApiKeys, PaymentLink, Routing, + RustLockerMigration, Gsm, User, } @@ -131,6 +132,7 @@ impl From for ApiIdentifier { Flow::Verification => Self::Verification, Flow::PaymentLinkInitiate | Flow::PaymentLinkRetrieve => Self::PaymentLink, + Flow::RustLockerMigration => Self::RustLockerMigration, Flow::GsmRuleCreate | Flow::GsmRuleRetrieve | Flow::GsmRuleUpdate diff --git a/crates/router/src/routes/locker_migration.rs b/crates/router/src/routes/locker_migration.rs new file mode 100644 index 000000000000..892dc5941bd6 --- /dev/null +++ b/crates/router/src/routes/locker_migration.rs @@ -0,0 +1,27 @@ +use actix_web::{web, HttpRequest, HttpResponse}; +use router_env::Flow; + +use super::AppState; +use crate::{ + core::{api_locking, locker_migration}, + services::{api, authentication as auth}, +}; + +pub async fn rust_locker_migration( + state: web::Data, + req: HttpRequest, + path: web::Path, +) -> HttpResponse { + let flow = Flow::RustLockerMigration; + let merchant_id = path.into_inner(); + api::server_wrap( + flow, + state, + &req, + &merchant_id, + |state, _, _| locker_migration::rust_locker_migration(state, &merchant_id), + &auth::AdminApiAuth, + api_locking::LockAction::NotApplicable, + ) + .await +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 9cd678083959..3bfd1ef7d9f8 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -235,6 +235,8 @@ pub enum Flow { BusinessProfileList, /// Different verification flows Verification, + /// Rust locker migration + RustLockerMigration, /// Gsm Rule Creation flow GsmRuleCreate, /// Gsm Rule Retrieve flow diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index f70fc656d8e3..2fb729fb7b90 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -30,6 +30,7 @@ jwt_secret = "secret" [locker] host = "" +host_rs = "" mock_locker = true basilisk_host = "" @@ -48,6 +49,7 @@ locker_encryption_key2 = "" locker_decryption_key1 = "" locker_decryption_key2 = "" vault_encryption_key = "" +rust_locker_encryption_key = "" vault_private_key = "" [webhooks] From 0176c9173cac3171492927565fdd65f8f1792955 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 16 Nov 2023 14:32:15 +0000 Subject: [PATCH 020/146] chore(version): v1.80.0 --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bffafeb53bd9..67bb169aebd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.80.0 (2023-11-16) + +### Features + +- **router:** Add api to migrate card from basilisk to rust ([#2853](https://github.com/juspay/hyperswitch/pull/2853)) ([`b8b20c4`](https://github.com/juspay/hyperswitch/commit/b8b20c412df0485bf395f9aa21e6e34e90d97acd)) +- Spawn webhooks and async scheduling in background ([#2780](https://github.com/juspay/hyperswitch/pull/2780)) ([`f248fe2`](https://github.com/juspay/hyperswitch/commit/f248fe2889c9cb68af4464ab0db1735224ab5c8d)) + +### Refactors + +- **router:** Add openapi spec support for gsm apis ([#2871](https://github.com/juspay/hyperswitch/pull/2871)) ([`62c9cca`](https://github.com/juspay/hyperswitch/commit/62c9ccae6ab0d128c54962675b88739ad7797fe6)) + +**Full Changelog:** [`v1.79.0...v1.80.0`](https://github.com/juspay/hyperswitch/compare/v1.79.0...v1.80.0) + +- - - + + ## 1.79.0 (2023-11-16) ### Features From e8de3a710710b92f5c2351c5d67c22352c2b0a30 Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Thu, 16 Nov 2023 19:40:53 +0530 Subject: [PATCH 021/146] feat(connector): [BANKOFAMERICA] Implement Cards for Bank of America (#2765) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: swangi-kumari --- .github/secrets/connector_auth.toml.gpg | Bin 3310 -> 3395 bytes crates/api_models/src/enums.rs | 4 +- crates/api_models/src/routing.rs | 3 +- crates/euclid/src/enums.rs | 3 +- crates/router/src/configs/defaults.rs | 132 + crates/router/src/connector/bankofamerica.rs | 398 +- .../connector/bankofamerica/transformers.rs | 781 ++- .../connector/multisafepay/transformers.rs | 12 +- .../src/connector/payeezy/transformers.rs | 13 +- crates/router/src/connector/utils.rs | 2 + crates/router/src/core/admin.rs | 8 +- .../src/core/payments/routing/transformers.rs | 3 +- crates/router/src/types/api.rs | 2 +- crates/router/src/types/transformers.rs | 6 +- openapi/openapi_spec.json | 1 + .../collection-dir/bankofamerica/.auth.json | 22 + .../bankofamerica/.event.meta.json | 6 + .../collection-dir/bankofamerica/.info.json | 9 + .../collection-dir/bankofamerica/.meta.json | 9 + .../bankofamerica/.variable.json | 100 + .../bankofamerica/API Key/.meta.json | 9 + .../API Key/Create API Key/.event.meta.json | 5 + .../API Key/Create API Key/event.test.js | 46 + .../API Key/Create API Key/request.json | 52 + .../API Key/Create API Key/response.json | 1 + .../API Key/Delete API Key/.event.meta.json | 5 + .../API Key/Delete API Key/event.test.js | 17 + .../API Key/Delete API Key/request.json | 49 + .../API Key/Delete API Key/response.json | 1 + .../API Key/List API Keys/.event.meta.json | 5 + .../API Key/List API Keys/event.test.js | 46 + .../API Key/List API Keys/request.json | 45 + .../API Key/List API Keys/response.json | 1 + .../API Key/Retrieve API Key/.event.meta.json | 5 + .../API Key/Retrieve API Key/event.test.js | 49 + .../API Key/Retrieve API Key/request.json | 49 + .../API Key/Retrieve API Key/response.json | 1 + .../API Key/Update API Key/.event.meta.json | 5 + .../API Key/Update API Key/event.test.js | 49 + .../API Key/Update API Key/request.json | 57 + .../API Key/Update API Key/response.json | 1 + .../bankofamerica/Flow Testcases/.meta.json | 6 + .../Flow Testcases/Happy Cases/.meta.json | 9 + .../.meta.json | 6 + .../Payments - Create/.event.meta.json | 5 + .../Payments - Create/event.test.js | 80 + .../Payments - Create/request.json | 98 + .../Payments - Create/response.json | 1 + .../Payments - Retrieve/.event.meta.json | 5 + .../Payments - Retrieve/event.test.js | 80 + .../Payments - Retrieve/request.json | 33 + .../Payments - Retrieve/response.json | 1 + .../.meta.json | 7 + .../Payments - Confirm/.event.meta.json | 6 + .../Payments - Confirm/event.prerequest.js | 0 .../Payments - Confirm/event.test.js | 103 + .../Payments - Confirm/request.json | 63 + .../Payments - Confirm/response.json | 1 + .../Payments - Create/.event.meta.json | 5 + .../Payments - Create/event.test.js | 71 + .../Payments - Create/request.json | 98 + .../Payments - Create/response.json | 1 + .../Payments - Retrieve/.event.meta.json | 5 + .../Payments - Retrieve/event.test.js | 91 + .../Payments - Retrieve/request.json | 33 + .../Payments - Retrieve/response.json | 1 + .../.meta.json | 7 + .../Payments - Confirm/.event.meta.json | 6 + .../Payments - Confirm/event.prerequest.js | 0 .../Payments - Confirm/event.test.js | 73 + .../Payments - Confirm/request.json | 73 + .../Payments - Confirm/response.json | 1 + .../Payments - Create/.event.meta.json | 5 + .../Payments - Create/event.test.js | 71 + .../Payments - Create/request.json | 84 + .../Payments - Create/response.json | 1 + .../Payments - Retrieve/.event.meta.json | 5 + .../Payments - Retrieve/event.test.js | 71 + .../Payments - Retrieve/request.json | 33 + .../Payments - Retrieve/response.json | 1 + .../.meta.json | 7 + .../Payments - Capture/.event.meta.json | 5 + .../Payments - Capture/event.test.js | 94 + .../Payments - Capture/request.json | 45 + .../Payments - Capture/response.json | 1 + .../Payments - Create/.event.meta.json | 5 + .../Payments - Create/event.test.js | 71 + .../Payments - Create/request.json | 98 + .../Payments - Create/response.json | 1 + .../Payments - Retrieve/.event.meta.json | 5 + .../Payments - Retrieve/event.test.js | 71 + .../Payments - Retrieve/request.json | 33 + .../Payments - Retrieve/response.json | 1 + .../Scenario5-Void the payment/.meta.json | 7 + .../Payments - Cancel/.event.meta.json | 5 + .../Payments - Cancel/event.test.js | 61 + .../Payments - Cancel/request.json | 43 + .../Payments - Cancel/response.json | 1 + .../Payments - Create/.event.meta.json | 5 + .../Payments - Create/event.test.js | 71 + .../Payments - Create/request.json | 98 + .../Payments - Create/response.json | 1 + .../Payments - Retrieve/.event.meta.json | 5 + .../Payments - Retrieve/event.test.js | 71 + .../Payments - Retrieve/request.json | 33 + .../Payments - Retrieve/response.json | 1 + .../Flow Testcases/QuickStart/.meta.json | 9 + .../API Key - Create/.event.meta.json | 5 + .../QuickStart/API Key - Create/event.test.js | 46 + .../QuickStart/API Key - Create/request.json | 52 + .../QuickStart/API Key - Create/response.json | 1 + .../.event.meta.json | 5 + .../Merchant Account - Create/event.test.js | 56 + .../Merchant Account - Create/request.json | 95 + .../Merchant Account - Create/response.json | 1 + .../.event.meta.json | 5 + .../Payment Connector - Create/event.test.js | 39 + .../Payment Connector - Create/request.json | 108 + .../Payment Connector - Create/response.json | 1 + .../Payments - Create/.event.meta.json | 5 + .../Payments - Create/event.test.js | 61 + .../QuickStart/Payments - Create/request.json | 103 + .../Payments - Create/response.json | 1 + .../Payments - Retrieve/.event.meta.json | 5 + .../Payments - Retrieve/event.test.js | 61 + .../Payments - Retrieve/request.json | 27 + .../Payments - Retrieve/response.json | 1 + .../bankofamerica/Health check/.meta.json | 5 + .../Health check/New Request/.event.meta.json | 5 + .../Health check/New Request/event.test.js | 4 + .../Health check/New Request/request.json | 20 + .../Health check/New Request/response.json | 1 + .../bankofamerica/MerchantAccounts/.meta.json | 8 + .../.event.meta.json | 6 + .../event.prerequest.js | 0 .../Merchant Account - Create/event.test.js | 77 + .../Merchant Account - Create/request.json | 95 + .../Merchant Account - Create/response.json | 1 + .../Merchant Account - List/.event.meta.json | 5 + .../Merchant Account - List/event.test.js | 43 + .../Merchant Account - List/request.json | 53 + .../Merchant Account - List/response.json | 1 + .../.event.meta.json | 5 + .../Merchant Account - Retrieve/event.test.js | 43 + .../Merchant Account - Retrieve/request.json | 47 + .../Merchant Account - Retrieve/response.json | 1 + .../.event.meta.json | 5 + .../Merchant Account - Update/event.test.js | 46 + .../Merchant Account - Update/request.json | 98 + .../Merchant Account - Update/response.json | 1 + .../PaymentConnectors/.meta.json | 10 + .../List Connectors by MID/.event.meta.json | 5 + .../List Connectors by MID/event.test.js | 17 + .../List Connectors by MID/request.json | 41 + .../List Connectors by MID/response.json | 1 + .../.event.meta.json | 5 + .../Merchant Account - Delete/event.test.js | 42 + .../Merchant Account - Delete/request.json | 47 + .../Merchant Account - Delete/response.json | 1 + .../.event.meta.json | 5 + .../Payment Connector - Create/event.test.js | 47 + .../Payment Connector - Create/request.json | 108 + .../Payment Connector - Create/response.json | 1 + .../.event.meta.json | 5 + .../Payment Connector - Delete/event.test.js | 39 + .../Payment Connector - Delete/request.json | 52 + .../Payment Connector - Delete/response.json | 1 + .../.event.meta.json | 5 + .../event.test.js | 39 + .../Payment Connector - Retrieve/request.json | 54 + .../response.json | 1 + .../.event.meta.json | 5 + .../Payment Connector - Update/event.test.js | 47 + .../Payment Connector - Update/request.json | 109 + .../Payment Connector - Update/response.json | 1 + .../bankofamerica/event.prerequest.js | 0 .../bankofamerica/event.test.js | 13 + .../bankofamerica.postman_collection.json | 4310 +++++++++++++++++ 178 files changed, 10152 insertions(+), 207 deletions(-) create mode 100644 postman/collection-dir/bankofamerica/.auth.json create mode 100644 postman/collection-dir/bankofamerica/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/.info.json create mode 100644 postman/collection-dir/bankofamerica/.meta.json create mode 100644 postman/collection-dir/bankofamerica/.variable.json create mode 100644 postman/collection-dir/bankofamerica/API Key/.meta.json create mode 100644 postman/collection-dir/bankofamerica/API Key/Create API Key/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/API Key/Create API Key/event.test.js create mode 100644 postman/collection-dir/bankofamerica/API Key/Create API Key/request.json create mode 100644 postman/collection-dir/bankofamerica/API Key/Create API Key/response.json create mode 100644 postman/collection-dir/bankofamerica/API Key/Delete API Key/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/API Key/Delete API Key/event.test.js create mode 100644 postman/collection-dir/bankofamerica/API Key/Delete API Key/request.json create mode 100644 postman/collection-dir/bankofamerica/API Key/Delete API Key/response.json create mode 100644 postman/collection-dir/bankofamerica/API Key/List API Keys/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/API Key/List API Keys/event.test.js create mode 100644 postman/collection-dir/bankofamerica/API Key/List API Keys/request.json create mode 100644 postman/collection-dir/bankofamerica/API Key/List API Keys/response.json create mode 100644 postman/collection-dir/bankofamerica/API Key/Retrieve API Key/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/API Key/Retrieve API Key/event.test.js create mode 100644 postman/collection-dir/bankofamerica/API Key/Retrieve API Key/request.json create mode 100644 postman/collection-dir/bankofamerica/API Key/Retrieve API Key/response.json create mode 100644 postman/collection-dir/bankofamerica/API Key/Update API Key/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/API Key/Update API Key/event.test.js create mode 100644 postman/collection-dir/bankofamerica/API Key/Update API Key/request.json create mode 100644 postman/collection-dir/bankofamerica/API Key/Update API Key/response.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/.meta.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/.meta.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/.meta.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/event.test.js create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/response.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/event.test.js create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/request.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/response.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/.meta.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.prerequest.js create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/response.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.test.js create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/response.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/event.test.js create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/response.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/.meta.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/event.prerequest.js create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/event.test.js create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/request.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/response.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/event.test.js create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/response.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/event.test.js create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/request.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/response.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/.meta.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/request.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/response.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/event.test.js create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/request.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/response.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/event.test.js create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/request.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/response.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/.meta.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/event.test.js create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/request.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/response.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/event.test.js create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/request.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/response.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/event.test.js create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/request.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/response.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/.meta.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/API Key - Create/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/API Key - Create/event.test.js create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/API Key - Create/request.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/API Key - Create/response.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Merchant Account - Create/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Merchant Account - Create/event.test.js create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Merchant Account - Create/request.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Merchant Account - Create/response.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payment Connector - Create/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payment Connector - Create/event.test.js create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payment Connector - Create/request.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payment Connector - Create/response.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Create/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Create/event.test.js create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Create/request.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Create/response.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Retrieve/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Retrieve/event.test.js create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Retrieve/request.json create mode 100644 postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Retrieve/response.json create mode 100644 postman/collection-dir/bankofamerica/Health check/.meta.json create mode 100644 postman/collection-dir/bankofamerica/Health check/New Request/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/Health check/New Request/event.test.js create mode 100644 postman/collection-dir/bankofamerica/Health check/New Request/request.json create mode 100644 postman/collection-dir/bankofamerica/Health check/New Request/response.json create mode 100644 postman/collection-dir/bankofamerica/MerchantAccounts/.meta.json create mode 100644 postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/event.prerequest.js create mode 100644 postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/event.test.js create mode 100644 postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/request.json create mode 100644 postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/response.json create mode 100644 postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - List/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - List/event.test.js create mode 100644 postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - List/request.json create mode 100644 postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - List/response.json create mode 100644 postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Retrieve/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Retrieve/event.test.js create mode 100644 postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Retrieve/request.json create mode 100644 postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Retrieve/response.json create mode 100644 postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Update/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Update/event.test.js create mode 100644 postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Update/request.json create mode 100644 postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Update/response.json create mode 100644 postman/collection-dir/bankofamerica/PaymentConnectors/.meta.json create mode 100644 postman/collection-dir/bankofamerica/PaymentConnectors/List Connectors by MID/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/PaymentConnectors/List Connectors by MID/event.test.js create mode 100644 postman/collection-dir/bankofamerica/PaymentConnectors/List Connectors by MID/request.json create mode 100644 postman/collection-dir/bankofamerica/PaymentConnectors/List Connectors by MID/response.json create mode 100644 postman/collection-dir/bankofamerica/PaymentConnectors/Merchant Account - Delete/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/PaymentConnectors/Merchant Account - Delete/event.test.js create mode 100644 postman/collection-dir/bankofamerica/PaymentConnectors/Merchant Account - Delete/request.json create mode 100644 postman/collection-dir/bankofamerica/PaymentConnectors/Merchant Account - Delete/response.json create mode 100644 postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Create/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Create/event.test.js create mode 100644 postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Create/request.json create mode 100644 postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Create/response.json create mode 100644 postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Delete/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Delete/event.test.js create mode 100644 postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Delete/request.json create mode 100644 postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Delete/response.json create mode 100644 postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Retrieve/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Retrieve/event.test.js create mode 100644 postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Retrieve/request.json create mode 100644 postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Retrieve/response.json create mode 100644 postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Update/.event.meta.json create mode 100644 postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Update/event.test.js create mode 100644 postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Update/request.json create mode 100644 postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Update/response.json create mode 100644 postman/collection-dir/bankofamerica/event.prerequest.js create mode 100644 postman/collection-dir/bankofamerica/event.test.js create mode 100644 postman/collection-json/bankofamerica.postman_collection.json diff --git a/.github/secrets/connector_auth.toml.gpg b/.github/secrets/connector_auth.toml.gpg index 487e436df4638117aa49684ae4f7f45f682e44cd..7da9189ade58a1bab00649a724f5f1c4f26fee7f 100644 GIT binary patch literal 3395 zcmV-J4ZQM<4Fm}T2wG4r!^LpaX8Y3W0ZVS|^j)3kaW~$@Z$|xD!@3GwP4w$K(<2$o zlLY+w3CK|D-v1@*z~Nt?WUP&Oa<(o4+SzXZ)*ohBhM2ZyUF@mN(rQI)6cr!D0I08w zJyGIrA!kTvq+d)F(8rYwe`tLxM5cOeg)}^oBL35%EzZ3spBjd+!H2e=CGEc9R@=C3V|2FX$Z!O^hTkoXi?43BglJB@!04mGa zD{6PoP8C084R{b-JbtG9HM>LkUE*EoUjvc}PC1J+{Z{iaUIOJGe^n7nxit^|TWs$! zhONQbNDGu!J8db7-5{K+&ZZ7H(ip*>MZbfWlfWCnpp$44?dfveKX0oT&xo#qHquCl z`SB1jSC3XWYmruA=Nr5<+2_Y!Y>DPJL~0BgWc6`ku9iF6?$+5xYhA&YaTdvI!y?GE z*7#`)g7rjVD7*g2w#r66Qn2-55hTef`x&Jm?L! z4qT*@$qdaT=N<{pY7g(^pa5%w^6XU?5S-cGAVISe@SCrCRliGUXXpIkzLC)a*(2!W zzQ+oskf;2^XztBe$?wbDo`dW?z`5Ofy)5x=YY5}m(x~J5Nd+Ue2s8$TK@$qf@z6hF zLPGqpP^Vj_^gYi{{?%m+5ZVoHRZ+r;DRb&GHzu#2Y3IF7!Z2J{?!$mbSq!(Md-b_Q zzKZY#p*a>yBA{lFC5H~?cy^L@@t;)$TIBFA_?uwRzNR(k?m@-YpD@t;jno(ZGpp~O zbYTFokT;F++Rqlez6NvuAnI=q!d+ut_dwml3_urda4+1-zKtPjj296FEU8$!{bMT_ zVpinv!(|$IbAKf9W(_AHN6|@mF>5s1>6_K)dC^XvgP9*^9TI^FJbwZ+-w0xJ6)}_F z%0d4Y7eH`kIejTb&ZKWd2Rs9reYUc=@_Z>K`8hu-4@4x;2}hR}_WGcLn#5>X?tQ4ysEyeqowUv>=;{0e^9Y~qca}dP?92* z@vA3vD4(ef(z85wN21@_3IT?GJm?epi~Ok+?4#Pc&`Mxu(_V-RE;4=C?k(ZvG+&az zvOzM=1b)rJ33GJ^OChdxG1@(Nyp9Fxowkie|VFAo0l?k1ro5_TK9c^eyGsE1EdF zuN%Bu@qMiMHBo_8eIxVu$OYrX18Rztr2yY-=q^|@g{LS<+y1ZBb5`>i(ie~CX5(mb zkrY|I4h^*B?QED9+1-(;143Qn)^1w+TwrDZUDaSf^ETS9^><-GIk+mW;nd<*0yD4uGsZ7K)#^-exjTEIrHwrcFpl0`sc1;$h(MGiG5>}`=HW54HE zKRZ1UgE^%ElFTBqMq37#HtwG(ZxlmD2QQoYMlXfERNE%+G3C^@K*(zWGTaxS&NxVn z7`Wd$4+r)h1&=gFnD~Z;O@EG5T$GQ{rnAL@zAW#l2epnAn7D@77B8qt6l=F^_b`R42#5x);0z*k^C=e{E%Xr&ku2?8YosLpkkd%5_V?^CCa3P zs?km+8CfI0`Js4RN3HWi=3Rm3bBWVIa|3<+_^u(Quug{2M|rE9FMW;oB9(s37{S)5{M> z<<$(rkm84zZw~8Xc}_H_J-i6^_}c0ySMB2eb_*i|q$;Nns;c1l@p@)&fXbZ;`YekX zLCpLAQ0@ll!ZO@>YrA`o&-HN6udHTtVxOfqJqG}v`FsYZyL&L!-(9wn)R%2)EhG=w zZ5fQGBs2~h>&o9OBo6QGiP!QX!gH{$D_w;}!y-vuSZ(LtlFy0*U{C zB_$8y`Y@0dj)$P9euGyi0YnD~E7}9XOQQNseGKE++#iAW(Nu4x+s-f8yPT09*sdK# zg6Xf(_W(cowMo;CXq>=g^=_|Ecc@=UYszI0z%&`>c^pLuNP&#XOH}B18Y#C?I15AL z|F)nk`U$z^en?1yD3Kgi%ubN748*1sN6~GN>>$sbdL=wpV<|d&wq8lrUQgn%$KjBY zWCE)TkPZXGSU&BjaI$pRjcSW*Pul;syCrAkXjfxb1uUZ~OR0Kfc7RS!m#lPB$iy>5 zi(Q4U@a18vN+v;R%v0C@hu?H{+<>R6Km#H6L~h-s20{C=OC^7hz0vr`OpJpl9YmE$ z7D(}Jjp|b$jwd&I zVAPpIfy~GrD69t{3O2NG7oE`55<#jb7NeQj{C_Det~kejavM*j7qz79jAGLTH zB9-qgu+c}$?%6eCc2rWM-6GlikG0)Q9wQG@MN$ENr37}LGs9>i;$0xI?k&VGr>k2_ zM(LeaUC!wWVcZz`Q7xirQLYQhec(ANOtEt^Qe9Zrq*dP}A{mAKzjV%L;7;+(@wpaW zi`Q&c+IY3}1lq2(;d|w*cF6_SnP_pp8(Rg-CG}SN5C$Yus&c%#*(3R$c_X3xJ|6kP z0)3g@ZOicSoB7}tTWAo<7&Oj6|JF2c@-Z!by{FAIsnCDZa_y^}eM;52{vN?|7P!oZ zJ-X_tXa*3Q!wv&=bPXk9a(%zj4S}tF!VlX|`2oeO!OR zDg%e$R`E{u5+r@pzs6v+m`C%87n4MQvR*IZ%4woK@bE{CrGMhoU~)}Y9#~0AS0Bcz zK#!_BC=7xjOP`}q0NQJ$Acb!yXdveKebb=3kyUQRx`tbfdzb`qgZyIlauzd+3l`Q% zNMkh0_y^jFe+~OTaMaa-{|JEtW$AJkew1rb=m(zf^lCiGIJ5l!2v!n&WDAR35df>N zTS0&@V&Ms7-h0qNVRw#j zZy4%FW?WwO%@pw)^{cL~I-%l+)?n7G;CaNSJ($CvJju+aJ;Gj$8*BR|aJ3eq(E5z|e3z=%RK{(KuS043{Hx2ATPP0<-Ek$q=TO`N+p}HW=*Xl=B9>-Sj zHrDTZP})=44LShDA_DmVPFHm~lPW^odOb0IE zGoj>8;tnBkz%~X;IPKg<8NX;r@h8fZQSHGm8$JdH>5J{{fzTp@L(WGp5}X+2h0 z`Xsj?vj>b`ao+EX)R7>a3B%}kl(TR>=8n^;kTp)VY!2;SlltlhrG~1?-DjU4_w1dhkr78!r$I*tnlqV z7|{_nCtsV@8Dw5ce`W@;B%KBa$P&J!;1-5u%^PJj`3>d??XXVGeW*0u z6NPl?R#V;%mgcBuxukxrSr<(AYLs$A=C*tpGp@jHFACCbppAIj4Dc9#nr{#ad<59| zIuE$VgAX{LkKZ%@W_7SrB5`1l-)W%}Rx0v68j7pAKL=Zfiv1&`UDGAh;~6a(UdW)k zKOXj1A(1NdDnW9e(j#coreBj|4(rvi_f4K;v-)^t*(x+#1sz=`>8a7jUZs)X@^j z?Sb&uy#On^RBnZ*k@YxvAvf_8Tj&1(+ipUGLtD?antwuh9_SeCj1`=DK_1IzpJRjA zHsd%NR`3TKf`_-)k2qe(x~xju7NiLYxi(PW@ml~^7BXp_a(XDHFk}W+P!0Dc5+if% zWV{~Tq_E?tqa`Nt+2W#U-#%2~y#>WRhW!L7xInUJdTR++nI0`pe))dejx2EHh2NMG zu*N;@T^tM4TT%^w@K9e`FnB_YV!ov_j8?bNnNSu-_)X&n&V;^g%tE>tkH0#(P!mPo zB{L=%1AiX#yi!lJ9|bSGF1ZG)`Ar#?HS^fS%`I>f0Pw0+(Q5+`Ha{kcoh935ry^?G zIJ|$pEP>kt{kw6%{WjNS=OkO2xSLG^Y4}D8?|~<8r%VB zBG&q|`=WEi2Sk}ALR@A0klZA#CahoP(<}v|#*WLIypVZbA*OEZY|mg^@U?7F1l|rNPB-iXiLh9z>Buea z_dYTWN4%qsu=4R^OIsJ;#3s5DYa9W&;HE-%z%CBU@4qD$a_psbkY4^XoW1)N)9p%%KWyk738qTYaW3p5dgA$uX*2ey|hX|)d`NtJSA zVF@fNe79(6UIrEOdWxMZ`kRA-PfteihO$GAziL@CB@I`Zv8Hq*am!D{)XP_s^e7`B~}H*dTnX&YjGjiLx+=YE|lX zHQ{|zj|{C{n3ocGTbmtVbv3$_*OarX!q51gNMXkh-XxaC9^hRpJ^aGx^9O?fQ#Dwt zh5FimU_@qE>KLZ8aNOo4gRZ@-tMESHtY@3IQLxOKSaQURw}dSdz~M)OB)F?VnE*U0 z96%(gI1_8QFcp=VIvg?PB$&uJjNr3} zj@=0uUU*SM1Ool(YpF>XND(hcgGWcqIFb)l9ATT^6VW@?@l5>4W zZ>F>Z?vuvHBeiD0nL6}rC+t?BF+QeQ;dwBG0!Nj?K~8n3V!gnKZIZtOa8`wue*6SW zviRFz)G{8EqU3VvVYqwkGz@)XhK-<_77M2zuGC!jLc{He+sStL1zok920(YA11h_6 z2_DY_EU;Aoc2R%J*5xl@J!(H}H2Sq|4=)MXI*?N5Zw{wrRE($5g^PQjbVGkiu9;6q zu;d6)G=h!$`%j{IE$q#3cX{qFYC|&VlAr$!TFXe>`k@!_6-7Gtsp>z*n)54b6t~Bv zYBFeE8YAW4SFiRw@rqWCC|!PfYJfpBT**Da9{~1dp9E_!dh!XPztIsfX;G2daxIeX zmz8S*l6{$X^Z#;A|6EQ#A4srsx^>yj1WTBui!mQ#93YK^`c8D`8t?LJ#I-XAj7l4W z65?$HTe~(>h6oVpbH>nFIrE0y%+U5<@4J*KQ*#1K$D>sKM{M&$g2(71*$v}mmh7-n z^7=sbc$r3mYvL2K@>xBWJTIWo00(q{c?#5$Es*=hGVnO9;X!tj9S#O6M)=?4gK>a7 zy<3;>S~NWfeZMm+gzm|+H@5M*%U9hIcU1N6 z^btXKy&*cWH%>jA-@oM(RFuYc5!YIh-Ys)W;qLzi{5#nIU^?AZHGG0-2aG;~P2u25 z&uRS&r!4ys9qj;%$q{c}?2G-%EUBgd?B47_N`x*W(-!HQ#Z^s{rI+@~WWWS*n1CCg z?D}AEU7h$VCSz!Ax>KRz2H_@@e0?nApkCje>CF4&EM-{jN~mTMzb4psNK&kukl@9v8c3DY%$58^T7n0ClzCB~6n*NVP^8Hr3^~pqAvYzZc?eI2UO`|z5^+T_YT;EMZgX&bwuc%0N zsnZVdSP7T!t}SQq5i1rok03<_Ve>$GNn5XA?%xr(NMxgu&yS1GWp_;+pQjKCALhd! z4~Zxmb+!UqthXq4m;(genlz6pQ-8#pa!Uq*^-ZBg^&Di)EgdX#Ai~zdh_pabT{Bs( z1_HM|e@vqw4T{F!M16+p`N}gU)wf=%FO%fgff)#6XTg#yx9djL?3CkmZJ|)bCu{5h zz@o@Y*sR!c9Xo#YI!+fvHcQH;UdD8Q9xFcXQnV5lr@f3lZtjfO28G(G?Zj%7;ps~{ z$^J9)4NpPJZNSXm^|wQpR^_z*pW=F~G`$bR@wBNAIJUuzNzrMgF%dUhDG<~abcdH1=WqD4jIH&So?hDDgRm>(Q-ER<)J#DFb1?M1fYq3LA0GXQ zcG>E6GY`V&x}O*aOmpc-~}K2z}0%LyOs<;=SmB=IzxfC2Cb0nNx?T&)%;Bh?>F zgZz95Df`XDg@gv$+z0~Dr7b`?DeiOPm5BGdq@S8u@t#UYke2XtA*(=9!cWlGor%?P zF}5&(u06+t+pKhsCW&Ao3^m3SoRtK`gy$UPY0G??EiI{CeW0hIF*_`Lm5oHs?*2AR z1jLZM=3IMRXYMm#slc2x4DtZDq;LfdkzHXL{-#8s+9sChpOu@i#VDr=KtmpxeWatp z*5Gvr)2P^Zf2)0+9pIfWL3RlH`r~TBxl*C3XBwhRdv7x{6s*dU){%g-5D(~54Lwk<`-@!??(Z{Lv@sxO@Z7kx?} z3KK1&tt!_h9+kvdqxB$ey5z3)aAcU~IRombAgXImQ2-xofrOBrQ2q2!a}-JN*$Y>@~F0u@M0 z_t}e3-Q$D+KI!|qSHa4w`H-qLKfjuzM82`)S=8}~vxGt8QSrPrA$f+hkZ+_s`1;@G zulMeS$}*k{Ldx`D?RO9tt^Yl~Kn(Wx8)8vW2JtmQmcB)c@et;^Lw?2))byI~f-p?} z_J;4hVQD7_Z7&Q|uEF*z8B&%5_ce%`HRg6TCR!!QT8<2snr|MJ>a2;b&LvE;*t_71 s_Sy+{An_^L*Qbd0DMs*_fFEN)%u3nS3!R@JrnrOXXDHeQgilF&;>e+F8vp for ast::ConnectorChoice { RoutableConnectors::Adyen => euclid_enums::Connector::Adyen, RoutableConnectors::Airwallex => euclid_enums::Connector::Airwallex, RoutableConnectors::Authorizedotnet => euclid_enums::Connector::Authorizedotnet, - RoutableConnectors::Bitpay => euclid_enums::Connector::Bitpay, RoutableConnectors::Bambora => euclid_enums::Connector::Bambora, + RoutableConnectors::Bankofamerica => euclid_enums::Connector::Bankofamerica, + RoutableConnectors::Bitpay => euclid_enums::Connector::Bitpay, RoutableConnectors::Bluesnap => euclid_enums::Connector::Bluesnap, RoutableConnectors::Boku => euclid_enums::Connector::Boku, RoutableConnectors::Braintree => euclid_enums::Connector::Braintree, diff --git a/crates/euclid/src/enums.rs b/crates/euclid/src/enums.rs index 4188860ab90f..da5c99816715 100644 --- a/crates/euclid/src/enums.rs +++ b/crates/euclid/src/enums.rs @@ -86,8 +86,9 @@ pub enum Connector { Adyen, Airwallex, Authorizedotnet, - Bitpay, Bambora, + Bankofamerica, + Bitpay, Bluesnap, Boku, Braintree, diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index 18a70a8100aa..b71e2aad5b5d 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -459,6 +459,138 @@ impl Default for super::settings::RequiredFields { common: HashMap::new(), } ), + ( + enums::Connector::Bankofamerica, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from( + [ + ( + "payment_method_data.card.card_number".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_number".to_string(), + display_name: "card_number".to_string(), + field_type: enums::FieldType::UserCardNumber, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_month".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_month".to_string(), + display_name: "card_exp_month".to_string(), + field_type: enums::FieldType::UserCardExpiryMonth, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_year".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_year".to_string(), + display_name: "card_exp_year".to_string(), + field_type: enums::FieldType::UserCardExpiryYear, + value: None, + } + ), + ( + "payment_method_data.card.card_cvc".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_cvc".to_string(), + display_name: "card_cvc".to_string(), + field_type: enums::FieldType::UserCardCvc, + value: None, + } + ), + ( + "payment_method_data.card.card_holder_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_holder_name".to_string(), + display_name: "card_holder_name".to_string(), + field_type: enums::FieldType::UserFullName, + value: None, + } + ), + ( + "email".to_string(), + RequiredFieldInfo { + required_field: "email".to_string(), + display_name: "email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.first_name".to_string(), + display_name: "billing_first_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "billing_last_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "billing.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserAddressCity, + value: None, + } + ), + ( + "billing.address.state".to_string(), + RequiredFieldInfo { + required_field: "billing.address.state".to_string(), + display_name: "state".to_string(), + field_type: enums::FieldType::UserAddressState, + value: None, + } + ), + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressline1, + value: None, + } + ), + ] + ), + common: HashMap::new(), + } + ), ( enums::Connector::Bluesnap, RequiredFieldFinal { diff --git a/crates/router/src/connector/bankofamerica.rs b/crates/router/src/connector/bankofamerica.rs index 84870f7407fb..51a1d722dc51 100644 --- a/crates/router/src/connector/bankofamerica.rs +++ b/crates/router/src/connector/bankofamerica.rs @@ -2,12 +2,19 @@ pub mod transformers; use std::fmt::Debug; +use base64::Engine; +use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; -use masking::ExposeInterface; +use masking::{ExposeInterface, PeekInterface}; +use ring::{digest, hmac}; +use time::OffsetDateTime; use transformers as bankofamerica; +use url::Url; use crate::{ configs::settings, + connector::{utils as connector_utils, utils::RefundsRequestData}, + consts, core::errors::{self, CustomResult}, headers, services::{ @@ -23,6 +30,8 @@ use crate::{ utils::{self, BytesExt}, }; +pub const V_C_MERCHANT_ID: &str = "v-c-merchant-id"; + #[derive(Debug, Clone)] pub struct Bankofamerica; @@ -39,6 +48,54 @@ impl api::RefundExecute for Bankofamerica {} impl api::RefundSync for Bankofamerica {} impl api::PaymentToken for Bankofamerica {} +impl Bankofamerica { + pub fn generate_digest(&self, payload: &[u8]) -> String { + let payload_digest = digest::digest(&digest::SHA256, payload); + consts::BASE64_ENGINE.encode(payload_digest) + } + + pub fn generate_signature( + &self, + auth: bankofamerica::BankOfAmericaAuthType, + host: String, + resource: &str, + payload: &String, + date: OffsetDateTime, + http_method: services::Method, + ) -> CustomResult { + let bankofamerica::BankOfAmericaAuthType { + api_key, + merchant_account, + api_secret, + } = auth; + let is_post_method = matches!(http_method, services::Method::Post); + let digest_str = if is_post_method { "digest " } else { "" }; + let headers = format!("host date (request-target) {digest_str}{V_C_MERCHANT_ID}"); + let request_target = if is_post_method { + format!("(request-target): post {resource}\ndigest: SHA-256={payload}\n") + } else { + format!("(request-target): get {resource}\n") + }; + let signature_string = format!( + "host: {host}\ndate: {date}\n{request_target}{V_C_MERCHANT_ID}: {}", + merchant_account.peek() + ); + let key_value = consts::BASE64_ENGINE + .decode(api_secret.expose()) + .into_report() + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let key = hmac::Key::new(hmac::HMAC_SHA256, &key_value); + let signature_value = + consts::BASE64_ENGINE.encode(hmac::sign(&key, signature_string.as_bytes()).as_ref()); + let signature_header = format!( + r#"keyid="{}", algorithm="HmacSHA256", headers="{headers}", signature="{signature_value}""#, + api_key.peek() + ); + + Ok(signature_header) + } +} + impl ConnectorIntegration< api::PaymentMethodToken, @@ -56,15 +113,63 @@ where fn build_headers( &self, req: &types::RouterData, - _connectors: &settings::Connectors, - ) -> CustomResult)>, errors::ConnectorError> { - let mut header = vec![( - headers::CONTENT_TYPE.to_string(), - self.get_content_type().to_string().into(), - )]; - let mut api_key = self.get_auth_header(&req.connector_auth_type)?; - header.append(&mut api_key); - Ok(header) + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> + { + let date = OffsetDateTime::now_utc(); + let boa_req = self.get_request_body(req, connectors)?; + let http_method = self.get_http_method(); + let auth = bankofamerica::BankOfAmericaAuthType::try_from(&req.connector_auth_type)?; + let merchant_account = auth.merchant_account.clone(); + let base_url = connectors.bankofamerica.base_url.as_str(); + let boa_host = Url::parse(base_url) + .into_report() + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let host = boa_host + .host_str() + .ok_or(errors::ConnectorError::RequestEncodingFailed)?; + let path: String = self + .get_url(req, connectors)? + .chars() + .skip(base_url.len() - 1) + .collect(); + let sha256 = self.generate_digest( + boa_req + .map_or("{}".to_string(), |s| { + types::RequestBody::get_inner_value(s).expose() + }) + .as_bytes(), + ); + let signature = self.generate_signature( + auth, + host.to_string(), + path.as_str(), + &sha256, + date, + http_method, + )?; + + let mut headers = vec![ + ( + headers::CONTENT_TYPE.to_string(), + self.get_content_type().to_string().into(), + ), + ( + headers::ACCEPT.to_string(), + "application/hal+json;charset=utf-8".to_string().into(), + ), + (V_C_MERCHANT_ID.to_string(), merchant_account.into_masked()), + ("Date".to_string(), date.to_string().into()), + ("Host".to_string(), host.to_string().into()), + ("Signature".to_string(), signature.into_masked()), + ]; + if matches!(http_method, services::Method::Post | services::Method::Put) { + headers.push(( + "Digest".to_string(), + format!("SHA-256={sha256}").into_masked(), + )); + } + Ok(headers) } } @@ -74,50 +179,77 @@ impl ConnectorCommon for Bankofamerica { } fn get_currency_unit(&self) -> api::CurrencyUnit { - api::CurrencyUnit::Minor + api::CurrencyUnit::Base } fn common_get_content_type(&self) -> &'static str { - "application/json" + "application/json;charset=utf-8" } fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { connectors.bankofamerica.base_url.as_ref() } - fn get_auth_header( - &self, - auth_type: &types::ConnectorAuthType, - ) -> CustomResult)>, errors::ConnectorError> { - let auth = bankofamerica::BankofamericaAuthType::try_from(auth_type) - .change_context(errors::ConnectorError::FailedToObtainAuthType)?; - Ok(vec![( - headers::AUTHORIZATION.to_string(), - auth.api_key.expose().into_masked(), - )]) - } - fn build_error_response( &self, res: Response, ) -> CustomResult { - let response: bankofamerica::BankofamericaErrorResponse = res + let response: bankofamerica::BankOfAmericaErrorResponse = res .response - .parse_struct("BankofamericaErrorResponse") + .parse_struct("BankOfAmerica ErrorResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let error_message = if res.status_code == 401 { + consts::CONNECTOR_UNAUTHORIZED_ERROR + } else { + consts::NO_ERROR_MESSAGE + }; + + let (code, message) = match response.error_information { + Some(ref error_info) => (error_info.reason.clone(), error_info.message.clone()), + None => ( + response + .reason + .map_or(consts::NO_ERROR_CODE.to_string(), |reason| { + reason.to_string() + }), + response + .message + .map_or(error_message.to_string(), |message| message), + ), + }; + let connector_reason = match response.details { + Some(details) => details + .iter() + .map(|det| format!("{} : {}", det.field, det.reason)) + .collect::>() + .join(", "), + None => message.clone(), + }; + Ok(ErrorResponse { status_code: res.status_code, - code: response.code, - message: response.message, - reason: response.reason, + code, + message, + reason: Some(connector_reason), attempt_status: None, }) } } impl ConnectorValidation for Bankofamerica { - //TODO: implement functions when support enabled + fn validate_capture_method( + &self, + capture_method: Option, + ) -> CustomResult<(), errors::ConnectorError> { + let capture_method = capture_method.unwrap_or_default(); + match capture_method { + enums::CaptureMethod::Automatic | enums::CaptureMethod::Manual => Ok(()), + enums::CaptureMethod::ManualMultiple | enums::CaptureMethod::Scheduled => Err( + connector_utils::construct_not_implemented_error_report(capture_method, self.id()), + ), + } + } } impl ConnectorIntegration @@ -158,9 +290,12 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!( + "{}pts/v2/payments/", + api::ConnectorCommon::base_url(self, connectors) + )) } fn get_request_body( @@ -168,20 +303,20 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_router_data = bankofamerica::BankofamericaRouterData::try_from(( + let connector_router_data = bankofamerica::BankOfAmericaRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let req_obj = - bankofamerica::BankofamericaPaymentsRequest::try_from(&connector_router_data)?; - let bankofamerica_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, + let connector_request = + bankofamerica::BankOfAmericaPaymentsRequest::try_from(&connector_router_data)?; + let bankofamerica_payments_request = types::RequestBody::log_and_get_request_body( + &connector_request, + utils::Encode::::encode_to_string_of_json, ) .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bankofamerica_req)) + Ok(Some(bankofamerica_payments_request)) } fn build_request( @@ -211,9 +346,9 @@ impl ConnectorIntegration CustomResult { - let response: bankofamerica::BankofamericaPaymentsResponse = res + let response: bankofamerica::BankOfAmericaPaymentsResponse = res .response - .parse_struct("Bankofamerica PaymentsAuthorizeResponse") + .parse_struct("BankOfAmerica PaymentResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, @@ -245,12 +380,24 @@ impl ConnectorIntegration services::Method { + services::Method::Get + } + fn get_url( &self, - _req: &types::PaymentsSyncRouterData, - _connectors: &settings::Connectors, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let connector_payment_id = req + .request + .connector_transaction_id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; + Ok(format!( + "{}tss/v2/transactions/{connector_payment_id}", + self.base_url(connectors) + )) } fn build_request( @@ -273,9 +420,9 @@ impl ConnectorIntegration CustomResult { - let response: bankofamerica::BankofamericaPaymentsResponse = res + let response: bankofamerica::BankOfAmericaTransactionResponse = res .response - .parse_struct("bankofamerica PaymentsSyncResponse") + .parse_struct("BankOfAmerica PaymentSyncResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, @@ -309,18 +456,35 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let connector_payment_id = req.request.connector_transaction_id.clone(); + Ok(format!( + "{}pts/v2/payments/{connector_payment_id}/captures", + self.base_url(connectors) + )) } fn get_request_body( &self, - _req: &types::PaymentsCaptureRouterData, + req: &types::PaymentsCaptureRouterData, _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) + let connector_router_data = bankofamerica::BankOfAmericaRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount_to_capture, + req, + ))?; + let connector_request = + bankofamerica::BankOfAmericaCaptureRequest::try_from(&connector_router_data)?; + let bankofamerica_capture_request = types::RequestBody::log_and_get_request_body( + &connector_request, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(bankofamerica_capture_request)) } fn build_request( @@ -348,9 +512,9 @@ impl ConnectorIntegration CustomResult { - let response: bankofamerica::BankofamericaPaymentsResponse = res + let response: bankofamerica::BankOfAmericaPaymentsResponse = res .response - .parse_struct("Bankofamerica PaymentsCaptureResponse") + .parse_struct("BankOfAmerica PaymentResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, @@ -370,6 +534,100 @@ impl ConnectorIntegration for Bankofamerica { + fn get_headers( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_url( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let connector_payment_id = req.request.connector_transaction_id.clone(); + Ok(format!( + "{}pts/v2/payments/{connector_payment_id}/reversals", + self.base_url(connectors) + )) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_request_body( + &self, + req: &types::PaymentsCancelRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let connector_router_data = bankofamerica::BankOfAmericaRouterData::try_from(( + &self.get_currency_unit(), + req.request + .currency + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "Currency", + })?, + req.request + .amount + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "Amount", + })?, + req, + ))?; + let connector_request = + bankofamerica::BankOfAmericaVoidRequest::try_from(&connector_router_data)?; + + let bankofamerica_void_request = types::RequestBody::log_and_get_request_body( + &connector_request, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(bankofamerica_void_request)) + } + + fn build_request( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) + .body(types::PaymentsVoidType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsCancelRouterData, + res: Response, + ) -> CustomResult { + let response: bankofamerica::BankOfAmericaPaymentsResponse = res + .response + .parse_struct("BankOfAmerica PaymentResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } } impl ConnectorIntegration @@ -389,10 +647,14 @@ impl ConnectorIntegration, - _connectors: &settings::Connectors, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let connector_payment_id = req.request.connector_transaction_id.clone(); + Ok(format!( + "{}pts/v2/payments/{connector_payment_id}/refunds", + self.base_url(connectors) + )) } fn get_request_body( @@ -400,16 +662,16 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - let connector_router_data = bankofamerica::BankofamericaRouterData::try_from(( + let connector_router_data = bankofamerica::BankOfAmericaRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let req_obj = bankofamerica::BankofamericaRefundRequest::try_from(&connector_router_data)?; + let req_obj = bankofamerica::BankOfAmericaRefundRequest::try_from(&connector_router_data)?; let bankofamerica_req = types::RequestBody::log_and_get_request_body( &req_obj, - utils::Encode::::encode_to_string_of_json, + utils::Encode::::encode_to_string_of_json, ) .change_context(errors::ConnectorError::RequestEncodingFailed)?; Ok(Some(bankofamerica_req)) @@ -439,7 +701,7 @@ impl ConnectorIntegration, res: Response, ) -> CustomResult, errors::ConnectorError> { - let response: bankofamerica::RefundResponse = res + let response: bankofamerica::BankOfAmericaRefundResponse = res .response .parse_struct("bankofamerica RefundResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -473,12 +735,20 @@ impl ConnectorIntegration services::Method { + services::Method::Get + } + fn get_url( &self, - _req: &types::RefundSyncRouterData, - _connectors: &settings::Connectors, + req: &types::RefundSyncRouterData, + connectors: &settings::Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let refund_id = req.request.get_connector_refund_id()?; + Ok(format!( + "{}tss/v2/transactions/{refund_id}", + self.base_url(connectors) + )) } fn build_request( @@ -504,7 +774,7 @@ impl ConnectorIntegration CustomResult { - let response: bankofamerica::RefundResponse = res + let response: bankofamerica::BankOfAmericaRsyncResponse = res .response .parse_struct("bankofamerica RefundSyncResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index a396c47a4ced..20b2af48b168 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -1,15 +1,51 @@ +use api_models::payments; +use common_utils::pii; use masking::Secret; use serde::{Deserialize, Serialize}; use crate::{ - connector::utils::PaymentsAuthorizeRequestData, + connector::utils::{ + self, AddressDetailsData, CardData, CardIssuer, PaymentsAuthorizeRequestData, + PaymentsSyncRequestData, RouterData, + }, + consts, core::errors, - types::{self, api, storage::enums}, + types::{ + self, + api::{self, enums as api_enums}, + storage::enums, + transformers::ForeignFrom, + }, }; -//TODO: Fill the struct with respective fields -pub struct BankofamericaRouterData { - pub amount: i64, // The type of amount that a connector accepts, for example, String, i64, f64, etc. +pub struct BankOfAmericaAuthType { + pub(super) api_key: Secret, + pub(super) merchant_account: Secret, + pub(super) api_secret: Secret, +} + +impl TryFrom<&types::ConnectorAuthType> for BankOfAmericaAuthType { + type Error = error_stack::Report; + fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + if let types::ConnectorAuthType::SignatureKey { + api_key, + key1, + api_secret, + } = auth_type + { + Ok(Self { + api_key: api_key.to_owned(), + merchant_account: key1.to_owned(), + api_secret: api_secret.to_owned(), + }) + } else { + Err(errors::ConnectorError::FailedToObtainAuthType)? + } + } +} + +pub struct BankOfAmericaRouterData { + pub amount: String, pub router_data: T, } @@ -19,18 +55,18 @@ impl types::storage::enums::Currency, i64, T, - )> for BankofamericaRouterData + )> for BankOfAmericaRouterData { type Error = error_stack::Report; fn try_from( - (_currency_unit, _currency, amount, item): ( + (currency_unit, currency, amount, item): ( &types::api::CurrencyUnit, types::storage::enums::Currency, i64, T, ), ) -> Result { - //Todo : use utils to convert the amount to the type of amount that a connector accepts + let amount = utils::get_amount_as_string(currency_unit, amount, currency)?; Ok(Self { amount, router_data: item, @@ -38,184 +74,633 @@ impl } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct BankofamericaPaymentsRequest { - amount: i64, - card: BankofamericaCard, +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaPaymentsRequest { + processing_information: ProcessingInformation, + payment_information: PaymentInformation, + order_information: OrderInformationWithBill, + client_reference_information: ClientReferenceInformation, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct BankofamericaCard { - name: Secret, +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ProcessingInformation { + capture: bool, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CaptureOptions { + capture_sequence_number: u32, + total_capture_count: u32, +} + +#[derive(Debug, Serialize)] +pub struct PaymentInformation { + card: Card, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Card { number: cards::CardNumber, - expiry_month: Secret, - expiry_year: Secret, - cvc: Secret, - complete: bool, + expiration_month: Secret, + expiration_year: Secret, + security_code: Secret, + #[serde(rename = "type")] + card_type: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct OrderInformationWithBill { + amount_details: Amount, + bill_to: BillTo, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Amount { + total_amount: String, + currency: api_models::enums::Currency, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BillTo { + first_name: Secret, + last_name: Secret, + address1: Secret, + locality: String, + administrative_area: Secret, + postal_code: Secret, + country: api_enums::CountryAlpha2, + email: pii::Email, +} + +// for bankofamerica each item in Billing is mandatory +fn build_bill_to( + address_details: &payments::Address, + email: pii::Email, +) -> Result> { + let address = address_details + .address + .as_ref() + .ok_or_else(utils::missing_field_err("billing.address"))?; + Ok(BillTo { + first_name: address.get_first_name()?.to_owned(), + last_name: address.get_last_name()?.to_owned(), + address1: address.get_line1()?.to_owned(), + locality: address.get_city()?.to_owned(), + administrative_area: address.to_state_code()?, + postal_code: address.get_zip()?.to_owned(), + country: address.get_country()?.to_owned(), + email, + }) +} + +impl From for String { + fn from(card_issuer: CardIssuer) -> Self { + let card_type = match card_issuer { + CardIssuer::AmericanExpress => "003", + CardIssuer::Master => "002", + //"042" is the type code for Masetro Cards(International). For Maestro Cards(UK-Domestic) the mapping should be "024" + CardIssuer::Maestro => "042", + CardIssuer::Visa => "001", + CardIssuer::Discover => "004", + CardIssuer::DinersClub => "005", + CardIssuer::CarteBlanche => "006", + CardIssuer::JCB => "007", + }; + card_type.to_string() + } } -impl TryFrom<&BankofamericaRouterData<&types::PaymentsAuthorizeRouterData>> - for BankofamericaPaymentsRequest +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientReferenceInformation { + code: Option, +} + +impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>> + for BankOfAmericaPaymentsRequest { type Error = error_stack::Report; fn try_from( - item: &BankofamericaRouterData<&types::PaymentsAuthorizeRouterData>, + item: &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, ) -> Result { match item.router_data.request.payment_method_data.clone() { - api::PaymentMethodData::Card(req_card) => { - let card = BankofamericaCard { - name: req_card.card_holder_name, - number: req_card.card_number, - expiry_month: req_card.card_exp_month, - expiry_year: req_card.card_exp_year, - cvc: req_card.card_cvc, - complete: item.router_data.request.is_auto_capture()?, + api::PaymentMethodData::Card(ccard) => { + let email = item.router_data.request.get_email()?; + let bill_to = build_bill_to(item.router_data.get_billing()?, email)?; + + let order_information = OrderInformationWithBill { + amount_details: Amount { + total_amount: item.amount.to_owned(), + currency: item.router_data.request.currency, + }, + bill_to, + }; + let card_issuer = ccard.get_card_issuer(); + let card_type = match card_issuer { + Ok(issuer) => Some(String::from(issuer)), + Err(_) => None, + }; + let payment_information = PaymentInformation { + card: Card { + number: ccard.card_number, + expiration_month: ccard.card_exp_month, + expiration_year: ccard.card_exp_year, + security_code: ccard.card_cvc, + card_type, + }, }; + + let processing_information = ProcessingInformation { + capture: matches!( + item.router_data.request.capture_method, + Some(enums::CaptureMethod::Automatic) | None + ), + }; + + let client_reference_information = ClientReferenceInformation { + code: Some(item.router_data.connector_request_reference_id.clone()), + }; + Ok(Self { - amount: item.amount.to_owned(), - card, + processing_information, + payment_information, + order_information, + client_reference_information, }) } - _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), + payments::PaymentMethodData::CardRedirect(_) + | payments::PaymentMethodData::Wallet(_) + | payments::PaymentMethodData::PayLater(_) + | payments::PaymentMethodData::BankRedirect(_) + | payments::PaymentMethodData::BankDebit(_) + | payments::PaymentMethodData::BankTransfer(_) + | payments::PaymentMethodData::Crypto(_) + | payments::PaymentMethodData::MandatePayment + | payments::PaymentMethodData::Reward + | payments::PaymentMethodData::Upi(_) + | payments::PaymentMethodData::Voucher(_) + | payments::PaymentMethodData::GiftCard(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Bank of America"), + ) + .into()) + } } } } -//TODO: Fill the struct with respective fields -// Auth Struct -pub struct BankofamericaAuthType { - pub(super) api_key: Secret, +#[derive(Debug, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum BankofamericaPaymentStatus { + Authorized, + Succeeded, + Failed, + Voided, + Reversed, + Pending, + Declined, + AuthorizedPendingReview, + Transmitted, } -impl TryFrom<&types::ConnectorAuthType> for BankofamericaAuthType { +impl ForeignFrom<(BankofamericaPaymentStatus, bool)> for enums::AttemptStatus { + fn foreign_from((status, auto_capture): (BankofamericaPaymentStatus, bool)) -> Self { + match status { + BankofamericaPaymentStatus::Authorized => { + if auto_capture { + // Because BankOfAmerica will return Payment Status as Authorized even in AutoCapture Payment + Self::Pending + } else { + Self::Authorized + } + } + BankofamericaPaymentStatus::AuthorizedPendingReview => Self::Authorized, + BankofamericaPaymentStatus::Succeeded | BankofamericaPaymentStatus::Transmitted => { + Self::Charged + } + BankofamericaPaymentStatus::Voided | BankofamericaPaymentStatus::Reversed => { + Self::Voided + } + BankofamericaPaymentStatus::Failed | BankofamericaPaymentStatus::Declined => { + Self::Failure + } + BankofamericaPaymentStatus::Pending => Self::Pending, + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum BankOfAmericaPaymentsResponse { + ClientReferenceInformation(BankOfAmericaClientReferenceResponse), + ErrorInformation(BankOfAmericaErrorInformationResponse), +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaClientReferenceResponse { + id: String, + status: BankofamericaPaymentStatus, + client_reference_information: ClientReferenceInformation, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaErrorInformationResponse { + id: String, + error_information: BankOfAmericaErrorInformation, +} + +#[derive(Debug, Deserialize)] +pub struct BankOfAmericaErrorInformation { + reason: Option, + message: String, +} + +impl + TryFrom< + types::ResponseRouterData< + F, + BankOfAmericaPaymentsResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ type Error = error_stack::Report; - fn try_from(auth_type: &types::ConnectorAuthType) -> Result { - match auth_type { - types::ConnectorAuthType::HeaderKey { api_key } => Ok(Self { - api_key: api_key.to_owned(), + fn try_from( + item: types::ResponseRouterData< + F, + BankOfAmericaPaymentsResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + ) -> Result { + match item.response { + BankOfAmericaPaymentsResponse::ClientReferenceInformation(info_response) => Ok(Self { + status: enums::AttemptStatus::foreign_from(( + info_response.status, + item.data.request.is_auto_capture()?, + )), + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + info_response.id.clone(), + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some( + info_response + .client_reference_information + .code + .unwrap_or(info_response.id), + ), + }), + ..item.data + }), + BankOfAmericaPaymentsResponse::ErrorInformation(error_response) => Ok(Self { + response: Err(types::ErrorResponse { + code: consts::NO_ERROR_CODE.to_string(), + message: error_response.error_information.message, + reason: error_response.error_information.reason, + status_code: item.http_code, + attempt_status: None, + }), + ..item.data }), - _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), } } } -// PaymentsResponse -//TODO: Append the remaining status flags -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum BankofamericaPaymentStatus { - Succeeded, - Failed, - #[default] - Processing, + +impl + TryFrom< + types::ResponseRouterData< + F, + BankOfAmericaPaymentsResponse, + types::PaymentsCaptureData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + BankOfAmericaPaymentsResponse, + types::PaymentsCaptureData, + types::PaymentsResponseData, + >, + ) -> Result { + match item.response { + BankOfAmericaPaymentsResponse::ClientReferenceInformation(info_response) => Ok(Self { + status: enums::AttemptStatus::foreign_from((info_response.status, true)), + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + info_response.id.clone(), + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some( + info_response + .client_reference_information + .code + .unwrap_or(info_response.id), + ), + }), + ..item.data + }), + BankOfAmericaPaymentsResponse::ErrorInformation(error_response) => Ok(Self { + response: Err(types::ErrorResponse { + code: consts::NO_ERROR_CODE.to_string(), + message: error_response.error_information.message, + reason: error_response.error_information.reason, + status_code: item.http_code, + attempt_status: None, + }), + ..item.data + }), + } + } } -impl From for enums::AttemptStatus { - fn from(item: BankofamericaPaymentStatus) -> Self { - match item { - BankofamericaPaymentStatus::Succeeded => Self::Charged, - BankofamericaPaymentStatus::Failed => Self::Failure, - BankofamericaPaymentStatus::Processing => Self::Authorizing, +impl + TryFrom< + types::ResponseRouterData< + F, + BankOfAmericaPaymentsResponse, + types::PaymentsCancelData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + BankOfAmericaPaymentsResponse, + types::PaymentsCancelData, + types::PaymentsResponseData, + >, + ) -> Result { + match item.response { + BankOfAmericaPaymentsResponse::ClientReferenceInformation(info_response) => Ok(Self { + status: enums::AttemptStatus::foreign_from((info_response.status, false)), + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + info_response.id.clone(), + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some( + info_response + .client_reference_information + .code + .unwrap_or(info_response.id), + ), + }), + ..item.data + }), + BankOfAmericaPaymentsResponse::ErrorInformation(error_response) => Ok(Self { + response: Err(types::ErrorResponse { + code: consts::NO_ERROR_CODE.to_string(), + message: error_response.error_information.message, + reason: error_response.error_information.reason, + status_code: item.http_code, + attempt_status: None, + }), + ..item.data + }), } } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct BankofamericaPaymentsResponse { - status: BankofamericaPaymentStatus, +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum BankOfAmericaTransactionResponse { + ApplicationInformation(BankOfAmericaApplicationInfoResponse), + ErrorInformation(BankOfAmericaErrorInformationResponse), +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaApplicationInfoResponse { id: String, + application_information: ApplicationInformation, + client_reference_information: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ApplicationInformation { + status: BankofamericaPaymentStatus, } -impl +impl TryFrom< - types::ResponseRouterData, - > for types::RouterData + types::ResponseRouterData< + F, + BankOfAmericaTransactionResponse, + types::PaymentsSyncData, + types::PaymentsResponseData, + >, + > for types::RouterData { type Error = error_stack::Report; fn try_from( item: types::ResponseRouterData< F, - BankofamericaPaymentsResponse, - T, + BankOfAmericaTransactionResponse, + types::PaymentsSyncData, types::PaymentsResponseData, >, ) -> Result { - Ok(Self { - status: enums::AttemptStatus::from(item.response.status), - response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(item.response.id), - redirection_data: None, - mandate_reference: None, - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: None, + match item.response { + BankOfAmericaTransactionResponse::ApplicationInformation(app_response) => Ok(Self { + status: enums::AttemptStatus::foreign_from(( + app_response.application_information.status, + item.data.request.is_auto_capture()?, + )), + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(app_response.id.clone()), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: app_response + .client_reference_information + .map(|cref| cref.code) + .unwrap_or(Some(app_response.id)), + }), + ..item.data }), - ..item.data + BankOfAmericaTransactionResponse::ErrorInformation(error_response) => Ok(Self { + status: item.data.status, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + error_response.id.clone(), + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some(error_response.id), + }), + ..item.data + }), + } + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct OrderInformation { + amount_details: Amount, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaCaptureRequest { + order_information: OrderInformation, + client_reference_information: ClientReferenceInformation, +} + +impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsCaptureRouterData>> + for BankOfAmericaCaptureRequest +{ + type Error = error_stack::Report; + fn try_from( + value: &BankOfAmericaRouterData<&types::PaymentsCaptureRouterData>, + ) -> Result { + Ok(Self { + order_information: OrderInformation { + amount_details: Amount { + total_amount: value.amount.to_owned(), + currency: value.router_data.request.currency, + }, + }, + client_reference_information: ClientReferenceInformation { + code: Some(value.router_data.connector_request_reference_id.clone()), + }, }) } } -//TODO: Fill the struct with respective fields -// REFUND : -// Type definition for RefundRequest -#[derive(Default, Debug, Serialize)] -pub struct BankofamericaRefundRequest { - pub amount: i64, +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaVoidRequest { + client_reference_information: ClientReferenceInformation, + reversal_information: ReversalInformation, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ReversalInformation { + amount_details: Amount, + reason: String, } -impl TryFrom<&BankofamericaRouterData<&types::RefundsRouterData>> - for BankofamericaRefundRequest +impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsCancelRouterData>> + for BankOfAmericaVoidRequest { type Error = error_stack::Report; fn try_from( - item: &BankofamericaRouterData<&types::RefundsRouterData>, + value: &BankOfAmericaRouterData<&types::PaymentsCancelRouterData>, ) -> Result { Ok(Self { - amount: item.amount.to_owned(), + client_reference_information: ClientReferenceInformation { + code: Some(value.router_data.connector_request_reference_id.clone()), + }, + reversal_information: ReversalInformation { + amount_details: Amount { + total_amount: value.amount.to_owned(), + currency: value.router_data.request.currency.ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "Currency", + }, + )?, + }, + reason: value + .router_data + .request + .cancellation_reason + .clone() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "Cancellation Reason", + })?, + }, }) } } -// Type definition for Refund Response +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaRefundRequest { + order_information: OrderInformation, + client_reference_information: ClientReferenceInformation, +} -#[allow(dead_code)] -#[derive(Debug, Serialize, Default, Deserialize, Clone)] -pub enum RefundStatus { - Succeeded, - Failed, - #[default] - Processing, +impl TryFrom<&BankOfAmericaRouterData<&types::RefundsRouterData>> + for BankOfAmericaRefundRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &BankOfAmericaRouterData<&types::RefundsRouterData>, + ) -> Result { + Ok(Self { + order_information: OrderInformation { + amount_details: Amount { + total_amount: item.amount.clone(), + currency: item.router_data.request.currency, + }, + }, + client_reference_information: ClientReferenceInformation { + code: Some(item.router_data.request.refund_id.clone()), + }, + }) + } } -impl From for enums::RefundStatus { - fn from(item: RefundStatus) -> Self { +impl From for enums::RefundStatus { + fn from(item: BankofamericaRefundStatus) -> Self { match item { - RefundStatus::Succeeded => Self::Success, - RefundStatus::Failed => Self::Failure, - RefundStatus::Processing => Self::Pending, - //TODO: Review mapping + BankofamericaRefundStatus::Succeeded | BankofamericaRefundStatus::Transmitted => { + Self::Success + } + BankofamericaRefundStatus::Failed => Self::Failure, + BankofamericaRefundStatus::Pending => Self::Pending, } } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct RefundResponse { +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaRefundResponse { id: String, - status: RefundStatus, + status: BankofamericaRefundStatus, } -impl TryFrom> +impl TryFrom> for types::RefundsRouterData { type Error = error_stack::Report; fn try_from( - item: types::RefundsResponseRouterData, + item: types::RefundsResponseRouterData, ) -> Result { Ok(Self { response: Ok(types::RefundsResponseData { - connector_refund_id: item.response.id.to_string(), + connector_refund_id: item.response.id, refund_status: enums::RefundStatus::from(item.response.status), }), ..item.data @@ -223,28 +708,86 @@ impl TryFrom> } } -impl TryFrom> +#[derive(Debug, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum BankofamericaRefundStatus { + Succeeded, + Transmitted, + Failed, + Pending, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RsyncApplicationInformation { + status: BankofamericaRefundStatus, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaRsyncResponse { + id: String, + application_information: RsyncApplicationInformation, +} + +impl TryFrom> for types::RefundsRouterData { type Error = error_stack::Report; fn try_from( - item: types::RefundsResponseRouterData, + item: types::RefundsResponseRouterData, ) -> Result { Ok(Self { response: Ok(types::RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), + connector_refund_id: item.response.id, + refund_status: enums::RefundStatus::from( + item.response.application_information.status, + ), }), ..item.data }) } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct BankofamericaErrorResponse { - pub status_code: u16, - pub code: String, +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaErrorResponse { + pub error_information: Option, + pub status: Option, + pub message: Option, + pub reason: Option, + pub details: Option>, +} + +#[derive(Debug, Deserialize, strum::Display)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum Reason { + MissingField, + InvalidData, + DuplicateRequest, + InvalidCard, + AuthAlreadyReversed, + CardTypeNotAccepted, + InvalidMerchantConfiguration, + ProcessorUnavailable, + InvalidAmount, + InvalidCardType, + InvalidPaymentId, + NotSupported, + SystemError, + ServerTimeout, + ServiceTimeout, +} + +#[derive(Debug, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Details { + pub field: String, + pub reason: String, +} + +#[derive(Debug, Default, Deserialize)] +pub struct ErrorInformation { pub message: String, - pub reason: Option, + pub reason: String, } diff --git a/crates/router/src/connector/multisafepay/transformers.rs b/crates/router/src/connector/multisafepay/transformers.rs index 6e371b1e1a2b..ee70d26ffbda 100644 --- a/crates/router/src/connector/multisafepay/transformers.rs +++ b/crates/router/src/connector/multisafepay/transformers.rs @@ -260,13 +260,13 @@ impl TryFrom for Gateway { utils::CardIssuer::Maestro => Ok(Self::Maestro), utils::CardIssuer::Discover => Ok(Self::Discover), utils::CardIssuer::Visa => Ok(Self::Visa), - utils::CardIssuer::DinersClub | utils::CardIssuer::JCB => { - Err(errors::ConnectorError::NotSupported { - message: issuer.to_string(), - connector: "Multisafe pay", - } - .into()) + utils::CardIssuer::DinersClub + | utils::CardIssuer::JCB + | utils::CardIssuer::CarteBlanche => Err(errors::ConnectorError::NotSupported { + message: issuer.to_string(), + connector: "Multisafe pay", } + .into()), } } } diff --git a/crates/router/src/connector/payeezy/transformers.rs b/crates/router/src/connector/payeezy/transformers.rs index 3a859b325300..e2e837929c41 100644 --- a/crates/router/src/connector/payeezy/transformers.rs +++ b/crates/router/src/connector/payeezy/transformers.rs @@ -69,13 +69,14 @@ impl TryFrom for PayeezyCardType { utils::CardIssuer::Discover => Ok(Self::Discover), utils::CardIssuer::Visa => Ok(Self::Visa), - utils::CardIssuer::Maestro | utils::CardIssuer::DinersClub | utils::CardIssuer::JCB => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Payeezy", - } - .into()) + utils::CardIssuer::Maestro + | utils::CardIssuer::DinersClub + | utils::CardIssuer::JCB + | utils::CardIssuer::CarteBlanche => Err(errors::ConnectorError::NotSupported { + message: utils::SELECTED_PAYMENT_METHOD.to_string(), + connector: "Payeezy", } + .into()), } } } diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 8600fe802195..efabbf87aeba 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -638,6 +638,7 @@ static CARD_REGEX: Lazy>> = Lazy CardIssuer::JCB, Regex::new(r"^(3(?:088|096|112|158|337|5(?:2[89]|[3-8][0-9]))\d{12})$"), ); + map.insert(CardIssuer::CarteBlanche, Regex::new(r"^389[0-9]{11}$")); map }); @@ -650,6 +651,7 @@ pub enum CardIssuer { Discover, DinersClub, JCB, + CarteBlanche, } pub trait CardData { diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 5ccd9e964866..f5ca2f8b26e9 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1511,10 +1511,10 @@ pub(crate) fn validate_auth_and_metadata_type( authorizedotnet::transformers::AuthorizedotnetAuthType::try_from(val)?; Ok(()) } - // api_enums::Connector::Bankofamerica => { - // bankofamerica::transformers::BankofamericaAuthType::try_from(val)?; - // Ok(()) - // } Added as template code for future usage + api_enums::Connector::Bankofamerica => { + bankofamerica::transformers::BankOfAmericaAuthType::try_from(val)?; + Ok(()) + } api_enums::Connector::Bitpay => { bitpay::transformers::BitpayAuthType::try_from(val)?; Ok(()) diff --git a/crates/router/src/core/payments/routing/transformers.rs b/crates/router/src/core/payments/routing/transformers.rs index de94a36248ff..d7061a1502de 100644 --- a/crates/router/src/core/payments/routing/transformers.rs +++ b/crates/router/src/core/payments/routing/transformers.rs @@ -74,8 +74,9 @@ impl ForeignFrom for dsl_enums::Connector { api_enums::RoutableConnectors::Adyen => Self::Adyen, api_enums::RoutableConnectors::Airwallex => Self::Airwallex, api_enums::RoutableConnectors::Authorizedotnet => Self::Authorizedotnet, - api_enums::RoutableConnectors::Bitpay => Self::Bitpay, api_enums::RoutableConnectors::Bambora => Self::Bambora, + api_enums::RoutableConnectors::Bankofamerica => Self::Bankofamerica, + api_enums::RoutableConnectors::Bitpay => Self::Bitpay, api_enums::RoutableConnectors::Bluesnap => Self::Bluesnap, api_enums::RoutableConnectors::Boku => Self::Boku, api_enums::RoutableConnectors::Braintree => Self::Braintree, diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 67d2d37f4fea..2aa8f4a97c76 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -329,7 +329,7 @@ impl ConnectorData { enums::Connector::Airwallex => Ok(Box::new(&connector::Airwallex)), enums::Connector::Authorizedotnet => Ok(Box::new(&connector::Authorizedotnet)), enums::Connector::Bambora => Ok(Box::new(&connector::Bambora)), - // enums::Connector::Bankofamerica => Ok(Box::new(&connector::Bankofamerica)), Added as template code for future usage + enums::Connector::Bankofamerica => Ok(Box::new(&connector::Bankofamerica)), enums::Connector::Bitpay => Ok(Box::new(&connector::Bitpay)), enums::Connector::Bluesnap => Ok(Box::new(&connector::Bluesnap)), enums::Connector::Boku => Ok(Box::new(&connector::Boku)), diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 69ce2df974f6..2ba4ea483c45 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -193,8 +193,9 @@ impl ForeignTryFrom for api_enums::RoutableConnectors { api_enums::Connector::Adyen => Self::Adyen, api_enums::Connector::Airwallex => Self::Airwallex, api_enums::Connector::Authorizedotnet => Self::Authorizedotnet, - api_enums::Connector::Bitpay => Self::Bitpay, api_enums::Connector::Bambora => Self::Bambora, + api_enums::Connector::Bankofamerica => Self::Bankofamerica, + api_enums::Connector::Bitpay => Self::Bitpay, api_enums::Connector::Bluesnap => Self::Bluesnap, api_enums::Connector::Boku => Self::Boku, api_enums::Connector::Braintree => Self::Braintree, @@ -272,8 +273,9 @@ impl ForeignFrom for api_enums::RoutableConnectors { dsl_enums::Connector::Adyen => Self::Adyen, dsl_enums::Connector::Airwallex => Self::Airwallex, dsl_enums::Connector::Authorizedotnet => Self::Authorizedotnet, - dsl_enums::Connector::Bitpay => Self::Bitpay, dsl_enums::Connector::Bambora => Self::Bambora, + dsl_enums::Connector::Bankofamerica => Self::Bankofamerica, + dsl_enums::Connector::Bitpay => Self::Bitpay, dsl_enums::Connector::Bluesnap => Self::Bluesnap, dsl_enums::Connector::Boku => Self::Boku, dsl_enums::Connector::Braintree => Self::Braintree, diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index c576c6c8a99c..d154ee5a6407 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -4059,6 +4059,7 @@ "airwallex", "authorizedotnet", "bambora", + "bankofamerica", "bitpay", "bluesnap", "boku", diff --git a/postman/collection-dir/bankofamerica/.auth.json b/postman/collection-dir/bankofamerica/.auth.json new file mode 100644 index 000000000000..915a28357900 --- /dev/null +++ b/postman/collection-dir/bankofamerica/.auth.json @@ -0,0 +1,22 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + } +} diff --git a/postman/collection-dir/bankofamerica/.event.meta.json b/postman/collection-dir/bankofamerica/.event.meta.json new file mode 100644 index 000000000000..2df9d47d936d --- /dev/null +++ b/postman/collection-dir/bankofamerica/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.prerequest.js", + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/.info.json b/postman/collection-dir/bankofamerica/.info.json new file mode 100644 index 000000000000..2a1b8f809c01 --- /dev/null +++ b/postman/collection-dir/bankofamerica/.info.json @@ -0,0 +1,9 @@ +{ + "info": { + "_postman_id": "646f7167-da26-4a24-adb0-4157fd3a1781", + "name": "bankofamerica", + "description": "## Get started\n\nJuspay Router provides a collection of APIs that enable you to process and manage payments. Our APIs accept and return JSON in the HTTP body, and return standard HTTP response codes. \nYou can consume the APIs directly using your favorite HTTP/REST library. \nWe have a testing environment referred to \"sandbox\", which you can setup to test API calls without affecting production data.\n\n### Base URLs\n\nUse the following base URLs when making requests to the APIs:\n\n| Environment | Base URL |\n| --- | --- |\n| Sandbox | [https://sandbox.hyperswitch.io](https://sandbox.hyperswitch.io) |\n| Production | [https://router.juspay.io](https://router.juspay.io) |\n\n# Authentication\n\nWhen you sign up for an account, you are given a secret key (also referred as api-key). You may authenticate all API requests with Juspay server by providing the appropriate key in the request Authorization header. \nNever share your secret api keys. Keep them guarded and secure.\n\nContact Support: \nName: Juspay Support \nEmail: [support@juspay.in](mailto:support@juspay.in)", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "28305597" + } +} diff --git a/postman/collection-dir/bankofamerica/.meta.json b/postman/collection-dir/bankofamerica/.meta.json new file mode 100644 index 000000000000..e578098f721e --- /dev/null +++ b/postman/collection-dir/bankofamerica/.meta.json @@ -0,0 +1,9 @@ +{ + "childrenOrder": [ + "Health check", + "MerchantAccounts", + "API Key", + "PaymentConnectors", + "Flow Testcases" + ] +} diff --git a/postman/collection-dir/bankofamerica/.variable.json b/postman/collection-dir/bankofamerica/.variable.json new file mode 100644 index 000000000000..492c3b7ed0cb --- /dev/null +++ b/postman/collection-dir/bankofamerica/.variable.json @@ -0,0 +1,100 @@ +{ + "variable": [ + { + "key": "baseUrl", + "value": "", + "type": "string" + }, + { + "key": "admin_api_key", + "value": "", + "type": "string" + }, + { + "key": "api_key", + "value": "", + "type": "string" + }, + { + "key": "merchant_id", + "value": "" + }, + { + "key": "payment_id", + "value": "" + }, + { + "key": "customer_id", + "value": "" + }, + { + "key": "mandate_id", + "value": "" + }, + { + "key": "payment_method_id", + "value": "" + }, + { + "key": "refund_id", + "value": "" + }, + { + "key": "merchant_connector_id", + "value": "" + }, + { + "key": "client_secret", + "value": "", + "type": "string" + }, + { + "key": "connector_api_key", + "value": "", + "type": "string" + }, + { + "key": "publishable_key", + "value": "", + "type": "string" + }, + { + "key": "api_key_id", + "value": "", + "type": "string" + }, + { + "key": "payment_token", + "value": "" + }, + { + "key": "gateway_merchant_id", + "value": "", + "type": "string" + }, + { + "key": "certificate", + "value": "", + "type": "string" + }, + { + "key": "certificate_keys", + "value": "", + "type": "string" + }, + { + "key": "organization_id", + "value": "" + }, + { + "key": "connector_api_secret", + "value": "", + "type": "string" + }, + { + "key": "connector_key1", + "value": "", + "type": "string" + } + ] +} diff --git a/postman/collection-dir/bankofamerica/API Key/.meta.json b/postman/collection-dir/bankofamerica/API Key/.meta.json new file mode 100644 index 000000000000..0388c2d61b4b --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/.meta.json @@ -0,0 +1,9 @@ +{ + "childrenOrder": [ + "Create API Key", + "Update API Key", + "Retrieve API Key", + "List API Keys", + "Delete API Key" + ] +} diff --git a/postman/collection-dir/bankofamerica/API Key/Create API Key/.event.meta.json b/postman/collection-dir/bankofamerica/API Key/Create API Key/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/Create API Key/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/API Key/Create API Key/event.test.js b/postman/collection-dir/bankofamerica/API Key/Create API Key/event.test.js new file mode 100644 index 000000000000..4e27c5a50253 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/Create API Key/event.test.js @@ -0,0 +1,46 @@ +// Validate status 2xx +pm.test("[POST]::/api_keys/:merchant_id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/api_keys/:merchant_id - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id +if (jsonData?.key_id) { + pm.collectionVariables.set("api_key_id", jsonData.key_id); + console.log( + "- use {{api_key_id}} as collection variable for value", + jsonData.key_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.", + ); +} + +// pm.collectionVariables - Set api_key as variable for jsonData.api_key +if (jsonData?.api_key) { + pm.collectionVariables.set("api_key", jsonData.api_key); + console.log( + "- use {{api_key}} as collection variable for value", + jsonData.api_key, + ); +} else { + console.log( + "INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.", + ); +} diff --git a/postman/collection-dir/bankofamerica/API Key/Create API Key/request.json b/postman/collection-dir/bankofamerica/API Key/Create API Key/request.json new file mode 100644 index 000000000000..4e4c66284978 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/Create API Key/request.json @@ -0,0 +1,52 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw_json_formatted": { + "name": "API Key 1", + "description": null, + "expiration": "2069-09-23T01:02:03.000Z" + } + }, + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + } + ] + } +} diff --git a/postman/collection-dir/bankofamerica/API Key/Create API Key/response.json b/postman/collection-dir/bankofamerica/API Key/Create API Key/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/Create API Key/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/API Key/Delete API Key/.event.meta.json b/postman/collection-dir/bankofamerica/API Key/Delete API Key/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/Delete API Key/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/API Key/Delete API Key/event.test.js b/postman/collection-dir/bankofamerica/API Key/Delete API Key/event.test.js new file mode 100644 index 000000000000..bed2232f1a37 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/Delete API Key/event.test.js @@ -0,0 +1,17 @@ +// Validate status 2xx +pm.test( + "[DELETE]::/api_keys/:merchant_id/:api-key - Status code is 2xx", + function () { + pm.response.to.be.success; + }, +); + +// Validate if response header has matching content-type +pm.test( + "[DELETE]::/api_keys/:merchant_id/:api-key - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); diff --git a/postman/collection-dir/bankofamerica/API Key/Delete API Key/request.json b/postman/collection-dir/bankofamerica/API Key/Delete API Key/request.json new file mode 100644 index 000000000000..a83d12a2bebc --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/Delete API Key/request.json @@ -0,0 +1,49 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id/:api-key", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id", + ":api-key" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + }, + { + "key": "api-key", + "value": "{{api_key_id}}" + } + ] + } +} diff --git a/postman/collection-dir/bankofamerica/API Key/Delete API Key/response.json b/postman/collection-dir/bankofamerica/API Key/Delete API Key/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/Delete API Key/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/API Key/List API Keys/.event.meta.json b/postman/collection-dir/bankofamerica/API Key/List API Keys/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/List API Keys/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/API Key/List API Keys/event.test.js b/postman/collection-dir/bankofamerica/API Key/List API Keys/event.test.js new file mode 100644 index 000000000000..c6cbb8742e2c --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/List API Keys/event.test.js @@ -0,0 +1,46 @@ +// Validate status 2xx +pm.test("[GET]::/api_keys/:merchant_id/list - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[GET]::/api_keys/:merchant_id/list - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id +if (jsonData?.key_id) { + pm.collectionVariables.set("api_key_id", jsonData.key_id); + console.log( + "- use {{api_key_id}} as collection variable for value", + jsonData.key_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.", + ); +} + +// pm.collectionVariables - Set api_key as variable for jsonData.api_key +if (jsonData?.api_key) { + pm.collectionVariables.set("api_key", jsonData.api_key); + console.log( + "- use {{api_key}} as collection variable for value", + jsonData.api_key, + ); +} else { + console.log( + "INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.", + ); +} diff --git a/postman/collection-dir/bankofamerica/API Key/List API Keys/request.json b/postman/collection-dir/bankofamerica/API Key/List API Keys/request.json new file mode 100644 index 000000000000..86d12e8c7418 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/List API Keys/request.json @@ -0,0 +1,45 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id/list", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id", + "list" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + } + ] + } +} diff --git a/postman/collection-dir/bankofamerica/API Key/List API Keys/response.json b/postman/collection-dir/bankofamerica/API Key/List API Keys/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/List API Keys/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/API Key/Retrieve API Key/.event.meta.json b/postman/collection-dir/bankofamerica/API Key/Retrieve API Key/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/Retrieve API Key/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/API Key/Retrieve API Key/event.test.js b/postman/collection-dir/bankofamerica/API Key/Retrieve API Key/event.test.js new file mode 100644 index 000000000000..bef13cc35779 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/Retrieve API Key/event.test.js @@ -0,0 +1,49 @@ +// Validate status 2xx +pm.test( + "[GET]::/api_keys/:merchant_id/:api_key_id - Status code is 2xx", + function () { + pm.response.to.be.success; + }, +); + +// Validate if response header has matching content-type +pm.test( + "[GET]::/api_keys/:merchant_id/:api_key_id - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id +if (jsonData?.key_id) { + pm.collectionVariables.set("api_key_id", jsonData.key_id); + console.log( + "- use {{api_key_id}} as collection variable for value", + jsonData.key_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.", + ); +} + +// pm.collectionVariables - Set api_key as variable for jsonData.api_key +if (jsonData?.api_key) { + pm.collectionVariables.set("api_key", jsonData.api_key); + console.log( + "- use {{api_key}} as collection variable for value", + jsonData.api_key, + ); +} else { + console.log( + "INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.", + ); +} diff --git a/postman/collection-dir/bankofamerica/API Key/Retrieve API Key/request.json b/postman/collection-dir/bankofamerica/API Key/Retrieve API Key/request.json new file mode 100644 index 000000000000..958049e90879 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/Retrieve API Key/request.json @@ -0,0 +1,49 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id/:api_key_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id", + ":api_key_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + }, + { + "key": "api_key_id", + "value": "{{api_key_id}}" + } + ] + } +} diff --git a/postman/collection-dir/bankofamerica/API Key/Retrieve API Key/response.json b/postman/collection-dir/bankofamerica/API Key/Retrieve API Key/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/Retrieve API Key/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/API Key/Update API Key/.event.meta.json b/postman/collection-dir/bankofamerica/API Key/Update API Key/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/Update API Key/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/API Key/Update API Key/event.test.js b/postman/collection-dir/bankofamerica/API Key/Update API Key/event.test.js new file mode 100644 index 000000000000..fd6ed957bdff --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/Update API Key/event.test.js @@ -0,0 +1,49 @@ +// Validate status 2xx +pm.test( + "[POST]::/api_keys/:merchant_id/:api_key_id - Status code is 2xx", + function () { + pm.response.to.be.success; + }, +); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/api_keys/:merchant_id/:api_key_id - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id +if (jsonData?.key_id) { + pm.collectionVariables.set("api_key_id", jsonData.key_id); + console.log( + "- use {{api_key_id}} as collection variable for value", + jsonData.key_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.", + ); +} + +// pm.collectionVariables - Set api_key as variable for jsonData.api_key +if (jsonData?.api_key) { + pm.collectionVariables.set("api_key", jsonData.api_key); + console.log( + "- use {{api_key}} as collection variable for value", + jsonData.api_key, + ); +} else { + console.log( + "INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.", + ); +} diff --git a/postman/collection-dir/bankofamerica/API Key/Update API Key/request.json b/postman/collection-dir/bankofamerica/API Key/Update API Key/request.json new file mode 100644 index 000000000000..af2f450969b6 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/Update API Key/request.json @@ -0,0 +1,57 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw_json_formatted": { + "name": null, + "description": "My very awesome API key", + "expiration": null + } + }, + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id/:api_key_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id", + ":api_key_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + }, + { + "key": "api_key_id", + "value": "{{api_key_id}}" + } + ] + } +} diff --git a/postman/collection-dir/bankofamerica/API Key/Update API Key/response.json b/postman/collection-dir/bankofamerica/API Key/Update API Key/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/Update API Key/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/.meta.json new file mode 100644 index 000000000000..bd972090b19e --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/.meta.json @@ -0,0 +1,6 @@ +{ + "childrenOrder": [ + "QuickStart", + "Happy Cases" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/.meta.json new file mode 100644 index 000000000000..d85baac8fbe6 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/.meta.json @@ -0,0 +1,9 @@ +{ + "childrenOrder": [ + "Scenario1-Create payment with confirm true", + "Scenario2-Create payment with confirm false", + "Scenario3-Create payment without PMD", + "Scenario4-Create payment with Manual capture", + "Scenario5-Void the payment" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/.meta.json new file mode 100644 index 000000000000..60051ecca220 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/.meta.json @@ -0,0 +1,6 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/event.test.js new file mode 100644 index 000000000000..ac3f862e43f4 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/event.test.js @@ -0,0 +1,80 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "processing" for "status" because payment gets succeeded after one day. +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'processing'", + function () { + pm.expect(jsonData.status).to.eql("processing"); + }, + ); +} + +// Response body should have "connector_transaction_id" +pm.test( + "[POST]::/payments - Content check if 'connector_transaction_id' exists", + function () { + pm.expect(typeof jsonData.connector_transaction_id !== "undefined").to.be + .true; + }, +); diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json new file mode 100644 index 000000000000..21f054843897 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json @@ -0,0 +1,98 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "confirm": true, + "business_country": "US", + "business_label": "default", + "capture_method": "automatic", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 1, + "customer_id": "bernard123", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "setup_future_usage": "on_session", + "payment_method": "card", + "payment_method_type": "debit", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "01", + "card_exp_year": "24", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari", + "last_name": "sundari" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari", + "last_name": "sundari" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + }, + "routing": { + "type": "single", + "data": "stripe" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..a6976d95f69e --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/event.test.js @@ -0,0 +1,80 @@ +// Validate status 2xx +// pm.test("[GET]::/payments/:id - Status code is 2xx", function () { +// pm.response.to.be.success; +// }); + +// Validate if response header has matching content-type +pm.test("[GET]::/payments/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[GET]::/payments/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "processing" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id - Content check if value for 'status' matches 'processing'", + function () { + pm.expect(jsonData.status).to.eql("processing"); + }, + ); +} + +// Response body should have "connector_transaction_id" +// pm.test( +// "[POST]::/payments - Content check if 'connector_transaction_id' exists", +// function () { +// pm.expect(typeof jsonData.connector_transaction_id !== "undefined").to.be +// .true; +// }, +// ); diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/request.json @@ -0,0 +1,33 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/.meta.json new file mode 100644 index 000000000000..57d3f8e2bc7e --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/.meta.json @@ -0,0 +1,7 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Confirm", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json new file mode 100644 index 000000000000..4ac527d834af --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.prerequest.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js new file mode 100644 index 000000000000..b160ad9dc04b --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js @@ -0,0 +1,103 @@ +// Validate status 2xx +pm.test("[POST]::/payments/:id/confirm - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/payments/:id/confirm - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Validate if response has JSON Body +pm.test("[POST]::/payments/:id/confirm - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "6540" for "amount" +if (jsonData?.amount) { + pm.test( + "[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'", + function () { + pm.expect(jsonData.amount).to.eql(6540); + }, + ); +} + +// Response body should have value "6540" for "amount_capturable" +if (jsonData?.amount) { + pm.test( + "[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'", + function () { + pm.expect(jsonData.amount_capturable).to.eql(0); + }, + ); +} + +// Response body should have value "processing" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'", + function () { + pm.expect(jsonData.status).to.eql("processing"); + }, + ); +} + +// Response body should have "connector_transaction_id" +pm.test( + "[POST]::/payments - Content check if 'connector_transaction_id' exists", + function () { + pm.expect(typeof jsonData.connector_transaction_id !== "undefined").to.be + .true; + }, +); diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json new file mode 100644 index 000000000000..16f6e13983f8 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json @@ -0,0 +1,63 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "client_secret": "{{client_secret}}" + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.test.js new file mode 100644 index 000000000000..55dc35b91280 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "requires_confirmation" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'", + function () { + pm.expect(jsonData.status).to.eql("requires_confirmation"); + }, + ); +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json new file mode 100644 index 000000000000..b1d5ad5ebbf8 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json @@ -0,0 +1,98 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "confirm": false, + "business_country": "US", + "business_label": "default", + "capture_method": "automatic", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 1, + "customer_id": "bernard123", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "setup_future_usage": "on_session", + "payment_method": "card", + "payment_method_type": "debit", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "01", + "card_exp_year": "24", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari", + "last_name": "sundari" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari", + "last_name": "sundari" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + }, + "routing": { + "type": "single", + "data": "stripe" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..f87069589f0a --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/event.test.js @@ -0,0 +1,91 @@ +// Validate status 2xx +// pm.test("[GET]::/payments/:id - Status code is 2xx", function () { +// pm.response.to.be.success; +// }); + +// Validate if response header has matching content-type +pm.test("[GET]::/payments/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[GET]::/payments/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "processing" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments:id - Content check if value for 'status' matches 'processing'", + function () { + pm.expect(jsonData.status).to.eql("processing"); + }, + ); +} + +// Response body should have value "6540" for "amount" +if (jsonData?.amount) { + pm.test( + "[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'", + function () { + pm.expect(jsonData.amount).to.eql(6540); + }, + ); +} + +// Response body should have value "6540" for "amount_capturable" +if (jsonData?.amount) { + pm.test( + "[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'", + function () { + pm.expect(jsonData.amount_capturable).to.eql(0); + }, + ); +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json @@ -0,0 +1,33 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/.meta.json new file mode 100644 index 000000000000..57d3f8e2bc7e --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/.meta.json @@ -0,0 +1,7 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Confirm", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/.event.meta.json new file mode 100644 index 000000000000..4ac527d834af --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/event.prerequest.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/event.test.js new file mode 100644 index 000000000000..255743af78c7 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/event.test.js @@ -0,0 +1,73 @@ +// Validate status 2xx +pm.test("[POST]::/payments/:id/confirm - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/payments/:id/confirm - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Validate if response has JSON Body +pm.test("[POST]::/payments/:id/confirm - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} +// Response body should have value "processing" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments:id/confirm - Content check if value for 'status' matches 'processing'", + function () { + pm.expect(jsonData.status).to.eql("processing"); + }, + ); +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/request.json new file mode 100644 index 000000000000..8ac0a623f77d --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/request.json @@ -0,0 +1,73 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "payment_method": "card", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + }, + "client_secret": "{{client_secret}}" + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/event.test.js new file mode 100644 index 000000000000..0444324000a6 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "requires_payment_method" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'", + function () { + pm.expect(jsonData.status).to.eql("requires_payment_method"); + }, + ); +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json new file mode 100644 index 000000000000..71cc91069581 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json @@ -0,0 +1,84 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "confirm": false, + "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": "+65", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari", + "last_name": "abcd" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari", + "last_name": "abcd" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + }, + "routing": { + "type": "single", + "data": "bankofamerica" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..4fbefdb8494a --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +// pm.test("[GET]::/payments/:id - Status code is 2xx", function () { +// pm.response.to.be.success; +// }); + +// Validate if response header has matching content-type +pm.test("[GET]::/payments/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[GET]::/payments/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "processing" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments:id - Content check if value for 'status' matches 'processing'", + function () { + pm.expect(jsonData.status).to.eql("processing"); + }, + ); +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/request.json @@ -0,0 +1,33 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/.meta.json new file mode 100644 index 000000000000..e4ef30e39e8d --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/.meta.json @@ -0,0 +1,7 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Capture", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js new file mode 100644 index 000000000000..fa6deebe16a8 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js @@ -0,0 +1,94 @@ +// Validate status 2xx +pm.test("[POST]::/payments/:id/capture - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/payments/:id/capture - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Validate if response has JSON Body +pm.test("[POST]::/payments/:id/capture - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "processing" for "status" +if (jsonData?.status) { + pm.test( + "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'processing'", + function () { + pm.expect(jsonData.status).to.eql("processing"); + }, + ); +} + +// Response body should have value "6540" for "amount" +if (jsonData?.amount) { + pm.test( + "[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'", + function () { + pm.expect(jsonData.amount).to.eql(6540); + }, + ); +} + +// Response body should have value "6000" for "amount_received" +if (jsonData?.amount_received) { + pm.test( + "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'", + function () { + pm.expect(jsonData.amount_received).to.eql(6000); + }, + ); +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/request.json new file mode 100644 index 000000000000..8975575ca40e --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/request.json @@ -0,0 +1,45 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount_to_capture": 6000, + "statement_descriptor_name": "Joseph", + "statement_descriptor_suffix": "JS" + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/event.test.js new file mode 100644 index 000000000000..d683186aa007 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "requires_capture" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'requires_capture'", + function () { + pm.expect(jsonData.status).to.eql("requires_capture"); + }, + ); +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/request.json new file mode 100644 index 000000000000..5e3ff0e70ad2 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/request.json @@ -0,0 +1,98 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "confirm": true, + "business_country": "US", + "business_label": "default", + "capture_method": "manual", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 1, + "customer_id": "bernard123", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "setup_future_usage": "on_session", + "payment_method": "card", + "payment_method_type": "debit", + "payment_method_data": { + "card": { + "card_number": "3566111111111113", + "card_exp_month": "12", + "card_exp_year": "30", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari", + "last_name": "sundari" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari", + "last_name": "sundari" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + }, + "routing": { + "type": "single", + "data": "stripe" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..b1b53a360e32 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +// pm.test("[GET]::/payments/:id - Status code is 2xx", function () { +// pm.response.to.be.success; +// }); + +// Validate if response header has matching content-type +pm.test("[GET]::/payments/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[GET]::/payments/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "processing" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'processing'", + function () { + pm.expect(jsonData.status).to.eql("processing"); + }, + ); +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/request.json @@ -0,0 +1,33 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/.meta.json new file mode 100644 index 000000000000..14bab2fbd260 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/.meta.json @@ -0,0 +1,7 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Cancel", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/event.test.js new file mode 100644 index 000000000000..dcf3f1916430 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/event.test.js @@ -0,0 +1,61 @@ +// Validate status 2xx +pm.test("[POST]::/payments/:id/cancel - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/payments/:id/cancel - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Validate if response has JSON Body +pm.test("[POST]::/payments/:id/cancel - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "cancelled" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/cancel - Content check if value for 'status' matches 'cancelled'", + function () { + pm.expect(jsonData.status).to.eql("cancelled"); + }, + ); +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/request.json new file mode 100644 index 000000000000..f64e37a125a2 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/request.json @@ -0,0 +1,43 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "cancellation_reason": "requested_by_customer" + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/cancel", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "cancel" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/event.test.js new file mode 100644 index 000000000000..d683186aa007 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "requires_capture" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'requires_capture'", + function () { + pm.expect(jsonData.status).to.eql("requires_capture"); + }, + ); +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/request.json new file mode 100644 index 000000000000..5e3ff0e70ad2 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/request.json @@ -0,0 +1,98 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "confirm": true, + "business_country": "US", + "business_label": "default", + "capture_method": "manual", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 1, + "customer_id": "bernard123", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "setup_future_usage": "on_session", + "payment_method": "card", + "payment_method_type": "debit", + "payment_method_data": { + "card": { + "card_number": "3566111111111113", + "card_exp_month": "12", + "card_exp_year": "30", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari", + "last_name": "sundari" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari", + "last_name": "sundari" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + }, + "routing": { + "type": "single", + "data": "stripe" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..5e52e13a59e1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[GET]::/payments/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/payments/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[GET]::/payments/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "cancelled" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id - Content check if value for 'status' matches 'cancelled'", + function () { + pm.expect(jsonData.status).to.eql("cancelled"); + }, + ); +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/request.json @@ -0,0 +1,33 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/.meta.json new file mode 100644 index 000000000000..e3596ba357bc --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/.meta.json @@ -0,0 +1,9 @@ +{ + "childrenOrder": [ + "Merchant Account - Create", + "API Key - Create", + "Payment Connector - Create", + "Payments - Create", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/API Key - Create/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/API Key - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/API Key - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/API Key - Create/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/API Key - Create/event.test.js new file mode 100644 index 000000000000..4e27c5a50253 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/API Key - Create/event.test.js @@ -0,0 +1,46 @@ +// Validate status 2xx +pm.test("[POST]::/api_keys/:merchant_id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/api_keys/:merchant_id - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id +if (jsonData?.key_id) { + pm.collectionVariables.set("api_key_id", jsonData.key_id); + console.log( + "- use {{api_key_id}} as collection variable for value", + jsonData.key_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.", + ); +} + +// pm.collectionVariables - Set api_key as variable for jsonData.api_key +if (jsonData?.api_key) { + pm.collectionVariables.set("api_key", jsonData.api_key); + console.log( + "- use {{api_key}} as collection variable for value", + jsonData.api_key, + ); +} else { + console.log( + "INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.", + ); +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/API Key - Create/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/API Key - Create/request.json new file mode 100644 index 000000000000..4e4c66284978 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/API Key - Create/request.json @@ -0,0 +1,52 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw_json_formatted": { + "name": "API Key 1", + "description": null, + "expiration": "2069-09-23T01:02:03.000Z" + } + }, + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + } + ] + } +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/API Key - Create/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/API Key - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/API Key - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Merchant Account - Create/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Merchant Account - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Merchant Account - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Merchant Account - Create/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Merchant Account - Create/event.test.js new file mode 100644 index 000000000000..7de0d5beb316 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Merchant Account - Create/event.test.js @@ -0,0 +1,56 @@ +// Validate status 2xx +pm.test("[POST]::/accounts - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/accounts - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id +if (jsonData?.merchant_id) { + pm.collectionVariables.set("merchant_id", jsonData.merchant_id); + console.log( + "- use {{merchant_id}} as collection variable for value", + jsonData.merchant_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.", + ); +} + +// pm.collectionVariables - Set api_key as variable for jsonData.api_key +if (jsonData?.api_key) { + pm.collectionVariables.set("api_key", jsonData.api_key); + console.log( + "- use {{api_key}} as collection variable for value", + jsonData.api_key, + ); +} else { + console.log( + "INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.", + ); +} + +// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key +if (jsonData?.publishable_key) { + pm.collectionVariables.set("publishable_key", jsonData.publishable_key); + console.log( + "- use {{publishable_key}} as collection variable for value", + jsonData.publishable_key, + ); +} else { + console.log( + "INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.", + ); +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Merchant Account - Create/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Merchant Account - Create/request.json new file mode 100644 index 000000000000..5313e3e6b486 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Merchant Account - Create/request.json @@ -0,0 +1,95 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "merchant_id": "postman_merchant_GHAction_{{$guid}}", + "locker_id": "m0010", + "merchant_name": "NewAge Retailer", + "merchant_details": { + "primary_contact_person": "John Test", + "primary_email": "JohnTest@test.com", + "primary_phone": "sunt laborum", + "secondary_contact_person": "John Test2", + "secondary_email": "JohnTest2@test.com", + "secondary_phone": "cillum do dolor id", + "website": "www.example.com", + "about_business": "Online Retail with a wide selection of organic products for North America", + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US" + } + }, + "return_url": "https://duck.com/success", + "webhook_details": { + "webhook_version": "1.0.1", + "webhook_username": "ekart_retail", + "webhook_password": "password_ekart@123", + "payment_created_enabled": true, + "payment_succeeded_enabled": true, + "payment_failed_enabled": true + }, + "sub_merchants_enabled": false, + "metadata": { + "city": "NY", + "unit": "245" + }, + "primary_business_details": [ + { + "country": "US", + "business": "default" + } + ] + } + }, + "url": { + "raw": "{{baseUrl}}/accounts", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts" + ] + }, + "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Merchant Account - Create/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Merchant Account - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Merchant Account - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payment Connector - Create/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payment Connector - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payment Connector - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payment Connector - Create/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payment Connector - Create/event.test.js new file mode 100644 index 000000000000..88e92d8d84a2 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payment Connector - Create/event.test.js @@ -0,0 +1,39 @@ +// Validate status 2xx +pm.test( + "[POST]::/account/:account_id/connectors - Status code is 2xx", + function () { + pm.response.to.be.success; + }, +); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/account/:account_id/connectors - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id +if (jsonData?.merchant_connector_id) { + pm.collectionVariables.set( + "merchant_connector_id", + jsonData.merchant_connector_id, + ); + console.log( + "- use {{merchant_connector_id}} as collection variable for value", + jsonData.merchant_connector_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.", + ); +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payment Connector - Create/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payment Connector - Create/request.json new file mode 100644 index 000000000000..8ab41d88236e --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payment Connector - Create/request.json @@ -0,0 +1,108 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "connector_type": "fiz_operations", + "connector_name": "bankofamerica", + "business_country": "US", + "business_label": "default", + "connector_label": "first_boa_connector", + "connector_account_details": { + "auth_type": "SignatureKey", + "api_key": "{{connector_api_key}}", + "api_secret": "{{connector_api_secret}}", + "key1": "{{connector_key1}}" + }, + "test_mode": false, + "disabled": false, + "payment_methods_enabled": [ + { + "payment_method": "card", + "payment_method_types": [ + { + "payment_method_type": "credit", + "card_networks": [ + "Visa", + "Mastercard" + ], + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "debit", + "card_networks": [ + "Visa", + "Mastercard" + ], + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + } + ], + "metadata": { + "city": "NY", + "unit": "245" + } + } + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payment Connector - Create/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payment Connector - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payment Connector - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Create/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Create/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Create/event.test.js new file mode 100644 index 000000000000..a6947db94c0b --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Create/event.test.js @@ -0,0 +1,61 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Create/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Create/request.json new file mode 100644 index 000000000000..6af4c897162c --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Create/request.json @@ -0,0 +1,103 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "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://duck.com", + "payment_method": "card", + "payment_method_type": "credit", + "payment_method_data": { + "card": { + "card_number": "4111111111111111", + "card_exp_month": "12", + "card_exp_year": "30", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "joseph", + "last_name": "Doe" + }, + "phone": { + "number": "8056594427", + "country_code": "+91" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "joseph", + "last_name": "Doe" + }, + "phone": { + "number": "8056594427", + "country_code": "+91" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + }, + "routing": { + "type": "single", + "data": "bankofamerica" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Create/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Retrieve/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Retrieve/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..d0a02af74367 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Retrieve/event.test.js @@ -0,0 +1,61 @@ +// Validate status 2xx +pm.test("[GET]::/payments/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/payments/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// Validate if response has JSON Body +pm.test("[GET]::/payments/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Retrieve/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Retrieve/request.json new file mode 100644 index 000000000000..c71774083b2c --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Retrieve/request.json @@ -0,0 +1,27 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Retrieve/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Health check/.meta.json b/postman/collection-dir/bankofamerica/Health check/.meta.json new file mode 100644 index 000000000000..66ee7e50cab8 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Health check/.meta.json @@ -0,0 +1,5 @@ +{ + "childrenOrder": [ + "New Request" + ] +} diff --git a/postman/collection-dir/bankofamerica/Health check/New Request/.event.meta.json b/postman/collection-dir/bankofamerica/Health check/New Request/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Health check/New Request/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Health check/New Request/event.test.js b/postman/collection-dir/bankofamerica/Health check/New Request/event.test.js new file mode 100644 index 000000000000..b490b8be090f --- /dev/null +++ b/postman/collection-dir/bankofamerica/Health check/New Request/event.test.js @@ -0,0 +1,4 @@ +// Validate status 2xx +pm.test("[POST]::/accounts - Status code is 2xx", function () { + pm.response.to.be.success; +}); diff --git a/postman/collection-dir/bankofamerica/Health check/New Request/request.json b/postman/collection-dir/bankofamerica/Health check/New Request/request.json new file mode 100644 index 000000000000..4cc8d4b1a966 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Health check/New Request/request.json @@ -0,0 +1,20 @@ +{ + "method": "GET", + "header": [ + { + "key": "x-feature", + "value": "router-custom", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{baseUrl}}/health", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "health" + ] + } +} diff --git a/postman/collection-dir/bankofamerica/Health check/New Request/response.json b/postman/collection-dir/bankofamerica/Health check/New Request/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Health check/New Request/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/.meta.json b/postman/collection-dir/bankofamerica/MerchantAccounts/.meta.json new file mode 100644 index 000000000000..02ea600d2eb8 --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/.meta.json @@ -0,0 +1,8 @@ +{ + "childrenOrder": [ + "Merchant Account - Create", + "Merchant Account - Retrieve", + "Merchant Account - List", + "Merchant Account - Update" + ] +} diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/.event.meta.json b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/.event.meta.json new file mode 100644 index 000000000000..4ac527d834af --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/event.prerequest.js b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/event.test.js b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/event.test.js new file mode 100644 index 000000000000..41eecccf83fc --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/event.test.js @@ -0,0 +1,77 @@ +// Validate status 2xx +pm.test("[POST]::/accounts - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/accounts - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) { } + +// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id +if (jsonData?.merchant_id) { + pm.collectionVariables.set("merchant_id", jsonData.merchant_id); + console.log( + "- use {{merchant_id}} as collection variable for value", + jsonData.merchant_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.", + ); +} + +// pm.collectionVariables - Set api_key as variable for jsonData.api_key +if (jsonData?.api_key) { + pm.collectionVariables.set("api_key", jsonData.api_key); + console.log( + "- use {{api_key}} as collection variable for value", + jsonData.api_key, + ); +} else { + console.log( + "INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.", + ); +} + +// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key +if (jsonData?.publishable_key) { + pm.collectionVariables.set("publishable_key", jsonData.publishable_key); + console.log( + "- use {{publishable_key}} as collection variable for value", + jsonData.publishable_key, + ); +} else { + console.log( + "INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.", + ); +} + +// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id +if (jsonData?.merchant_id) { + pm.collectionVariables.set("organization_id", jsonData.organization_id); + console.log( + "- use {{organization_id}} as collection variable for value", + jsonData.organization_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{organization_id}}, as jsonData.organization_id is undefined.", + ); +} + +// Response body should have "mandate_id" +pm.test( + "[POST]::/accounts - Organization id is generated", + function () { + pm.expect(typeof jsonData.organization_id !== "undefined").to.be.true; + }, +); diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/request.json b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/request.json new file mode 100644 index 000000000000..5313e3e6b486 --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/request.json @@ -0,0 +1,95 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "merchant_id": "postman_merchant_GHAction_{{$guid}}", + "locker_id": "m0010", + "merchant_name": "NewAge Retailer", + "merchant_details": { + "primary_contact_person": "John Test", + "primary_email": "JohnTest@test.com", + "primary_phone": "sunt laborum", + "secondary_contact_person": "John Test2", + "secondary_email": "JohnTest2@test.com", + "secondary_phone": "cillum do dolor id", + "website": "www.example.com", + "about_business": "Online Retail with a wide selection of organic products for North America", + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US" + } + }, + "return_url": "https://duck.com/success", + "webhook_details": { + "webhook_version": "1.0.1", + "webhook_username": "ekart_retail", + "webhook_password": "password_ekart@123", + "payment_created_enabled": true, + "payment_succeeded_enabled": true, + "payment_failed_enabled": true + }, + "sub_merchants_enabled": false, + "metadata": { + "city": "NY", + "unit": "245" + }, + "primary_business_details": [ + { + "country": "US", + "business": "default" + } + ] + } + }, + "url": { + "raw": "{{baseUrl}}/accounts", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts" + ] + }, + "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." +} diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/response.json b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - List/.event.meta.json b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - List/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - List/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - List/event.test.js b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - List/event.test.js new file mode 100644 index 000000000000..0ba15a15ee6a --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - List/event.test.js @@ -0,0 +1,43 @@ +// Validate status 2xx +pm.test("[GET]::/accounts/list - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/accounts/list - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) { } + +// pm.collectionVariables - Set api_key as variable for jsonData.api_key +if (jsonData?.api_key) { + pm.collectionVariables.set("api_key", jsonData.api_key); + console.log( + "- use {{api_key}} as collection variable for value", + jsonData.api_key, + ); +} else { + console.log( + "INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.", + ); +} + +// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key +if (jsonData?.publishable_key) { + pm.collectionVariables.set("publishable_key", jsonData.publishable_key); + console.log( + "- use {{publishable_key}} as collection variable for value", + jsonData.publishable_key, + ); +} else { + console.log( + "INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.", + ); +} diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - List/request.json b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - List/request.json new file mode 100644 index 000000000000..ed2324e03082 --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - List/request.json @@ -0,0 +1,53 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/accounts/list?organization_id={{organization_id}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts", + "list" + ], + "query": [ + { + "key": "organization_id", + "value": "{{organization_id}}" + } + ], + "variable": [ + { + "key": "organization_id", + "value": "{{organization_id}}", + "description": "(Required) - Organization id" + } + ] + }, + "description": "List merchant accounts for an organization" +} diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - List/response.json b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - List/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - List/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Retrieve/.event.meta.json b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Retrieve/event.test.js b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Retrieve/event.test.js new file mode 100644 index 000000000000..7694684a1770 --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Retrieve/event.test.js @@ -0,0 +1,43 @@ +// Validate status 2xx +pm.test("[GET]::/accounts/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/accounts/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set api_key as variable for jsonData.api_key +if (jsonData?.api_key) { + pm.collectionVariables.set("api_key", jsonData.api_key); + console.log( + "- use {{api_key}} as collection variable for value", + jsonData.api_key, + ); +} else { + console.log( + "INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.", + ); +} + +// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key +if (jsonData?.publishable_key) { + pm.collectionVariables.set("publishable_key", jsonData.publishable_key); + console.log( + "- use {{publishable_key}} as collection variable for value", + jsonData.publishable_key, + ); +} else { + console.log( + "INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.", + ); +} diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Retrieve/request.json b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Retrieve/request.json new file mode 100644 index 000000000000..536ad17268f5 --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Retrieve/request.json @@ -0,0 +1,47 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/accounts/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Retrieve a merchant account details." +} diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Retrieve/response.json b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Update/.event.meta.json b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Update/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Update/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Update/event.test.js b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Update/event.test.js new file mode 100644 index 000000000000..ecd9d862b3f9 --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Update/event.test.js @@ -0,0 +1,46 @@ +// Validate status 2xx +pm.test("[POST]::/accounts/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/accounts/:id - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set api_key as variable for jsonData.api_key +if (jsonData?.api_key) { + pm.collectionVariables.set("api_key", jsonData.api_key); + console.log( + "- use {{api_key}} as collection variable for value", + jsonData.api_key, + ); +} else { + console.log( + "INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.", + ); +} + +// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key +if (jsonData?.publishable_key) { + pm.collectionVariables.set("publishable_key", jsonData.publishable_key); + console.log( + "- use {{publishable_key}} as collection variable for value", + jsonData.publishable_key, + ); +} else { + console.log( + "INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.", + ); +} diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Update/request.json b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Update/request.json new file mode 100644 index 000000000000..c58b1202d111 --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Update/request.json @@ -0,0 +1,98 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "merchant_id": "{{merchant_id}}", + "merchant_name": "NewAge Retailer", + "locker_id": "m0010", + "merchant_details": { + "primary_contact_person": "joseph Test", + "primary_email": "josephTest@test.com", + "primary_phone": "veniam aute officia ullamco esse", + "secondary_contact_person": "joseph Test2", + "secondary_email": "josephTest2@test.com", + "secondary_phone": "proident adipisicing officia nulla", + "website": "www.example.com", + "about_business": "Online Retail with a wide selection of organic products for North America", + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US" + } + }, + "return_url": "https://duck.com", + "webhook_details": { + "webhook_version": "1.0.1", + "webhook_username": "ekart_retail", + "webhook_password": "password_ekart@123", + "payment_created_enabled": true, + "payment_succeeded_enabled": true, + "payment_failed_enabled": true + }, + "sub_merchants_enabled": false, + "parent_merchant_id": "xkkdf909012sdjki2dkh5sdf", + "metadata": { + "city": "NY", + "unit": "245" + } + } + }, + "url": { + "raw": "{{baseUrl}}/accounts/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "To update an existing merchant account. Helpful in updating merchant details such as email, contact deteails, or other configuration details like webhook, routing algorithm etc" +} diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Update/response.json b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Update/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Update/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/.meta.json b/postman/collection-dir/bankofamerica/PaymentConnectors/.meta.json new file mode 100644 index 000000000000..3f8bc360dd41 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/.meta.json @@ -0,0 +1,10 @@ +{ + "childrenOrder": [ + "Payment Connector - Create", + "Payment Connector - Retrieve", + "Payment Connector - Update", + "List Connectors by MID", + "Payment Connector - Delete", + "Merchant Account - Delete" + ] +} diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/List Connectors by MID/.event.meta.json b/postman/collection-dir/bankofamerica/PaymentConnectors/List Connectors by MID/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/List Connectors by MID/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/List Connectors by MID/event.test.js b/postman/collection-dir/bankofamerica/PaymentConnectors/List Connectors by MID/event.test.js new file mode 100644 index 000000000000..c685ff160bf9 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/List Connectors by MID/event.test.js @@ -0,0 +1,17 @@ +// Validate status 2xx +pm.test( + "[GET]::/account/:account_id/connectors - Status code is 2xx", + function () { + pm.response.to.be.success; + }, +); + +// Validate if response header has matching content-type +pm.test( + "[GET]::/account/:account_id/connectors - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/List Connectors by MID/request.json b/postman/collection-dir/bankofamerica/PaymentConnectors/List Connectors by MID/request.json new file mode 100644 index 000000000000..89aa4e594064 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/List Connectors by MID/request.json @@ -0,0 +1,41 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}" + } + ] + } +} diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/List Connectors by MID/response.json b/postman/collection-dir/bankofamerica/PaymentConnectors/List Connectors by MID/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/List Connectors by MID/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Merchant Account - Delete/.event.meta.json b/postman/collection-dir/bankofamerica/PaymentConnectors/Merchant Account - Delete/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Merchant Account - Delete/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Merchant Account - Delete/event.test.js b/postman/collection-dir/bankofamerica/PaymentConnectors/Merchant Account - Delete/event.test.js new file mode 100644 index 000000000000..596c14630df9 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Merchant Account - Delete/event.test.js @@ -0,0 +1,42 @@ +// Validate status 2xx +pm.test("[DELETE]::/accounts/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[DELETE]::/accounts/:id - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Response Validation +const schema = { + type: "object", + description: "Merchant Account", + required: ["merchant_id", "deleted"], + properties: { + merchant_id: { + type: "string", + description: "The identifier for the MerchantAccount object.", + maxLength: 255, + example: "y3oqhf46pyzuxjbcn2giaqnb44", + }, + deleted: { + type: "boolean", + description: + "Indicates the deletion status of the Merchant Account object.", + example: true, + }, + }, +}; + +// Validate if response matches JSON schema +pm.test("[DELETE]::/accounts/:id - Schema is valid", function () { + pm.response.to.have.jsonSchema(schema, { + unknownFormats: ["int32", "int64", "float", "double"], + }); +}); diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Merchant Account - Delete/request.json b/postman/collection-dir/bankofamerica/PaymentConnectors/Merchant Account - Delete/request.json new file mode 100644 index 000000000000..17d56a57ea45 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Merchant Account - Delete/request.json @@ -0,0 +1,47 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/accounts/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Delete a Merchant Account" +} diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Merchant Account - Delete/response.json b/postman/collection-dir/bankofamerica/PaymentConnectors/Merchant Account - Delete/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Merchant Account - Delete/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Create/.event.meta.json b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Create/event.test.js b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Create/event.test.js new file mode 100644 index 000000000000..679a01ff33ed --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Create/event.test.js @@ -0,0 +1,47 @@ +// Validate status 2xx +pm.test( + "[POST]::/accounts/:account_id/connectors - Status code is 2xx", + function () { + pm.response.to.be.success; + }, +); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/accounts/:account_id/connectors - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) { } + +// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id +if (jsonData?.merchant_connector_id) { + pm.collectionVariables.set( + "merchant_connector_id", + jsonData.merchant_connector_id, + ); + console.log( + "- use {{merchant_connector_id}} as collection variable for value", + jsonData.merchant_connector_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.", + ); +} + +// Validate if the connector label is the one that is passed in the request +pm.test( + "[POST]::/accounts/:account_id/connectors - connector_label is not autogenerated", + function () { + pm.expect(jsonData.connector_label).to.eql("first_boa_connector") + }, +); \ No newline at end of file diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Create/request.json b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Create/request.json new file mode 100644 index 000000000000..8ab41d88236e --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Create/request.json @@ -0,0 +1,108 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "connector_type": "fiz_operations", + "connector_name": "bankofamerica", + "business_country": "US", + "business_label": "default", + "connector_label": "first_boa_connector", + "connector_account_details": { + "auth_type": "SignatureKey", + "api_key": "{{connector_api_key}}", + "api_secret": "{{connector_api_secret}}", + "key1": "{{connector_key1}}" + }, + "test_mode": false, + "disabled": false, + "payment_methods_enabled": [ + { + "payment_method": "card", + "payment_method_types": [ + { + "payment_method_type": "credit", + "card_networks": [ + "Visa", + "Mastercard" + ], + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "debit", + "card_networks": [ + "Visa", + "Mastercard" + ], + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + } + ], + "metadata": { + "city": "NY", + "unit": "245" + } + } + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." +} diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Create/response.json b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Delete/.event.meta.json b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Delete/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Delete/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Delete/event.test.js b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Delete/event.test.js new file mode 100644 index 000000000000..a8f03ce767fd --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Delete/event.test.js @@ -0,0 +1,39 @@ +// Validate status 2xx +pm.test( + "[DELETE]::/account/:account_id/connectors/:connector_id - Status code is 2xx", + function () { + pm.response.to.be.success; + }, +); + +// Validate if response header has matching content-type +pm.test( + "[DELETE]::/account/:account_id/connectors/:connector_id - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id +if (jsonData?.merchant_connector_id) { + pm.collectionVariables.set( + "merchant_connector_id", + jsonData.merchant_connector_id, + ); + console.log( + "- use {{merchant_connector_id}} as collection variable for value", + jsonData.merchant_connector_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.", + ); +} diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Delete/request.json b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Delete/request.json new file mode 100644 index 000000000000..6d7939d6762a --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Delete/request.json @@ -0,0 +1,52 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors/:connector_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors", + ":connector_id" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}" + }, + { + "key": "connector_id", + "value": "{{merchant_connector_id}}" + } + ] + }, + "description": "Delete or Detach a Payment Connector from Merchant Account" +} diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Delete/response.json b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Delete/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Delete/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Retrieve/.event.meta.json b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Retrieve/event.test.js b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Retrieve/event.test.js new file mode 100644 index 000000000000..8125c4e1bb73 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Retrieve/event.test.js @@ -0,0 +1,39 @@ +// Validate status 2xx +pm.test( + "[GET]::/accounts/:account_id/connectors/:connector_id - Status code is 2xx", + function () { + pm.response.to.be.success; + }, +); + +// Validate if response header has matching content-type +pm.test( + "[GET]::/accounts/:account_id/connectors/:connector_id - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id +if (jsonData?.merchant_connector_id) { + pm.collectionVariables.set( + "merchant_connector_id", + jsonData.merchant_connector_id, + ); + console.log( + "- use {{merchant_connector_id}} as collection variable for value", + jsonData.merchant_connector_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.", + ); +} diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Retrieve/request.json b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Retrieve/request.json new file mode 100644 index 000000000000..b87e65381250 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Retrieve/request.json @@ -0,0 +1,54 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors/:connector_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors", + ":connector_id" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + }, + { + "key": "connector_id", + "value": "{{merchant_connector_id}}", + "description": "(Required) The unique identifier for the payment connector" + } + ] + }, + "description": "Retrieve Payment Connector details." +} diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Retrieve/response.json b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Update/.event.meta.json b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Update/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Update/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Update/event.test.js b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Update/event.test.js new file mode 100644 index 000000000000..98f405d8bb85 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Update/event.test.js @@ -0,0 +1,47 @@ +// Validate status 2xx +pm.test( + "[POST]::/account/:account_id/connectors/:connector_id - Status code is 2xx", + function () { + pm.response.to.be.success; + }, +); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/account/:account_id/connectors/:connector_id - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) { } + +// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id +if (jsonData?.merchant_connector_id) { + pm.collectionVariables.set( + "merchant_connector_id", + jsonData.merchant_connector_id, + ); + console.log( + "- use {{merchant_connector_id}} as collection variable for value", + jsonData.merchant_connector_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.", + ); +} + +// Validate if the connector label is the one that is passed in the request +pm.test( + "[POST]::/accounts/:account_id/connectors - connector_label is not autogenerated", + function () { + pm.expect(jsonData.connector_label).to.eql("updated_stripe_connector") + }, +); \ No newline at end of file diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Update/request.json b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Update/request.json new file mode 100644 index 000000000000..3cb7be2537ad --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Update/request.json @@ -0,0 +1,109 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "connector_type": "fiz_operations", + "connector_account_details": { + "auth_type": "SignatureKey", + "api_key": "{{connector_api_key}}", + "api_secret": "{{connector_api_secret}}", + "key1": "{{connector_key1}}" + }, + "connector_label": "updated_stripe_connector", + "test_mode": false, + "disabled": false, + "payment_methods_enabled": [ + { + "payment_method": "card", + "payment_method_types": [ + { + "payment_method_type": "credit", + "card_networks": [ + "Visa", + "Mastercard" + ], + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "debit", + "card_networks": [ + "Visa", + "Mastercard" + ], + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + } + ], + "metadata": { + "city": "NY", + "unit": "245" + } + } + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors/:connector_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors", + ":connector_id" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}" + }, + { + "key": "connector_id", + "value": "{{merchant_connector_id}}" + } + ] + }, + "description": "To update an existing Payment Connector. Helpful in enabling / disabling different payment methods and other settings for the connector etc" +} diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Update/response.json b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Update/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Update/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/event.prerequest.js b/postman/collection-dir/bankofamerica/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/bankofamerica/event.test.js b/postman/collection-dir/bankofamerica/event.test.js new file mode 100644 index 000000000000..fb52caec30fc --- /dev/null +++ b/postman/collection-dir/bankofamerica/event.test.js @@ -0,0 +1,13 @@ +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log("[LOG]::payment_id - " + jsonData.payment_id); +} + +console.log("[LOG]::x-request-id - " + pm.response.headers.get("x-request-id")); diff --git a/postman/collection-json/bankofamerica.postman_collection.json b/postman/collection-json/bankofamerica.postman_collection.json new file mode 100644 index 000000000000..2b1a8fdc4704 --- /dev/null +++ b/postman/collection-json/bankofamerica.postman_collection.json @@ -0,0 +1,4310 @@ +{ + "info": { + "_postman_id": "646f7167-da26-4a24-adb0-4157fd3a1781", + "name": "bankofamerica", + "description": "## Get started\n\nJuspay Router provides a collection of APIs that enable you to process and manage payments. Our APIs accept and return JSON in the HTTP body, and return standard HTTP response codes. \nYou can consume the APIs directly using your favorite HTTP/REST library. \nWe have a testing environment referred to \"sandbox\", which you can setup to test API calls without affecting production data.\n\n### Base URLs\n\nUse the following base URLs when making requests to the APIs:\n\n| Environment | Base URL |\n| --- | --- |\n| Sandbox | [https://sandbox.hyperswitch.io](https://sandbox.hyperswitch.io) |\n| Production | [https://router.juspay.io](https://router.juspay.io) |\n\n# Authentication\n\nWhen you sign up for an account, you are given a secret key (also referred as api-key). You may authenticate all API requests with Juspay server by providing the appropriate key in the request Authorization header. \nNever share your secret api keys. Keep them guarded and secure.\n\nContact Support: \nName: Juspay Support \nEmail: [support@juspay.in](mailto:support@juspay.in)", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "28305597" + }, + "item": [ + { + "name": "Health check", + "item": [ + { + "name": "New Request", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "x-feature", + "value": "router-custom", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{baseUrl}}/health", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "health" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "MerchantAccounts", + "item": [ + { + "name": "Merchant Account - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/accounts - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) { }", + "", + "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", + "if (jsonData?.merchant_id) {", + " pm.collectionVariables.set(\"merchant_id\", jsonData.merchant_id);", + " console.log(", + " \"- use {{merchant_id}} as collection variable for value\",", + " jsonData.merchant_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", + "if (jsonData?.publishable_key) {", + " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", + " console.log(", + " \"- use {{publishable_key}} as collection variable for value\",", + " jsonData.publishable_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", + "if (jsonData?.merchant_id) {", + " pm.collectionVariables.set(\"organization_id\", jsonData.organization_id);", + " console.log(", + " \"- use {{organization_id}} as collection variable for value\",", + " jsonData.organization_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{organization_id}}, as jsonData.organization_id is undefined.\",", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/accounts - Organization id is generated\",", + " function () {", + " pm.expect(typeof jsonData.organization_id !== \"undefined\").to.be.true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"merchant_id\": \"postman_merchant_GHAction_{{$guid}}\",\n \"locker_id\": \"m0010\",\n \"merchant_name\": \"NewAge Retailer\",\n \"merchant_details\": {\n \"primary_contact_person\": \"John Test\",\n \"primary_email\": \"JohnTest@test.com\",\n \"primary_phone\": \"sunt laborum\",\n \"secondary_contact_person\": \"John Test2\",\n \"secondary_email\": \"JohnTest2@test.com\",\n \"secondary_phone\": \"cillum do dolor id\",\n \"website\": \"www.example.com\",\n \"about_business\": \"Online Retail with a wide selection of organic products for North America\",\n \"address\": {\n \"line1\": \"1467\",\n \"line2\": \"Harrison Street\",\n \"line3\": \"Harrison Street\",\n \"city\": \"San Fransico\",\n \"state\": \"California\",\n \"zip\": \"94122\",\n \"country\": \"US\"\n }\n },\n \"return_url\": \"https://duck.com/success\",\n \"webhook_details\": {\n \"webhook_version\": \"1.0.1\",\n \"webhook_username\": \"ekart_retail\",\n \"webhook_password\": \"password_ekart@123\",\n \"payment_created_enabled\": true,\n \"payment_succeeded_enabled\": true,\n \"payment_failed_enabled\": true\n },\n \"sub_merchants_enabled\": false,\n \"metadata\": {\n \"city\": \"NY\",\n \"unit\": \"245\"\n },\n \"primary_business_details\": [\n {\n \"country\": \"US\",\n \"business\": \"default\"\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/accounts", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts" + ] + }, + "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." + }, + "response": [] + }, + { + "name": "Merchant Account - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/accounts/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/accounts/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", + "if (jsonData?.publishable_key) {", + " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", + " console.log(", + " \"- use {{publishable_key}} as collection variable for value\",", + " jsonData.publishable_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/accounts/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Retrieve a merchant account details." + }, + "response": [] + }, + { + "name": "Merchant Account - List", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/accounts/list - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/accounts/list - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) { }", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", + "if (jsonData?.publishable_key) {", + " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", + " console.log(", + " \"- use {{publishable_key}} as collection variable for value\",", + " jsonData.publishable_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/accounts/list?organization_id={{organization_id}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts", + "list" + ], + "query": [ + { + "key": "organization_id", + "value": "{{organization_id}}" + } + ], + "variable": [ + { + "key": "organization_id", + "value": "{{organization_id}}", + "description": "(Required) - Organization id" + } + ] + }, + "description": "List merchant accounts for an organization" + }, + "response": [] + }, + { + "name": "Merchant Account - Update", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/accounts/:id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", + "if (jsonData?.publishable_key) {", + " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", + " console.log(", + " \"- use {{publishable_key}} as collection variable for value\",", + " jsonData.publishable_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"merchant_id\":\"{{merchant_id}}\",\"merchant_name\":\"NewAge Retailer\",\"locker_id\":\"m0010\",\"merchant_details\":{\"primary_contact_person\":\"joseph Test\",\"primary_email\":\"josephTest@test.com\",\"primary_phone\":\"veniam aute officia ullamco esse\",\"secondary_contact_person\":\"joseph Test2\",\"secondary_email\":\"josephTest2@test.com\",\"secondary_phone\":\"proident adipisicing officia nulla\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"parent_merchant_id\":\"xkkdf909012sdjki2dkh5sdf\",\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/accounts/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "To update an existing merchant account. Helpful in updating merchant details such as email, contact deteails, or other configuration details like webhook, routing algorithm etc" + }, + "response": [] + } + ] + }, + { + "name": "API Key", + "item": [ + { + "name": "Create API Key", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/api_keys/:merchant_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/api_keys/:merchant_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", + "if (jsonData?.key_id) {", + " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", + " console.log(", + " \"- use {{api_key_id}} as collection variable for value\",", + " jsonData.key_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"API Key 1\",\n \"description\": null,\n \"expiration\": \"2069-09-23T01:02:03.000Z\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Update API Key", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[POST]::/api_keys/:merchant_id/:api_key_id - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/api_keys/:merchant_id/:api_key_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", + "if (jsonData?.key_id) {", + " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", + " console.log(", + " \"- use {{api_key_id}} as collection variable for value\",", + " jsonData.key_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"name\":null,\"description\":\"My very awesome API key\",\"expiration\":null}" + }, + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id/:api_key_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id", + ":api_key_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + }, + { + "key": "api_key_id", + "value": "{{api_key_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Retrieve API Key", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[GET]::/api_keys/:merchant_id/:api_key_id - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[GET]::/api_keys/:merchant_id/:api_key_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", + "if (jsonData?.key_id) {", + " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", + " console.log(", + " \"- use {{api_key_id}} as collection variable for value\",", + " jsonData.key_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id/:api_key_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id", + ":api_key_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + }, + { + "key": "api_key_id", + "value": "{{api_key_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "List API Keys", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/api_keys/:merchant_id/list - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[GET]::/api_keys/:merchant_id/list - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", + "if (jsonData?.key_id) {", + " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", + " console.log(", + " \"- use {{api_key_id}} as collection variable for value\",", + " jsonData.key_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id/list", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id", + "list" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Delete API Key", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[DELETE]::/api_keys/:merchant_id/:api-key - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[DELETE]::/api_keys/:merchant_id/:api-key - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id/:api-key", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id", + ":api-key" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + }, + { + "key": "api-key", + "value": "{{api_key_id}}" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "PaymentConnectors", + "item": [ + { + "name": "Payment Connector - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[POST]::/accounts/:account_id/connectors - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/accounts/:account_id/connectors - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) { }", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "", + "// Validate if the connector label is the one that is passed in the request", + "pm.test(", + " \"[POST]::/accounts/:account_id/connectors - connector_label is not autogenerated\",", + " function () {", + " pm.expect(jsonData.connector_label).to.eql(\"first_boa_connector\")", + " },", + ");" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"connector_type\": \"fiz_operations\",\n \"connector_name\": \"bankofamerica\",\n \"business_country\": \"US\",\n \"business_label\": \"default\",\n \"connector_label\": \"first_boa_connector\",\n \"connector_account_details\": {\n \"auth_type\": \"SignatureKey\",\n \"api_key\": \"{{connector_api_key}}\",\n \"api_secret\": \"{{connector_api_secret}}\",\n \"key1\": \"{{connector_key1}}\"\n },\n \"test_mode\": false,\n \"disabled\": false,\n \"payment_methods_enabled\": [\n {\n \"payment_method\": \"card\",\n \"payment_method_types\": [\n {\n \"payment_method_type\": \"credit\",\n \"card_networks\": [\n \"Visa\",\n \"Mastercard\"\n ],\n \"minimum_amount\": 1,\n \"maximum_amount\": 68607706,\n \"recurring_enabled\": true,\n \"installment_payment_enabled\": true\n },\n {\n \"payment_method_type\": \"debit\",\n \"card_networks\": [\n \"Visa\",\n \"Mastercard\"\n ],\n \"minimum_amount\": 1,\n \"maximum_amount\": 68607706,\n \"recurring_enabled\": true,\n \"installment_payment_enabled\": true\n }\n ]\n }\n ],\n \"metadata\": {\n \"city\": \"NY\",\n \"unit\": \"245\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." + }, + "response": [] + }, + { + "name": "Payment Connector - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[GET]::/accounts/:account_id/connectors/:connector_id - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[GET]::/accounts/:account_id/connectors/:connector_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors/:connector_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors", + ":connector_id" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + }, + { + "key": "connector_id", + "value": "{{merchant_connector_id}}", + "description": "(Required) The unique identifier for the payment connector" + } + ] + }, + "description": "Retrieve Payment Connector details." + }, + "response": [] + }, + { + "name": "Payment Connector - Update", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[POST]::/account/:account_id/connectors/:connector_id - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/account/:account_id/connectors/:connector_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) { }", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "", + "// Validate if the connector label is the one that is passed in the request", + "pm.test(", + " \"[POST]::/accounts/:account_id/connectors - connector_label is not autogenerated\",", + " function () {", + " pm.expect(jsonData.connector_label).to.eql(\"updated_stripe_connector\")", + " },", + ");" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"connector_type\": \"fiz_operations\",\n \"connector_account_details\": {\n \"auth_type\": \"SignatureKey\",\n \"api_key\": \"{{connector_api_key}}\",\n \"api_secret\": \"{{connector_api_secret}}\",\n \"key1\": \"{{connector_key1}}\"\n },\n \"connector_label\": \"updated_stripe_connector\",\n \"test_mode\": false,\n \"disabled\": false,\n \"payment_methods_enabled\": [\n {\n \"payment_method\": \"card\",\n \"payment_method_types\": [\n {\n \"payment_method_type\": \"credit\",\n \"card_networks\": [\n \"Visa\",\n \"Mastercard\"\n ],\n \"minimum_amount\": 1,\n \"maximum_amount\": 68607706,\n \"recurring_enabled\": true,\n \"installment_payment_enabled\": true\n },\n {\n \"payment_method_type\": \"debit\",\n \"card_networks\": [\n \"Visa\",\n \"Mastercard\"\n ],\n \"minimum_amount\": 1,\n \"maximum_amount\": 68607706,\n \"recurring_enabled\": true,\n \"installment_payment_enabled\": true\n }\n ]\n }\n ],\n \"metadata\": {\n \"city\": \"NY\",\n \"unit\": \"245\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors/:connector_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors", + ":connector_id" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}" + }, + { + "key": "connector_id", + "value": "{{merchant_connector_id}}" + } + ] + }, + "description": "To update an existing Payment Connector. Helpful in enabling / disabling different payment methods and other settings for the connector etc" + }, + "response": [] + }, + { + "name": "List Connectors by MID", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[GET]::/account/:account_id/connectors - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[GET]::/account/:account_id/connectors - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Payment Connector - Delete", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[DELETE]::/account/:account_id/connectors/:connector_id - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[DELETE]::/account/:account_id/connectors/:connector_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors/:connector_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors", + ":connector_id" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}" + }, + { + "key": "connector_id", + "value": "{{merchant_connector_id}}" + } + ] + }, + "description": "Delete or Detach a Payment Connector from Merchant Account" + }, + "response": [] + }, + { + "name": "Merchant Account - Delete", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[DELETE]::/accounts/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[DELETE]::/accounts/:id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Response Validation", + "const schema = {", + " type: \"object\",", + " description: \"Merchant Account\",", + " required: [\"merchant_id\", \"deleted\"],", + " properties: {", + " merchant_id: {", + " type: \"string\",", + " description: \"The identifier for the MerchantAccount object.\",", + " maxLength: 255,", + " example: \"y3oqhf46pyzuxjbcn2giaqnb44\",", + " },", + " deleted: {", + " type: \"boolean\",", + " description:", + " \"Indicates the deletion status of the Merchant Account object.\",", + " example: true,", + " },", + " },", + "};", + "", + "// Validate if response matches JSON schema", + "pm.test(\"[DELETE]::/accounts/:id - Schema is valid\", function () {", + " pm.response.to.have.jsonSchema(schema, {", + " unknownFormats: [\"int32\", \"int64\", \"float\", \"double\"],", + " });", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/accounts/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Delete a Merchant Account" + }, + "response": [] + } + ] + }, + { + "name": "Flow Testcases", + "item": [ + { + "name": "QuickStart", + "item": [ + { + "name": "Merchant Account - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/accounts - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", + "if (jsonData?.merchant_id) {", + " pm.collectionVariables.set(\"merchant_id\", jsonData.merchant_id);", + " console.log(", + " \"- use {{merchant_id}} as collection variable for value\",", + " jsonData.merchant_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", + "if (jsonData?.publishable_key) {", + " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", + " console.log(", + " \"- use {{publishable_key}} as collection variable for value\",", + " jsonData.publishable_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"merchant_id\": \"postman_merchant_GHAction_{{$guid}}\",\n \"locker_id\": \"m0010\",\n \"merchant_name\": \"NewAge Retailer\",\n \"merchant_details\": {\n \"primary_contact_person\": \"John Test\",\n \"primary_email\": \"JohnTest@test.com\",\n \"primary_phone\": \"sunt laborum\",\n \"secondary_contact_person\": \"John Test2\",\n \"secondary_email\": \"JohnTest2@test.com\",\n \"secondary_phone\": \"cillum do dolor id\",\n \"website\": \"www.example.com\",\n \"about_business\": \"Online Retail with a wide selection of organic products for North America\",\n \"address\": {\n \"line1\": \"1467\",\n \"line2\": \"Harrison Street\",\n \"line3\": \"Harrison Street\",\n \"city\": \"San Fransico\",\n \"state\": \"California\",\n \"zip\": \"94122\",\n \"country\": \"US\"\n }\n },\n \"return_url\": \"https://duck.com/success\",\n \"webhook_details\": {\n \"webhook_version\": \"1.0.1\",\n \"webhook_username\": \"ekart_retail\",\n \"webhook_password\": \"password_ekart@123\",\n \"payment_created_enabled\": true,\n \"payment_succeeded_enabled\": true,\n \"payment_failed_enabled\": true\n },\n \"sub_merchants_enabled\": false,\n \"metadata\": {\n \"city\": \"NY\",\n \"unit\": \"245\"\n },\n \"primary_business_details\": [\n {\n \"country\": \"US\",\n \"business\": \"default\"\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/accounts", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts" + ] + }, + "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." + }, + "response": [] + }, + { + "name": "API Key - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/api_keys/:merchant_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/api_keys/:merchant_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", + "if (jsonData?.key_id) {", + " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", + " console.log(", + " \"- use {{api_key_id}} as collection variable for value\",", + " jsonData.key_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"name\":\"API Key 1\",\"description\":null,\"expiration\":\"2069-09-23T01:02:03.000Z\"}" + }, + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Payment Connector - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[POST]::/account/:account_id/connectors - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/account/:account_id/connectors - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"connector_type\": \"fiz_operations\",\n \"connector_name\": \"bankofamerica\",\n \"business_country\": \"US\",\n \"business_label\": \"default\",\n \"connector_label\": \"first_boa_connector\",\n \"connector_account_details\": {\n \"auth_type\": \"SignatureKey\",\n \"api_key\": \"{{connector_api_key}}\",\n \"api_secret\": \"{{connector_api_secret}}\",\n \"key1\": \"{{connector_key1}}\"\n },\n \"test_mode\": false,\n \"disabled\": false,\n \"payment_methods_enabled\": [\n {\n \"payment_method\": \"card\",\n \"payment_method_types\": [\n {\n \"payment_method_type\": \"credit\",\n \"card_networks\": [\n \"Visa\",\n \"Mastercard\"\n ],\n \"minimum_amount\": 1,\n \"maximum_amount\": 68607706,\n \"recurring_enabled\": true,\n \"installment_payment_enabled\": true\n },\n {\n \"payment_method_type\": \"debit\",\n \"card_networks\": [\n \"Visa\",\n \"Mastercard\"\n ],\n \"minimum_amount\": 1,\n \"maximum_amount\": 68607706,\n \"recurring_enabled\": true,\n \"installment_payment_enabled\": true\n }\n ]\n }\n ],\n \"metadata\": {\n \"city\": \"NY\",\n \"unit\": \"245\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." + }, + "response": [] + }, + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"amount\": 6540,\n \"currency\": \"USD\",\n \"confirm\": true,\n \"capture_method\": \"automatic\",\n \"capture_on\": \"2022-09-10T10:11:12Z\",\n \"amount_to_capture\": 6540,\n \"customer_id\": \"StripeCustomer\",\n \"email\": \"guest@example.com\",\n \"name\": \"John Doe\",\n \"phone\": \"999999999\",\n \"phone_country_code\": \"+1\",\n \"description\": \"Its my first payment request\",\n \"authentication_type\": \"no_three_ds\",\n \"return_url\": \"https://duck.com\",\n \"payment_method\": \"card\",\n \"payment_method_type\": \"credit\",\n \"payment_method_data\": {\n \"card\": {\n \"card_number\": \"4111111111111111\",\n \"card_exp_month\": \"12\",\n \"card_exp_year\": \"30\",\n \"card_holder_name\": \"joseph Doe\",\n \"card_cvc\": \"123\"\n }\n },\n \"billing\": {\n \"address\": {\n \"line1\": \"1467\",\n \"line2\": \"Harrison Street\",\n \"line3\": \"Harrison Street\",\n \"city\": \"San Fransico\",\n \"state\": \"California\",\n \"zip\": \"94122\",\n \"country\": \"US\",\n \"first_name\": \"joseph\",\n \"last_name\": \"Doe\"\n },\n \"phone\": {\n \"number\": \"8056594427\",\n \"country_code\": \"+91\"\n }\n },\n \"shipping\": {\n \"address\": {\n \"line1\": \"1467\",\n \"line2\": \"Harrison Street\",\n \"line3\": \"Harrison Street\",\n \"city\": \"San Fransico\",\n \"state\": \"California\",\n \"zip\": \"94122\",\n \"country\": \"US\",\n \"first_name\": \"joseph\",\n \"last_name\": \"Doe\"\n },\n \"phone\": {\n \"number\": \"8056594427\",\n \"country_code\": \"+91\"\n }\n },\n \"statement_descriptor_name\": \"joseph\",\n \"statement_descriptor_suffix\": \"JS\",\n \"metadata\": {\n \"udf1\": \"value1\",\n \"new_customer\": \"true\",\n \"login_date\": \"2019-09-10T10:11:12Z\"\n },\n \"routing\": {\n \"type\": \"single\",\n \"data\": \"bankofamerica\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Happy Cases", + "item": [ + { + "name": "Scenario1-Create payment with confirm true", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\" because payment gets succeeded after one day.", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// Response body should have \"connector_transaction_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", + " .true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "// pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "// pm.response.to.be.success;", + "// });", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// Response body should have \"connector_transaction_id\"", + "// pm.test(", + "// \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", + "// function () {", + "// pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", + "// .true;", + "// },", + "// );", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario2-Create payment with confirm false", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_confirmation\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// Response body should have \"connector_transaction_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", + " .true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "// pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "// pm.response.to.be.success;", + "// });", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario3-Create payment without PMD", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"amount\": 6540,\n \"currency\": \"USD\",\n \"confirm\": false,\n \"capture_method\": \"automatic\",\n \"capture_on\": \"2022-09-10T10:11:12Z\",\n \"amount_to_capture\": 6540,\n \"customer_id\": \"StripeCustomer\",\n \"email\": \"guest@example.com\",\n \"name\": \"John Doe\",\n \"phone\": \"999999999\",\n \"phone_country_code\": \"+65\",\n \"description\": \"Its my first payment request\",\n \"authentication_type\": \"no_three_ds\",\n \"return_url\": \"https://duck.com\",\n \"billing\": {\n \"address\": {\n \"line1\": \"1467\",\n \"line2\": \"Harrison Street\",\n \"line3\": \"Harrison Street\",\n \"city\": \"San Fransico\",\n \"state\": \"California\",\n \"zip\": \"94122\",\n \"country\": \"US\",\n \"first_name\": \"sundari\",\n \"last_name\": \"abcd\"\n }\n },\n \"shipping\": {\n \"address\": {\n \"line1\": \"1467\",\n \"line2\": \"Harrison Street\",\n \"line3\": \"Harrison Street\",\n \"city\": \"San Fransico\",\n \"state\": \"California\",\n \"zip\": \"94122\",\n \"country\": \"US\",\n \"first_name\": \"sundari\",\n \"last_name\": \"abcd\"\n }\n },\n \"statement_descriptor_name\": \"joseph\",\n \"statement_descriptor_suffix\": \"JS\",\n \"metadata\": {\n \"udf1\": \"value1\",\n \"new_customer\": \"true\",\n \"login_date\": \"2019-09-10T10:11:12Z\"\n },\n \"routing\": {\n \"type\": \"single\",\n \"data\": \"bankofamerica\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id/confirm - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "// pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "// pm.response.to.be.success;", + "// });", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario4-Create payment with Manual capture", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"3566111111111113\",\"card_exp_month\":\"12\",\"card_exp_year\":\"30\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6000);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "// pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "// pm.response.to.be.success;", + "// });", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario5-Void the payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"3566111111111113\",\"card_exp_month\":\"12\",\"card_exp_year\":\"30\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Cancel", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/cancel - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"cancelled\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'status' matches 'cancelled'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"cancelled\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"cancellation_reason\":\"requested_by_customer\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/cancel", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "cancel" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"cancelled\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'cancelled'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"cancelled\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + } + ] + } + ] + } + ], + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"[LOG]::payment_id - \" + jsonData.payment_id);", + "}", + "", + "console.log(\"[LOG]::x-request-id - \" + pm.response.headers.get(\"x-request-id\"));", + "" + ] + } + } + ], + "variable": [ + { + "key": "baseUrl", + "value": "", + "type": "string" + }, + { + "key": "admin_api_key", + "value": "", + "type": "string" + }, + { + "key": "api_key", + "value": "", + "type": "string" + }, + { + "key": "merchant_id", + "value": "" + }, + { + "key": "payment_id", + "value": "" + }, + { + "key": "customer_id", + "value": "" + }, + { + "key": "mandate_id", + "value": "" + }, + { + "key": "payment_method_id", + "value": "" + }, + { + "key": "refund_id", + "value": "" + }, + { + "key": "merchant_connector_id", + "value": "" + }, + { + "key": "client_secret", + "value": "", + "type": "string" + }, + { + "key": "connector_api_key", + "value": "", + "type": "string" + }, + { + "key": "publishable_key", + "value": "", + "type": "string" + }, + { + "key": "api_key_id", + "value": "", + "type": "string" + }, + { + "key": "payment_token", + "value": "" + }, + { + "key": "gateway_merchant_id", + "value": "", + "type": "string" + }, + { + "key": "certificate", + "value": "", + "type": "string" + }, + { + "key": "certificate_keys", + "value": "", + "type": "string" + }, + { + "key": "organization_id", + "value": "" + }, + { + "key": "connector_api_secret", + "value": "", + "type": "string" + }, + { + "key": "connector_key1", + "value": "", + "type": "string" + } + ] +} \ No newline at end of file From 8d4adc52af57ed0994e6efbb5b2d0d3df3fb3150 Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Thu, 16 Nov 2023 20:42:08 +0530 Subject: [PATCH 022/146] feat(connector): [ProphetPay] Implement Card Redirect PaymentMethodType and flows for Authorize, CompleteAuthorize, Psync, Refund, Rsync and Void (#2641) Co-authored-by: Arjun Karthik --- config/config.example.toml | 3 + config/development.toml | 3 + config/docker_compose.toml | 3 + crates/api_models/src/enums.rs | 4 +- crates/api_models/src/payments.rs | 2 + crates/api_models/src/routing.rs | 1 + crates/common_enums/src/enums.rs | 1 + crates/common_enums/src/transformers.rs | 1 + crates/common_utils/src/consts.rs | 6 + crates/euclid/src/enums.rs | 1 + crates/euclid/src/frontend/dir/enums.rs | 1 + crates/euclid/src/frontend/dir/lowering.rs | 1 + .../euclid/src/frontend/dir/transformers.rs | 3 + crates/kgraph_utils/src/transformers.rs | 3 + .../src/connector/adyen/transformers.rs | 6 + crates/router/src/connector/klarna.rs | 1 + .../src/connector/paypal/transformers.rs | 3 +- crates/router/src/connector/prophetpay.rs | 262 +++++++-- .../src/connector/prophetpay/transformers.rs | 523 +++++++++++++++--- .../src/connector/stripe/transformers.rs | 12 +- .../router/src/connector/zen/transformers.rs | 3 +- crates/router/src/core/admin.rs | 4 + crates/router/src/core/payments/flows.rs | 1 - crates/router/src/core/payments/helpers.rs | 1 + .../src/core/payments/routing/transformers.rs | 1 + crates/router/src/types/api.rs | 2 +- crates/router/src/types/transformers.rs | 5 +- crates/router/tests/connectors/prophetpay.rs | 3 +- openapi/openapi_spec.json | 13 + 29 files changed, 722 insertions(+), 151 deletions(-) diff --git a/config/config.example.toml b/config/config.example.toml index 40590128a5d4..02eff1d42979 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -417,6 +417,9 @@ ach = { currency = "USD" } [pm_filters.stripe] cashapp = {country = "US", currency = "USD"} +[pm_filters.prophetpay] +card_redirect = { currency = "USD" } + [connector_customer] connector_list = "gocardless,stax,stripe" payout_connector_list = "wise" diff --git a/config/development.toml b/config/development.toml index d3cad47a23fc..c82607a704c3 100644 --- a/config/development.toml +++ b/config/development.toml @@ -355,6 +355,9 @@ credit = { currency = "USD" } debit = { currency = "USD" } ach = { currency = "USD" } +[pm_filters.prophetpay] +card_redirect = { currency = "USD" } + [pm_filters.trustpay] credit = { not_available_flows = { capture_method = "manual" } } debit = { not_available_flows = { capture_method = "manual" } } diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 39e8fad0fcaa..a5294546de41 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -285,6 +285,9 @@ red_pagos = { country = "UY", currency = "UYU" } [pm_filters.stripe] cashapp = {country = "US", currency = "USD"} +[pm_filters.prophetpay] +card_redirect = { currency = "USD" } + [pm_filters.stax] credit = { currency = "USD" } debit = { currency = "USD" } diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index 493a421ece36..c4e4aa90c4b8 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -108,7 +108,7 @@ pub enum Connector { Paypal, Payu, Powertranz, - // Prophetpay, added as a template code for future usage + Prophetpay, Rapyd, Shift4, Square, @@ -229,7 +229,7 @@ pub enum RoutableConnectors { Paypal, Payu, Powertranz, - // Prophetpay, added as a template code for future usage + Prophetpay, Rapyd, Shift4, Square, diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index cf0259f26951..d924fb2e4f62 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -704,6 +704,7 @@ pub enum CardRedirectData { Knet {}, Benefit {}, MomoAtm {}, + CardRedirect {}, } #[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)] @@ -868,6 +869,7 @@ impl GetPaymentMethodType for CardRedirectData { Self::Knet {} => api_enums::PaymentMethodType::Knet, Self::Benefit {} => api_enums::PaymentMethodType::Benefit, Self::MomoAtm {} => api_enums::PaymentMethodType::MomoAtm, + Self::CardRedirect {} => api_enums::PaymentMethodType::CardRedirect, } } } diff --git a/crates/api_models/src/routing.rs b/crates/api_models/src/routing.rs index 650c4517f1b1..47a44ea7443e 100644 --- a/crates/api_models/src/routing.rs +++ b/crates/api_models/src/routing.rs @@ -337,6 +337,7 @@ impl From for ast::ConnectorChoice { RoutableConnectors::Paypal => euclid_enums::Connector::Paypal, RoutableConnectors::Payu => euclid_enums::Connector::Payu, RoutableConnectors::Powertranz => euclid_enums::Connector::Powertranz, + RoutableConnectors::Prophetpay => euclid_enums::Connector::Prophetpay, RoutableConnectors::Rapyd => euclid_enums::Connector::Rapyd, RoutableConnectors::Shift4 => euclid_enums::Connector::Shift4, RoutableConnectors::Square => euclid_enums::Connector::Square, diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index f0386fc2f42e..48b0664c16d3 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -990,6 +990,7 @@ pub enum PaymentMethodType { BcaBankTransfer, BniVa, BriVa, + CardRedirect, CimbVa, #[serde(rename = "classic")] ClassicReward, diff --git a/crates/common_enums/src/transformers.rs b/crates/common_enums/src/transformers.rs index 73f736cdeefa..63abfdb3f73a 100644 --- a/crates/common_enums/src/transformers.rs +++ b/crates/common_enums/src/transformers.rs @@ -1807,6 +1807,7 @@ impl From for PaymentMethod { PaymentMethodType::Bizum => Self::BankRedirect, PaymentMethodType::Blik => Self::BankRedirect, PaymentMethodType::Alfamart => Self::Voucher, + PaymentMethodType::CardRedirect => Self::CardRedirect, PaymentMethodType::CimbVa => Self::BankTransfer, PaymentMethodType::ClassicReward => Self::Reward, PaymentMethodType::Credit => Self::Card, diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index 7bc248bf8d1b..60756192d66e 100644 --- a/crates/common_utils/src/consts.rs +++ b/crates/common_utils/src/consts.rs @@ -41,3 +41,9 @@ pub const DEFAULT_PRODUCT_IMG: &str = "https://i.imgur.com/On3VtKF.png"; /// Default Merchant Logo Link pub const DEFAULT_MERCHANT_LOGO: &str = "https://i.imgur.com/RfxPFQo.png"; + +/// Redirect url for Prophetpay +pub const PROPHETPAY_REDIRECT_URL: &str = "https://ccm-thirdparty.cps.golf/hp/tokenize/"; + +/// Variable which store the card token for Prophetpay +pub const PROPHETPAY_TOKEN: &str = "cctoken"; diff --git a/crates/euclid/src/enums.rs b/crates/euclid/src/enums.rs index da5c99816715..dc6d9f66a58f 100644 --- a/crates/euclid/src/enums.rs +++ b/crates/euclid/src/enums.rs @@ -117,6 +117,7 @@ pub enum Connector { Paypal, Payu, Powertranz, + Prophetpay, Rapyd, Shift4, Square, diff --git a/crates/euclid/src/frontend/dir/enums.rs b/crates/euclid/src/frontend/dir/enums.rs index 17699940363f..f049ad35328e 100644 --- a/crates/euclid/src/frontend/dir/enums.rs +++ b/crates/euclid/src/frontend/dir/enums.rs @@ -225,6 +225,7 @@ pub enum CardRedirectType { Benefit, Knet, MomoAtm, + CardRedirect, } #[derive( diff --git a/crates/euclid/src/frontend/dir/lowering.rs b/crates/euclid/src/frontend/dir/lowering.rs index 516e10e0389e..b1f03e8dd557 100644 --- a/crates/euclid/src/frontend/dir/lowering.rs +++ b/crates/euclid/src/frontend/dir/lowering.rs @@ -134,6 +134,7 @@ impl From for global_enums::PaymentMethodType { enums::CardRedirectType::Benefit => Self::Benefit, enums::CardRedirectType::Knet => Self::Knet, enums::CardRedirectType::MomoAtm => Self::MomoAtm, + enums::CardRedirectType::CardRedirect => Self::CardRedirect, } } } diff --git a/crates/euclid/src/frontend/dir/transformers.rs b/crates/euclid/src/frontend/dir/transformers.rs index da413d380c0f..c99b39e36f46 100644 --- a/crates/euclid/src/frontend/dir/transformers.rs +++ b/crates/euclid/src/frontend/dir/transformers.rs @@ -161,6 +161,9 @@ impl IntoDirValue for (global_enums::PaymentMethodType, global_enums::PaymentMet } global_enums::PaymentMethodType::MomoAtm => Ok(dirval!(CardRedirectType = MomoAtm)), global_enums::PaymentMethodType::Oxxo => Ok(dirval!(VoucherType = Oxxo)), + global_enums::PaymentMethodType::CardRedirect => { + Ok(dirval!(CardRedirectType = CardRedirect)) + } } } } diff --git a/crates/kgraph_utils/src/transformers.rs b/crates/kgraph_utils/src/transformers.rs index 3d32cce38bd8..b1636418aa17 100644 --- a/crates/kgraph_utils/src/transformers.rs +++ b/crates/kgraph_utils/src/transformers.rs @@ -280,6 +280,9 @@ impl IntoDirValue for (api_enums::PaymentMethodType, api_enums::PaymentMethod) { } api_enums::PaymentMethodType::MomoAtm => Ok(dirval!(CardRedirectType = MomoAtm)), api_enums::PaymentMethodType::Oxxo => Ok(dirval!(VoucherType = Oxxo)), + api_enums::PaymentMethodType::CardRedirect => { + Ok(dirval!(CardRedirectType = CardRedirect)) + } } } } diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index 8bb287812800..ec21c9baa5e9 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -2201,6 +2201,12 @@ impl<'a> TryFrom<&api_models::payments::CardRedirectData> for AdyenPaymentMethod payments::CardRedirectData::Knet {} => Ok(AdyenPaymentMethod::Knet), payments::CardRedirectData::Benefit {} => Ok(AdyenPaymentMethod::Benefit), payments::CardRedirectData::MomoAtm {} => Ok(AdyenPaymentMethod::MomoAtm), + payments::CardRedirectData::CardRedirect {} => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Adyen"), + ) + .into()) + } } } } diff --git a/crates/router/src/connector/klarna.rs b/crates/router/src/connector/klarna.rs index 8737d2b30474..3670f65a2f02 100644 --- a/crates/router/src/connector/klarna.rs +++ b/crates/router/src/connector/klarna.rs @@ -323,6 +323,7 @@ impl | api_models::enums::PaymentMethodType::BcaBankTransfer | api_models::enums::PaymentMethodType::BniVa | api_models::enums::PaymentMethodType::BriVa + | api_models::enums::PaymentMethodType::CardRedirect | api_models::enums::PaymentMethodType::CimbVa | api_models::enums::PaymentMethodType::ClassicReward | api_models::enums::PaymentMethodType::Credit diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs index 0092363523e5..5468c6bb8061 100644 --- a/crates/router/src/connector/paypal/transformers.rs +++ b/crates/router/src/connector/paypal/transformers.rs @@ -439,7 +439,8 @@ impl TryFrom<&api_models::payments::CardRedirectData> for PaypalPaymentsRequest match value { api_models::payments::CardRedirectData::Knet {} | api_models::payments::CardRedirectData::Benefit {} - | api_models::payments::CardRedirectData::MomoAtm {} => { + | api_models::payments::CardRedirectData::MomoAtm {} + | api_models::payments::CardRedirectData::CardRedirect {} => { Err(errors::ConnectorError::NotSupported { message: utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "Paypal", diff --git a/crates/router/src/connector/prophetpay.rs b/crates/router/src/connector/prophetpay.rs index 417c34207e05..6765fad2653d 100644 --- a/crates/router/src/connector/prophetpay.rs +++ b/crates/router/src/connector/prophetpay.rs @@ -2,12 +2,14 @@ pub mod transformers; use std::fmt::Debug; +use base64::Engine; use error_stack::{IntoReport, ResultExt}; -use masking::ExposeInterface; +use masking::PeekInterface; use transformers as prophetpay; use crate::{ configs::settings, + consts, core::errors::{self, CustomResult}, headers, services::{ @@ -37,6 +39,7 @@ impl api::Refund for Prophetpay {} impl api::RefundExecute for Prophetpay {} impl api::RefundSync for Prophetpay {} impl api::PaymentToken for Prophetpay {} +impl api::payments::PaymentsCompleteAuthorize for Prophetpay {} impl ConnectorIntegration< @@ -73,7 +76,7 @@ impl ConnectorCommon for Prophetpay { } fn get_currency_unit(&self) -> api::CurrencyUnit { - api::CurrencyUnit::Minor + api::CurrencyUnit::Base } fn common_get_content_type(&self) -> &'static str { @@ -90,9 +93,13 @@ impl ConnectorCommon for Prophetpay { ) -> CustomResult)>, errors::ConnectorError> { let auth = prophetpay::ProphetpayAuthType::try_from(auth_type) .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + + let auth_val = format!("{}:{}", auth.user_name.peek(), auth.password.peek()); + let basic_token = format!("Basic {}", consts::BASE64_ENGINE.encode(auth_val)); + Ok(vec![( headers::AUTHORIZATION.to_string(), - auth.api_key.expose().into_masked(), + basic_token.into_masked(), )]) } @@ -107,9 +114,9 @@ impl ConnectorCommon for Prophetpay { Ok(ErrorResponse { status_code: res.status_code, - code: response.code, - message: response.message, - reason: response.reason, + code: response.status.to_string(), + message: response.title, + reason: Some(response.errors.to_string()), attempt_status: None, }) } @@ -157,9 +164,12 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!( + "{}hp/api/HostedTokenize/CreateHostedTokenize", + self.base_url(connectors) + )) } fn get_request_body( @@ -173,10 +183,11 @@ impl ConnectorIntegration::encode_to_string_of_json, + utils::Encode::::encode_to_string_of_json, ) .change_context(errors::ConnectorError::RequestEncodingFailed)?; Ok(Some(prophetpay_req)) @@ -208,10 +219,114 @@ impl ConnectorIntegration CustomResult { - let response: prophetpay::ProphetpayPaymentsResponse = res + ) -> CustomResult + where + types::PaymentsResponseData: Clone, + { + let response: prophetpay::ProphetpayTokenResponse = res .response - .parse_struct("Prophetpay PaymentsAuthorizeResponse") + .parse_struct("prophetpay ProphetpayTokenResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl + ConnectorIntegration< + api::CompleteAuthorize, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + > for Prophetpay +{ + fn get_headers( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::PaymentsCompleteAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}hp/api/Transactions/ProcessTransaction", + self.base_url(connectors) + )) + } + + fn get_request_body( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let connector_router_data = prophetpay::ProphetpayRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount, + req, + ))?; + let req_obj = prophetpay::ProphetpayCompleteRequest::try_from(&connector_router_data)?; + + let prophetpay_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(prophetpay_req)) + } + + fn build_request( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsCompleteAuthorizeType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsCompleteAuthorizeType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsCompleteAuthorizeType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsCompleteAuthorizeRouterData, + res: Response, + ) -> CustomResult + where + types::PaymentsResponseData: Clone, + { + let response: prophetpay::ProphetpayResponse = res + .response + .parse_struct("prophetpay ProphetpayResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, @@ -246,9 +361,27 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!( + "{}hp/api/Transactions/ProcessTransaction", + self.base_url(connectors) + )) + } + + fn get_request_body( + &self, + req: &types::PaymentsSyncRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = prophetpay::ProphetpaySyncRequest::try_from(req)?; + + let prophetpay_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(prophetpay_req)) } fn build_request( @@ -258,10 +391,13 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { Ok(Some( services::RequestBuilder::new() - .method(services::Method::Get) + .method(services::Method::Post) .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) + .body(types::PaymentsSyncType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -271,9 +407,9 @@ impl ConnectorIntegration CustomResult { - let response: prophetpay::ProphetpayPaymentsResponse = res + let response: prophetpay::ProphetpayResponse = res .response - .parse_struct("prophetpay PaymentsSyncResponse") + .parse_struct("prophetpay ProphetpayResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, @@ -292,10 +428,15 @@ impl ConnectorIntegration for Prophetpay +{ +} + +impl ConnectorIntegration + for Prophetpay { fn get_headers( &self, - req: &types::PaymentsCaptureRouterData, + req: &types::PaymentsCancelRouterData, connectors: &settings::Connectors, ) -> CustomResult)>, errors::ConnectorError> { self.build_headers(req, connectors) @@ -307,34 +448,42 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!( + "{}hp/api/Transactions/ProcessTransaction", + self.base_url(connectors) + )) } fn get_request_body( &self, - _req: &types::PaymentsCaptureRouterData, + req: &types::PaymentsCancelRouterData, _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) + let req_obj = prophetpay::ProphetpayVoidRequest::try_from(req)?; + + let prophetpay_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(prophetpay_req)) } fn build_request( &self, - req: &types::PaymentsCaptureRouterData, + req: &types::PaymentsCancelRouterData, connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { Ok(Some( services::RequestBuilder::new() - .method(services::Method::Post) - .url(&types::PaymentsCaptureType::get_url(self, req, connectors)?) + .method(services::Method::Get) + .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) .attach_default_headers() - .headers(types::PaymentsCaptureType::get_headers( - self, req, connectors, - )?) - .body(types::PaymentsCaptureType::get_request_body( + .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) + .body(types::PaymentsVoidType::get_request_body( self, req, connectors, )?) .build(), @@ -343,12 +492,12 @@ impl ConnectorIntegration CustomResult { - let response: prophetpay::ProphetpayPaymentsResponse = res + ) -> CustomResult { + let response: prophetpay::ProphetpayResponse = res .response - .parse_struct("Prophetpay PaymentsCaptureResponse") + .parse_struct("prophetpay ProphetpayResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, @@ -365,11 +514,6 @@ impl ConnectorIntegration - for Prophetpay -{ -} - impl ConnectorIntegration for Prophetpay { @@ -388,9 +532,12 @@ impl ConnectorIntegration, - _connectors: &settings::Connectors, + connectors: &settings::Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!( + "{}hp/api/Transactions/ProcessTransaction", + self.base_url(connectors) + )) } fn get_request_body( @@ -437,10 +584,11 @@ impl ConnectorIntegration, res: Response, ) -> CustomResult, errors::ConnectorError> { - let response: prophetpay::RefundResponse = res + let response: prophetpay::ProphetpayRefundResponse = res .response - .parse_struct("prophetpay RefundResponse") + .parse_struct("prophetpay ProphetpayRefundResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { response, data: data.clone(), @@ -474,9 +622,27 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!( + "{}hp/api/Transactions/ProcessTransaction", + self.base_url(connectors) + )) + } + + fn get_request_body( + &self, + req: &types::RefundSyncRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = prophetpay::ProphetpayRefundSyncRequest::try_from(req)?; + + let prophetpay_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(prophetpay_req)) } fn build_request( @@ -502,9 +668,9 @@ impl ConnectorIntegration CustomResult { - let response: prophetpay::RefundResponse = res + let response: prophetpay::ProphetpayRefundResponse = res .response - .parse_struct("prophetpay RefundSyncResponse") + .parse_struct("prophetpay ProphetpayRefundResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, diff --git a/crates/router/src/connector/prophetpay/transformers.rs b/crates/router/src/connector/prophetpay/transformers.rs index 1066c88df3e1..74071d5b85cb 100644 --- a/crates/router/src/connector/prophetpay/transformers.rs +++ b/crates/router/src/connector/prophetpay/transformers.rs @@ -1,14 +1,20 @@ -use masking::Secret; +use std::collections::HashMap; + +use common_utils::{consts, errors::CustomResult}; +use error_stack::{IntoReport, ResultExt}; +use masking::{PeekInterface, Secret}; use serde::{Deserialize, Serialize}; +use url::Url; use crate::{ - connector::utils::PaymentsAuthorizeRequestData, + connector::utils, core::errors, + services, types::{self, api, storage::enums}, }; pub struct ProphetpayRouterData { - pub amount: i64, // The type of amount that a connector accepts, for example, String, i64, f64, etc. + pub amount: f64, pub router_data: T, } @@ -22,13 +28,14 @@ impl { type Error = error_stack::Report; fn try_from( - (_currency_unit, _currency, amount, item): ( + (currency_unit, currency, amount, item): ( &types::api::CurrencyUnit, types::storage::enums::Currency, i64, T, ), ) -> Result { + let amount = utils::get_amount_as_f64(currency_unit, amount, currency)?; Ok(Self { amount, router_data: item, @@ -36,108 +43,370 @@ impl } } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct ProphetpayPaymentsRequest { - amount: i64, - card: ProphetpayCard, +pub struct ProphetpayAuthType { + pub(super) user_name: Secret, + pub(super) password: Secret, + pub(super) profile_id: Secret, +} + +impl TryFrom<&types::ConnectorAuthType> for ProphetpayAuthType { + type Error = error_stack::Report; + fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + match auth_type { + types::ConnectorAuthType::SignatureKey { + api_key, + key1, + api_secret, + } => Ok(Self { + user_name: api_key.to_owned(), + password: key1.to_owned(), + profile_id: api_secret.to_owned(), + }), + _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), + } + } +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct ProphetpayTokenRequest { + ref_info: String, + profile: Secret, + entry_method: i8, + token_type: i8, + card_entry_context: i8, +} + +#[derive(Debug, Clone)] +pub enum ProphetpayEntryMethod { + ManualEntry, + CardSwipe, +} + +impl ProphetpayEntryMethod { + fn get_entry_method(&self) -> i8 { + match self { + Self::ManualEntry => 1, + Self::CardSwipe => 2, + } + } +} + +#[derive(Debug, Clone)] +#[repr(i8)] +pub enum ProphetpayTokenType { + Normal, + SaleTab, + TemporarySave, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct ProphetpayCard { - name: Secret, - number: cards::CardNumber, - expiry_month: Secret, - expiry_year: Secret, - cvc: Secret, - complete: bool, +impl ProphetpayTokenType { + fn get_token_type(&self) -> i8 { + match self { + Self::Normal => 0, + Self::SaleTab => 1, + Self::TemporarySave => 2, + } + } +} + +#[derive(Debug, Clone)] +#[repr(i8)] +pub enum ProphetpayCardContext { + NotApplicable, + WebConsumerInitiated, +} + +impl ProphetpayCardContext { + fn get_card_context(&self) -> i8 { + match self { + Self::NotApplicable => 0, + Self::WebConsumerInitiated => 5, + } + } } impl TryFrom<&ProphetpayRouterData<&types::PaymentsAuthorizeRouterData>> - for ProphetpayPaymentsRequest + for ProphetpayTokenRequest { type Error = error_stack::Report; fn try_from( item: &ProphetpayRouterData<&types::PaymentsAuthorizeRouterData>, ) -> Result { - match item.router_data.request.payment_method_data.clone() { - api::PaymentMethodData::Card(req_card) => { - let card = ProphetpayCard { - name: req_card.card_holder_name, - number: req_card.card_number, - expiry_month: req_card.card_exp_month, - expiry_year: req_card.card_exp_year, - cvc: req_card.card_cvc, - complete: item.router_data.request.is_auto_capture()?, - }; - Ok(Self { - amount: item.amount.to_owned(), - card, - }) + if item.router_data.request.currency == api_models::enums::Currency::USD { + match item.router_data.request.payment_method_data.clone() { + api::PaymentMethodData::CardRedirect( + api_models::payments::CardRedirectData::CardRedirect {}, + ) => { + let auth_data = + ProphetpayAuthType::try_from(&item.router_data.connector_auth_type)?; + Ok(Self { + ref_info: item.router_data.connector_request_reference_id.to_owned(), + profile: auth_data.profile_id, + entry_method: ProphetpayEntryMethod::get_entry_method( + &ProphetpayEntryMethod::ManualEntry, + ), + token_type: ProphetpayTokenType::get_token_type( + &ProphetpayTokenType::SaleTab, + ), + card_entry_context: ProphetpayCardContext::get_card_context( + &ProphetpayCardContext::WebConsumerInitiated, + ), + }) + } + _ => Err( + errors::ConnectorError::NotImplemented("Payment methods".to_string()).into(), + ), } - _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), + } else { + Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()) } } } -pub struct ProphetpayAuthType { - pub(super) api_key: Secret, +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProphetpayTokenResponse { + hosted_tokenize_id: String, } -impl TryFrom<&types::ConnectorAuthType> for ProphetpayAuthType { +impl + TryFrom< + types::ResponseRouterData< + F, + ProphetpayTokenResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ type Error = error_stack::Report; - fn try_from(auth_type: &types::ConnectorAuthType) -> Result { - match auth_type { - types::ConnectorAuthType::HeaderKey { api_key } => Ok(Self { - api_key: api_key.to_owned(), + fn try_from( + item: types::ResponseRouterData< + F, + ProphetpayTokenResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + ) -> Result { + let url_data = format!( + "{}{}", + consts::PROPHETPAY_REDIRECT_URL, + item.response.hosted_tokenize_id + ); + + let redirect_url = Url::parse(url_data.as_str()) + .into_report() + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + let redirection_data = get_redirect_url_form( + redirect_url, + item.data.request.complete_authorize_url.clone(), + ) + .ok(); + + Ok(Self { + status: enums::AttemptStatus::AuthenticationPending, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::NoResponseId, + redirection_data, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, }), - _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), + ..item.data + }) + } +} + +fn get_redirect_url_form( + mut redirect_url: Url, + complete_auth_url: Option, +) -> CustomResult { + let mut form_fields = std::collections::HashMap::::new(); + + form_fields.insert( + String::from("redirectUrl"), + complete_auth_url.ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "complete_auth_url", + })?, + ); + + // Do not include query params in the endpoint + redirect_url.set_query(None); + + Ok(services::RedirectForm::Form { + endpoint: redirect_url.to_string(), + method: services::Method::Get, + form_fields, + }) +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ProphetpayCompleteRequest { + amount: f64, + ref_info: String, + inquiry_reference: String, + profile: Secret, + action_type: i8, + card_token: String, +} + +impl TryFrom<&ProphetpayRouterData<&types::PaymentsCompleteAuthorizeRouterData>> + for ProphetpayCompleteRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &ProphetpayRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + ) -> Result { + let auth_data = ProphetpayAuthType::try_from(&item.router_data.connector_auth_type)?; + let card_token = get_card_token(item.router_data.request.redirect_response.clone())?; + Ok(Self { + amount: item.amount.to_owned(), + ref_info: item.router_data.connector_request_reference_id.to_owned(), + inquiry_reference: format!( + "inquiry_{}", + item.router_data.connector_request_reference_id + ), + profile: auth_data.profile_id, + action_type: ProphetpayActionType::get_action_type(&ProphetpayActionType::Charge), + card_token, + }) + } +} + +fn get_card_token( + response: Option, +) -> CustomResult { + let res = response.ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "redirect_response", + })?; + let queries_params = res + .params + .map(|param| { + let mut queries = HashMap::::new(); + let values = param.peek().split('&').collect::>(); + for value in values { + let pair = value.split('=').collect::>(); + queries.insert(pair[0].to_string(), pair[1].to_string()); + } + queries + }) + .ok_or(errors::ConnectorError::ResponseDeserializationFailed)?; + + for (key, val) in queries_params { + if key.as_str() == consts::PROPHETPAY_TOKEN { + return Ok(val); + } + } + + Err(errors::ConnectorError::MissingRequiredField { + field_name: "card_token", + }) + .into_report() +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ProphetpaySyncRequest { + transaction_id: String, + ref_info: String, + inquiry_reference: String, + profile: Secret, + action_type: i8, +} + +#[derive(Debug, Clone)] +pub enum ProphetpayActionType { + Charge, + Refund, + Inquiry, +} + +impl ProphetpayActionType { + fn get_action_type(&self) -> i8 { + match self { + Self::Charge => 1, + Self::Refund => 3, + Self::Inquiry => 7, } } } -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] +impl TryFrom<&types::PaymentsSyncRouterData> for ProphetpaySyncRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsSyncRouterData) -> Result { + let auth_data = ProphetpayAuthType::try_from(&item.connector_auth_type)?; + let transaction_id = item + .request + .connector_transaction_id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; + Ok(Self { + transaction_id, + ref_info: item.attempt_id.to_owned(), + inquiry_reference: format!("inquiry_{}", item.attempt_id), + profile: auth_data.profile_id, + action_type: ProphetpayActionType::get_action_type(&ProphetpayActionType::Inquiry), + }) + } +} + +#[derive(Debug, Clone, Deserialize)] pub enum ProphetpayPaymentStatus { - Succeeded, - Failed, - #[default] - Processing, + Success, + #[serde(rename = "Transaction Approved")] + Charged, + Failure, + #[serde(rename = "Transaction Voided")] + Voided, + #[serde(rename = "Requires a card on file.")] + CardTokenNotFound, + #[serde(rename = "RefInfo and InquiryReference are duplicated")] + DuplicateValue, + #[serde(rename = "Profile is missing")] + MissingProfile, + #[serde(rename = "RefInfo is empty.")] + EmptyRef, } impl From for enums::AttemptStatus { fn from(item: ProphetpayPaymentStatus) -> Self { match item { - ProphetpayPaymentStatus::Succeeded => Self::Charged, - ProphetpayPaymentStatus::Failed => Self::Failure, - ProphetpayPaymentStatus::Processing => Self::Authorizing, + ProphetpayPaymentStatus::Success | ProphetpayPaymentStatus::Charged => Self::Charged, + ProphetpayPaymentStatus::Failure + | ProphetpayPaymentStatus::CardTokenNotFound + | ProphetpayPaymentStatus::DuplicateValue + | ProphetpayPaymentStatus::MissingProfile + | ProphetpayPaymentStatus::EmptyRef => Self::Failure, + ProphetpayPaymentStatus::Voided => Self::Voided, } } } -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct ProphetpayPaymentsResponse { - status: ProphetpayPaymentStatus, - id: String, +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProphetpayResponse { + pub response_text: ProphetpayPaymentStatus, + #[serde(rename = "transactionID")] + pub transaction_id: String, } -impl - TryFrom< - types::ResponseRouterData, - > for types::RouterData +impl TryFrom> + for types::RouterData { type Error = error_stack::Report; fn try_from( - item: types::ResponseRouterData< - F, - ProphetpayPaymentsResponse, - T, - types::PaymentsResponseData, - >, + item: types::ResponseRouterData, ) -> Result { Ok(Self { - status: enums::AttemptStatus::from(item.response.status), + status: enums::AttemptStatus::from(item.response.response_text), response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(item.response.id), + resource_id: types::ResponseId::ConnectorTransactionId( + item.response.transaction_id, + ), redirection_data: None, mandate_reference: None, connector_metadata: None, @@ -150,8 +419,39 @@ impl } #[derive(Default, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ProphetpayVoidRequest { + pub transaction_id: String, + pub profile: Secret, + pub ref_info: String, + pub inquiry_reference: String, + pub action_type: i8, +} + +impl TryFrom<&types::PaymentsCancelRouterData> for ProphetpayVoidRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsCancelRouterData) -> Result { + let auth_data = ProphetpayAuthType::try_from(&item.connector_auth_type)?; + let transaction_id = item.request.connector_transaction_id.to_owned(); + Ok(Self { + transaction_id, + ref_info: item.attempt_id.to_owned(), + inquiry_reference: format!("inquiry_{}", item.attempt_id), + profile: auth_data.profile_id, + action_type: ProphetpayActionType::get_action_type(&ProphetpayActionType::Inquiry), + }) + } +} + +#[derive(Default, Debug, Serialize)] +#[serde(rename_all = "camelCase")] pub struct ProphetpayRefundRequest { - pub amount: i64, + pub amount: f64, + pub transaction_id: String, + pub profile: Secret, + pub ref_info: String, + pub inquiry_reference: String, + pub action_type: i8, } impl TryFrom<&ProphetpayRouterData<&types::RefundsRouterData>> for ProphetpayRefundRequest { @@ -159,75 +459,118 @@ impl TryFrom<&ProphetpayRouterData<&types::RefundsRouterData>> for Prophet fn try_from( item: &ProphetpayRouterData<&types::RefundsRouterData>, ) -> Result { + let auth_data = ProphetpayAuthType::try_from(&item.router_data.connector_auth_type)?; + let transaction_id = item.router_data.request.connector_transaction_id.to_owned(); Ok(Self { + transaction_id, amount: item.amount.to_owned(), + profile: auth_data.profile_id, + ref_info: item.router_data.request.refund_id.to_owned(), + inquiry_reference: format!("inquiry_{}", item.router_data.request.refund_id), + action_type: ProphetpayActionType::get_action_type(&ProphetpayActionType::Refund), }) } } #[allow(dead_code)] -#[derive(Debug, Serialize, Default, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone)] pub enum RefundStatus { - Succeeded, - Failed, - #[default] - Processing, + Success, + Failure, + #[serde(rename = "Transaction Voided")] + Voided, + #[serde(rename = "Requires a card on file.")] + CardTokenNotFound, + #[serde(rename = "RefInfo and InquiryReference are duplicated")] + DuplicateValue, + #[serde(rename = "Profile is missing")] + MissingProfile, + #[serde(rename = "RefInfo is empty.")] + EmptyRef, } impl From for enums::RefundStatus { fn from(item: RefundStatus) -> Self { match item { - RefundStatus::Succeeded => Self::Success, - RefundStatus::Failed => Self::Failure, - RefundStatus::Processing => Self::Pending, + RefundStatus::Success + // in retrieving refund, if it is successful, it is shown as voided + | RefundStatus::Voided => Self::Success, + RefundStatus::Failure + | RefundStatus::CardTokenNotFound + | RefundStatus::DuplicateValue + | RefundStatus::MissingProfile + | RefundStatus::EmptyRef => Self::Failure, } } } -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct RefundResponse { - id: String, - status: RefundStatus, +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProphetpayRefundResponse { + pub response_text: RefundStatus, } -impl TryFrom> +impl TryFrom> for types::RefundsRouterData { type Error = error_stack::Report; fn try_from( - item: types::RefundsResponseRouterData, + item: types::RefundsResponseRouterData, ) -> Result { Ok(Self { response: Ok(types::RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), + // no refund id is generated, rather transaction id is used for referring to status in refund also + connector_refund_id: item.data.request.connector_transaction_id.clone(), + refund_status: enums::RefundStatus::from(item.response.response_text), }), ..item.data }) } } -impl TryFrom> +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ProphetpayRefundSyncRequest { + ref_info: String, + profile: Secret, + action_type: i8, +} + +impl TryFrom<&types::RefundSyncRouterData> for ProphetpayRefundSyncRequest { + type Error = error_stack::Report; + fn try_from(item: &types::RefundSyncRouterData) -> Result { + let auth_data = ProphetpayAuthType::try_from(&item.connector_auth_type)?; + Ok(Self { + ref_info: item.attempt_id.to_owned(), + profile: auth_data.profile_id, + action_type: ProphetpayActionType::get_action_type(&ProphetpayActionType::Inquiry), + }) + } +} + +impl TryFrom> for types::RefundsRouterData { type Error = error_stack::Report; fn try_from( - item: types::RefundsResponseRouterData, + item: types::RefundsResponseRouterData, ) -> Result { Ok(Self { response: Ok(types::RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), + connector_refund_id: item.data.request.connector_transaction_id.clone(), + refund_status: enums::RefundStatus::from(item.response.response_text), }), ..item.data }) } } +// Error Response body is yet to be confirmed with the connector #[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] pub struct ProphetpayErrorResponse { - pub status_code: u16, - pub code: String, - pub message: String, - pub reason: Option, + pub status: u16, + pub title: String, + pub trace_id: String, + pub errors: serde_json::Value, } diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index a783fd23fe19..3f0d4f543ba4 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -614,6 +614,7 @@ impl TryFrom for StripePaymentMethodType { enums::PaymentMethodType::AliPay => Ok(Self::Alipay), enums::PaymentMethodType::Przelewy24 => Ok(Self::Przelewy24), enums::PaymentMethodType::Boleto + | enums::PaymentMethodType::CardRedirect | enums::PaymentMethodType::CryptoCurrency | enums::PaymentMethodType::GooglePay | enums::PaymentMethodType::Multibanco @@ -1391,11 +1392,14 @@ fn create_stripe_payment_method( payments::PaymentMethodData::CardRedirect(cardredirect_data) => match cardredirect_data { payments::CardRedirectData::Knet {} | payments::CardRedirectData::Benefit {} - | payments::CardRedirectData::MomoAtm {} => Err(errors::ConnectorError::NotSupported { - message: connector_util::SELECTED_PAYMENT_METHOD.to_string(), - connector: "stripe", + | payments::CardRedirectData::MomoAtm {} + | payments::CardRedirectData::CardRedirect {} => { + Err(errors::ConnectorError::NotSupported { + message: connector_util::SELECTED_PAYMENT_METHOD.to_string(), + connector: "stripe", + } + .into()) } - .into()), }, payments::PaymentMethodData::Reward => Err(errors::ConnectorError::NotImplemented( connector_util::get_unimplemented_payment_method_error_message("stripe"), diff --git a/crates/router/src/connector/zen/transformers.rs b/crates/router/src/connector/zen/transformers.rs index 6b0d46dec8d1..689894176b26 100644 --- a/crates/router/src/connector/zen/transformers.rs +++ b/crates/router/src/connector/zen/transformers.rs @@ -790,7 +790,8 @@ impl TryFrom<&api_models::payments::CardRedirectData> for ZenPaymentsRequest { match value { api_models::payments::CardRedirectData::Knet {} | api_models::payments::CardRedirectData::Benefit {} - | api_models::payments::CardRedirectData::MomoAtm {} => { + | api_models::payments::CardRedirectData::MomoAtm {} + | api_models::payments::CardRedirectData::CardRedirect {} => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Zen"), ) diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index f5ca2f8b26e9..39b4749535b7 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1639,6 +1639,10 @@ pub(crate) fn validate_auth_and_metadata_type( powertranz::transformers::PowertranzAuthType::try_from(val)?; Ok(()) } + api_enums::Connector::Prophetpay => { + prophetpay::transformers::ProphetpayAuthType::try_from(val)?; + Ok(()) + } api_enums::Connector::Rapyd => { rapyd::transformers::RapydAuthType::try_from(val)?; Ok(()) diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 0b253cdc6079..46eaca26f7cc 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -168,7 +168,6 @@ default_imp_for_complete_authorize!( connector::Opennode, connector::Payeezy, connector::Payu, - connector::Prophetpay, connector::Rapyd, connector::Square, connector::Stax, diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 4ee2fd4b94d3..b9e96ec36e11 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1817,6 +1817,7 @@ pub fn validate_payment_method_type_against_payment_method( api_enums::PaymentMethodType::Knet | api_enums::PaymentMethodType::Benefit | api_enums::PaymentMethodType::MomoAtm + | api_enums::PaymentMethodType::CardRedirect ), } } diff --git a/crates/router/src/core/payments/routing/transformers.rs b/crates/router/src/core/payments/routing/transformers.rs index d7061a1502de..5704f82f4983 100644 --- a/crates/router/src/core/payments/routing/transformers.rs +++ b/crates/router/src/core/payments/routing/transformers.rs @@ -105,6 +105,7 @@ impl ForeignFrom for dsl_enums::Connector { api_enums::RoutableConnectors::Paypal => Self::Paypal, api_enums::RoutableConnectors::Payu => Self::Payu, api_enums::RoutableConnectors::Powertranz => Self::Powertranz, + api_enums::RoutableConnectors::Prophetpay => Self::Prophetpay, api_enums::RoutableConnectors::Rapyd => Self::Rapyd, api_enums::RoutableConnectors::Shift4 => Self::Shift4, api_enums::RoutableConnectors::Square => Self::Square, diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 2aa8f4a97c76..dc615c4e41fa 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -371,7 +371,7 @@ impl ConnectorData { enums::Connector::Payme => Ok(Box::new(&connector::Payme)), enums::Connector::Payu => Ok(Box::new(&connector::Payu)), enums::Connector::Powertranz => Ok(Box::new(&connector::Powertranz)), - // enums::Connector::Prophetpay => Ok(Box::new(&connector::Prophetpay)), + enums::Connector::Prophetpay => Ok(Box::new(&connector::Prophetpay)), enums::Connector::Rapyd => Ok(Box::new(&connector::Rapyd)), enums::Connector::Shift4 => Ok(Box::new(&connector::Shift4)), enums::Connector::Square => Ok(Box::new(&connector::Square)), diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 2ba4ea483c45..f43abdf73ead 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -230,6 +230,7 @@ impl ForeignTryFrom for api_enums::RoutableConnectors { .into_report()? } api_enums::Connector::Powertranz => Self::Powertranz, + api_enums::Connector::Prophetpay => Self::Prophetpay, api_enums::Connector::Rapyd => Self::Rapyd, api_enums::Connector::Shift4 => Self::Shift4, api_enums::Connector::Signifyd => { @@ -304,6 +305,7 @@ impl ForeignFrom for api_enums::RoutableConnectors { dsl_enums::Connector::Paypal => Self::Paypal, dsl_enums::Connector::Payu => Self::Payu, dsl_enums::Connector::Powertranz => Self::Powertranz, + dsl_enums::Connector::Prophetpay => Self::Prophetpay, dsl_enums::Connector::Rapyd => Self::Rapyd, dsl_enums::Connector::Shift4 => Self::Shift4, dsl_enums::Connector::Square => Self::Square, @@ -503,7 +505,8 @@ impl ForeignFrom for api_enums::PaymentMethod { } api_enums::PaymentMethodType::Benefit | api_enums::PaymentMethodType::Knet - | api_enums::PaymentMethodType::MomoAtm => Self::CardRedirect, + | api_enums::PaymentMethodType::MomoAtm + | api_enums::PaymentMethodType::CardRedirect => Self::CardRedirect, } } } diff --git a/crates/router/tests/connectors/prophetpay.rs b/crates/router/tests/connectors/prophetpay.rs index 09e4ea422531..94220c11a6aa 100644 --- a/crates/router/tests/connectors/prophetpay.rs +++ b/crates/router/tests/connectors/prophetpay.rs @@ -12,8 +12,7 @@ impl utils::Connector for ProphetpayTest { use router::connector::Prophetpay; types::api::ConnectorData { connector: Box::new(&Prophetpay), - // Remove `dummy_connector` feature gate from module in `main.rs` when updating this to use actual connector variant - connector_name: types::Connector::DummyConnector1, + connector_name: types::Connector::Prophetpay, get_token: types::api::GetToken::Connector, merchant_connector_id: None, } diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index d154ee5a6407..9fddde01b49a 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -4038,6 +4038,17 @@ "type": "object" } } + }, + { + "type": "object", + "required": [ + "card_redirect" + ], + "properties": { + "card_redirect": { + "type": "object" + } + } } ] }, @@ -4089,6 +4100,7 @@ "paypal", "payu", "powertranz", + "prophetpay", "rapyd", "shift4", "square", @@ -8776,6 +8788,7 @@ "bca_bank_transfer", "bni_va", "bri_va", + "card_redirect", "cimb_va", "classic", "credit", From f8291973c38bde874c45ca15ff8d48c1f2de9781 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 16 Nov 2023 15:36:19 +0000 Subject: [PATCH 023/146] test(postman): update postman collection files --- .../bankofamerica.postman_collection.json | 8618 ++++++++--------- 1 file changed, 4309 insertions(+), 4309 deletions(-) diff --git a/postman/collection-json/bankofamerica.postman_collection.json b/postman/collection-json/bankofamerica.postman_collection.json index 2b1a8fdc4704..01524d91953d 100644 --- a/postman/collection-json/bankofamerica.postman_collection.json +++ b/postman/collection-json/bankofamerica.postman_collection.json @@ -1,4310 +1,4310 @@ { - "info": { - "_postman_id": "646f7167-da26-4a24-adb0-4157fd3a1781", - "name": "bankofamerica", - "description": "## Get started\n\nJuspay Router provides a collection of APIs that enable you to process and manage payments. Our APIs accept and return JSON in the HTTP body, and return standard HTTP response codes. \nYou can consume the APIs directly using your favorite HTTP/REST library. \nWe have a testing environment referred to \"sandbox\", which you can setup to test API calls without affecting production data.\n\n### Base URLs\n\nUse the following base URLs when making requests to the APIs:\n\n| Environment | Base URL |\n| --- | --- |\n| Sandbox | [https://sandbox.hyperswitch.io](https://sandbox.hyperswitch.io) |\n| Production | [https://router.juspay.io](https://router.juspay.io) |\n\n# Authentication\n\nWhen you sign up for an account, you are given a secret key (also referred as api-key). You may authenticate all API requests with Juspay server by providing the appropriate key in the request Authorization header. \nNever share your secret api keys. Keep them guarded and secure.\n\nContact Support: \nName: Juspay Support \nEmail: [support@juspay.in](mailto:support@juspay.in)", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "28305597" - }, - "item": [ - { - "name": "Health check", - "item": [ - { - "name": "New Request", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], - "url": { - "raw": "{{baseUrl}}/health", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "health" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "MerchantAccounts", - "item": [ - { - "name": "Merchant Account - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/accounts - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) { }", - "", - "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", - "if (jsonData?.merchant_id) {", - " pm.collectionVariables.set(\"merchant_id\", jsonData.merchant_id);", - " console.log(", - " \"- use {{merchant_id}} as collection variable for value\",", - " jsonData.merchant_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", - "if (jsonData?.api_key) {", - " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", - " console.log(", - " \"- use {{api_key}} as collection variable for value\",", - " jsonData.api_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", - "if (jsonData?.publishable_key) {", - " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", - " console.log(", - " \"- use {{publishable_key}} as collection variable for value\",", - " jsonData.publishable_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", - "if (jsonData?.merchant_id) {", - " pm.collectionVariables.set(\"organization_id\", jsonData.organization_id);", - " console.log(", - " \"- use {{organization_id}} as collection variable for value\",", - " jsonData.organization_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{organization_id}}, as jsonData.organization_id is undefined.\",", - " );", - "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/accounts - Organization id is generated\",", - " function () {", - " pm.expect(typeof jsonData.organization_id !== \"undefined\").to.be.true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"merchant_id\": \"postman_merchant_GHAction_{{$guid}}\",\n \"locker_id\": \"m0010\",\n \"merchant_name\": \"NewAge Retailer\",\n \"merchant_details\": {\n \"primary_contact_person\": \"John Test\",\n \"primary_email\": \"JohnTest@test.com\",\n \"primary_phone\": \"sunt laborum\",\n \"secondary_contact_person\": \"John Test2\",\n \"secondary_email\": \"JohnTest2@test.com\",\n \"secondary_phone\": \"cillum do dolor id\",\n \"website\": \"www.example.com\",\n \"about_business\": \"Online Retail with a wide selection of organic products for North America\",\n \"address\": {\n \"line1\": \"1467\",\n \"line2\": \"Harrison Street\",\n \"line3\": \"Harrison Street\",\n \"city\": \"San Fransico\",\n \"state\": \"California\",\n \"zip\": \"94122\",\n \"country\": \"US\"\n }\n },\n \"return_url\": \"https://duck.com/success\",\n \"webhook_details\": {\n \"webhook_version\": \"1.0.1\",\n \"webhook_username\": \"ekart_retail\",\n \"webhook_password\": \"password_ekart@123\",\n \"payment_created_enabled\": true,\n \"payment_succeeded_enabled\": true,\n \"payment_failed_enabled\": true\n },\n \"sub_merchants_enabled\": false,\n \"metadata\": {\n \"city\": \"NY\",\n \"unit\": \"245\"\n },\n \"primary_business_details\": [\n {\n \"country\": \"US\",\n \"business\": \"default\"\n }\n ]\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/accounts", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "accounts" - ] - }, - "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." - }, - "response": [] - }, - { - "name": "Merchant Account - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/accounts/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/accounts/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", - "if (jsonData?.api_key) {", - " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", - " console.log(", - " \"- use {{api_key}} as collection variable for value\",", - " jsonData.api_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", - "if (jsonData?.publishable_key) {", - " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", - " console.log(", - " \"- use {{publishable_key}} as collection variable for value\",", - " jsonData.publishable_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/accounts/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "accounts", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{merchant_id}}", - "description": "(Required) The unique identifier for the merchant account" - } - ] - }, - "description": "Retrieve a merchant account details." - }, - "response": [] - }, - { - "name": "Merchant Account - List", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/accounts/list - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/accounts/list - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) { }", - "", - "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", - "if (jsonData?.api_key) {", - " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", - " console.log(", - " \"- use {{api_key}} as collection variable for value\",", - " jsonData.api_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", - "if (jsonData?.publishable_key) {", - " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", - " console.log(", - " \"- use {{publishable_key}} as collection variable for value\",", - " jsonData.publishable_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/accounts/list?organization_id={{organization_id}}", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "accounts", - "list" - ], - "query": [ - { - "key": "organization_id", - "value": "{{organization_id}}" - } - ], - "variable": [ - { - "key": "organization_id", - "value": "{{organization_id}}", - "description": "(Required) - Organization id" - } - ] - }, - "description": "List merchant accounts for an organization" - }, - "response": [] - }, - { - "name": "Merchant Account - Update", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/accounts/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/accounts/:id - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", - "if (jsonData?.api_key) {", - " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", - " console.log(", - " \"- use {{api_key}} as collection variable for value\",", - " jsonData.api_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", - "if (jsonData?.publishable_key) {", - " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", - " console.log(", - " \"- use {{publishable_key}} as collection variable for value\",", - " jsonData.publishable_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"merchant_id\":\"{{merchant_id}}\",\"merchant_name\":\"NewAge Retailer\",\"locker_id\":\"m0010\",\"merchant_details\":{\"primary_contact_person\":\"joseph Test\",\"primary_email\":\"josephTest@test.com\",\"primary_phone\":\"veniam aute officia ullamco esse\",\"secondary_contact_person\":\"joseph Test2\",\"secondary_email\":\"josephTest2@test.com\",\"secondary_phone\":\"proident adipisicing officia nulla\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"parent_merchant_id\":\"xkkdf909012sdjki2dkh5sdf\",\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/accounts/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "accounts", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{merchant_id}}", - "description": "(Required) The unique identifier for the merchant account" - } - ] - }, - "description": "To update an existing merchant account. Helpful in updating merchant details such as email, contact deteails, or other configuration details like webhook, routing algorithm etc" - }, - "response": [] - } - ] - }, - { - "name": "API Key", - "item": [ - { - "name": "Create API Key", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/api_keys/:merchant_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/api_keys/:merchant_id - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", - "if (jsonData?.key_id) {", - " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", - " console.log(", - " \"- use {{api_key_id}} as collection variable for value\",", - " jsonData.key_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", - "if (jsonData?.api_key) {", - " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", - " console.log(", - " \"- use {{api_key}} as collection variable for value\",", - " jsonData.api_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"name\": \"API Key 1\",\n \"description\": null,\n \"expiration\": \"2069-09-23T01:02:03.000Z\"\n}" - }, - "url": { - "raw": "{{baseUrl}}/api_keys/:merchant_id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "api_keys", - ":merchant_id" - ], - "variable": [ - { - "key": "merchant_id", - "value": "{{merchant_id}}" - } - ] - } - }, - "response": [] - }, - { - "name": "Update API Key", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(", - " \"[POST]::/api_keys/:merchant_id/:api_key_id - Status code is 2xx\",", - " function () {", - " pm.response.to.be.success;", - " },", - ");", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/api_keys/:merchant_id/:api_key_id - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", - "if (jsonData?.key_id) {", - " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", - " console.log(", - " \"- use {{api_key_id}} as collection variable for value\",", - " jsonData.key_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", - "if (jsonData?.api_key) {", - " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", - " console.log(", - " \"- use {{api_key}} as collection variable for value\",", - " jsonData.api_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"name\":null,\"description\":\"My very awesome API key\",\"expiration\":null}" - }, - "url": { - "raw": "{{baseUrl}}/api_keys/:merchant_id/:api_key_id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "api_keys", - ":merchant_id", - ":api_key_id" - ], - "variable": [ - { - "key": "merchant_id", - "value": "{{merchant_id}}" - }, - { - "key": "api_key_id", - "value": "{{api_key_id}}" - } - ] - } - }, - "response": [] - }, - { - "name": "Retrieve API Key", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(", - " \"[GET]::/api_keys/:merchant_id/:api_key_id - Status code is 2xx\",", - " function () {", - " pm.response.to.be.success;", - " },", - ");", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[GET]::/api_keys/:merchant_id/:api_key_id - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", - "if (jsonData?.key_id) {", - " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", - " console.log(", - " \"- use {{api_key_id}} as collection variable for value\",", - " jsonData.key_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", - "if (jsonData?.api_key) {", - " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", - " console.log(", - " \"- use {{api_key}} as collection variable for value\",", - " jsonData.api_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/api_keys/:merchant_id/:api_key_id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "api_keys", - ":merchant_id", - ":api_key_id" - ], - "variable": [ - { - "key": "merchant_id", - "value": "{{merchant_id}}" - }, - { - "key": "api_key_id", - "value": "{{api_key_id}}" - } - ] - } - }, - "response": [] - }, - { - "name": "List API Keys", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/api_keys/:merchant_id/list - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[GET]::/api_keys/:merchant_id/list - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", - "if (jsonData?.key_id) {", - " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", - " console.log(", - " \"- use {{api_key_id}} as collection variable for value\",", - " jsonData.key_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", - "if (jsonData?.api_key) {", - " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", - " console.log(", - " \"- use {{api_key}} as collection variable for value\",", - " jsonData.api_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/api_keys/:merchant_id/list", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "api_keys", - ":merchant_id", - "list" - ], - "variable": [ - { - "key": "merchant_id", - "value": "{{merchant_id}}" - } - ] - } - }, - "response": [] - }, - { - "name": "Delete API Key", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(", - " \"[DELETE]::/api_keys/:merchant_id/:api-key - Status code is 2xx\",", - " function () {", - " pm.response.to.be.success;", - " },", - ");", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[DELETE]::/api_keys/:merchant_id/:api-key - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - } - ] - }, - "method": "DELETE", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/api_keys/:merchant_id/:api-key", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "api_keys", - ":merchant_id", - ":api-key" - ], - "variable": [ - { - "key": "merchant_id", - "value": "{{merchant_id}}" - }, - { - "key": "api-key", - "value": "{{api_key_id}}" - } - ] - } - }, - "response": [] - } - ] - }, - { - "name": "PaymentConnectors", - "item": [ - { - "name": "Payment Connector - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(", - " \"[POST]::/accounts/:account_id/connectors - Status code is 2xx\",", - " function () {", - " pm.response.to.be.success;", - " },", - ");", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/accounts/:account_id/connectors - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) { }", - "", - "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", - "if (jsonData?.merchant_connector_id) {", - " pm.collectionVariables.set(", - " \"merchant_connector_id\",", - " jsonData.merchant_connector_id,", - " );", - " console.log(", - " \"- use {{merchant_connector_id}} as collection variable for value\",", - " jsonData.merchant_connector_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", - " );", - "}", - "", - "// Validate if the connector label is the one that is passed in the request", - "pm.test(", - " \"[POST]::/accounts/:account_id/connectors - connector_label is not autogenerated\",", - " function () {", - " pm.expect(jsonData.connector_label).to.eql(\"first_boa_connector\")", - " },", - ");" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"connector_type\": \"fiz_operations\",\n \"connector_name\": \"bankofamerica\",\n \"business_country\": \"US\",\n \"business_label\": \"default\",\n \"connector_label\": \"first_boa_connector\",\n \"connector_account_details\": {\n \"auth_type\": \"SignatureKey\",\n \"api_key\": \"{{connector_api_key}}\",\n \"api_secret\": \"{{connector_api_secret}}\",\n \"key1\": \"{{connector_key1}}\"\n },\n \"test_mode\": false,\n \"disabled\": false,\n \"payment_methods_enabled\": [\n {\n \"payment_method\": \"card\",\n \"payment_method_types\": [\n {\n \"payment_method_type\": \"credit\",\n \"card_networks\": [\n \"Visa\",\n \"Mastercard\"\n ],\n \"minimum_amount\": 1,\n \"maximum_amount\": 68607706,\n \"recurring_enabled\": true,\n \"installment_payment_enabled\": true\n },\n {\n \"payment_method_type\": \"debit\",\n \"card_networks\": [\n \"Visa\",\n \"Mastercard\"\n ],\n \"minimum_amount\": 1,\n \"maximum_amount\": 68607706,\n \"recurring_enabled\": true,\n \"installment_payment_enabled\": true\n }\n ]\n }\n ],\n \"metadata\": {\n \"city\": \"NY\",\n \"unit\": \"245\"\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/account/:account_id/connectors", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "account", - ":account_id", - "connectors" - ], - "variable": [ - { - "key": "account_id", - "value": "{{merchant_id}}", - "description": "(Required) The unique identifier for the merchant account" - } - ] - }, - "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." - }, - "response": [] - }, - { - "name": "Payment Connector - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(", - " \"[GET]::/accounts/:account_id/connectors/:connector_id - Status code is 2xx\",", - " function () {", - " pm.response.to.be.success;", - " },", - ");", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[GET]::/accounts/:account_id/connectors/:connector_id - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", - "if (jsonData?.merchant_connector_id) {", - " pm.collectionVariables.set(", - " \"merchant_connector_id\",", - " jsonData.merchant_connector_id,", - " );", - " console.log(", - " \"- use {{merchant_connector_id}} as collection variable for value\",", - " jsonData.merchant_connector_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/account/:account_id/connectors/:connector_id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "account", - ":account_id", - "connectors", - ":connector_id" - ], - "variable": [ - { - "key": "account_id", - "value": "{{merchant_id}}", - "description": "(Required) The unique identifier for the merchant account" - }, - { - "key": "connector_id", - "value": "{{merchant_connector_id}}", - "description": "(Required) The unique identifier for the payment connector" - } - ] - }, - "description": "Retrieve Payment Connector details." - }, - "response": [] - }, - { - "name": "Payment Connector - Update", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(", - " \"[POST]::/account/:account_id/connectors/:connector_id - Status code is 2xx\",", - " function () {", - " pm.response.to.be.success;", - " },", - ");", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/account/:account_id/connectors/:connector_id - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) { }", - "", - "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", - "if (jsonData?.merchant_connector_id) {", - " pm.collectionVariables.set(", - " \"merchant_connector_id\",", - " jsonData.merchant_connector_id,", - " );", - " console.log(", - " \"- use {{merchant_connector_id}} as collection variable for value\",", - " jsonData.merchant_connector_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", - " );", - "}", - "", - "// Validate if the connector label is the one that is passed in the request", - "pm.test(", - " \"[POST]::/accounts/:account_id/connectors - connector_label is not autogenerated\",", - " function () {", - " pm.expect(jsonData.connector_label).to.eql(\"updated_stripe_connector\")", - " },", - ");" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"connector_type\": \"fiz_operations\",\n \"connector_account_details\": {\n \"auth_type\": \"SignatureKey\",\n \"api_key\": \"{{connector_api_key}}\",\n \"api_secret\": \"{{connector_api_secret}}\",\n \"key1\": \"{{connector_key1}}\"\n },\n \"connector_label\": \"updated_stripe_connector\",\n \"test_mode\": false,\n \"disabled\": false,\n \"payment_methods_enabled\": [\n {\n \"payment_method\": \"card\",\n \"payment_method_types\": [\n {\n \"payment_method_type\": \"credit\",\n \"card_networks\": [\n \"Visa\",\n \"Mastercard\"\n ],\n \"minimum_amount\": 1,\n \"maximum_amount\": 68607706,\n \"recurring_enabled\": true,\n \"installment_payment_enabled\": true\n },\n {\n \"payment_method_type\": \"debit\",\n \"card_networks\": [\n \"Visa\",\n \"Mastercard\"\n ],\n \"minimum_amount\": 1,\n \"maximum_amount\": 68607706,\n \"recurring_enabled\": true,\n \"installment_payment_enabled\": true\n }\n ]\n }\n ],\n \"metadata\": {\n \"city\": \"NY\",\n \"unit\": \"245\"\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/account/:account_id/connectors/:connector_id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "account", - ":account_id", - "connectors", - ":connector_id" - ], - "variable": [ - { - "key": "account_id", - "value": "{{merchant_id}}" - }, - { - "key": "connector_id", - "value": "{{merchant_connector_id}}" - } - ] - }, - "description": "To update an existing Payment Connector. Helpful in enabling / disabling different payment methods and other settings for the connector etc" - }, - "response": [] - }, - { - "name": "List Connectors by MID", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(", - " \"[GET]::/account/:account_id/connectors - Status code is 2xx\",", - " function () {", - " pm.response.to.be.success;", - " },", - ");", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[GET]::/account/:account_id/connectors - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/account/:account_id/connectors", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "account", - ":account_id", - "connectors" - ], - "variable": [ - { - "key": "account_id", - "value": "{{merchant_id}}" - } - ] - } - }, - "response": [] - }, - { - "name": "Payment Connector - Delete", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(", - " \"[DELETE]::/account/:account_id/connectors/:connector_id - Status code is 2xx\",", - " function () {", - " pm.response.to.be.success;", - " },", - ");", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[DELETE]::/account/:account_id/connectors/:connector_id - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", - "if (jsonData?.merchant_connector_id) {", - " pm.collectionVariables.set(", - " \"merchant_connector_id\",", - " jsonData.merchant_connector_id,", - " );", - " console.log(", - " \"- use {{merchant_connector_id}} as collection variable for value\",", - " jsonData.merchant_connector_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "DELETE", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/account/:account_id/connectors/:connector_id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "account", - ":account_id", - "connectors", - ":connector_id" - ], - "variable": [ - { - "key": "account_id", - "value": "{{merchant_id}}" - }, - { - "key": "connector_id", - "value": "{{merchant_connector_id}}" - } - ] - }, - "description": "Delete or Detach a Payment Connector from Merchant Account" - }, - "response": [] - }, - { - "name": "Merchant Account - Delete", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[DELETE]::/accounts/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[DELETE]::/accounts/:id - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Response Validation", - "const schema = {", - " type: \"object\",", - " description: \"Merchant Account\",", - " required: [\"merchant_id\", \"deleted\"],", - " properties: {", - " merchant_id: {", - " type: \"string\",", - " description: \"The identifier for the MerchantAccount object.\",", - " maxLength: 255,", - " example: \"y3oqhf46pyzuxjbcn2giaqnb44\",", - " },", - " deleted: {", - " type: \"boolean\",", - " description:", - " \"Indicates the deletion status of the Merchant Account object.\",", - " example: true,", - " },", - " },", - "};", - "", - "// Validate if response matches JSON schema", - "pm.test(\"[DELETE]::/accounts/:id - Schema is valid\", function () {", - " pm.response.to.have.jsonSchema(schema, {", - " unknownFormats: [\"int32\", \"int64\", \"float\", \"double\"],", - " });", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "DELETE", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/accounts/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "accounts", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{merchant_id}}", - "description": "(Required) The unique identifier for the merchant account" - } - ] - }, - "description": "Delete a Merchant Account" - }, - "response": [] - } - ] - }, - { - "name": "Flow Testcases", - "item": [ - { - "name": "QuickStart", - "item": [ - { - "name": "Merchant Account - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/accounts - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", - "if (jsonData?.merchant_id) {", - " pm.collectionVariables.set(\"merchant_id\", jsonData.merchant_id);", - " console.log(", - " \"- use {{merchant_id}} as collection variable for value\",", - " jsonData.merchant_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", - "if (jsonData?.api_key) {", - " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", - " console.log(", - " \"- use {{api_key}} as collection variable for value\",", - " jsonData.api_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", - "if (jsonData?.publishable_key) {", - " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", - " console.log(", - " \"- use {{publishable_key}} as collection variable for value\",", - " jsonData.publishable_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"merchant_id\": \"postman_merchant_GHAction_{{$guid}}\",\n \"locker_id\": \"m0010\",\n \"merchant_name\": \"NewAge Retailer\",\n \"merchant_details\": {\n \"primary_contact_person\": \"John Test\",\n \"primary_email\": \"JohnTest@test.com\",\n \"primary_phone\": \"sunt laborum\",\n \"secondary_contact_person\": \"John Test2\",\n \"secondary_email\": \"JohnTest2@test.com\",\n \"secondary_phone\": \"cillum do dolor id\",\n \"website\": \"www.example.com\",\n \"about_business\": \"Online Retail with a wide selection of organic products for North America\",\n \"address\": {\n \"line1\": \"1467\",\n \"line2\": \"Harrison Street\",\n \"line3\": \"Harrison Street\",\n \"city\": \"San Fransico\",\n \"state\": \"California\",\n \"zip\": \"94122\",\n \"country\": \"US\"\n }\n },\n \"return_url\": \"https://duck.com/success\",\n \"webhook_details\": {\n \"webhook_version\": \"1.0.1\",\n \"webhook_username\": \"ekart_retail\",\n \"webhook_password\": \"password_ekart@123\",\n \"payment_created_enabled\": true,\n \"payment_succeeded_enabled\": true,\n \"payment_failed_enabled\": true\n },\n \"sub_merchants_enabled\": false,\n \"metadata\": {\n \"city\": \"NY\",\n \"unit\": \"245\"\n },\n \"primary_business_details\": [\n {\n \"country\": \"US\",\n \"business\": \"default\"\n }\n ]\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/accounts", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "accounts" - ] - }, - "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." - }, - "response": [] - }, - { - "name": "API Key - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/api_keys/:merchant_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/api_keys/:merchant_id - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", - "if (jsonData?.key_id) {", - " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", - " console.log(", - " \"- use {{api_key_id}} as collection variable for value\",", - " jsonData.key_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", - "if (jsonData?.api_key) {", - " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", - " console.log(", - " \"- use {{api_key}} as collection variable for value\",", - " jsonData.api_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"name\":\"API Key 1\",\"description\":null,\"expiration\":\"2069-09-23T01:02:03.000Z\"}" - }, - "url": { - "raw": "{{baseUrl}}/api_keys/:merchant_id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "api_keys", - ":merchant_id" - ], - "variable": [ - { - "key": "merchant_id", - "value": "{{merchant_id}}" - } - ] - } - }, - "response": [] - }, - { - "name": "Payment Connector - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(", - " \"[POST]::/account/:account_id/connectors - Status code is 2xx\",", - " function () {", - " pm.response.to.be.success;", - " },", - ");", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/account/:account_id/connectors - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", - "if (jsonData?.merchant_connector_id) {", - " pm.collectionVariables.set(", - " \"merchant_connector_id\",", - " jsonData.merchant_connector_id,", - " );", - " console.log(", - " \"- use {{merchant_connector_id}} as collection variable for value\",", - " jsonData.merchant_connector_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"connector_type\": \"fiz_operations\",\n \"connector_name\": \"bankofamerica\",\n \"business_country\": \"US\",\n \"business_label\": \"default\",\n \"connector_label\": \"first_boa_connector\",\n \"connector_account_details\": {\n \"auth_type\": \"SignatureKey\",\n \"api_key\": \"{{connector_api_key}}\",\n \"api_secret\": \"{{connector_api_secret}}\",\n \"key1\": \"{{connector_key1}}\"\n },\n \"test_mode\": false,\n \"disabled\": false,\n \"payment_methods_enabled\": [\n {\n \"payment_method\": \"card\",\n \"payment_method_types\": [\n {\n \"payment_method_type\": \"credit\",\n \"card_networks\": [\n \"Visa\",\n \"Mastercard\"\n ],\n \"minimum_amount\": 1,\n \"maximum_amount\": 68607706,\n \"recurring_enabled\": true,\n \"installment_payment_enabled\": true\n },\n {\n \"payment_method_type\": \"debit\",\n \"card_networks\": [\n \"Visa\",\n \"Mastercard\"\n ],\n \"minimum_amount\": 1,\n \"maximum_amount\": 68607706,\n \"recurring_enabled\": true,\n \"installment_payment_enabled\": true\n }\n ]\n }\n ],\n \"metadata\": {\n \"city\": \"NY\",\n \"unit\": \"245\"\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/account/:account_id/connectors", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "account", - ":account_id", - "connectors" - ], - "variable": [ - { - "key": "account_id", - "value": "{{merchant_id}}", - "description": "(Required) The unique identifier for the merchant account" - } - ] - }, - "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." - }, - "response": [] - }, - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"amount\": 6540,\n \"currency\": \"USD\",\n \"confirm\": true,\n \"capture_method\": \"automatic\",\n \"capture_on\": \"2022-09-10T10:11:12Z\",\n \"amount_to_capture\": 6540,\n \"customer_id\": \"StripeCustomer\",\n \"email\": \"guest@example.com\",\n \"name\": \"John Doe\",\n \"phone\": \"999999999\",\n \"phone_country_code\": \"+1\",\n \"description\": \"Its my first payment request\",\n \"authentication_type\": \"no_three_ds\",\n \"return_url\": \"https://duck.com\",\n \"payment_method\": \"card\",\n \"payment_method_type\": \"credit\",\n \"payment_method_data\": {\n \"card\": {\n \"card_number\": \"4111111111111111\",\n \"card_exp_month\": \"12\",\n \"card_exp_year\": \"30\",\n \"card_holder_name\": \"joseph Doe\",\n \"card_cvc\": \"123\"\n }\n },\n \"billing\": {\n \"address\": {\n \"line1\": \"1467\",\n \"line2\": \"Harrison Street\",\n \"line3\": \"Harrison Street\",\n \"city\": \"San Fransico\",\n \"state\": \"California\",\n \"zip\": \"94122\",\n \"country\": \"US\",\n \"first_name\": \"joseph\",\n \"last_name\": \"Doe\"\n },\n \"phone\": {\n \"number\": \"8056594427\",\n \"country_code\": \"+91\"\n }\n },\n \"shipping\": {\n \"address\": {\n \"line1\": \"1467\",\n \"line2\": \"Harrison Street\",\n \"line3\": \"Harrison Street\",\n \"city\": \"San Fransico\",\n \"state\": \"California\",\n \"zip\": \"94122\",\n \"country\": \"US\",\n \"first_name\": \"joseph\",\n \"last_name\": \"Doe\"\n },\n \"phone\": {\n \"number\": \"8056594427\",\n \"country_code\": \"+91\"\n }\n },\n \"statement_descriptor_name\": \"joseph\",\n \"statement_descriptor_suffix\": \"JS\",\n \"metadata\": {\n \"udf1\": \"value1\",\n \"new_customer\": \"true\",\n \"login_date\": \"2019-09-10T10:11:12Z\"\n },\n \"routing\": {\n \"type\": \"single\",\n \"data\": \"bankofamerica\"\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Happy Cases", - "item": [ - { - "name": "Scenario1-Create payment with confirm true", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"processing\" for \"status\" because payment gets succeeded after one day.", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", - " },", - " );", - "}", - "", - "// Response body should have \"connector_transaction_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", - " .true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "// pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - "// pm.response.to.be.success;", - "// });", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"processing\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'processing'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", - " },", - " );", - "}", - "", - "// Response body should have \"connector_transaction_id\"", - "// pm.test(", - "// \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", - "// function () {", - "// pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", - "// .true;", - "// },", - "// );", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario2-Create payment with confirm false", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_confirmation\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "", - "// Response body should have value \"processing\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", - " },", - " );", - "}", - "", - "// Response body should have \"connector_transaction_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", - " .true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"client_secret\":\"{{client_secret}}\"}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "// pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - "// pm.response.to.be.success;", - "// });", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"processing\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'processing'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario3-Create payment without PMD", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"amount\": 6540,\n \"currency\": \"USD\",\n \"confirm\": false,\n \"capture_method\": \"automatic\",\n \"capture_on\": \"2022-09-10T10:11:12Z\",\n \"amount_to_capture\": 6540,\n \"customer_id\": \"StripeCustomer\",\n \"email\": \"guest@example.com\",\n \"name\": \"John Doe\",\n \"phone\": \"999999999\",\n \"phone_country_code\": \"+65\",\n \"description\": \"Its my first payment request\",\n \"authentication_type\": \"no_three_ds\",\n \"return_url\": \"https://duck.com\",\n \"billing\": {\n \"address\": {\n \"line1\": \"1467\",\n \"line2\": \"Harrison Street\",\n \"line3\": \"Harrison Street\",\n \"city\": \"San Fransico\",\n \"state\": \"California\",\n \"zip\": \"94122\",\n \"country\": \"US\",\n \"first_name\": \"sundari\",\n \"last_name\": \"abcd\"\n }\n },\n \"shipping\": {\n \"address\": {\n \"line1\": \"1467\",\n \"line2\": \"Harrison Street\",\n \"line3\": \"Harrison Street\",\n \"city\": \"San Fransico\",\n \"state\": \"California\",\n \"zip\": \"94122\",\n \"country\": \"US\",\n \"first_name\": \"sundari\",\n \"last_name\": \"abcd\"\n }\n },\n \"statement_descriptor_name\": \"joseph\",\n \"statement_descriptor_suffix\": \"JS\",\n \"metadata\": {\n \"udf1\": \"value1\",\n \"new_customer\": \"true\",\n \"login_date\": \"2019-09-10T10:11:12Z\"\n },\n \"routing\": {\n \"type\": \"single\",\n \"data\": \"bankofamerica\"\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "// Response body should have value \"processing\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id/confirm - Content check if value for 'status' matches 'processing'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\"}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "// pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - "// pm.response.to.be.success;", - "// });", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"processing\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'processing'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario4-Create payment with Manual capture", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_capture\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"3566111111111113\",\"card_exp_month\":\"12\",\"card_exp_year\":\"30\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"processing\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'processing'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "// pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - "// pm.response.to.be.success;", - "// });", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"processing\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario5-Void the payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_capture\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"3566111111111113\",\"card_exp_month\":\"12\",\"card_exp_year\":\"30\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Cancel", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/cancel - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"cancelled\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'status' matches 'cancelled'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"cancelled\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"cancellation_reason\":\"requested_by_customer\"}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/cancel", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "cancel" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"cancelled\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'cancelled'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"cancelled\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - } - ] - } - ] - } - ], - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"[LOG]::payment_id - \" + jsonData.payment_id);", - "}", - "", - "console.log(\"[LOG]::x-request-id - \" + pm.response.headers.get(\"x-request-id\"));", - "" - ] - } - } - ], - "variable": [ - { - "key": "baseUrl", - "value": "", - "type": "string" - }, - { - "key": "admin_api_key", - "value": "", - "type": "string" - }, - { - "key": "api_key", - "value": "", - "type": "string" - }, - { - "key": "merchant_id", - "value": "" - }, - { - "key": "payment_id", - "value": "" - }, - { - "key": "customer_id", - "value": "" - }, - { - "key": "mandate_id", - "value": "" - }, - { - "key": "payment_method_id", - "value": "" - }, - { - "key": "refund_id", - "value": "" - }, - { - "key": "merchant_connector_id", - "value": "" - }, - { - "key": "client_secret", - "value": "", - "type": "string" - }, - { - "key": "connector_api_key", - "value": "", - "type": "string" - }, - { - "key": "publishable_key", - "value": "", - "type": "string" - }, - { - "key": "api_key_id", - "value": "", - "type": "string" - }, - { - "key": "payment_token", - "value": "" - }, - { - "key": "gateway_merchant_id", - "value": "", - "type": "string" - }, - { - "key": "certificate", - "value": "", - "type": "string" - }, - { - "key": "certificate_keys", - "value": "", - "type": "string" - }, - { - "key": "organization_id", - "value": "" - }, - { - "key": "connector_api_secret", - "value": "", - "type": "string" - }, - { - "key": "connector_key1", - "value": "", - "type": "string" - } - ] -} \ No newline at end of file + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"[LOG]::payment_id - \" + jsonData.payment_id);", + "}", + "", + "console.log(\"[LOG]::x-request-id - \" + pm.response.headers.get(\"x-request-id\"));", + "" + ], + "type": "text/javascript" + } + } + ], + "item": [ + { + "name": "Health check", + "item": [ + { + "name": "New Request", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "x-feature", + "value": "router-custom", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{baseUrl}}/health", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "health" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "MerchantAccounts", + "item": [ + { + "name": "Merchant Account - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/accounts - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) { }", + "", + "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", + "if (jsonData?.merchant_id) {", + " pm.collectionVariables.set(\"merchant_id\", jsonData.merchant_id);", + " console.log(", + " \"- use {{merchant_id}} as collection variable for value\",", + " jsonData.merchant_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", + "if (jsonData?.publishable_key) {", + " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", + " console.log(", + " \"- use {{publishable_key}} as collection variable for value\",", + " jsonData.publishable_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", + "if (jsonData?.merchant_id) {", + " pm.collectionVariables.set(\"organization_id\", jsonData.organization_id);", + " console.log(", + " \"- use {{organization_id}} as collection variable for value\",", + " jsonData.organization_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{organization_id}}, as jsonData.organization_id is undefined.\",", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/accounts - Organization id is generated\",", + " function () {", + " pm.expect(typeof jsonData.organization_id !== \"undefined\").to.be.true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"merchant_id\":\"postman_merchant_GHAction_{{$guid}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"merchant_details\":{\"primary_contact_person\":\"John Test\",\"primary_email\":\"JohnTest@test.com\",\"primary_phone\":\"sunt laborum\",\"secondary_contact_person\":\"John Test2\",\"secondary_email\":\"JohnTest2@test.com\",\"secondary_phone\":\"cillum do dolor id\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com/success\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"},\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"}]}" + }, + "url": { + "raw": "{{baseUrl}}/accounts", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts" + ] + }, + "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." + }, + "response": [] + }, + { + "name": "Merchant Account - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/accounts/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/accounts/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", + "if (jsonData?.publishable_key) {", + " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", + " console.log(", + " \"- use {{publishable_key}} as collection variable for value\",", + " jsonData.publishable_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/accounts/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Retrieve a merchant account details." + }, + "response": [] + }, + { + "name": "Merchant Account - List", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/accounts/list - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/accounts/list - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) { }", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", + "if (jsonData?.publishable_key) {", + " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", + " console.log(", + " \"- use {{publishable_key}} as collection variable for value\",", + " jsonData.publishable_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/accounts/list?organization_id={{organization_id}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts", + "list" + ], + "query": [ + { + "key": "organization_id", + "value": "{{organization_id}}" + } + ], + "variable": [ + { + "key": "organization_id", + "value": "{{organization_id}}", + "description": "(Required) - Organization id" + } + ] + }, + "description": "List merchant accounts for an organization" + }, + "response": [] + }, + { + "name": "Merchant Account - Update", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/accounts/:id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", + "if (jsonData?.publishable_key) {", + " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", + " console.log(", + " \"- use {{publishable_key}} as collection variable for value\",", + " jsonData.publishable_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"merchant_id\":\"{{merchant_id}}\",\"merchant_name\":\"NewAge Retailer\",\"locker_id\":\"m0010\",\"merchant_details\":{\"primary_contact_person\":\"joseph Test\",\"primary_email\":\"josephTest@test.com\",\"primary_phone\":\"veniam aute officia ullamco esse\",\"secondary_contact_person\":\"joseph Test2\",\"secondary_email\":\"josephTest2@test.com\",\"secondary_phone\":\"proident adipisicing officia nulla\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"parent_merchant_id\":\"xkkdf909012sdjki2dkh5sdf\",\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" + }, + "url": { + "raw": "{{baseUrl}}/accounts/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "To update an existing merchant account. Helpful in updating merchant details such as email, contact deteails, or other configuration details like webhook, routing algorithm etc" + }, + "response": [] + } + ] + }, + { + "name": "API Key", + "item": [ + { + "name": "Create API Key", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/api_keys/:merchant_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/api_keys/:merchant_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", + "if (jsonData?.key_id) {", + " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", + " console.log(", + " \"- use {{api_key_id}} as collection variable for value\",", + " jsonData.key_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"name\":\"API Key 1\",\"description\":null,\"expiration\":\"2069-09-23T01:02:03.000Z\"}" + }, + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Update API Key", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[POST]::/api_keys/:merchant_id/:api_key_id - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/api_keys/:merchant_id/:api_key_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", + "if (jsonData?.key_id) {", + " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", + " console.log(", + " \"- use {{api_key_id}} as collection variable for value\",", + " jsonData.key_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"name\":null,\"description\":\"My very awesome API key\",\"expiration\":null}" + }, + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id/:api_key_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id", + ":api_key_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + }, + { + "key": "api_key_id", + "value": "{{api_key_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Retrieve API Key", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[GET]::/api_keys/:merchant_id/:api_key_id - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[GET]::/api_keys/:merchant_id/:api_key_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", + "if (jsonData?.key_id) {", + " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", + " console.log(", + " \"- use {{api_key_id}} as collection variable for value\",", + " jsonData.key_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id/:api_key_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id", + ":api_key_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + }, + { + "key": "api_key_id", + "value": "{{api_key_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "List API Keys", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/api_keys/:merchant_id/list - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[GET]::/api_keys/:merchant_id/list - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", + "if (jsonData?.key_id) {", + " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", + " console.log(", + " \"- use {{api_key_id}} as collection variable for value\",", + " jsonData.key_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id/list", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id", + "list" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Delete API Key", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[DELETE]::/api_keys/:merchant_id/:api-key - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[DELETE]::/api_keys/:merchant_id/:api-key - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id/:api-key", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id", + ":api-key" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + }, + { + "key": "api-key", + "value": "{{api_key_id}}" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "PaymentConnectors", + "item": [ + { + "name": "Payment Connector - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[POST]::/accounts/:account_id/connectors - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/accounts/:account_id/connectors - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) { }", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "", + "// Validate if the connector label is the one that is passed in the request", + "pm.test(", + " \"[POST]::/accounts/:account_id/connectors - connector_label is not autogenerated\",", + " function () {", + " pm.expect(jsonData.connector_label).to.eql(\"first_boa_connector\")", + " },", + ");" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"bankofamerica\",\"business_country\":\"US\",\"business_label\":\"default\",\"connector_label\":\"first_boa_connector\",\"connector_account_details\":{\"auth_type\":\"SignatureKey\",\"api_key\":\"{{connector_api_key}}\",\"api_secret\":\"{{connector_api_secret}}\",\"key1\":\"{{connector_key1}}\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." + }, + "response": [] + }, + { + "name": "Payment Connector - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[GET]::/accounts/:account_id/connectors/:connector_id - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[GET]::/accounts/:account_id/connectors/:connector_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors/:connector_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors", + ":connector_id" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + }, + { + "key": "connector_id", + "value": "{{merchant_connector_id}}", + "description": "(Required) The unique identifier for the payment connector" + } + ] + }, + "description": "Retrieve Payment Connector details." + }, + "response": [] + }, + { + "name": "Payment Connector - Update", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[POST]::/account/:account_id/connectors/:connector_id - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/account/:account_id/connectors/:connector_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) { }", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "", + "// Validate if the connector label is the one that is passed in the request", + "pm.test(", + " \"[POST]::/accounts/:account_id/connectors - connector_label is not autogenerated\",", + " function () {", + " pm.expect(jsonData.connector_label).to.eql(\"updated_stripe_connector\")", + " },", + ");" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_account_details\":{\"auth_type\":\"SignatureKey\",\"api_key\":\"{{connector_api_key}}\",\"api_secret\":\"{{connector_api_secret}}\",\"key1\":\"{{connector_key1}}\"},\"connector_label\":\"updated_stripe_connector\",\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors/:connector_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors", + ":connector_id" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}" + }, + { + "key": "connector_id", + "value": "{{merchant_connector_id}}" + } + ] + }, + "description": "To update an existing Payment Connector. Helpful in enabling / disabling different payment methods and other settings for the connector etc" + }, + "response": [] + }, + { + "name": "List Connectors by MID", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[GET]::/account/:account_id/connectors - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[GET]::/account/:account_id/connectors - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Payment Connector - Delete", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[DELETE]::/account/:account_id/connectors/:connector_id - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[DELETE]::/account/:account_id/connectors/:connector_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors/:connector_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors", + ":connector_id" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}" + }, + { + "key": "connector_id", + "value": "{{merchant_connector_id}}" + } + ] + }, + "description": "Delete or Detach a Payment Connector from Merchant Account" + }, + "response": [] + }, + { + "name": "Merchant Account - Delete", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[DELETE]::/accounts/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[DELETE]::/accounts/:id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Response Validation", + "const schema = {", + " type: \"object\",", + " description: \"Merchant Account\",", + " required: [\"merchant_id\", \"deleted\"],", + " properties: {", + " merchant_id: {", + " type: \"string\",", + " description: \"The identifier for the MerchantAccount object.\",", + " maxLength: 255,", + " example: \"y3oqhf46pyzuxjbcn2giaqnb44\",", + " },", + " deleted: {", + " type: \"boolean\",", + " description:", + " \"Indicates the deletion status of the Merchant Account object.\",", + " example: true,", + " },", + " },", + "};", + "", + "// Validate if response matches JSON schema", + "pm.test(\"[DELETE]::/accounts/:id - Schema is valid\", function () {", + " pm.response.to.have.jsonSchema(schema, {", + " unknownFormats: [\"int32\", \"int64\", \"float\", \"double\"],", + " });", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/accounts/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Delete a Merchant Account" + }, + "response": [] + } + ] + }, + { + "name": "Flow Testcases", + "item": [ + { + "name": "QuickStart", + "item": [ + { + "name": "Merchant Account - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/accounts - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", + "if (jsonData?.merchant_id) {", + " pm.collectionVariables.set(\"merchant_id\", jsonData.merchant_id);", + " console.log(", + " \"- use {{merchant_id}} as collection variable for value\",", + " jsonData.merchant_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", + "if (jsonData?.publishable_key) {", + " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", + " console.log(", + " \"- use {{publishable_key}} as collection variable for value\",", + " jsonData.publishable_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"merchant_id\":\"postman_merchant_GHAction_{{$guid}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"merchant_details\":{\"primary_contact_person\":\"John Test\",\"primary_email\":\"JohnTest@test.com\",\"primary_phone\":\"sunt laborum\",\"secondary_contact_person\":\"John Test2\",\"secondary_email\":\"JohnTest2@test.com\",\"secondary_phone\":\"cillum do dolor id\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com/success\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"},\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"}]}" + }, + "url": { + "raw": "{{baseUrl}}/accounts", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts" + ] + }, + "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." + }, + "response": [] + }, + { + "name": "API Key - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/api_keys/:merchant_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/api_keys/:merchant_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", + "if (jsonData?.key_id) {", + " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", + " console.log(", + " \"- use {{api_key_id}} as collection variable for value\",", + " jsonData.key_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"name\":\"API Key 1\",\"description\":null,\"expiration\":\"2069-09-23T01:02:03.000Z\"}" + }, + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Payment Connector - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[POST]::/account/:account_id/connectors - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/account/:account_id/connectors - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"bankofamerica\",\"business_country\":\"US\",\"business_label\":\"default\",\"connector_label\":\"first_boa_connector\",\"connector_account_details\":{\"auth_type\":\"SignatureKey\",\"api_key\":\"{{connector_api_key}}\",\"api_secret\":\"{{connector_api_secret}}\",\"key1\":\"{{connector_key1}}\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." + }, + "response": [] + }, + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"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://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4111111111111111\",\"card_exp_month\":\"12\",\"card_exp_year\":\"30\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"bankofamerica\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Happy Cases", + "item": [ + { + "name": "Scenario1-Create payment with confirm true", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\" because payment gets succeeded after one day.", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// Response body should have \"connector_transaction_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", + " .true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "// pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "// pm.response.to.be.success;", + "// });", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// Response body should have \"connector_transaction_id\"", + "// pm.test(", + "// \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", + "// function () {", + "// pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", + "// .true;", + "// },", + "// );", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario2-Create payment with confirm false", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_confirmation\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// Response body should have \"connector_transaction_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", + " .true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"client_secret\":\"{{client_secret}}\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "// pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "// pm.response.to.be.success;", + "// });", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario3-Create payment without PMD", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"abcd\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"abcd\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"bankofamerica\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id/confirm - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "// pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "// pm.response.to.be.success;", + "// });", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario4-Create payment with Manual capture", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"3566111111111113\",\"card_exp_month\":\"12\",\"card_exp_year\":\"30\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6000);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "// pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "// pm.response.to.be.success;", + "// });", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario5-Void the payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"3566111111111113\",\"card_exp_month\":\"12\",\"card_exp_year\":\"30\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Cancel", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/cancel - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"cancelled\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'status' matches 'cancelled'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"cancelled\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"cancellation_reason\":\"requested_by_customer\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/cancel", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "cancel" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"cancelled\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'cancelled'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"cancelled\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + } + ] + } + ] + } + ], + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "info": { + "_postman_id": "646f7167-da26-4a24-adb0-4157fd3a1781", + "name": "bankofamerica", + "description": "## Get started\n\nJuspay Router provides a collection of APIs that enable you to process and manage payments. Our APIs accept and return JSON in the HTTP body, and return standard HTTP response codes. \nYou can consume the APIs directly using your favorite HTTP/REST library. \nWe have a testing environment referred to \"sandbox\", which you can setup to test API calls without affecting production data.\n\n### Base URLs\n\nUse the following base URLs when making requests to the APIs:\n\n| Environment | Base URL |\n| --- | --- |\n| Sandbox | [https://sandbox.hyperswitch.io](https://sandbox.hyperswitch.io) |\n| Production | [https://router.juspay.io](https://router.juspay.io) |\n\n# Authentication\n\nWhen you sign up for an account, you are given a secret key (also referred as api-key). You may authenticate all API requests with Juspay server by providing the appropriate key in the request Authorization header. \nNever share your secret api keys. Keep them guarded and secure.\n\nContact Support: \nName: Juspay Support \nEmail: [support@juspay.in](mailto:support@juspay.in)", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "28305597" + }, + "variable": [ + { + "key": "baseUrl", + "value": "", + "type": "string" + }, + { + "key": "admin_api_key", + "value": "", + "type": "string" + }, + { + "key": "api_key", + "value": "", + "type": "string" + }, + { + "key": "merchant_id", + "value": "" + }, + { + "key": "payment_id", + "value": "" + }, + { + "key": "customer_id", + "value": "" + }, + { + "key": "mandate_id", + "value": "" + }, + { + "key": "payment_method_id", + "value": "" + }, + { + "key": "refund_id", + "value": "" + }, + { + "key": "merchant_connector_id", + "value": "" + }, + { + "key": "client_secret", + "value": "", + "type": "string" + }, + { + "key": "connector_api_key", + "value": "", + "type": "string" + }, + { + "key": "publishable_key", + "value": "", + "type": "string" + }, + { + "key": "api_key_id", + "value": "", + "type": "string" + }, + { + "key": "payment_token", + "value": "" + }, + { + "key": "gateway_merchant_id", + "value": "", + "type": "string" + }, + { + "key": "certificate", + "value": "", + "type": "string" + }, + { + "key": "certificate_keys", + "value": "", + "type": "string" + }, + { + "key": "organization_id", + "value": "" + }, + { + "key": "connector_api_secret", + "value": "", + "type": "string" + }, + { + "key": "connector_key1", + "value": "", + "type": "string" + } + ] +} From 6ebf14aabcf8d3bc15ad17e877c696af18b2c002 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 16 Nov 2023 15:36:20 +0000 Subject: [PATCH 024/146] chore(version): v1.81.0 --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67bb169aebd2..427fa7403e4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,23 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.81.0 (2023-11-16) + +### Features + +- **connector:** + - [BANKOFAMERICA] Implement Cards for Bank of America ([#2765](https://github.com/juspay/hyperswitch/pull/2765)) ([`e8de3a7`](https://github.com/juspay/hyperswitch/commit/e8de3a710710b92f5c2351c5d67c22352c2b0a30)) + - [ProphetPay] Implement Card Redirect PaymentMethodType and flows for Authorize, CompleteAuthorize, Psync, Refund, Rsync and Void ([#2641](https://github.com/juspay/hyperswitch/pull/2641)) ([`8d4adc5`](https://github.com/juspay/hyperswitch/commit/8d4adc52af57ed0994e6efbb5b2d0d3df3fb3150)) + +### Testing + +- **postman:** Update postman collection files ([`f829197`](https://github.com/juspay/hyperswitch/commit/f8291973c38bde874c45ca15ff8d48c1f2de9781)) + +**Full Changelog:** [`v1.80.0...v1.81.0`](https://github.com/juspay/hyperswitch/compare/v1.80.0...v1.81.0) + +- - - + + ## 1.80.0 (2023-11-16) ### Features From f735fb0551812fd781a2db8bac5a0deef4cabb2b Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Thu, 16 Nov 2023 22:54:14 +0530 Subject: [PATCH 025/146] feat(router): add fallback while add card and retrieve card from rust locker (#2888) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .typos.toml | 1 + config/config.example.toml | 199 +++++++++--------- crates/router/src/core/locker_migration.rs | 2 +- .../router/src/core/payment_methods/cards.rs | 119 ++++++++--- .../src/core/payment_methods/transformers.rs | 12 +- .../router/src/core/payments/tokenization.rs | 4 +- crates/router/src/core/payouts/helpers.rs | 2 +- 7 files changed, 208 insertions(+), 131 deletions(-) diff --git a/.typos.toml b/.typos.toml index 1ac38a005c9e..4ce21526604b 100644 --- a/.typos.toml +++ b/.typos.toml @@ -24,6 +24,7 @@ optin = "optin" # Boku preflow name optin_id = "optin_id" # Boku's id for optin flow deriver = "deriver" Deriver = "Deriver" +requestor_card_reference = "requestor_card_reference" [default.extend-words] aci = "aci" # Name of a connector diff --git a/config/config.example.toml b/config/config.example.toml index 02eff1d42979..7815f2400d04 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -21,25 +21,25 @@ idle_pool_connection_timeout = 90 # Timeout for idle pool connections (defaults # Main SQL data store credentials [master_database] -username = "db_user" # DB Username -password = "db_pass" # DB Password. Use base-64 encoded kms encrypted value here when kms is enabled -host = "localhost" # DB Host -port = 5432 # DB Port -dbname = "hyperswitch_db" # Name of Database -pool_size = 5 # Number of connections to keep open -connection_timeout = 10 # Timeout for database connection in seconds -queue_strategy = "Fifo" # Add the queue strategy used by the database bb8 client +username = "db_user" # DB Username +password = "db_pass" # DB Password. Use base-64 encoded kms encrypted value here when kms is enabled +host = "localhost" # DB Host +port = 5432 # DB Port +dbname = "hyperswitch_db" # Name of Database +pool_size = 5 # Number of connections to keep open +connection_timeout = 10 # Timeout for database connection in seconds +queue_strategy = "Fifo" # Add the queue strategy used by the database bb8 client # Replica SQL data store credentials [replica_database] -username = "replica_user" # DB Username -password = "db_pass" # DB Password. Use base-64 encoded kms encrypted value here when kms is enabled -host = "localhost" # DB Host -port = 5432 # DB Port -dbname = "hyperswitch_db" # Name of Database -pool_size = 5 # Number of connections to keep open -connection_timeout = 10 # Timeout for database connection in seconds -queue_strategy = "Fifo" # Add the queue strategy used by the database bb8 client +username = "replica_user" # DB Username +password = "db_pass" # DB Password. Use base-64 encoded kms encrypted value here when kms is enabled +host = "localhost" # DB Host +port = 5432 # DB Port +dbname = "hyperswitch_db" # Name of Database +pool_size = 5 # Number of connections to keep open +connection_timeout = 10 # Timeout for database connection in seconds +queue_strategy = "Fifo" # Add the queue strategy used by the database bb8 client # Redis credentials [redis] @@ -95,17 +95,17 @@ sampling_rate = 0.1 # decimal rate between 0.0 otel_exporter_otlp_endpoint = "http://localhost:4317" # endpoint to send metrics and traces to, can include port number otel_exporter_otlp_timeout = 5000 # timeout (in milliseconds) for sending metrics and traces use_xray_generator = false # Set this to true for AWS X-ray compatible traces -route_to_trace = [ "*/confirm" ] +route_to_trace = ["*/confirm"] # This section provides some secret values. [secrets] -master_enc_key = "sample_key" # Master Encryption key used to encrypt merchant wise encryption key. Should be 32-byte long. -admin_api_key = "test_admin" # admin API key for admin authentication. Only applicable when KMS is disabled. -kms_encrypted_admin_api_key = "" # Base64-encoded (KMS encrypted) ciphertext of the admin_api_key. Only applicable when KMS is enabled. -jwt_secret = "secret" # JWT secret used for user authentication. Only applicable when KMS is disabled. -kms_encrypted_jwt_secret = "" # Base64-encoded (KMS encrypted) ciphertext of the jwt_secret. Only applicable when KMS is enabled. -recon_admin_api_key = "recon_test_admin" # recon_admin API key for recon authentication. Only applicable when KMS is disabled. -kms_encrypted_recon_admin_api_key = "" # Base64-encoded (KMS encrypted) ciphertext of the recon_admin_api_key. Only applicable when KMS is enabled +master_enc_key = "sample_key" # Master Encryption key used to encrypt merchant wise encryption key. Should be 32-byte long. +admin_api_key = "test_admin" # admin API key for admin authentication. Only applicable when KMS is disabled. +kms_encrypted_admin_api_key = "" # Base64-encoded (KMS encrypted) ciphertext of the admin_api_key. Only applicable when KMS is enabled. +jwt_secret = "secret" # JWT secret used for user authentication. Only applicable when KMS is disabled. +kms_encrypted_jwt_secret = "" # Base64-encoded (KMS encrypted) ciphertext of the jwt_secret. Only applicable when KMS is enabled. +recon_admin_api_key = "recon_test_admin" # recon_admin API key for recon authentication. Only applicable when KMS is disabled. +kms_encrypted_recon_admin_api_key = "" # Base64-encoded (KMS encrypted) ciphertext of the recon_admin_api_key. Only applicable when KMS is enabled # Locker settings contain details for accessing a card locker, a # PCI Compliant storage entity which stores payment method information @@ -124,15 +124,15 @@ connectors_with_delayed_session_response = "trustpay,payme" # List of connectors connectors_with_webhook_source_verification_call = "paypal" # List of connectors which has additional source verification api-call [jwekey] # 4 priv/pub key pair -locker_key_identifier1 = "" # key identifier for key rotation , should be same as basilisk -locker_key_identifier2 = "" # key identifier for key rotation , should be same as basilisk -locker_encryption_key1 = "" # public key 1 in pem format, corresponding private key in basilisk -locker_encryption_key2 = "" # public key 2 in pem format, corresponding private key in basilisk -locker_decryption_key1 = "" # private key 1 in pem format, corresponding public key in basilisk -locker_decryption_key2 = "" # private key 2 in pem format, corresponding public key in basilisk -vault_encryption_key = "" # public key in pem format, corresponding private key in basilisk-hs +locker_key_identifier1 = "" # key identifier for key rotation , should be same as basilisk +locker_key_identifier2 = "" # key identifier for key rotation , should be same as basilisk +locker_encryption_key1 = "" # public key 1 in pem format, corresponding private key in basilisk +locker_encryption_key2 = "" # public key 2 in pem format, corresponding private key in basilisk +locker_decryption_key1 = "" # private key 1 in pem format, corresponding public key in basilisk +locker_decryption_key2 = "" # private key 2 in pem format, corresponding public key in basilisk +vault_encryption_key = "" # public key in pem format, corresponding private key in basilisk-hs rust_locker_encryption_key = "" # public key in pem format, corresponding private key in rust locker -vault_private_key = "" # private key in pem format, corresponding public key in basilisk-hs +vault_private_key = "" # private key in pem format, corresponding public key in basilisk-hs # Refund configuration @@ -234,11 +234,11 @@ adyen = { banks = "e_platby_vub,postova_banka,sporo_pay,tatra_pay,viamo" } # Bank redirect configs for allowed banks through online_banking_poland payment method [bank_config.online_banking_poland] -adyen = { banks = "blik_psp,place_zipko,m_bank,pay_with_ing,santander_przelew24,bank_pekaosa,bank_millennium,pay_with_alior_bank,banki_spoldzielcze,pay_with_inteligo,bnp_paribas_poland,bank_nowy_sa,credit_agricole,pay_with_bos,pay_with_citi_handlowy,pay_with_plus_bank,toyota_bank,velo_bank,e_transfer_pocztowy24"} +adyen = { banks = "blik_psp,place_zipko,m_bank,pay_with_ing,santander_przelew24,bank_pekaosa,bank_millennium,pay_with_alior_bank,banki_spoldzielcze,pay_with_inteligo,bnp_paribas_poland,bank_nowy_sa,credit_agricole,pay_with_bos,pay_with_citi_handlowy,pay_with_plus_bank,toyota_bank,velo_bank,e_transfer_pocztowy24" } # Bank redirect configs for allowed banks through open_banking_uk payment method [bank_config.open_banking_uk] -adyen = { banks = "aib,bank_of_scotland,danske_bank,first_direct,first_trust,halifax,lloyds,monzo,nat_west,nationwide_bank,royal_bank_of_scotland,starling,tsb_bank,tesco_bank,ulster_bank,barclays,hsbc_bank,revolut,santander_przelew24,open_bank_success,open_bank_failure,open_bank_cancelled"} +adyen = { banks = "aib,bank_of_scotland,danske_bank,first_direct,first_trust,halifax,lloyds,monzo,nat_west,nationwide_bank,royal_bank_of_scotland,starling,tsb_bank,tesco_bank,ulster_bank,barclays,hsbc_bank,revolut,santander_przelew24,open_bank_success,open_bank_failure,open_bank_cancelled" } # Bank redirect configs for allowed banks through przelewy24 payment method [bank_config.przelewy24] @@ -313,89 +313,92 @@ region = "" # The AWS region used by the KMS SDK for decrypting data. # EmailClient configuration. Only applicable when the `email` feature flag is enabled. [email] from_email = "notify@example.com" # Sender email -aws_region = "" # AWS region used by AWS SES -base_url = "" # Base url used when adding links that should redirect to self +aws_region = "" # AWS region used by AWS SES +base_url = "" # Base url used when adding links that should redirect to self #tokenization configuration which describe token lifetime and payment method for specific connector [tokenization] stripe = { long_lived_token = false, payment_method = "wallet", payment_method_type = { type = "disable_only", list = "google_pay" } } checkout = { long_lived_token = false, payment_method = "wallet" } -mollie = {long_lived_token = false, payment_method = "card"} +mollie = { long_lived_token = false, payment_method = "card" } stax = { long_lived_token = true, payment_method = "card,bank_debit" } -square = {long_lived_token = false, payment_method = "card"} +square = { long_lived_token = false, payment_method = "card" } braintree = { long_lived_token = false, payment_method = "card" } -gocardless = {long_lived_token = true, payment_method = "bank_debit"} +gocardless = { long_lived_token = true, payment_method = "bank_debit" } [temp_locker_enable_config] -stripe = {payment_method = "bank_transfer"} -nuvei = {payment_method = "card"} -shift4 = {payment_method = "card"} -bluesnap = {payment_method = "card"} +stripe = { payment_method = "bank_transfer" } +nuvei = { payment_method = "card" } +shift4 = { payment_method = "card" } +bluesnap = { payment_method = "card" } [dummy_connector] -enabled = true # Whether dummy connector is enabled or not -payment_ttl = 172800 # Time to live for dummy connector payment in redis -payment_duration = 1000 # Fake delay duration for dummy connector payment -payment_tolerance = 100 # Fake delay tolerance for dummy connector payment -payment_retrieve_duration = 500 # Fake delay duration for dummy connector payment sync -payment_retrieve_tolerance = 100 # Fake delay tolerance for dummy connector payment sync -payment_complete_duration = 500 # Fake delay duration for dummy connector payment complete -payment_complete_tolerance = 100 # Fake delay tolerance for dummy connector payment complete -refund_ttl = 172800 # Time to live for dummy connector refund in redis -refund_duration = 1000 # Fake delay duration for dummy connector refund -refund_tolerance = 100 # Fake delay tolerance for dummy connector refund -refund_retrieve_duration = 500 # Fake delay duration for dummy connector refund sync -refund_retrieve_tolerance = 100 # Fake delay tolerance for dummy connector refund sync -authorize_ttl = 36000 # Time to live for dummy connector authorize request in redis +enabled = true # Whether dummy connector is enabled or not +payment_ttl = 172800 # Time to live for dummy connector payment in redis +payment_duration = 1000 # Fake delay duration for dummy connector payment +payment_tolerance = 100 # Fake delay tolerance for dummy connector payment +payment_retrieve_duration = 500 # Fake delay duration for dummy connector payment sync +payment_retrieve_tolerance = 100 # Fake delay tolerance for dummy connector payment sync +payment_complete_duration = 500 # Fake delay duration for dummy connector payment complete +payment_complete_tolerance = 100 # Fake delay tolerance for dummy connector payment complete +refund_ttl = 172800 # Time to live for dummy connector refund in redis +refund_duration = 1000 # Fake delay duration for dummy connector refund +refund_tolerance = 100 # Fake delay tolerance for dummy connector refund +refund_retrieve_duration = 500 # Fake delay duration for dummy connector refund sync +refund_retrieve_tolerance = 100 # Fake delay tolerance for dummy connector refund sync +authorize_ttl = 36000 # Time to live for dummy connector authorize request in redis assets_base_url = "https://www.example.com/" # Base url for dummy connector assets default_return_url = "https://www.example.com/" # Default return url when no return url is passed while payment slack_invite_url = "https://www.example.com/" # Slack invite url for hyperswitch discord_invite_url = "https://www.example.com/" # Discord invite url for hyperswitch [mandates.supported_payment_methods] -card.credit = {connector_list = "stripe,adyen"} # Mandate supported payment method type and connector for card -wallet.paypal = {connector_list = "adyen"} # Mandate supported payment method type and connector for wallets -pay_later.klarna = {connector_list = "adyen"} # Mandate supported payment method type and connector for pay_later -bank_debit.ach = { connector_list = "gocardless"} # Mandate supported payment method type and connector for bank_debit -bank_debit.becs = { connector_list = "gocardless"} # Mandate supported payment method type and connector for bank_debit -bank_debit.sepa = { connector_list = "gocardless"} # Mandate supported payment method type and connector for bank_debit +card.credit = { connector_list = "stripe,adyen" } # Mandate supported payment method type and connector for card +wallet.paypal = { connector_list = "adyen" } # Mandate supported payment method type and connector for wallets +pay_later.klarna = { connector_list = "adyen" } # Mandate supported payment method type and connector for pay_later +bank_debit.ach = { connector_list = "gocardless" } # Mandate supported payment method type and connector for bank_debit +bank_debit.becs = { connector_list = "gocardless" } # Mandate supported payment method type and connector for bank_debit +bank_debit.sepa = { connector_list = "gocardless" } # Mandate supported payment method type and connector for bank_debit # Required fields info used while listing the payment_method_data [required_fields.pay_later] # payment_method = "pay_later" -afterpay_clearpay = {fields = {stripe = [ # payment_method_type = afterpay_clearpay, connector = "stripe" - # Required fields vector with its respective display name in front-end and field_type - { required_field = "shipping.address.first_name", display_name = "first_name", field_type = "text" }, - { required_field = "shipping.address.last_name", display_name = "last_name", field_type = "text" }, - { required_field = "shipping.address.country", display_name = "country", field_type = { drop_down = { options = [ "US", "IN" ] } } }, - ] } } +afterpay_clearpay = { fields = { stripe = [ # payment_method_type = afterpay_clearpay, connector = "stripe" + # Required fields vector with its respective display name in front-end and field_type + { required_field = "shipping.address.first_name", display_name = "first_name", field_type = "text" }, + { required_field = "shipping.address.last_name", display_name = "last_name", field_type = "text" }, + { required_field = "shipping.address.country", display_name = "country", field_type = { drop_down = { options = [ + "US", + "IN", + ] } } }, +] } } [payouts] -payout_eligibility = true # Defaults the eligibility of a payout method to true in case connector does not provide checks for payout eligibility +payout_eligibility = true # Defaults the eligibility of a payout method to true in case connector does not provide checks for payout eligibility [pm_filters.adyen] -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"} -permata_bank_transfer = {country = "ID", currency = "IDR"} -bca_bank_transfer = {country = "ID", currency = "IDR"} -bni_va = {country = "ID", currency = "IDR"} -bri_va = {country = "ID", currency = "IDR"} -cimb_va = {country = "ID", currency = "IDR"} -danamon_va = {country = "ID", currency = "IDR"} -mandiri_va = {country = "ID", currency = "IDR"} -alfamart = {country = "ID", currency = "IDR"} -indomaret = {country = "ID", currency = "IDR"} -open_banking_uk = {country = "GB", currency = "GBP"} -oxxo = {country = "MX", currency = "MXN"} -pay_safe_card = {country = "AT,AU,BE,BR,BE,CA,HR,CY,CZ,DK,FI,FR,GE,DE,GI,HU,IS,IE,KW,LV,IE,LI,LT,LU,MT,MX,MD,ME,NL,NZ,NO,PY,PE,PL,PT,RO,SA,RS,SK,SI,ES,SE,CH,TR,UAE,UK,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,ISK,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU"} -seven_eleven = {country = "JP", currency = "JPY"} -lawson = {country = "JP", currency = "JPY"} -mini_stop = {country = "JP", currency = "JPY"} -family_mart = {country = "JP", currency = "JPY"} -seicomart = {country = "JP", currency = "JPY"} -pay_easy = {country = "JP", currency = "JPY"} +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" } +permata_bank_transfer = { country = "ID", currency = "IDR" } +bca_bank_transfer = { country = "ID", currency = "IDR" } +bni_va = { country = "ID", currency = "IDR" } +bri_va = { country = "ID", currency = "IDR" } +cimb_va = { country = "ID", currency = "IDR" } +danamon_va = { country = "ID", currency = "IDR" } +mandiri_va = { country = "ID", currency = "IDR" } +alfamart = { country = "ID", currency = "IDR" } +indomaret = { country = "ID", currency = "IDR" } +open_banking_uk = { country = "GB", currency = "GBP" } +oxxo = { country = "MX", currency = "MXN" } +pay_safe_card = { country = "AT,AU,BE,BR,BE,CA,HR,CY,CZ,DK,FI,FR,GE,DE,GI,HU,IS,IE,KW,LV,IE,LI,LT,LU,MT,MX,MD,ME,NL,NZ,NO,PY,PE,PL,PT,RO,SA,RS,SK,SI,ES,SE,CH,TR,UAE,UK,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,ISK,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU" } +seven_eleven = { country = "JP", currency = "JPY" } +lawson = { country = "JP", currency = "JPY" } +mini_stop = { country = "JP", currency = "JPY" } +family_mart = { country = "JP", currency = "JPY" } +seicomart = { country = "JP", currency = "JPY" } +pay_easy = { country = "JP", currency = "JPY" } [pm_filters.zen] credit = { not_available_flows = { capture_method = "manual" } } @@ -415,7 +418,7 @@ debit = { currency = "USD" } ach = { currency = "USD" } [pm_filters.stripe] -cashapp = {country = "US", currency = "USD"} +cashapp = { country = "US", currency = "USD" } [pm_filters.prophetpay] card_redirect = { currency = "USD" } @@ -434,10 +437,10 @@ adyen.banks = "bangkok_bank,krungsri_bank,krung_thai_bank,the_siam_commercial_ba supported_connectors = "braintree" [applepay_decrypt_keys] -apple_pay_ppc = "APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE" #Payment Processing Certificate provided by Apple Pay (https://developer.apple.com/) Certificates, Identifiers & Profiles > Apple Pay Payment Processing Certificate -apple_pay_ppc_key = "APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE_KEY" #Private key generate by Elliptic-curve prime256v1 curve -apple_pay_merchant_cert = "APPLE_PAY_MERCHNAT_CERTIFICATE" #Merchant Certificate provided by Apple Pay (https://developer.apple.com/) Certificates, Identifiers & Profiles > Apple Pay Merchant Identity Certificate -apple_pay_merchant_cert_key = "APPLE_PAY_MERCHNAT_CERTIFICATE_KEY" #Private key generate by RSA:2048 algorithm +apple_pay_ppc = "APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE" #Payment Processing Certificate provided by Apple Pay (https://developer.apple.com/) Certificates, Identifiers & Profiles > Apple Pay Payment Processing Certificate +apple_pay_ppc_key = "APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE_KEY" #Private key generate by Elliptic-curve prime256v1 curve +apple_pay_merchant_cert = "APPLE_PAY_MERCHNAT_CERTIFICATE" #Merchant Certificate provided by Apple Pay (https://developer.apple.com/) Certificates, Identifiers & Profiles > Apple Pay Merchant Identity Certificate +apple_pay_merchant_cert_key = "APPLE_PAY_MERCHNAT_CERTIFICATE_KEY" #Private key generate by RSA:2048 algorithm [payment_link] sdk_url = "http://localhost:9090/dist/HyperLoader.js" diff --git a/crates/router/src/core/locker_migration.rs b/crates/router/src/core/locker_migration.rs index aa82b4a3a636..f036a03a2f0e 100644 --- a/crates/router/src/core/locker_migration.rs +++ b/crates/router/src/core/locker_migration.rs @@ -106,7 +106,7 @@ pub async fn call_to_locker( let (_add_card_rs_resp, _is_duplicate) = cards::add_card_hs( state, pm_create, - card_details, + &card_details, customer_id.to_string(), merchant_account, api_enums::LockerChoice::Tartarus, diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index f9c666cbb954..4ab7d334f883 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -103,16 +103,12 @@ pub async fn add_payment_method( let merchant_id = &merchant_account.merchant_id; let customer_id = req.customer_id.clone().get_required_value("customer_id")?; let response = match req.card.clone() { - Some(card) => add_card_to_locker( - &state, - req.clone(), - card, - customer_id.clone(), - merchant_account, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Add Card Failed"), + Some(card) => { + add_card_to_locker(&state, req.clone(), &card, &customer_id, merchant_account) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Add Card Failed") + } None => { let pm_id = generate_id(consts::ID_LENGTH, "pm"); let payment_method_response = api::PaymentMethodResponse { @@ -207,18 +203,18 @@ pub async fn update_customer_payment_method( pub async fn add_card_to_locker( state: &routes::AppState, req: api::PaymentMethodCreate, - card: api::CardDetail, - customer_id: String, + card: &api::CardDetail, + customer_id: &String, merchant_account: &domain::MerchantAccount, ) -> errors::CustomResult<(api::PaymentMethodResponse, bool), errors::VaultError> { metrics::STORED_TO_LOCKER.add(&metrics::CONTEXT, 1, &[]); - request::record_operation_time( + let add_card_to_hs_resp = request::record_operation_time( async { add_card_hs( state, - req, + req.clone(), card, - customer_id, + customer_id.to_string(), merchant_account, api_enums::LockerChoice::Basilisk, None, @@ -232,7 +228,34 @@ pub async fn add_card_to_locker( &metrics::CARD_ADD_TIME, &[], ) - .await + .await?; + logger::debug!("card added to basilisk locker"); + + let add_card_to_rs_resp = request::record_operation_time( + async { + add_card_hs( + state, + req, + card, + customer_id.to_string(), + merchant_account, + api_enums::LockerChoice::Tartarus, + Some(&add_card_to_hs_resp.0.payment_method_id), + ) + .await + .map_err(|error| { + metrics::CARD_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); + error + }) + }, + &metrics::CARD_ADD_TIME, + &[], + ) + .await?; + + logger::debug!("card added to rust locker"); + + Ok(add_card_to_rs_resp) } pub async fn get_card_from_locker( @@ -243,9 +266,38 @@ pub async fn get_card_from_locker( ) -> errors::RouterResult { metrics::GET_FROM_LOCKER.add(&metrics::CONTEXT, 1, &[]); - request::record_operation_time( + let get_card_from_rs_locker_resp = request::record_operation_time( async { - get_card_from_hs_locker(state, customer_id, merchant_id, card_reference) + get_card_from_hs_locker( + state, + customer_id, + merchant_id, + card_reference, + api_enums::LockerChoice::Tartarus, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while getting card from basilisk_hs") + .map_err(|error| { + metrics::CARD_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); + error + }) + }, + &metrics::CARD_GET_TIME, + &[], + ) + .await; + + match get_card_from_rs_locker_resp { + Err(_) => request::record_operation_time( + async { + get_card_from_hs_locker( + state, + customer_id, + merchant_id, + card_reference, + api_enums::LockerChoice::Basilisk, + ) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed while getting card from basilisk_hs") @@ -253,11 +305,20 @@ pub async fn get_card_from_locker( metrics::CARD_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); error }) - }, - &metrics::CARD_GET_TIME, - &[], - ) - .await + }, + &metrics::CARD_GET_TIME, + &[], + ) + .await + .map(|inner_card| { + logger::debug!("card retrieved from basilisk locker"); + inner_card + }), + Ok(_) => { + logger::debug!("card retrieved from rust locker"); + get_card_from_rs_locker_resp + } + } } pub async fn delete_card_from_locker( @@ -287,7 +348,7 @@ pub async fn delete_card_from_locker( pub async fn add_card_hs( state: &routes::AppState, req: api::PaymentMethodCreate, - card: api::CardDetail, + card: &api::CardDetail, customer_id: String, merchant_account: &domain::MerchantAccount, locker_choice: api_enums::LockerChoice, @@ -296,7 +357,7 @@ pub async fn add_card_hs( let payload = payment_methods::StoreLockerReq::LockerCard(payment_methods::StoreCardReq { merchant_id: &merchant_account.merchant_id, merchant_customer_id: customer_id.to_owned(), - card_reference: card_reference.map(str::to_string), + requestor_card_reference: card_reference.map(str::to_string), card: payment_methods::Card { card_number: card.card_number.to_owned(), name_on_card: card.card_holder_name.to_owned(), @@ -307,11 +368,12 @@ pub async fn add_card_hs( nick_name: card.nick_name.as_ref().map(masking::Secret::peek).cloned(), }, }); + let store_card_payload = call_to_locker_hs(state, &payload, &customer_id, locker_choice).await?; let payment_method_resp = payment_methods::mk_add_card_response_hs( - card, + card.clone(), store_card_payload.card_reference, req, &merchant_account.merchant_id, @@ -351,6 +413,7 @@ pub async fn get_payment_method_from_hs_locker<'a>( customer_id: &str, merchant_id: &str, payment_method_reference: &'a str, + locker_choice: Option, ) -> errors::CustomResult, errors::VaultError> { let locker = &state.conf.locker; #[cfg(not(feature = "kms"))] @@ -365,6 +428,7 @@ pub async fn get_payment_method_from_hs_locker<'a>( customer_id, merchant_id, payment_method_reference, + locker_choice, ) .await .change_context(errors::VaultError::FetchPaymentMethodFailed) @@ -466,6 +530,7 @@ pub async fn get_card_from_hs_locker<'a>( customer_id: &str, merchant_id: &str, card_reference: &'a str, + locker_choice: api_enums::LockerChoice, ) -> errors::CustomResult { let locker = &state.conf.locker; #[cfg(not(feature = "kms"))] @@ -480,6 +545,7 @@ pub async fn get_card_from_hs_locker<'a>( customer_id, merchant_id, card_reference, + Some(locker_choice), ) .await .change_context(errors::VaultError::FetchCardFailed) @@ -2193,6 +2259,7 @@ pub async fn get_lookup_key_for_payout_method( &pm.customer_id, &pm.merchant_id, &pm.payment_method_id, + None, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index 63a0479375e8..45182411c28c 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -28,7 +28,7 @@ pub struct StoreCardReq<'a> { pub merchant_id: &'a str, pub merchant_customer_id: String, #[serde(skip_serializing_if = "Option::is_none")] - pub card_reference: Option, + pub requestor_card_reference: Option, pub card: Card, } @@ -428,6 +428,7 @@ pub async fn mk_get_card_request_hs( customer_id: &str, merchant_id: &str, card_reference: &str, + locker_choice: Option, ) -> CustomResult { let merchant_customer_id = customer_id.to_owned(); let card_req_body = CardReqBody { @@ -448,11 +449,16 @@ pub async fn mk_get_card_request_hs( .await .change_context(errors::VaultError::RequestEncodingFailed)?; - let jwe_payload = mk_basilisk_req(jwekey, &jws, api_enums::LockerChoice::Basilisk).await?; + let target_locker = locker_choice.unwrap_or(api_enums::LockerChoice::Basilisk); + + let jwe_payload = mk_basilisk_req(jwekey, &jws, target_locker).await?; let body = utils::Encode::::encode_to_value(&jwe_payload) .change_context(errors::VaultError::RequestEncodingFailed)?; - let mut url = locker.host.to_owned(); + let mut url = match target_locker { + api_enums::LockerChoice::Basilisk => locker.host.to_owned(), + api_enums::LockerChoice::Tartarus => locker.host_rs.to_owned(), + }; url.push_str("/cards/retrieve"); let mut request = services::Request::new(services::Method::Post, &url); request.add_header(headers::CONTENT_TYPE, "application/json".into()); diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index 794180e2112e..551d1c8abb9a 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -183,8 +183,8 @@ pub async fn save_in_locker( Some(card) => payment_methods::cards::add_card_to_locker( state, payment_method_request, - card, - customer_id, + &card, + &customer_id, merchant_account, ) .await diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index c1e00b9b8000..9ddc8395738e 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -152,7 +152,7 @@ pub async fn save_payout_data_to_locker( card_isin: None, nick_name: None, }, - card_reference: None, + requestor_card_reference: None, }); ( payload, From cb88be01f22725948648976c2a5606a03b5ce92a Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Fri, 17 Nov 2023 10:04:34 +0530 Subject: [PATCH 026/146] fix(core): introduce new attempt and intent status to handle multiple partial captures (#2802) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com> --- crates/common_enums/src/enums.rs | 7 +- .../stripe/payment_intents/types.rs | 6 +- .../stripe/setup_intents/types.rs | 6 +- crates/router/src/connector/utils.rs | 48 +++++++++- crates/router/src/core/payments.rs | 6 +- crates/router/src/core/payments/helpers.rs | 6 +- .../payments/operations/payment_response.rs | 23 ++--- crates/router/src/core/payments/retry.rs | 1 + crates/router/src/core/payments/types.rs | 2 +- .../src/types/storage/payment_attempt.rs | 10 -- crates/router/src/types/transformers.rs | 9 +- .../down.sql | 2 + .../up.sql | 3 + openapi/openapi_spec.json | 4 +- .../Payments - Cancel/event.test.js | 4 +- .../Payments - Capture/event.test.js | 10 ++ .../Payments - Capture/event.test.js | 10 ++ .../Payments - Retrieve-copy/event.test.js | 10 ++ .../Payments - Retrieve/event.test.js | 10 ++ .../Payments - Capture - 1/event.test.js | 10 ++ .../Payments - Capture - 2/event.test.js | 10 ++ .../Payments - Capture - 3/event.test.js | 10 ++ .../Payments - Capture/event.test.js | 6 +- .../Payments - Retrieve-copy/event.test.js | 4 +- .../Payments - Capture/event.test.js | 4 +- .../Payments - Retrieve/event.test.js | 4 +- .../Payments - Capture/event.test.js | 4 +- .../Payments - Retrieve/event.test.js | 4 +- .../Payments - Capture/event.test.js | 4 +- .../Payments - Capture/request.json | 2 +- .../Payments - Retrieve-copy/event.test.js | 4 +- .../Payments - Capture/event.test.js | 4 +- .../Payments - Capture/request.json | 2 +- .../.meta.json | 7 ++ .../Payments - Capture/.event.meta.json | 3 + .../Payments - Capture/event.test.js | 94 +++++++++++++++++++ .../Payments - Capture/request.json | 39 ++++++++ .../Payments - Capture/response.json | 1 + .../Payments - Create/.event.meta.json | 3 + .../Payments - Create/event.test.js | 71 ++++++++++++++ .../Payments - Create/request.json | 88 +++++++++++++++++ .../Payments - Create/response.json | 1 + .../Payments - Retrieve/.event.meta.json | 3 + .../Payments - Retrieve/event.test.js | 71 ++++++++++++++ .../Payments - Retrieve/request.json | 28 ++++++ .../Payments - Retrieve/response.json | 1 + 46 files changed, 600 insertions(+), 59 deletions(-) create mode 100644 migrations/2023-11-06-153840_introduce_new_attempt_and_intent_status/down.sql create mode 100644 migrations/2023-11-06-153840_introduce_new_attempt_and_intent_status/up.sql create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/event.test.js create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/request.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/response.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/event.test.js create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/request.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/response.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/event.test.js create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/request.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/response.json diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 48b0664c16d3..8b1437fa8926 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -50,6 +50,7 @@ pub enum AttemptStatus { VoidFailed, AutoRefunded, PartialCharged, + PartialChargedAndChargeable, Unresolved, #[default] Pending, @@ -68,7 +69,8 @@ impl AttemptStatus { | Self::Voided | Self::VoidFailed | Self::CaptureFailed - | Self::Failure => true, + | Self::Failure + | Self::PartialCharged => true, Self::Started | Self::AuthenticationFailed | Self::AuthenticationPending @@ -79,7 +81,7 @@ impl AttemptStatus { | Self::CodInitiated | Self::VoidInitiated | Self::CaptureInitiated - | Self::PartialCharged + | Self::PartialChargedAndChargeable | Self::Unresolved | Self::Pending | Self::PaymentMethodAwaited @@ -861,6 +863,7 @@ pub enum IntentStatus { RequiresConfirmation, RequiresCapture, PartiallyCaptured, + PartiallyCapturedAndCapturable, } #[derive( diff --git a/crates/router/src/compatibility/stripe/payment_intents/types.rs b/crates/router/src/compatibility/stripe/payment_intents/types.rs index c713011b80c8..3c7d5f2918f1 100644 --- a/crates/router/src/compatibility/stripe/payment_intents/types.rs +++ b/crates/router/src/compatibility/stripe/payment_intents/types.rs @@ -405,7 +405,9 @@ pub enum StripePaymentStatus { impl From for StripePaymentStatus { fn from(item: api_enums::IntentStatus) -> Self { match item { - api_enums::IntentStatus::Succeeded => Self::Succeeded, + api_enums::IntentStatus::Succeeded | api_enums::IntentStatus::PartiallyCaptured => { + Self::Succeeded + } api_enums::IntentStatus::Failed => Self::Canceled, api_enums::IntentStatus::Processing => Self::Processing, api_enums::IntentStatus::RequiresCustomerAction @@ -413,7 +415,7 @@ impl From for StripePaymentStatus { api_enums::IntentStatus::RequiresPaymentMethod => Self::RequiresPaymentMethod, api_enums::IntentStatus::RequiresConfirmation => Self::RequiresConfirmation, api_enums::IntentStatus::RequiresCapture - | api_enums::IntentStatus::PartiallyCaptured => Self::RequiresCapture, + | api_enums::IntentStatus::PartiallyCapturedAndCapturable => Self::RequiresCapture, api_enums::IntentStatus::Cancelled => Self::Canceled, } } diff --git a/crates/router/src/compatibility/stripe/setup_intents/types.rs b/crates/router/src/compatibility/stripe/setup_intents/types.rs index dde378e55925..9d3f74af8cb8 100644 --- a/crates/router/src/compatibility/stripe/setup_intents/types.rs +++ b/crates/router/src/compatibility/stripe/setup_intents/types.rs @@ -313,7 +313,9 @@ pub enum StripeSetupStatus { impl From for StripeSetupStatus { fn from(item: api_enums::IntentStatus) -> Self { match item { - api_enums::IntentStatus::Succeeded => Self::Succeeded, + api_enums::IntentStatus::Succeeded | api_enums::IntentStatus::PartiallyCaptured => { + Self::Succeeded + } api_enums::IntentStatus::Failed => Self::Canceled, api_enums::IntentStatus::Processing => Self::Processing, api_enums::IntentStatus::RequiresCustomerAction => Self::RequiresAction, @@ -321,7 +323,7 @@ impl From for StripeSetupStatus { api_enums::IntentStatus::RequiresPaymentMethod => Self::RequiresPaymentMethod, api_enums::IntentStatus::RequiresConfirmation => Self::RequiresConfirmation, api_enums::IntentStatus::RequiresCapture - | api_enums::IntentStatus::PartiallyCaptured => { + | api_enums::IntentStatus::PartiallyCapturedAndCapturable => { logger::error!("Invalid status change"); Self::Canceled } diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index efabbf87aeba..9c19d4eed8f6 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -19,7 +19,10 @@ use serde::Serializer; use crate::{ consts, - core::errors::{self, CustomResult}, + core::{ + errors::{self, CustomResult}, + payments::PaymentData, + }, pii::PeekInterface, types::{self, api, transformers::ForeignTryFrom, PaymentsCancelData, ResponseId}, utils::{OptionExt, ValueExt}, @@ -74,6 +77,49 @@ pub trait RouterData { #[cfg(feature = "payouts")] fn get_quote_id(&self) -> Result; } + +pub trait PaymentResponseRouterData { + fn get_attempt_status_for_db_update( + &self, + payment_data: &PaymentData, + ) -> enums::AttemptStatus + where + F: Clone; +} + +impl PaymentResponseRouterData + for types::RouterData +where + Request: types::Capturable, +{ + fn get_attempt_status_for_db_update( + &self, + payment_data: &PaymentData, + ) -> enums::AttemptStatus + where + F: Clone, + { + match self.status { + enums::AttemptStatus::Voided => { + if payment_data.payment_intent.amount_captured > Some(0) { + enums::AttemptStatus::PartialCharged + } else { + self.status + } + } + enums::AttemptStatus::Charged => { + let captured_amount = types::Capturable::get_capture_amount(&self.request); + if Some(payment_data.payment_intent.amount) == captured_amount { + enums::AttemptStatus::Charged + } else { + enums::AttemptStatus::PartialCharged + } + } + _ => self.status, + } + } +} + pub const SELECTED_PAYMENT_METHOD: &str = "Selected payment method"; pub fn get_unimplemented_payment_method_error_message(connector: &str) -> String { diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 7e19b0b60571..000cadec0091 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1736,19 +1736,19 @@ pub fn should_call_connector( | storage_enums::IntentStatus::RequiresCustomerAction | storage_enums::IntentStatus::RequiresMerchantAction | storage_enums::IntentStatus::RequiresCapture - | storage_enums::IntentStatus::PartiallyCaptured + | storage_enums::IntentStatus::PartiallyCapturedAndCapturable ) && payment_data.force_sync.unwrap_or(false) } "PaymentCancel" => matches!( payment_data.payment_intent.status, storage_enums::IntentStatus::RequiresCapture - | storage_enums::IntentStatus::PartiallyCaptured + | storage_enums::IntentStatus::PartiallyCapturedAndCapturable ), "PaymentCapture" => { matches!( payment_data.payment_intent.status, storage_enums::IntentStatus::RequiresCapture - | storage_enums::IntentStatus::PartiallyCaptured + | storage_enums::IntentStatus::PartiallyCapturedAndCapturable ) || (matches!( payment_data.payment_intent.status, storage_enums::IntentStatus::Processing diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index b9e96ec36e11..cd056f81ebb4 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1579,7 +1579,7 @@ pub(crate) fn validate_status_with_capture_method( } utils::when( status != storage_enums::IntentStatus::RequiresCapture - && status != storage_enums::IntentStatus::PartiallyCaptured + && status != storage_enums::IntentStatus::PartiallyCapturedAndCapturable && status != storage_enums::IntentStatus::Processing, || { Err(report!(errors::ApiErrorResponse::PaymentUnexpectedState { @@ -2784,6 +2784,7 @@ pub fn get_attempt_type( | enums::AttemptStatus::Pending | enums::AttemptStatus::ConfirmationAwaited | enums::AttemptStatus::PartialCharged + | enums::AttemptStatus::PartialChargedAndChargeable | enums::AttemptStatus::Voided | enums::AttemptStatus::AutoRefunded | enums::AttemptStatus::PaymentMethodAwaited @@ -2844,6 +2845,7 @@ pub fn get_attempt_type( enums::IntentStatus::Cancelled | enums::IntentStatus::RequiresCapture | enums::IntentStatus::PartiallyCaptured + | enums::IntentStatus::PartiallyCapturedAndCapturable | enums::IntentStatus::Processing | enums::IntentStatus::Succeeded => { Err(report!(errors::ApiErrorResponse::PreconditionFailed { @@ -3023,6 +3025,7 @@ pub fn is_manual_retry_allowed( | enums::AttemptStatus::Pending | enums::AttemptStatus::ConfirmationAwaited | enums::AttemptStatus::PartialCharged + | enums::AttemptStatus::PartialChargedAndChargeable | enums::AttemptStatus::Voided | enums::AttemptStatus::AutoRefunded | enums::AttemptStatus::PaymentMethodAwaited @@ -3042,6 +3045,7 @@ pub fn is_manual_retry_allowed( enums::IntentStatus::Cancelled | enums::IntentStatus::RequiresCapture | enums::IntentStatus::PartiallyCaptured + | enums::IntentStatus::PartiallyCapturedAndCapturable | enums::IntentStatus::Processing | enums::IntentStatus::Succeeded => Some(false), diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index b55b0c46f6ad..1cfc37efa449 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -11,6 +11,7 @@ use tracing_futures::Instrument; use super::{Operation, PostUpdateTracker}; use crate::{ + connector::utils::PaymentResponseRouterData, core::{ errors::{self, RouterResult, StorageErrorExt}, mandate, @@ -26,7 +27,7 @@ use crate::{ self, enums, payment_attempt::{AttemptStatusExt, PaymentAttemptExt}, }, - transformers::ForeignTryFrom, + transformers::{ForeignFrom, ForeignTryFrom}, CaptureSyncResponse, }, utils, @@ -389,7 +390,7 @@ async fn payment_response_update_tracker( types::PreprocessingResponseId::ConnectorTransactionId(_) => None, }; let payment_attempt_update = storage::PaymentAttemptUpdate::PreprocessingUpdate { - status: router_data.status, + status: router_data.get_attempt_status_for_db_update(&payment_data), payment_method_id: Some(router_data.payment_method_id), connector_metadata, preprocessing_step_id, @@ -434,7 +435,7 @@ async fn payment_response_update_tracker( utils::add_apple_pay_payment_status_metrics( router_data.status, - router_data.apple_pay_flow, + router_data.apple_pay_flow.clone(), payment_data.payment_attempt.connector.clone(), payment_data.payment_attempt.merchant_id.clone(), ); @@ -456,7 +457,7 @@ async fn payment_response_update_tracker( None => ( None, Some(storage::PaymentAttemptUpdate::ResponseUpdate { - status: router_data.status, + status: router_data.get_attempt_status_for_db_update(&payment_data), connector: None, connector_transaction_id: connector_transaction_id.clone(), authentication_type: None, @@ -504,7 +505,7 @@ async fn payment_response_update_tracker( ( None, Some(storage::PaymentAttemptUpdate::UnresolvedResponseUpdate { - status: router_data.status, + status: router_data.get_attempt_status_for_db_update(&payment_data), connector: None, connector_transaction_id, payment_method_id: Some(router_data.payment_method_id), @@ -610,15 +611,15 @@ async fn payment_response_update_tracker( let payment_intent_update = match &router_data.response { Err(_) => storage::PaymentIntentUpdate::PGStatusUpdate { - status: payment_data - .payment_attempt - .get_intent_status(payment_data.payment_intent.amount_captured), + status: api_models::enums::IntentStatus::foreign_from( + payment_data.payment_attempt.status, + ), updated_by: storage_scheme.to_string(), }, Ok(_) => storage::PaymentIntentUpdate::ResponseUpdate { - status: payment_data - .payment_attempt - .get_intent_status(payment_data.payment_intent.amount_captured), + status: api_models::enums::IntentStatus::foreign_from( + payment_data.payment_attempt.status, + ), return_url: router_data.return_url.clone(), amount_captured, updated_by: storage_scheme.to_string(), diff --git a/crates/router/src/core/payments/retry.rs b/crates/router/src/core/payments/retry.rs index 376b9048c856..788e83b05e37 100644 --- a/crates/router/src/core/payments/retry.rs +++ b/crates/router/src/core/payments/retry.rs @@ -566,6 +566,7 @@ impl | storage_enums::AttemptStatus::AutoRefunded | storage_enums::AttemptStatus::CaptureFailed | storage_enums::AttemptStatus::PartialCharged + | storage_enums::AttemptStatus::PartialChargedAndChargeable | storage_enums::AttemptStatus::Pending | storage_enums::AttemptStatus::PaymentMethodAwaited | storage_enums::AttemptStatus::ConfirmationAwaited diff --git a/crates/router/src/core/payments/types.rs b/crates/router/src/core/payments/types.rs index f420a4b87a75..5e150a33d5c5 100644 --- a/crates/router/src/core/payments/types.rs +++ b/crates/router/src/core/payments/types.rs @@ -116,7 +116,7 @@ impl MultipleCaptureData { } let status_count_map = self.get_status_count(); if status_count_map.get(&storage_enums::CaptureStatus::Charged) > Some(&0) { - storage_enums::AttemptStatus::PartialCharged + storage_enums::AttemptStatus::PartialChargedAndChargeable } else { storage_enums::AttemptStatus::CaptureInitiated } diff --git a/crates/router/src/types/storage/payment_attempt.rs b/crates/router/src/types/storage/payment_attempt.rs index 0b415e716513..a4fbcb022005 100644 --- a/crates/router/src/types/storage/payment_attempt.rs +++ b/crates/router/src/types/storage/payment_attempt.rs @@ -16,7 +16,6 @@ pub trait PaymentAttemptExt { ) -> RouterResult; fn get_next_capture_id(&self) -> String; - fn get_intent_status(&self, amount_captured: Option) -> enums::IntentStatus; fn get_total_amount(&self) -> i64; } @@ -60,15 +59,6 @@ impl PaymentAttemptExt for PaymentAttempt { format!("{}_{}", self.attempt_id.clone(), next_sequence_number) } - fn get_intent_status(&self, amount_captured: Option) -> enums::IntentStatus { - let intent_status = enums::IntentStatus::foreign_from(self.status); - if intent_status == enums::IntentStatus::Cancelled && amount_captured > Some(0) { - enums::IntentStatus::Succeeded - } else { - intent_status - } - } - fn get_total_amount(&self) -> i64 { self.amount + self.surcharge_amount.unwrap_or(0) + self.tax_amount.unwrap_or(0) } diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index f43abdf73ead..3ffba5aff50a 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -86,6 +86,9 @@ impl ForeignFrom for storage_enums::IntentStatus { storage_enums::AttemptStatus::Unresolved => Self::RequiresMerchantAction, storage_enums::AttemptStatus::PartialCharged => Self::PartiallyCaptured, + storage_enums::AttemptStatus::PartialChargedAndChargeable => { + Self::PartiallyCapturedAndCapturable + } storage_enums::AttemptStatus::Started | storage_enums::AttemptStatus::AuthenticationSuccessful | storage_enums::AttemptStatus::Authorizing @@ -135,7 +138,8 @@ impl ForeignTryFrom for storage_enums::CaptureStat | storage_enums::AttemptStatus::Unresolved | storage_enums::AttemptStatus::PaymentMethodAwaited | storage_enums::AttemptStatus::ConfirmationAwaited - | storage_enums::AttemptStatus::DeviceDataCollectionPending => { + | storage_enums::AttemptStatus::DeviceDataCollectionPending + | storage_enums::AttemptStatus::PartialChargedAndChargeable=> { Err(errors::ApiErrorResponse::PreconditionFailed { message: "AttemptStatus must be one of these for multiple partial captures [Charged, PartialCharged, Pending, CaptureInitiated, Failure, CaptureFailed]".into(), }.into()) @@ -414,7 +418,8 @@ impl ForeignFrom for Option { api_enums::IntentStatus::RequiresPaymentMethod | api_enums::IntentStatus::RequiresConfirmation | api_enums::IntentStatus::RequiresCapture - | api_enums::IntentStatus::PartiallyCaptured => None, + | api_enums::IntentStatus::PartiallyCaptured + | api_enums::IntentStatus::PartiallyCapturedAndCapturable => None, } } } diff --git a/migrations/2023-11-06-153840_introduce_new_attempt_and_intent_status/down.sql b/migrations/2023-11-06-153840_introduce_new_attempt_and_intent_status/down.sql new file mode 100644 index 000000000000..c7c9cbeb4017 --- /dev/null +++ b/migrations/2023-11-06-153840_introduce_new_attempt_and_intent_status/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +SELECT 1; \ No newline at end of file diff --git a/migrations/2023-11-06-153840_introduce_new_attempt_and_intent_status/up.sql b/migrations/2023-11-06-153840_introduce_new_attempt_and_intent_status/up.sql new file mode 100644 index 000000000000..5b9acbaca48a --- /dev/null +++ b/migrations/2023-11-06-153840_introduce_new_attempt_and_intent_status/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TYPE "IntentStatus" ADD VALUE IF NOT EXISTS 'partially_captured_and_capturable'; +ALTER TYPE "AttemptStatus" ADD VALUE IF NOT EXISTS 'partial_charged_and_chargeable'; \ No newline at end of file diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 9fddde01b49a..55ff36c26ff7 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -2619,6 +2619,7 @@ "void_failed", "auto_refunded", "partial_charged", + "partial_charged_and_chargeable", "unresolved", "pending", "failure", @@ -6145,7 +6146,8 @@ "requires_payment_method", "requires_confirmation", "requires_capture", - "partially_captured" + "partially_captured", + "partially_captured_and_capturable" ] }, "JCSVoucherData": { diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Cancel/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Cancel/event.test.js index edeeb5a7b2b3..f5b74b41f5bd 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Cancel/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Cancel/event.test.js @@ -53,9 +53,9 @@ if (jsonData?.client_secret) { // Response body should have value "cancellation succeeded" for "payment status" if (jsonData?.status) { pm.test( - "[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'succeeded'", + "[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("partially_captured"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Capture/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Capture/event.test.js index e6f49ae73578..ea5c5df58982 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Capture/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Capture/event.test.js @@ -58,6 +58,16 @@ if (jsonData?.client_secret) { ); } +// Response body should have value "cancellation succeeded" for "payment status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'", + function () { + pm.expect(jsonData.status).to.eql("partially_captured_and_capturable"); + }, + ); +} + // Response body should have value "connector error" for "error type" if (jsonData?.error?.type) { pm.test( diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Capture/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Capture/event.test.js index e6f49ae73578..af4bbc618739 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Capture/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Capture/event.test.js @@ -67,3 +67,13 @@ if (jsonData?.error?.type) { }, ); } + +// Response body should have value "cancellation succeeded" for "payment status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'", + function () { + pm.expect(jsonData.status).to.eql("partially_captured_and_capturable"); + }, + ); +} \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/event.test.js index 5e5839fa2934..103f31cbb80f 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/event.test.js @@ -33,3 +33,13 @@ if (jsonData?.payment_id) { "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", ); } + +// Response body should have value "cancellation succeeded" for "payment status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'", + function () { + pm.expect(jsonData.status).to.eql("partially_captured_and_capturable"); + }, + ); +} diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/event.test.js index d0a02af74367..6939cfa39d2e 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/event.test.js @@ -59,3 +59,13 @@ if (jsonData?.client_secret) { "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", ); } + +// Response body should have value "cancellation succeeded" for "payment status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'requires_capture'", + function () { + pm.expect(jsonData.status).to.eql("requires_capture"); + }, + ); +} diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 1/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 1/event.test.js index e6f49ae73578..2c29f2cd3536 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 1/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 1/event.test.js @@ -67,3 +67,13 @@ if (jsonData?.error?.type) { }, ); } + +// Response body should have value "cancellation succeeded" for "payment status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'", + function () { + pm.expect(jsonData.status).to.eql("partially_captured_and_capturable"); + }, + ); +} diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 2/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 2/event.test.js index e6f49ae73578..2c29f2cd3536 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 2/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 2/event.test.js @@ -67,3 +67,13 @@ if (jsonData?.error?.type) { }, ); } + +// Response body should have value "cancellation succeeded" for "payment status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'", + function () { + pm.expect(jsonData.status).to.eql("partially_captured_and_capturable"); + }, + ); +} diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 3/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 3/event.test.js index e6f49ae73578..2d200c507ff5 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 3/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 3/event.test.js @@ -67,3 +67,13 @@ if (jsonData?.error?.type) { }, ); } + +// Response body should have value "cancellation succeeded" for "payment status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'succeeded'", + function () { + pm.expect(jsonData.status).to.eql("succeeded"); + }, + ); +} diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Capture/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Capture/event.test.js index 791a3bfbc320..d9ade9825b6e 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Capture/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Capture/event.test.js @@ -66,9 +66,9 @@ if (jsonData?.client_secret) { // Response body should have value "succeeded" for "status" if (jsonData?.status) { pm.test( - "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'", + "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("partially_captured"); }, ); } @@ -103,7 +103,7 @@ if (jsonData?.amount_capturable) { pm.test( "[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'", function () { - pm.expect(jsonData.amount_capturable).to.eql(6540); + pm.expect(jsonData.amount_capturable).to.eql(540); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Retrieve-copy/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Retrieve-copy/event.test.js index 22f7c74b5db4..ae68f8b79310 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Retrieve-copy/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Retrieve-copy/event.test.js @@ -63,9 +63,9 @@ if (jsonData?.client_secret) { // Response body should have value "Succeeded" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'", + "[POST]::/payments/:id - Content check if value for 'status' matches 'partially_captured'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("partially_captured"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Capture/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Capture/event.test.js index fc1ed092f8be..d9ade9825b6e 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Capture/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Capture/event.test.js @@ -66,9 +66,9 @@ if (jsonData?.client_secret) { // Response body should have value "succeeded" for "status" if (jsonData?.status) { pm.test( - "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'", + "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("partially_captured"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/event.test.js index cea10167ebce..c22795a2d483 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/event.test.js @@ -63,9 +63,9 @@ if (jsonData?.client_secret) { // Response body should have value "succeeded" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments - Content check if value for 'status' matches 'succeeded'", + "[POST]::/payments - Content check if value for 'status' matches 'partially_captured'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("partially_captured"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Capture/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Capture/event.test.js index fc1ed092f8be..d9ade9825b6e 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Capture/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Capture/event.test.js @@ -66,9 +66,9 @@ if (jsonData?.client_secret) { // Response body should have value "succeeded" for "status" if (jsonData?.status) { pm.test( - "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'", + "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("partially_captured"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/event.test.js index cea10167ebce..c22795a2d483 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/event.test.js @@ -63,9 +63,9 @@ if (jsonData?.client_secret) { // Response body should have value "succeeded" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments - Content check if value for 'status' matches 'succeeded'", + "[POST]::/payments - Content check if value for 'status' matches 'partially_captured'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("partially_captured"); }, ); } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Capture/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Capture/event.test.js index 8fd96aaddc5b..ee01079cab94 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Capture/event.test.js +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Capture/event.test.js @@ -91,9 +91,9 @@ if (jsonData?.amount) { // Response body should have value "6000" for "amount_received" if (jsonData?.amount_received) { pm.test( - "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'", + "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'", function () { - pm.expect(jsonData.amount_received).to.eql(6000); + pm.expect(jsonData.amount_received).to.eql(6540); }, ); } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Capture/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Capture/request.json index 8975575ca40e..8efb99d3c905 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Capture/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Capture/request.json @@ -18,7 +18,7 @@ } }, "raw_json_formatted": { - "amount_to_capture": 6000, + "amount_to_capture": 6540, "statement_descriptor_name": "Joseph", "statement_descriptor_suffix": "JS" } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Retrieve-copy/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Retrieve-copy/event.test.js index a3c023cb7ef9..0095c8cf19aa 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Retrieve-copy/event.test.js +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Retrieve-copy/event.test.js @@ -88,9 +88,9 @@ if (jsonData?.amount) { // Response body should have value "6000" for "amount_received" if (jsonData?.amount_received) { pm.test( - "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'", + "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'", function () { - pm.expect(jsonData.amount_received).to.eql(6000); + pm.expect(jsonData.amount_received).to.eql(6540); }, ); } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js index 2d7dbc507fb0..b9d5ecb464b7 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js @@ -86,9 +86,9 @@ if (jsonData?.amount) { // Response body should have value "6000" for "amount_received" if (jsonData?.amount_received) { pm.test( - "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'", + "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'", function () { - pm.expect(jsonData.amount_received).to.eql(6000); + pm.expect(jsonData.amount_received).to.eql(6540); }, ); } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/request.json index 9fe257ed85e6..cceb2b55f0a7 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/request.json @@ -18,7 +18,7 @@ } }, "raw_json_formatted": { - "amount_to_capture": 6000, + "amount_to_capture": 6540, "statement_descriptor_name": "Joseph", "statement_descriptor_suffix": "JS" } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/.meta.json new file mode 100644 index 000000000000..e4ef30e39e8d --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/.meta.json @@ -0,0 +1,7 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Capture", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/event.test.js new file mode 100644 index 000000000000..f560d84ea730 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/event.test.js @@ -0,0 +1,94 @@ +// Validate status 2xx +pm.test("[POST]::/payments/:id/capture - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/payments/:id/capture - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Validate if response has JSON Body +pm.test("[POST]::/payments/:id/capture - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'", + function () { + pm.expect(jsonData.status).to.eql("partially_captured"); + }, + ); +} + +// Response body should have value "6540" for "amount" +if (jsonData?.amount) { + pm.test( + "[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'", + function () { + pm.expect(jsonData.amount).to.eql(6540); + }, + ); +} + +// Response body should have value "6000" for "amount_received" +if (jsonData?.amount_received) { + pm.test( + "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'", + function () { + pm.expect(jsonData.amount_received).to.eql(6000); + }, + ); +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/request.json new file mode 100644 index 000000000000..9fe257ed85e6 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/request.json @@ -0,0 +1,39 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount_to_capture": 6000, + "statement_descriptor_name": "Joseph", + "statement_descriptor_suffix": "JS" + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": ["{{baseUrl}}"], + "path": ["payments", ":id", "capture"], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/event.test.js new file mode 100644 index 000000000000..d683186aa007 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "requires_capture" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'requires_capture'", + function () { + pm.expect(jsonData.status).to.eql("requires_capture"); + }, + ); +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/request.json new file mode 100644 index 000000000000..0619498e38c7 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/request.json @@ -0,0 +1,88 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "confirm": true, + "capture_method": "manual", + "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": "+65", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "payment_method": "card", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + }, + "routing": { + "type": "single", + "data": "stripe" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": ["{{baseUrl}}"], + "path": ["payments"] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..ca68dd7045be --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[GET]::/payments/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/payments/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[GET]::/payments/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'partially_captured'", + function () { + pm.expect(jsonData.status).to.eql("partially_captured"); + }, + ); +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/request.json new file mode 100644 index 000000000000..6cd4b7d96c52 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/request.json @@ -0,0 +1,28 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": ["{{baseUrl}}"], + "path": ["payments", ":id"], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] From 7d05b74b950d9e078b063e17d046cbeb501d006a Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 17 Nov 2023 06:43:59 +0000 Subject: [PATCH 027/146] test(postman): update postman collection files --- .../checkout.postman_collection.json | 101 ++++- .../stripe.postman_collection.json | 408 +++++++++++++++++- 2 files changed, 485 insertions(+), 24 deletions(-) diff --git a/postman/collection-json/checkout.postman_collection.json b/postman/collection-json/checkout.postman_collection.json index b65320387429..2bd0ac0f26e0 100644 --- a/postman/collection-json/checkout.postman_collection.json +++ b/postman/collection-json/checkout.postman_collection.json @@ -3802,9 +3802,9 @@ "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", @@ -3839,7 +3839,7 @@ " pm.test(", " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(6540);", + " pm.expect(jsonData.amount_capturable).to.eql(540);", " },", " );", "}", @@ -3964,9 +3964,9 @@ "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", @@ -5929,9 +5929,9 @@ "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", @@ -6091,9 +6091,9 @@ "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", @@ -6883,9 +6883,9 @@ "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", @@ -7045,9 +7045,9 @@ "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", @@ -9061,6 +9061,16 @@ " },", " );", "}", + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", + " },", + " );", + "}", "" ], "type": "text/javascript" @@ -9195,6 +9205,16 @@ " },", " );", "}", + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", + " },", + " );", + "}", "" ], "type": "text/javascript" @@ -9329,6 +9349,16 @@ " },", " );", "}", + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", "" ], "type": "text/javascript" @@ -9916,6 +9946,16 @@ " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", " );", "}", + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", "" ], "type": "text/javascript" @@ -10042,7 +10082,16 @@ " },", " );", "}", - "" + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", + " },", + " );", + "}" ], "type": "text/javascript" } @@ -10142,6 +10191,16 @@ " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", " );", "}", + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", + " },", + " );", + "}", "" ], "type": "text/javascript" @@ -10503,6 +10562,16 @@ " );", "}", "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", + " },", + " );", + "}", + "", "// Response body should have value \"connector error\" for \"error type\"", "if (jsonData?.error?.type) {", " pm.test(", @@ -10632,9 +10701,9 @@ "// Response body should have value \"cancellation succeeded\" for \"payment status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", diff --git a/postman/collection-json/stripe.postman_collection.json b/postman/collection-json/stripe.postman_collection.json index 5d308dd0fe53..06ccae91b2c7 100644 --- a/postman/collection-json/stripe.postman_collection.json +++ b/postman/collection-json/stripe.postman_collection.json @@ -7954,9 +7954,9 @@ "// Response body should have value \"6000\" for \"amount_received\"", "if (jsonData?.amount_received) {", " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'\",", " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", + " pm.expect(jsonData.amount_received).to.eql(6540);", " },", " );", "}", @@ -7995,7 +7995,7 @@ "language": "json" } }, - "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + "raw": "{\"amount_to_capture\":6540,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" }, "url": { "raw": "{{baseUrl}}/payments/:id/capture", @@ -8116,9 +8116,9 @@ "// Response body should have value \"6000\" for \"amount_received\"", "if (jsonData?.amount_received) {", " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'\",", " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", + " pm.expect(jsonData.amount_received).to.eql(6540);", " },", " );", "}", @@ -8929,6 +8929,398 @@ } ] }, + { + "name": "Scenario4-Create payment with manual_multiple capture", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6000);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'partially_captured'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, { "name": "Scenario1-Create payment with confirm true", "item": [ @@ -10255,9 +10647,9 @@ "// Response body should have value \"6000\" for \"amount_received\"", "if (jsonData?.amount_received) {", " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'\",", " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", + " pm.expect(jsonData.amount_received).to.eql(6540);", " },", " );", "}", @@ -10286,7 +10678,7 @@ "language": "json" } }, - "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + "raw": "{\"amount_to_capture\":6540,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" }, "url": { "raw": "{{baseUrl}}/payments/:id/capture", From 57173860da8f9f067c8aa6bf8074420bf762ad58 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 17 Nov 2023 06:44:00 +0000 Subject: [PATCH 028/146] chore(version): v1.82.0 --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 427fa7403e4c..4270442611a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,25 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.82.0 (2023-11-17) + +### Features + +- **router:** Add fallback while add card and retrieve card from rust locker ([#2888](https://github.com/juspay/hyperswitch/pull/2888)) ([`f735fb0`](https://github.com/juspay/hyperswitch/commit/f735fb0551812fd781a2db8bac5a0deef4cabb2b)) + +### Bug Fixes + +- **core:** Introduce new attempt and intent status to handle multiple partial captures ([#2802](https://github.com/juspay/hyperswitch/pull/2802)) ([`cb88be0`](https://github.com/juspay/hyperswitch/commit/cb88be01f22725948648976c2a5606a03b5ce92a)) + +### Testing + +- **postman:** Update postman collection files ([`7d05b74`](https://github.com/juspay/hyperswitch/commit/7d05b74b950d9e078b063e17d046cbeb501d006a)) + +**Full Changelog:** [`v1.81.0...v1.82.0`](https://github.com/juspay/hyperswitch/compare/v1.81.0...v1.82.0) + +- - - + + ## 1.81.0 (2023-11-16) ### Features From 375108b6df50e041fc9dbeb35a6a6b46b146037a Mon Sep 17 00:00:00 2001 From: Nitesh <126162378+nitesh-balla@users.noreply.github.com> Date: Fri, 17 Nov 2023 12:47:21 +0530 Subject: [PATCH 029/146] docs(README): replace cloudformation deployment template with latest s3 url. (#2891) --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 129a0512d4a0..bc528da9bbf5 100644 --- a/README.md +++ b/README.md @@ -64,9 +64,7 @@ The fastest and easiest way to try hyperswitch is via our CDK scripts 1. Click on the following button for a quick standalone deployment on AWS, suitable for prototyping. No code or setup is required in your system and the deployment is covered within the AWS free-tier setup. -   Click here if you have not bootstrapped your region before deploying - -   +   2. Sign-in to your AWS console. From aea390a6a1c331f8e0dbea4f41218e43f7323508 Mon Sep 17 00:00:00 2001 From: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> Date: Fri, 17 Nov 2023 13:38:23 +0530 Subject: [PATCH 030/146] feat(events): add incoming webhook payload to api events logger (#2852) Co-authored-by: Sampras lopes --- Cargo.lock | 1 + connector-template/mod.rs | 2 +- crates/common_utils/src/ext_traits.rs | 1 + crates/masking/Cargo.toml | 3 +- crates/masking/src/lib.rs | 4 +- crates/masking/src/serde.rs | 25 +++++ crates/router/Cargo.toml | 1 + crates/router/src/connector/aci.rs | 2 +- crates/router/src/connector/adyen.rs | 8 +- crates/router/src/connector/airwallex.rs | 4 +- .../src/connector/airwallex/transformers.rs | 3 +- .../router/src/connector/authorizedotnet.rs | 10 +- crates/router/src/connector/bambora.rs | 2 +- crates/router/src/connector/bankofamerica.rs | 2 +- crates/router/src/connector/bitpay.rs | 7 +- crates/router/src/connector/bluesnap.rs | 6 +- crates/router/src/connector/boku.rs | 2 +- crates/router/src/connector/braintree.rs | 8 +- crates/router/src/connector/cashtocode.rs | 7 +- crates/router/src/connector/checkout.rs | 7 +- crates/router/src/connector/coinbase.rs | 6 +- crates/router/src/connector/cryptopay.rs | 6 +- crates/router/src/connector/cybersource.rs | 2 +- crates/router/src/connector/dlocal.rs | 2 +- crates/router/src/connector/dummyconnector.rs | 2 +- crates/router/src/connector/fiserv.rs | 2 +- crates/router/src/connector/forte.rs | 2 +- crates/router/src/connector/globalpay.rs | 11 +- crates/router/src/connector/globepay.rs | 2 +- crates/router/src/connector/gocardless.rs | 21 ++-- .../src/connector/gocardless/transformers.rs | 20 ++-- crates/router/src/connector/helcim.rs | 2 +- crates/router/src/connector/iatapay.rs | 6 +- crates/router/src/connector/klarna.rs | 2 +- crates/router/src/connector/mollie.rs | 2 +- crates/router/src/connector/multisafepay.rs | 2 +- crates/router/src/connector/nexinets.rs | 2 +- crates/router/src/connector/nmi.rs | 2 +- crates/router/src/connector/noon.rs | 8 +- crates/router/src/connector/nuvei.rs | 7 +- crates/router/src/connector/opayo.rs | 2 +- crates/router/src/connector/opennode.rs | 6 +- crates/router/src/connector/payeezy.rs | 2 +- crates/router/src/connector/payme.rs | 24 ++--- crates/router/src/connector/paypal.rs | 29 ++--- crates/router/src/connector/payu.rs | 2 +- crates/router/src/connector/powertranz.rs | 2 +- crates/router/src/connector/prophetpay.rs | 2 +- crates/router/src/connector/rapyd.rs | 4 +- crates/router/src/connector/shift4.rs | 6 +- crates/router/src/connector/square.rs | 15 +-- crates/router/src/connector/stax.rs | 4 +- crates/router/src/connector/stripe.rs | 4 +- crates/router/src/connector/trustpay.rs | 8 +- crates/router/src/connector/tsys.rs | 2 +- crates/router/src/connector/volt.rs | 2 +- crates/router/src/connector/wise.rs | 2 +- crates/router/src/connector/worldline.rs | 6 +- crates/router/src/connector/worldpay.rs | 7 +- crates/router/src/connector/zen.rs | 4 +- crates/router/src/core/webhooks.rs | 102 ++++++++++++++---- crates/router/src/events/event_logger.rs | 1 + crates/router/src/routes/webhooks.rs | 3 +- crates/router/src/services/authentication.rs | 6 +- crates/router/src/types/api/webhooks.rs | 2 +- 65 files changed, 259 insertions(+), 202 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a03340093c88..730b08774fa3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4797,6 +4797,7 @@ dependencies = [ "digest 0.9.0", "dyn-clone", "encoding_rs", + "erased-serde", "error-stack", "euclid", "external_services", diff --git a/connector-template/mod.rs b/connector-template/mod.rs index 7f21962109de..e441b0e5879a 100644 --- a/connector-template/mod.rs +++ b/connector-template/mod.rs @@ -485,7 +485,7 @@ impl api::IncomingWebhook for {{project-name | downcase | pascal_case}} { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/common_utils/src/ext_traits.rs b/crates/common_utils/src/ext_traits.rs index e76fe7dff5fb..d3296f989533 100644 --- a/crates/common_utils/src/ext_traits.rs +++ b/crates/common_utils/src/ext_traits.rs @@ -223,6 +223,7 @@ pub trait ByteSliceExt { } impl ByteSliceExt for [u8] { + #[track_caller] fn parse_struct<'de, T>( &'de self, type_name: &'static str, diff --git a/crates/masking/Cargo.toml b/crates/masking/Cargo.toml index 21d791642895..bf92e867dc6c 100644 --- a/crates/masking/Cargo.toml +++ b/crates/masking/Cargo.toml @@ -10,6 +10,7 @@ license.workspace = true [features] default = ["alloc", "serde", "diesel"] alloc = ["zeroize/alloc"] +serde = ["dep:serde", "dep:serde_json"] [package.metadata.docs.rs] all-features = true @@ -19,7 +20,7 @@ rustdoc-args = ["--cfg", "docsrs"] bytes = { version = "1", optional = true } diesel = { version = "2.1.0", features = ["postgres", "serde_json", "time"], optional = true } serde = { version = "1", features = ["derive"], optional = true } -serde_json = "1.0.96" +serde_json = { version = "1.0.96", optional = true } subtle = "=2.4.1" zeroize = { version = "1.6", default-features = false } diff --git a/crates/masking/src/lib.rs b/crates/masking/src/lib.rs index d092a1b5a8b6..cb836e188428 100644 --- a/crates/masking/src/lib.rs +++ b/crates/masking/src/lib.rs @@ -42,7 +42,9 @@ mod vec; #[cfg(feature = "serde")] mod serde; #[cfg(feature = "serde")] -pub use crate::serde::{masked_serialize, Deserialize, SerializableSecret, Serialize}; +pub use crate::serde::{ + masked_serialize, Deserialize, ErasedMaskSerialize, SerializableSecret, Serialize, +}; /// This module should be included with asterisk. /// diff --git a/crates/masking/src/serde.rs b/crates/masking/src/serde.rs index e57ed0301c2f..d1845ee29033 100644 --- a/crates/masking/src/serde.rs +++ b/crates/masking/src/serde.rs @@ -91,6 +91,31 @@ pub fn masked_serialize(value: &T) -> Result because of Rust's "object safety" rules. +/// In particular, the trait contains generic methods which cannot be made into a trait object. +/// In this case we remove the generic for assuming the serialization to be of 2 types only raw json or masked json +pub trait ErasedMaskSerialize { + /// Masked serialization. + fn masked_serialize(&self) -> Result; + /// Normal serialization. + fn raw_serialize(&self) -> Result; +} + +impl ErasedMaskSerialize for T { + fn masked_serialize(&self) -> Result { + masked_serialize(self) + } + + fn raw_serialize(&self) -> Result { + serde_json::to_value(self) + } +} + use pii_serializer::PIISerializer; mod pii_serializer { diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 01595dc18cd5..25feb373b734 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -115,6 +115,7 @@ router_derive = { version = "0.1.0", path = "../router_derive" } router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"] } scheduler = { version = "0.1.0", path = "../scheduler", default-features = false } storage_impl = { version = "0.1.0", path = "../storage_impl", default-features = false } +erased-serde = "0.3.31" [build-dependencies] router_env = { version = "0.1.0", path = "../router_env", default-features = false } diff --git a/crates/router/src/connector/aci.rs b/crates/router/src/connector/aci.rs index f6389c802f9e..f51c91f441df 100644 --- a/crates/router/src/connector/aci.rs +++ b/crates/router/src/connector/aci.rs @@ -572,7 +572,7 @@ impl api::IncomingWebhook for Aci { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/adyen.rs b/crates/router/src/connector/adyen.rs index ef10fbb692fd..676f15d2f564 100644 --- a/crates/router/src/connector/adyen.rs +++ b/crates/router/src/connector/adyen.rs @@ -1600,17 +1600,13 @@ impl api::IncomingWebhook for Adyen { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let notif = get_webhook_object_from_body(request.body) .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; let response: adyen::Response = notif.into(); - let res_json = serde_json::to_value(response) - .into_report() - .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - - Ok(res_json) + Ok(Box::new(response)) } fn get_webhook_api_response( diff --git a/crates/router/src/connector/airwallex.rs b/crates/router/src/connector/airwallex.rs index 5de7fc065e80..33e3dae72871 100644 --- a/crates/router/src/connector/airwallex.rs +++ b/crates/router/src/connector/airwallex.rs @@ -1081,13 +1081,13 @@ impl api::IncomingWebhook for Airwallex { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let details: airwallex::AirwallexWebhookObjectResource = request .body .parse_struct("AirwallexWebhookObjectResource") .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - Ok(details.data.object) + Ok(Box::new(details.data.object)) } fn get_dispute_details( diff --git a/crates/router/src/connector/airwallex/transformers.rs b/crates/router/src/connector/airwallex/transformers.rs index 031a8276bb0d..457b8d075487 100644 --- a/crates/router/src/connector/airwallex/transformers.rs +++ b/crates/router/src/connector/airwallex/transformers.rs @@ -824,7 +824,8 @@ pub enum AirwallexDisputeStage { #[derive(Debug, Deserialize)] pub struct AirwallexWebhookDataResource { - pub object: serde_json::Value, + // Should this be a secret by default since it represents webhook payload + pub object: Secret, } #[derive(Debug, Deserialize)] diff --git a/crates/router/src/connector/authorizedotnet.rs b/crates/router/src/connector/authorizedotnet.rs index 7c3c234daecf..f3cdf0415b91 100644 --- a/crates/router/src/connector/authorizedotnet.rs +++ b/crates/router/src/connector/authorizedotnet.rs @@ -875,17 +875,15 @@ impl api::IncomingWebhook for Authorizedotnet { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let payload: authorizedotnet::AuthorizedotnetWebhookObjectId = request .body .parse_struct("AuthorizedotnetWebhookObjectId") .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - let sync_payload = serde_json::to_value( + + Ok(Box::new( authorizedotnet::AuthorizedotnetSyncResponse::try_from(payload)?, - ) - .into_report() - .change_context(errors::ConnectorError::ResponseHandlingFailed)?; - Ok(sync_payload) + )) } } diff --git a/crates/router/src/connector/bambora.rs b/crates/router/src/connector/bambora.rs index 802be26408df..ff6fdcb46769 100644 --- a/crates/router/src/connector/bambora.rs +++ b/crates/router/src/connector/bambora.rs @@ -685,7 +685,7 @@ impl api::IncomingWebhook for Bambora { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/bankofamerica.rs b/crates/router/src/connector/bankofamerica.rs index 51a1d722dc51..b6e19fa0d296 100644 --- a/crates/router/src/connector/bankofamerica.rs +++ b/crates/router/src/connector/bankofamerica.rs @@ -812,7 +812,7 @@ impl api::IncomingWebhook for Bankofamerica { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/bitpay.rs b/crates/router/src/connector/bitpay.rs index dc4571b75746..856d0a9ec9d7 100644 --- a/crates/router/src/connector/bitpay.rs +++ b/crates/router/src/connector/bitpay.rs @@ -23,7 +23,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{self, BytesExt, Encode}, + utils::{self, BytesExt}, }; #[derive(Debug, Clone)] @@ -393,12 +393,11 @@ impl api::IncomingWebhook for Bitpay { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let notif: BitpayWebhookDetails = request .body .parse_struct("BitpayWebhookDetails") .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; - Encode::::encode_to_value(¬if) - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed) + Ok(Box::new(notif)) } } diff --git a/crates/router/src/connector/bluesnap.rs b/crates/router/src/connector/bluesnap.rs index 7bd2ce052538..d1aa1fa25ee6 100644 --- a/crates/router/src/connector/bluesnap.rs +++ b/crates/router/src/connector/bluesnap.rs @@ -1119,15 +1119,13 @@ impl api::IncomingWebhook for Bluesnap { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let resource: bluesnap::BluesnapWebhookObjectResource = serde_urlencoded::from_bytes(request.body) .into_report() .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - let res_json = serde_json::Value::try_from(resource)?; - - Ok(res_json) + Ok(Box::new(resource)) } } diff --git a/crates/router/src/connector/boku.rs b/crates/router/src/connector/boku.rs index 7c2c1af0986b..87e8fd0eb96a 100644 --- a/crates/router/src/connector/boku.rs +++ b/crates/router/src/connector/boku.rs @@ -627,7 +627,7 @@ impl api::IncomingWebhook for Boku { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/braintree.rs b/crates/router/src/connector/braintree.rs index 6f5b13890367..99f6b9955d57 100644 --- a/crates/router/src/connector/braintree.rs +++ b/crates/router/src/connector/braintree.rs @@ -1418,17 +1418,13 @@ impl api::IncomingWebhook for Braintree { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let notif = get_webhook_object_from_body(request.body) .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; let response = decode_webhook_payload(notif.bt_payload.replace('\n', "").as_bytes())?; - let res_json = serde_json::to_value(response) - .into_report() - .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - - Ok(res_json) + Ok(Box::new(response)) } fn get_webhook_api_response( diff --git a/crates/router/src/connector/cashtocode.rs b/crates/router/src/connector/cashtocode.rs index 12a52e485396..a8d7d6d80504 100644 --- a/crates/router/src/connector/cashtocode.rs +++ b/crates/router/src/connector/cashtocode.rs @@ -391,16 +391,13 @@ impl api::IncomingWebhook for Cashtocode { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let webhook: transformers::CashtocodeIncomingWebhook = request .body .parse_struct("CashtocodeIncomingWebhook") .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - let res_json = - utils::Encode::::encode_to_value(&webhook) - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - Ok(res_json) + Ok(Box::new(webhook)) } fn get_webhook_api_response( diff --git a/crates/router/src/connector/checkout.rs b/crates/router/src/connector/checkout.rs index f24c08233ed7..ca2556544f90 100644 --- a/crates/router/src/connector/checkout.rs +++ b/crates/router/src/connector/checkout.rs @@ -1261,7 +1261,7 @@ impl api::IncomingWebhook for Checkout { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let event_type_data: checkout::CheckoutWebhookEventTypeBody = request .body .parse_struct("CheckoutWebhookBody") @@ -1281,7 +1281,10 @@ impl api::IncomingWebhook for Checkout { utils::Encode::::encode_to_value(&payment_response) .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)? }; - Ok(resource_object) + // Ideally this should be a strict type that has type information + // PII information is likely being logged here when this response will be logged. + + Ok(Box::new(resource_object)) } fn get_dispute_details( diff --git a/crates/router/src/connector/coinbase.rs b/crates/router/src/connector/coinbase.rs index 5704ea15b005..9c0a06a52c90 100644 --- a/crates/router/src/connector/coinbase.rs +++ b/crates/router/src/connector/coinbase.rs @@ -426,12 +426,12 @@ impl api::IncomingWebhook for Coinbase { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let notif: CoinbaseWebhookDetails = request .body .parse_struct("CoinbaseWebhookDetails") .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - Encode::::encode_to_value(¬if.event) - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed) + + Ok(Box::new(notif.event)) } } diff --git a/crates/router/src/connector/cryptopay.rs b/crates/router/src/connector/cryptopay.rs index d2d8fa0f1ec2..417a36145b92 100644 --- a/crates/router/src/connector/cryptopay.rs +++ b/crates/router/src/connector/cryptopay.rs @@ -455,13 +455,13 @@ impl api::IncomingWebhook for Cryptopay { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let notif: CryptopayWebhookDetails = request .body .parse_struct("CryptopayWebhookDetails") .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - Encode::::encode_to_value(¬if) - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed) + + Ok(Box::new(notif)) } } diff --git a/crates/router/src/connector/cybersource.rs b/crates/router/src/connector/cybersource.rs index ee6e93aebbd0..f69701f73958 100644 --- a/crates/router/src/connector/cybersource.rs +++ b/crates/router/src/connector/cybersource.rs @@ -805,7 +805,7 @@ impl api::IncomingWebhook for Cybersource { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/dlocal.rs b/crates/router/src/connector/dlocal.rs index 64d3e6f1c12f..4ae3a292fdae 100644 --- a/crates/router/src/connector/dlocal.rs +++ b/crates/router/src/connector/dlocal.rs @@ -674,7 +674,7 @@ impl api::IncomingWebhook for Dlocal { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/dummyconnector.rs b/crates/router/src/connector/dummyconnector.rs index b501936b8713..9edcd957ff09 100644 --- a/crates/router/src/connector/dummyconnector.rs +++ b/crates/router/src/connector/dummyconnector.rs @@ -579,7 +579,7 @@ impl api::IncomingWebhook for DummyConnector { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/fiserv.rs b/crates/router/src/connector/fiserv.rs index 093f71b3da14..2bdb7177d941 100644 --- a/crates/router/src/connector/fiserv.rs +++ b/crates/router/src/connector/fiserv.rs @@ -787,7 +787,7 @@ impl api::IncomingWebhook for Fiserv { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/forte.rs b/crates/router/src/connector/forte.rs index 40448c01fabf..3aa7cee32878 100644 --- a/crates/router/src/connector/forte.rs +++ b/crates/router/src/connector/forte.rs @@ -669,7 +669,7 @@ impl api::IncomingWebhook for Forte { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/globalpay.rs b/crates/router/src/connector/globalpay.rs index cfa1349633b2..26494d349b88 100644 --- a/crates/router/src/connector/globalpay.rs +++ b/crates/router/src/connector/globalpay.rs @@ -932,14 +932,15 @@ impl api::IncomingWebhook for Globalpay { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let details = std::str::from_utf8(request.body) .into_report() .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - let res_json = serde_json::from_str(details) - .into_report() - .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - Ok(res_json) + Ok(Box::new( + serde_json::from_str(details) + .into_report() + .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?, + )) } } diff --git a/crates/router/src/connector/globepay.rs b/crates/router/src/connector/globepay.rs index 547bf66fb7d5..9ebea6087f42 100644 --- a/crates/router/src/connector/globepay.rs +++ b/crates/router/src/connector/globepay.rs @@ -508,7 +508,7 @@ impl api::IncomingWebhook for Globepay { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/gocardless.rs b/crates/router/src/connector/gocardless.rs index 1a6ac8441652..d25357121b66 100644 --- a/crates/router/src/connector/gocardless.rs +++ b/crates/router/src/connector/gocardless.rs @@ -843,7 +843,7 @@ impl api::IncomingWebhook for Gocardless { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let details: gocardless::GocardlessWebhookEvent = request .body .parse_struct("GocardlessWebhookEvent") @@ -851,19 +851,14 @@ impl api::IncomingWebhook for Gocardless { let first_event = details .events .first() - .ok_or_else(|| errors::ConnectorError::WebhookReferenceIdNotFound)?; + .ok_or_else(|| errors::ConnectorError::WebhookReferenceIdNotFound)? + .clone(); match first_event.resource_type { - transformers::WebhookResourceType::Payments => serde_json::to_value( - gocardless::GocardlessPaymentsResponse::try_from(first_event)?, - ) - .into_report() - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed), - transformers::WebhookResourceType::Refunds => serde_json::to_value(first_event) - .into_report() - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed), - transformers::WebhookResourceType::Mandates => serde_json::to_value(first_event) - .into_report() - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed), + transformers::WebhookResourceType::Payments => Ok(Box::new( + gocardless::GocardlessPaymentsResponse::try_from(&first_event)?, + )), + transformers::WebhookResourceType::Refunds + | transformers::WebhookResourceType::Mandates => Ok(Box::new(first_event)), } } } diff --git a/crates/router/src/connector/gocardless/transformers.rs b/crates/router/src/connector/gocardless/transformers.rs index d3b2d244760f..72204b511518 100644 --- a/crates/router/src/connector/gocardless/transformers.rs +++ b/crates/router/src/connector/gocardless/transformers.rs @@ -862,14 +862,14 @@ pub struct GocardlessWebhookEvent { pub events: Vec, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct WebhookEvent { pub resource_type: WebhookResourceType, pub action: WebhookAction, pub links: WebhooksLink, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum WebhookResourceType { Payments, @@ -877,7 +877,7 @@ pub enum WebhookResourceType { Mandates, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(untagged)] pub enum WebhookAction { PaymentsAction(PaymentsAction), @@ -885,7 +885,7 @@ pub enum WebhookAction { MandatesAction(MandatesAction), } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum PaymentsAction { Created, @@ -901,7 +901,7 @@ pub enum PaymentsAction { ResubmissionRequired, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum RefundsAction { Created, @@ -912,7 +912,7 @@ pub enum RefundsAction { FundsReturned, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum MandatesAction { Created, @@ -931,7 +931,7 @@ pub enum MandatesAction { Blocked, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(untagged)] pub enum WebhooksLink { PaymentWebhooksLink(PaymentWebhooksLink), @@ -939,17 +939,17 @@ pub enum WebhooksLink { MandateWebhookLink(MandateWebhookLink), } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct RefundWebhookLink { pub refund: String, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct PaymentWebhooksLink { pub payment: String, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct MandateWebhookLink { pub mandate: String, } diff --git a/crates/router/src/connector/helcim.rs b/crates/router/src/connector/helcim.rs index f7089bbd41b5..a1781a92ddf5 100644 --- a/crates/router/src/connector/helcim.rs +++ b/crates/router/src/connector/helcim.rs @@ -771,7 +771,7 @@ impl api::IncomingWebhook for Helcim { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/iatapay.rs b/crates/router/src/connector/iatapay.rs index 008047c1d366..ba4b95f43808 100644 --- a/crates/router/src/connector/iatapay.rs +++ b/crates/router/src/connector/iatapay.rs @@ -691,13 +691,13 @@ impl api::IncomingWebhook for Iatapay { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let notif: IatapayPaymentsResponse = request .body .parse_struct("IatapayPaymentsResponse") .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - Encode::::encode_to_value(¬if) - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed) + + Ok(Box::new(notif)) } } diff --git a/crates/router/src/connector/klarna.rs b/crates/router/src/connector/klarna.rs index 3670f65a2f02..f34414e737ff 100644 --- a/crates/router/src/connector/klarna.rs +++ b/crates/router/src/connector/klarna.rs @@ -520,7 +520,7 @@ impl api::IncomingWebhook for Klarna { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/mollie.rs b/crates/router/src/connector/mollie.rs index ef3eb6a3e7b3..76deb0b2be88 100644 --- a/crates/router/src/connector/mollie.rs +++ b/crates/router/src/connector/mollie.rs @@ -582,7 +582,7 @@ impl api::IncomingWebhook for Mollie { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/multisafepay.rs b/crates/router/src/connector/multisafepay.rs index 9dc54e7b72e3..1f1099af0e71 100644 --- a/crates/router/src/connector/multisafepay.rs +++ b/crates/router/src/connector/multisafepay.rs @@ -523,7 +523,7 @@ impl api::IncomingWebhook for Multisafepay { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/nexinets.rs b/crates/router/src/connector/nexinets.rs index f2e57792f284..a67a29d74ffe 100644 --- a/crates/router/src/connector/nexinets.rs +++ b/crates/router/src/connector/nexinets.rs @@ -682,7 +682,7 @@ impl api::IncomingWebhook for Nexinets { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/nmi.rs b/crates/router/src/connector/nmi.rs index d7e9cd78bb88..eaede225d38f 100644 --- a/crates/router/src/connector/nmi.rs +++ b/crates/router/src/connector/nmi.rs @@ -667,7 +667,7 @@ impl api::IncomingWebhook for Nmi { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/noon.rs b/crates/router/src/connector/noon.rs index 0ea73efd94bd..b6ed231e5b50 100644 --- a/crates/router/src/connector/noon.rs +++ b/crates/router/src/connector/noon.rs @@ -744,16 +744,12 @@ impl api::IncomingWebhook for Noon { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let resource: noon::NoonWebhookObject = request .body .parse_struct("NoonWebhookObject") .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - let res_json = serde_json::to_value(noon::NoonPaymentsResponse::from(resource)) - .into_report() - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - - Ok(res_json) + Ok(Box::new(noon::NoonPaymentsResponse::from(resource))) } } diff --git a/crates/router/src/connector/nuvei.rs b/crates/router/src/connector/nuvei.rs index 15702829d378..7a9f3af37f0c 100644 --- a/crates/router/src/connector/nuvei.rs +++ b/crates/router/src/connector/nuvei.rs @@ -25,7 +25,7 @@ use crate::{ storage::enums, ErrorResponse, Response, }, - utils::{self as common_utils, ByteSliceExt, Encode}, + utils::{self as common_utils, ByteSliceExt}, }; #[derive(Debug, Clone)] @@ -963,12 +963,13 @@ impl api::IncomingWebhook for Nuvei { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let body = serde_urlencoded::from_str::(&request.query_params) .into_report() .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; let payment_response = nuvei::NuveiPaymentsResponse::from(body); - Encode::::encode_to_value(&payment_response).switch() + + Ok(Box::new(payment_response)) } } diff --git a/crates/router/src/connector/opayo.rs b/crates/router/src/connector/opayo.rs index cc517ca1f3b8..ba0fb2046b7c 100644 --- a/crates/router/src/connector/opayo.rs +++ b/crates/router/src/connector/opayo.rs @@ -533,7 +533,7 @@ impl api::IncomingWebhook for Opayo { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/opennode.rs b/crates/router/src/connector/opennode.rs index 3151403a5534..41d1e6c3d88c 100644 --- a/crates/router/src/connector/opennode.rs +++ b/crates/router/src/connector/opennode.rs @@ -420,11 +420,11 @@ impl api::IncomingWebhook for Opennode { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let notif = serde_urlencoded::from_bytes::(request.body) .into_report() .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - Encode::::encode_to_value(¬if.status) - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed) + + Ok(Box::new(notif.status)) } } diff --git a/crates/router/src/connector/payeezy.rs b/crates/router/src/connector/payeezy.rs index 8bb8eaa8b4c2..33a8ec65152e 100644 --- a/crates/router/src/connector/payeezy.rs +++ b/crates/router/src/connector/payeezy.rs @@ -585,7 +585,7 @@ impl api::IncomingWebhook for Payeezy { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/payme.rs b/crates/router/src/connector/payme.rs index ef10c6d00878..1e67f8a9f350 100644 --- a/crates/router/src/connector/payme.rs +++ b/crates/router/src/connector/payme.rs @@ -1077,32 +1077,24 @@ impl api::IncomingWebhook for Payme { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let resource = serde_urlencoded::from_bytes::(request.body) .into_report() .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - let res_json = match resource.notify_type { + match resource.notify_type { transformers::NotifyType::SaleComplete | transformers::NotifyType::SaleAuthorized | transformers::NotifyType::SaleFailure => { - serde_json::to_value(payme::PaymePaySaleResponse::from(resource)) - .into_report() - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed) - } - transformers::NotifyType::Refund => { - serde_json::to_value(payme::PaymeQueryTransactionResponse::from(resource)) - .into_report() - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed) + Ok(Box::new(payme::PaymePaySaleResponse::from(resource))) } + transformers::NotifyType::Refund => Ok(Box::new( + payme::PaymeQueryTransactionResponse::from(resource), + )), transformers::NotifyType::SaleChargeback - | transformers::NotifyType::SaleChargebackRefund => serde_json::to_value(resource) - .into_report() - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed), - }?; - - Ok(res_json) + | transformers::NotifyType::SaleChargebackRefund => Ok(Box::new(resource)), + } } fn get_dispute_details( diff --git a/crates/router/src/connector/paypal.rs b/crates/router/src/connector/paypal.rs index d4ab481eb9de..e514ebbed2fc 100644 --- a/crates/router/src/connector/paypal.rs +++ b/crates/router/src/connector/paypal.rs @@ -1189,33 +1189,24 @@ impl api::IncomingWebhook for Paypal { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let details: paypal::PaypalWebhooksBody = request .body .parse_struct("PaypalWebhooksBody") .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - let sync_payload = match details.resource { - paypal::PaypalResource::PaypalCardWebhooks(resource) => serde_json::to_value( + Ok(match details.resource { + paypal::PaypalResource::PaypalCardWebhooks(resource) => Box::new( paypal::PaypalPaymentsSyncResponse::try_from((*resource, details.event_type))?, - ) - .into_report() - .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?, - paypal::PaypalResource::PaypalRedirectsWebhooks(resource) => serde_json::to_value( + ), + paypal::PaypalResource::PaypalRedirectsWebhooks(resource) => Box::new( paypal::PaypalOrdersResponse::try_from((*resource, details.event_type))?, - ) - .into_report() - .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?, - paypal::PaypalResource::PaypalRefundWebhooks(resource) => serde_json::to_value( + ), + paypal::PaypalResource::PaypalRefundWebhooks(resource) => Box::new( paypal::RefundSyncResponse::try_from((*resource, details.event_type))?, - ) - .into_report() - .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?, - paypal::PaypalResource::PaypalDisputeWebhooks(_) => serde_json::to_value(details) - .into_report() - .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?, - }; - Ok(sync_payload) + ), + paypal::PaypalResource::PaypalDisputeWebhooks(_) => Box::new(details), + }) } fn get_dispute_details( diff --git a/crates/router/src/connector/payu.rs b/crates/router/src/connector/payu.rs index 9a8d4734f837..2868b5de0523 100644 --- a/crates/router/src/connector/payu.rs +++ b/crates/router/src/connector/payu.rs @@ -758,7 +758,7 @@ impl api::IncomingWebhook for Payu { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/powertranz.rs b/crates/router/src/connector/powertranz.rs index 04851dd1781a..d24fd27f1052 100644 --- a/crates/router/src/connector/powertranz.rs +++ b/crates/router/src/connector/powertranz.rs @@ -610,7 +610,7 @@ impl api::IncomingWebhook for Powertranz { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/prophetpay.rs b/crates/router/src/connector/prophetpay.rs index 6765fad2653d..e5ebe6331ba2 100644 --- a/crates/router/src/connector/prophetpay.rs +++ b/crates/router/src/connector/prophetpay.rs @@ -706,7 +706,7 @@ impl api::IncomingWebhook for Prophetpay { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/rapyd.rs b/crates/router/src/connector/rapyd.rs index cd8893d0d7b1..91a538f9991b 100644 --- a/crates/router/src/connector/rapyd.rs +++ b/crates/router/src/connector/rapyd.rs @@ -900,7 +900,7 @@ impl api::IncomingWebhook for Rapyd { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let webhook: transformers::RapydIncomingWebhook = request .body .parse_struct("RapydIncomingWebhook") @@ -923,7 +923,7 @@ impl api::IncomingWebhook for Rapyd { .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)? } }; - Ok(res_json) + Ok(Box::new(res_json)) } fn get_dispute_details( diff --git a/crates/router/src/connector/shift4.rs b/crates/router/src/connector/shift4.rs index 98eb895db548..6f3a2b802014 100644 --- a/crates/router/src/connector/shift4.rs +++ b/crates/router/src/connector/shift4.rs @@ -815,11 +815,13 @@ impl api::IncomingWebhook for Shift4 { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let details: shift4::Shift4WebhookObjectResource = request .body .parse_struct("Shift4WebhookObjectResource") .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - Ok(details.data) + // Ideally this should be a strict type that has type information + // PII information is likely being logged here when this response will be logged + Ok(Box::new(details.data)) } } diff --git a/crates/router/src/connector/square.rs b/crates/router/src/connector/square.rs index 1d4d7e95dfa3..d836285755d4 100644 --- a/crates/router/src/connector/square.rs +++ b/crates/router/src/connector/square.rs @@ -915,24 +915,19 @@ impl api::IncomingWebhook for Square { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let details: square::SquareWebhookBody = request .body .parse_struct("SquareWebhookObject") .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; - let reference_object = match details.data.object { + Ok(match details.data.object { square::SquareWebhookObject::Payment(square_payments_response_details) => { - serde_json::to_value(square_payments_response_details) - .into_report() - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)? + Box::new(square_payments_response_details) } square::SquareWebhookObject::Refund(square_refund_response_details) => { - serde_json::to_value(square_refund_response_details) - .into_report() - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)? + Box::new(square_refund_response_details) } - }; - Ok(reference_object) + }) } } diff --git a/crates/router/src/connector/stax.rs b/crates/router/src/connector/stax.rs index 0cfd2b89cd1a..024211c8caaa 100644 --- a/crates/router/src/connector/stax.rs +++ b/crates/router/src/connector/stax.rs @@ -886,10 +886,10 @@ impl api::IncomingWebhook for Stax { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let reference_object: serde_json::Value = serde_json::from_slice(request.body) .into_report() .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - Ok(reference_object) + Ok(Box::new(reference_object)) } } diff --git a/crates/router/src/connector/stripe.rs b/crates/router/src/connector/stripe.rs index 3f1263657e83..ccf843ec78d6 100644 --- a/crates/router/src/connector/stripe.rs +++ b/crates/router/src/connector/stripe.rs @@ -2057,13 +2057,13 @@ impl api::IncomingWebhook for Stripe { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let details: stripe::WebhookEventObjectResource = request .body .parse_struct("WebhookEventObjectResource") .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - Ok(details.data.object) + Ok(Box::new(details.data.object)) } fn get_dispute_details( &self, diff --git a/crates/router/src/connector/trustpay.rs b/crates/router/src/connector/trustpay.rs index 7509131afeef..65ab5a7ba58d 100644 --- a/crates/router/src/connector/trustpay.rs +++ b/crates/router/src/connector/trustpay.rs @@ -906,16 +906,12 @@ impl api::IncomingWebhook for Trustpay { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let details: trustpay::TrustpayWebhookResponse = request .body .parse_struct("TrustpayWebhookResponse") .switch()?; - let res_json = utils::Encode::::encode_to_value( - &details.payment_information, - ) - .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - Ok(res_json) + Ok(Box::new(details.payment_information)) } fn get_webhook_source_verification_algorithm( diff --git a/crates/router/src/connector/tsys.rs b/crates/router/src/connector/tsys.rs index 71cef4be2afd..0143f5855ade 100644 --- a/crates/router/src/connector/tsys.rs +++ b/crates/router/src/connector/tsys.rs @@ -625,7 +625,7 @@ impl api::IncomingWebhook for Tsys { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/volt.rs b/crates/router/src/connector/volt.rs index 3697b8c8923f..43b6b3a3406d 100644 --- a/crates/router/src/connector/volt.rs +++ b/crates/router/src/connector/volt.rs @@ -589,7 +589,7 @@ impl api::IncomingWebhook for Volt { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/wise.rs b/crates/router/src/connector/wise.rs index 5eba54eab4f7..865dcd5fff35 100644 --- a/crates/router/src/connector/wise.rs +++ b/crates/router/src/connector/wise.rs @@ -710,7 +710,7 @@ impl api::IncomingWebhook for Wise { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/worldline.rs b/crates/router/src/connector/worldline.rs index 7fcca08d8bfe..3d928624df8f 100644 --- a/crates/router/src/connector/worldline.rs +++ b/crates/router/src/connector/worldline.rs @@ -808,14 +808,16 @@ impl api::IncomingWebhook for Worldline { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let details = request .body .parse_struct::("WorldlineWebhookObjectId") .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)? .payment .ok_or(errors::ConnectorError::WebhookResourceObjectNotFound)?; - Ok(details) + // Ideally this should be a strict type that has type information + // PII information is likely being logged here when this response will be logged + Ok(Box::new(details)) } fn get_webhook_api_response( diff --git a/crates/router/src/connector/worldpay.rs b/crates/router/src/connector/worldpay.rs index 60579fb5dd3e..ef01aa9a6ada 100644 --- a/crates/router/src/connector/worldpay.rs +++ b/crates/router/src/connector/worldpay.rs @@ -754,15 +754,12 @@ impl api::IncomingWebhook for Worldpay { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let body: WorldpayWebhookEventType = request .body .parse_struct("WorldpayWebhookEventType") .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; let psync_body = WorldpayEventResponse::try_from(body)?; - let res_json = serde_json::to_value(psync_body) - .into_report() - .change_context(errors::ConnectorError::WebhookResponseEncodingFailed)?; - Ok(res_json) + Ok(Box::new(psync_body)) } } diff --git a/crates/router/src/connector/zen.rs b/crates/router/src/connector/zen.rs index bdbdf623f934..102d54bab427 100644 --- a/crates/router/src/connector/zen.rs +++ b/crates/router/src/connector/zen.rs @@ -668,11 +668,11 @@ impl api::IncomingWebhook for Zen { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let reference_object: serde_json::Value = serde_json::from_slice(request.body) .into_report() .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - Ok(reference_object) + Ok(Box::new(reference_object)) } fn get_webhook_api_response( &self, diff --git a/crates/router/src/core/webhooks.rs b/crates/router/src/core/webhooks.rs index db53a3b56a15..9bbe35ba2a9d 100644 --- a/crates/router/src/core/webhooks.rs +++ b/crates/router/src/core/webhooks.rs @@ -1,16 +1,17 @@ pub mod types; pub mod utils; -use std::str::FromStr; +use std::{str::FromStr, time::Instant}; +use actix_web::FromRequest; use api_models::{ payments::HeaderPayload, webhooks::{self, WebhookResponseTracker}, }; -use common_utils::errors::ReportSwitchExt; +use common_utils::{errors::ReportSwitchExt, events::ApiEventsType}; use error_stack::{report, IntoReport, ResultExt}; use masking::ExposeInterface; -use router_env::{instrument, tracing}; +use router_env::{instrument, tracing, tracing_actix_web::RequestId}; use super::{errors::StorageErrorExt, metrics}; #[cfg(feature = "stripe")] @@ -24,9 +25,10 @@ use crate::{ payments, refunds, }, db::StorageInterface, + events::api_logs::ApiEvent, logger, - routes::{lock_utils, metrics::request::add_attributes, AppState}, - services, + routes::{app::AppStateInfo, lock_utils, metrics::request::add_attributes, AppState}, + services::{self, authentication as auth}, types::{ self as router_types, api::{self, mandates::MandateResponseExt}, @@ -860,6 +862,7 @@ pub async fn trigger_webhook_to_merchant( } pub async fn webhooks_wrapper( + flow: &impl router_env::types::FlowMetric, state: AppState, req: &actix_web::HttpRequest, merchant_account: domain::MerchantAccount, @@ -867,21 +870,64 @@ pub async fn webhooks_wrapper RouterResponse { - let (application_response, _webhooks_response_tracker) = Box::pin(webhooks_core::( - state, - req, - merchant_account, - key_store, - connector_name_or_mca_id, - body, - )) - .await?; + let start_instant = Instant::now(); + let (application_response, webhooks_response_tracker, serialized_req) = + Box::pin(webhooks_core::( + state.clone(), + req, + merchant_account.clone(), + key_store, + connector_name_or_mca_id, + body.clone(), + )) + .await?; + let request_duration = Instant::now() + .saturating_duration_since(start_instant) + .as_millis(); + + let request_id = RequestId::extract(req) + .await + .into_report() + .attach_printable("Unable to extract request id from request") + .change_context(errors::ApiErrorResponse::InternalServerError)?; + let auth_type = auth::AuthenticationType::WebhookAuth { + merchant_id: merchant_account.merchant_id.clone(), + }; + let status_code = 200; + let api_event = ApiEventsType::Webhooks { + connector: connector_name_or_mca_id.to_string(), + payment_id: webhooks_response_tracker.get_payment_id(), + }; + let response_value = serde_json::to_value(&webhooks_response_tracker) + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not convert webhook effect to string")?; + + let api_event = ApiEvent::new( + flow, + &request_id, + request_duration, + status_code, + serialized_req, + Some(response_value), + None, + auth_type, + api_event, + req, + ); + match api_event.clone().try_into() { + Ok(event) => { + state.event_handler().log_event(event); + } + Err(err) => { + logger::error!(error=?err, event=?api_event, "Error Logging API Event"); + } + } Ok(application_response) } #[instrument(skip_all)] - pub async fn webhooks_core( state: AppState, req: &actix_web::HttpRequest, @@ -892,6 +938,7 @@ pub async fn webhooks_core errors::RouterResult<( services::ApplicationResponse, WebhookResponseTracker, + serde_json::Value, )> { metrics::WEBHOOK_INCOMING_COUNT.add( &metrics::CONTEXT, @@ -973,7 +1020,11 @@ pub async fn webhooks_core = Box::new(serde_json::Value::Null); let webhook_effect = if process_webhook_further && !matches!(flow_type, api::WebhookFlow::ReturnResponse) { @@ -1072,14 +1124,21 @@ pub async fn webhooks_core::encode_to_vec(&event_object) + resource_object: event_object + .raw_serialize() + .and_then(|ref val| serde_json::to_vec(val)) + .into_report() + .change_context(errors::ParsingError::EncodeError("byte-vec")) + .attach_printable_lazy(|| { + "Unable to convert webhook paylaod to a value".to_string() + }) .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable( "There was an issue when encoding the incoming webhook body to bytes", @@ -1184,7 +1243,12 @@ pub async fn webhooks_core( let (merchant_id, connector_id_or_name) = path.into_inner(); Box::pin(api::server_wrap( - flow, + flow.clone(), state, &req, (), |state, auth, _| { webhooks::webhooks_wrapper::( + &flow, state.to_owned(), &req, auth.merchant_account, diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index da4dec2eec8a..4277205b0231 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -54,6 +54,9 @@ pub enum AuthenticationType { PublishableKey { merchant_id: String, }, + WebhookAuth { + merchant_id: String, + }, NoAuth, } @@ -69,7 +72,8 @@ impl AuthenticationType { | Self::MerchantJWT { merchant_id, user_id: _, - } => Some(merchant_id.as_ref()), + } + | Self::WebhookAuth { merchant_id } => Some(merchant_id.as_ref()), Self::AdminApiKey | Self::NoAuth => None, } } diff --git a/crates/router/src/types/api/webhooks.rs b/crates/router/src/types/api/webhooks.rs index 4bde2608c93a..52f5300d9be5 100644 --- a/crates/router/src/types/api/webhooks.rs +++ b/crates/router/src/types/api/webhooks.rs @@ -254,7 +254,7 @@ pub trait IncomingWebhook: ConnectorCommon + Sync { fn get_webhook_resource_object( &self, _request: &IncomingWebhookRequestDetails<'_>, - ) -> CustomResult; + ) -> CustomResult, errors::ConnectorError>; fn get_webhook_api_response( &self, From c39beb2501e63bbf7fd41bbc947280d7ff5a71dc Mon Sep 17 00:00:00 2001 From: Sahkal Poddar Date: Fri, 17 Nov 2023 13:40:29 +0530 Subject: [PATCH 031/146] feat(router): Custom payment link config for payment create (#2741) Co-authored-by: Kashif <46213975+kashif-m@users.noreply.github.com> Co-authored-by: Kashif Co-authored-by: Sahkal Poddar --- crates/api_models/src/admin.rs | 5 +++ crates/api_models/src/payments.rs | 2 ++ crates/diesel_models/src/payment_link.rs | 5 ++- crates/diesel_models/src/schema.rs | 1 + crates/router/src/core/payment_link.rs | 32 ++++++++++++------- .../payments/operations/payment_create.rs | 6 ++++ .../down.sql | 2 ++ .../up.sql | 2 ++ openapi/openapi_spec.json | 10 +++++- 9 files changed, 51 insertions(+), 14 deletions(-) create mode 100644 migrations/2023-10-31-070509_add_payment_link_config_in_payment_link_db/down.sql create mode 100644 migrations/2023-10-31-070509_add_payment_link_config_in_payment_link_db/up.sql diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index 979214a071a9..6b9928734cef 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -455,6 +455,11 @@ pub struct PrimaryBusinessDetails { #[derive(Clone, Debug, Deserialize, ToSchema, Serialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct PaymentLinkConfig { + #[schema( + max_length = 255, + max_length = 255, + example = "https://i.imgur.com/RfxPFQo.png" + )] pub merchant_logo: Option, pub color_scheme: Option, } diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index d924fb2e4f62..b479f4442ba6 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -3150,6 +3150,8 @@ pub struct PaymentLinkObject { #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub link_expiry: Option, pub merchant_custom_domain_name: Option, + #[schema(value_type = PaymentLinkConfig)] + pub payment_link_config: Option, /// Custom merchant name for payment link pub custom_merchant_name: Option, } diff --git a/crates/diesel_models/src/payment_link.rs b/crates/diesel_models/src/payment_link.rs index 50cc5e89cee9..264cc915b35a 100644 --- a/crates/diesel_models/src/payment_link.rs +++ b/crates/diesel_models/src/payment_link.rs @@ -4,7 +4,7 @@ use time::PrimitiveDateTime; use crate::{enums as storage_enums, schema::payment_link}; -#[derive(Clone, Debug, Eq, PartialEq, Identifiable, Queryable, Serialize, Deserialize)] +#[derive(Clone, Debug, Identifiable, Queryable, Serialize, Deserialize)] #[diesel(table_name = payment_link)] #[diesel(primary_key(payment_link_id))] pub struct PaymentLink { @@ -21,7 +21,9 @@ pub struct PaymentLink { #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub fulfilment_time: Option, pub custom_merchant_name: Option, + pub payment_link_config: Option, } + #[derive( Clone, Debug, @@ -48,4 +50,5 @@ pub struct PaymentLinkNew { #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub fulfilment_time: Option, pub custom_merchant_name: Option, + pub payment_link_config: Option, } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 72d5217038c1..e9db5714bed8 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -668,6 +668,7 @@ diesel::table! { fulfilment_time -> Nullable, #[max_length = 64] custom_merchant_name -> Nullable, + payment_link_config -> Nullable, } } diff --git a/crates/router/src/core/payment_link.rs b/crates/router/src/core/payment_link.rs index 2ea6a4d7f219..89d345b28674 100644 --- a/crates/router/src/core/payment_link.rs +++ b/crates/router/src/core/payment_link.rs @@ -3,7 +3,7 @@ use common_utils::{ consts::{ DEFAULT_BACKGROUND_COLOR, DEFAULT_MERCHANT_LOGO, DEFAULT_PRODUCT_IMG, DEFAULT_SDK_THEME, }, - ext_traits::ValueExt, + ext_traits::{OptionExt, ValueExt}, }; use error_stack::{IntoReport, ResultExt}; use masking::{PeekInterface, Secret}; @@ -15,7 +15,6 @@ use crate::{ routes::AppState, services, types::{domain, storage::enums as storage_enums, transformers::ForeignFrom}, - utils::OptionExt, }; pub async fn retrieve_payment_link( @@ -71,16 +70,11 @@ pub async fn intiate_payment_link_flow( .await .to_not_found_response(errors::ApiErrorResponse::PaymentLinkNotFound)?; - let payment_link_config = merchant_account - .payment_link_config - .map(|pl_config| { - serde_json::from_value::(pl_config) - .into_report() - .change_context(errors::ApiErrorResponse::InvalidDataValue { - field_name: "payment_link_config", - }) - }) - .transpose()?; + let payment_link_config = if let Some(pl_config) = payment_link.payment_link_config.clone() { + extract_payment_link_config(Some(pl_config))? + } else { + extract_payment_link_config(merchant_account.payment_link_config.clone())? + }; let order_details = validate_order_details(payment_intent.order_details)?; @@ -235,3 +229,17 @@ fn validate_order_details( }); Ok(updated_order_details) } + +fn extract_payment_link_config( + pl_config: Option, +) -> Result, error_stack::Report> { + pl_config + .map(|config| { + serde_json::from_value::(config) + .into_report() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "payment_link_config", + }) + }) + .transpose() +} diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 974f5e6ab5b6..1fd4c7014c35 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -800,6 +800,11 @@ async fn create_payment_link( merchant_id.clone(), payment_id.clone() ); + + let payment_link_config = payment_link_object.payment_link_config.map(|pl_config|{ + common_utils::ext_traits::Encode::::encode_to_value(&pl_config) + }).transpose().change_context(errors::ApiErrorResponse::InvalidDataValue { field_name: "payment_link_config" })?; + let payment_link_req = storage::PaymentLinkNew { payment_link_id: payment_link_id.clone(), payment_id: payment_id.clone(), @@ -810,6 +815,7 @@ async fn create_payment_link( created_at, last_modified_at, fulfilment_time: payment_link_object.link_expiry, + payment_link_config, custom_merchant_name: payment_link_object.custom_merchant_name, }; let payment_link_db = db diff --git a/migrations/2023-10-31-070509_add_payment_link_config_in_payment_link_db/down.sql b/migrations/2023-10-31-070509_add_payment_link_config_in_payment_link_db/down.sql new file mode 100644 index 000000000000..b5ffba726937 --- /dev/null +++ b/migrations/2023-10-31-070509_add_payment_link_config_in_payment_link_db/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_link DROP COLUMN IF EXISTS payment_link_config; \ No newline at end of file diff --git a/migrations/2023-10-31-070509_add_payment_link_config_in_payment_link_db/up.sql b/migrations/2023-10-31-070509_add_payment_link_config_in_payment_link_db/up.sql new file mode 100644 index 000000000000..8940273ecd25 --- /dev/null +++ b/migrations/2023-10-31-070509_add_payment_link_config_in_payment_link_db/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE payment_link ADD COLUMN IF NOT EXISTS payment_link_config JSONB NULL; diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 55ff36c26ff7..be66a1bff92c 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -8221,7 +8221,9 @@ "properties": { "merchant_logo": { "type": "string", - "nullable": true + "example": "https://i.imgur.com/RfxPFQo.png", + "nullable": true, + "maxLength": 255 }, "color_scheme": { "allOf": [ @@ -8250,6 +8252,9 @@ }, "PaymentLinkObject": { "type": "object", + "required": [ + "payment_link_config" + ], "properties": { "link_expiry": { "type": "string", @@ -8260,6 +8265,9 @@ "type": "string", "nullable": true }, + "payment_link_config": { + "$ref": "#/components/schemas/PaymentLinkConfig" + }, "custom_merchant_name": { "type": "string", "description": "Custom merchant name for payment link", From 9a201ae698c2cf52e617660f82d5bf1df2e797ae Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Fri, 17 Nov 2023 17:48:42 +0530 Subject: [PATCH 032/146] fix(router): add rust locker url in proxy_bypass_urls (#2902) --- crates/router/src/services/api/client.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/router/src/services/api/client.rs b/crates/router/src/services/api/client.rs index 8eb6ab72f988..cc7353dcda6b 100644 --- a/crates/router/src/services/api/client.rs +++ b/crates/router/src/services/api/client.rs @@ -110,11 +110,15 @@ pub(super) fn create_client( pub fn proxy_bypass_urls(locker: &Locker) -> Vec { let locker_host = locker.host.to_owned(); + let locker_host_rs = locker.host_rs.to_owned(); let basilisk_host = locker.basilisk_host.to_owned(); vec![ format!("{locker_host}/cards/add"), format!("{locker_host}/cards/retrieve"), format!("{locker_host}/cards/delete"), + format!("{locker_host_rs}/cards/add"), + format!("{locker_host_rs}/cards/retrieve"), + format!("{locker_host_rs}/cards/delete"), format!("{locker_host}/card/addCard"), format!("{locker_host}/card/getCard"), format!("{locker_host}/card/deleteCard"), From 1d48a83c485c27cced7ce1441060333bbb54dbc7 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 17 Nov 2023 12:21:10 +0000 Subject: [PATCH 033/146] chore(version): v1.83.0 --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4270442611a8..d5d04d15669e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,26 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.83.0 (2023-11-17) + +### Features + +- **events:** Add incoming webhook payload to api events logger ([#2852](https://github.com/juspay/hyperswitch/pull/2852)) ([`aea390a`](https://github.com/juspay/hyperswitch/commit/aea390a6a1c331f8e0dbea4f41218e43f7323508)) +- **router:** Custom payment link config for payment create ([#2741](https://github.com/juspay/hyperswitch/pull/2741)) ([`c39beb2`](https://github.com/juspay/hyperswitch/commit/c39beb2501e63bbf7fd41bbc947280d7ff5a71dc)) + +### Bug Fixes + +- **router:** Add rust locker url in proxy_bypass_urls ([#2902](https://github.com/juspay/hyperswitch/pull/2902)) ([`9a201ae`](https://github.com/juspay/hyperswitch/commit/9a201ae698c2cf52e617660f82d5bf1df2e797ae)) + +### Documentation + +- **README:** Replace cloudformation deployment template with latest s3 url. ([#2891](https://github.com/juspay/hyperswitch/pull/2891)) ([`375108b`](https://github.com/juspay/hyperswitch/commit/375108b6df50e041fc9dbeb35a6a6b46b146037a)) + +**Full Changelog:** [`v1.82.0...v1.83.0`](https://github.com/juspay/hyperswitch/compare/v1.82.0...v1.83.0) + +- - - + + ## 1.82.0 (2023-11-17) ### Features From 606daa9367cac8c2ea926313019deab2f938b591 Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Fri, 17 Nov 2023 21:19:41 +0530 Subject: [PATCH 034/146] fix(router): add choice to use the appropriate key for jws verification (#2917) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../router/src/core/payment_methods/cards.rs | 39 +++++++++++-------- .../src/core/payment_methods/transformers.rs | 17 +++++++- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 4ab7d334f883..80daf66a6926 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -440,10 +440,11 @@ pub async fn get_payment_method_from_hs_locker<'a>( let jwe_body: services::JweBody = response .get_response_inner("JweBody") .change_context(errors::VaultError::FetchPaymentMethodFailed)?; - let decrypted_payload = payment_methods::get_decrypted_response_payload(jwekey, jwe_body) - .await - .change_context(errors::VaultError::FetchPaymentMethodFailed) - .attach_printable("Error getting decrypted response payload for get card")?; + let decrypted_payload = + payment_methods::get_decrypted_response_payload(jwekey, jwe_body, locker_choice) + .await + .change_context(errors::VaultError::FetchPaymentMethodFailed) + .attach_printable("Error getting decrypted response payload for get card")?; let get_card_resp: payment_methods::RetrieveCardResp = decrypted_payload .parse_struct("RetrieveCardResp") .change_context(errors::VaultError::FetchPaymentMethodFailed)?; @@ -490,10 +491,11 @@ pub async fn call_to_locker_hs<'a>( .get_response_inner("JweBody") .change_context(errors::VaultError::FetchCardFailed)?; - let decrypted_payload = payment_methods::get_decrypted_response_payload(jwekey, jwe_body) - .await - .change_context(errors::VaultError::SaveCardFailed) - .attach_printable("Error getting decrypted response payload")?; + let decrypted_payload = + payment_methods::get_decrypted_response_payload(jwekey, jwe_body, Some(locker_choice)) + .await + .change_context(errors::VaultError::SaveCardFailed) + .attach_printable("Error getting decrypted response payload")?; let stored_card_resp: payment_methods::StoreCardResp = decrypted_payload .parse_struct("StoreCardResp") .change_context(errors::VaultError::ResponseDeserializationFailed)?; @@ -557,10 +559,11 @@ pub async fn get_card_from_hs_locker<'a>( let jwe_body: services::JweBody = response .get_response_inner("JweBody") .change_context(errors::VaultError::FetchCardFailed)?; - let decrypted_payload = payment_methods::get_decrypted_response_payload(jwekey, jwe_body) - .await - .change_context(errors::VaultError::FetchCardFailed) - .attach_printable("Error getting decrypted response payload for get card")?; + let decrypted_payload = + payment_methods::get_decrypted_response_payload(jwekey, jwe_body, Some(locker_choice)) + .await + .change_context(errors::VaultError::FetchCardFailed) + .attach_printable("Error getting decrypted response payload for get card")?; let get_card_resp: payment_methods::RetrieveCardResp = decrypted_payload .parse_struct("RetrieveCardResp") .change_context(errors::VaultError::FetchCardFailed)?; @@ -609,10 +612,14 @@ pub async fn delete_card_from_hs_locker<'a>( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed while executing call_connector_api for delete card"); let jwe_body: services::JweBody = response.get_response_inner("JweBody")?; - let decrypted_payload = payment_methods::get_decrypted_response_payload(jwekey, jwe_body) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting decrypted response payload for delete card")?; + let decrypted_payload = payment_methods::get_decrypted_response_payload( + jwekey, + jwe_body, + Some(api_enums::LockerChoice::Basilisk), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error getting decrypted response payload for delete card")?; let delete_card_resp: payment_methods::DeleteCardResp = decrypted_payload .parse_struct("DeleteCardResp") .change_context(errors::ApiErrorResponse::InternalServerError)?; diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index 45182411c28c..3b4d057e6025 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -189,14 +189,27 @@ pub async fn get_decrypted_response_payload( #[cfg(not(feature = "kms"))] jwekey: &settings::Jwekey, #[cfg(feature = "kms")] jwekey: &settings::ActiveKmsSecrets, jwe_body: encryption::JweBody, + locker_choice: Option, ) -> CustomResult { + let target_locker = locker_choice.unwrap_or(api_enums::LockerChoice::Basilisk); + #[cfg(feature = "kms")] - let public_key = jwekey.jwekey.peek().vault_encryption_key.as_bytes(); + let public_key = match target_locker { + api_enums::LockerChoice::Basilisk => jwekey.jwekey.peek().vault_encryption_key.as_bytes(), + api_enums::LockerChoice::Tartarus => { + jwekey.jwekey.peek().rust_locker_encryption_key.as_bytes() + } + }; + #[cfg(feature = "kms")] let private_key = jwekey.jwekey.peek().vault_private_key.as_bytes(); #[cfg(not(feature = "kms"))] - let public_key = jwekey.vault_encryption_key.as_bytes(); + let public_key = match target_locker { + api_enums::LockerChoice::Basilisk => jwekey.vault_encryption_key.as_bytes(), + api_enums::LockerChoice::Tartarus => jwekey.rust_locker_encryption_key.as_bytes(), + }; + #[cfg(not(feature = "kms"))] let private_key = jwekey.vault_private_key.as_bytes(); From 0a88336b443e240d16837c43f6edd59455ad4cb6 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 17 Nov 2023 15:51:54 +0000 Subject: [PATCH 035/146] chore(version): v1.83.1 --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5d04d15669e..021a0326f025 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.83.1 (2023-11-17) + +### Bug Fixes + +- **router:** Add choice to use the appropriate key for jws verification ([#2917](https://github.com/juspay/hyperswitch/pull/2917)) ([`606daa9`](https://github.com/juspay/hyperswitch/commit/606daa9367cac8c2ea926313019deab2f938b591)) + +**Full Changelog:** [`v1.83.0...v1.83.1`](https://github.com/juspay/hyperswitch/compare/v1.83.0...v1.83.1) + +- - - + + ## 1.83.0 (2023-11-17) ### Features From bdcc138e8d84577fc99f9a9aef3484b66f98209a Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Fri, 17 Nov 2023 21:38:52 +0530 Subject: [PATCH 036/146] feat(connector): [BANKOFAMERICA] PSYNC Bugfix (#2897) --- .../connector/bankofamerica/transformers.rs | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index 20b2af48b168..a6fa8652b27d 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -273,7 +273,8 @@ pub enum BankofamericaPaymentStatus { impl ForeignFrom<(BankofamericaPaymentStatus, bool)> for enums::AttemptStatus { fn foreign_from((status, auto_capture): (BankofamericaPaymentStatus, bool)) -> Self { match status { - BankofamericaPaymentStatus::Authorized => { + BankofamericaPaymentStatus::Authorized + | BankofamericaPaymentStatus::AuthorizedPendingReview => { if auto_capture { // Because BankOfAmerica will return Payment Status as Authorized even in AutoCapture Payment Self::Pending @@ -281,7 +282,6 @@ impl ForeignFrom<(BankofamericaPaymentStatus, bool)> for enums::AttemptStatus { Self::Authorized } } - BankofamericaPaymentStatus::AuthorizedPendingReview => Self::Authorized, BankofamericaPaymentStatus::Succeeded | BankofamericaPaymentStatus::Transmitted => { Self::Charged } @@ -321,7 +321,7 @@ pub struct BankOfAmericaErrorInformationResponse { #[derive(Debug, Deserialize)] pub struct BankOfAmericaErrorInformation { reason: Option, - message: String, + message: Option, } impl @@ -369,7 +369,10 @@ impl BankOfAmericaPaymentsResponse::ErrorInformation(error_response) => Ok(Self { response: Err(types::ErrorResponse { code: consts::NO_ERROR_CODE.to_string(), - message: error_response.error_information.message, + message: error_response + .error_information + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), reason: error_response.error_information.reason, status_code: item.http_code, attempt_status: None, @@ -422,7 +425,10 @@ impl BankOfAmericaPaymentsResponse::ErrorInformation(error_response) => Ok(Self { response: Err(types::ErrorResponse { code: consts::NO_ERROR_CODE.to_string(), - message: error_response.error_information.message, + message: error_response + .error_information + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), reason: error_response.error_information.reason, status_code: item.http_code, attempt_status: None, @@ -475,7 +481,10 @@ impl BankOfAmericaPaymentsResponse::ErrorInformation(error_response) => Ok(Self { response: Err(types::ErrorResponse { code: consts::NO_ERROR_CODE.to_string(), - message: error_response.error_information.message, + message: error_response + .error_information + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), reason: error_response.error_information.reason, status_code: item.http_code, attempt_status: None, From 94897d841e25d0be8debdfe1ec674f28848e2ad4 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 17 Nov 2023 16:57:32 +0000 Subject: [PATCH 037/146] chore(version): v1.84.0 --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 021a0326f025..141bfd40ac5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.84.0 (2023-11-17) + +### Features + +- **connector:** [BANKOFAMERICA] PSYNC Bugfix ([#2897](https://github.com/juspay/hyperswitch/pull/2897)) ([`bdcc138`](https://github.com/juspay/hyperswitch/commit/bdcc138e8d84577fc99f9a9aef3484b66f98209a)) + +**Full Changelog:** [`v1.83.1...v1.84.0`](https://github.com/juspay/hyperswitch/compare/v1.83.1...v1.84.0) + +- - - + + ## 1.83.1 (2023-11-17) ### Bug Fixes From 25cef386b8876b43893f20b93cd68ece6e68412d Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Mon, 20 Nov 2023 12:54:55 +0530 Subject: [PATCH 038/146] feat(mca): Add new `auth_type` and a status field for mca (#2883) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- crates/api_models/src/admin.rs | 9 +++ crates/common_enums/src/enums.rs | 22 +++++++ crates/diesel_models/src/enums.rs | 7 ++- .../src/merchant_connector_account.rs | 4 ++ crates/diesel_models/src/schema.rs | 1 + crates/kgraph_utils/benches/evaluation.rs | 1 + crates/kgraph_utils/src/mca.rs | 1 + .../src/connector/square/transformers.rs | 1 + crates/router/src/core/admin.rs | 62 ++++++++++++++++++- crates/router/src/core/verification/utils.rs | 1 + .../src/db/merchant_connector_account.rs | 2 + crates/router/src/openapi.rs | 1 + crates/router/src/types.rs | 1 + .../domain/merchant_connector_account.rs | 7 +++ crates/router/src/types/transformers.rs | 1 + .../down.sql | 3 + .../up.sql | 11 ++++ openapi/openapi_spec.json | 25 +++++++- 18 files changed, 152 insertions(+), 8 deletions(-) create mode 100644 migrations/2023-11-12-131143_connector-status-column/down.sql create mode 100644 migrations/2023-11-12-131143_connector-status-column/up.sql diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index 6b9928734cef..efde4a048323 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -609,6 +609,9 @@ pub struct MerchantConnectorCreate { pub profile_id: Option, pub pm_auth_config: Option, + + #[schema(value_type = ConnectorStatus, example = "inactive")] + pub status: Option, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] @@ -714,6 +717,9 @@ pub struct MerchantConnectorResponse { pub applepay_verified_domains: Option>, pub pm_auth_config: Option, + + #[schema(value_type = ConnectorStatus, example = "inactive")] + pub status: api_enums::ConnectorStatus, } /// Create a new Merchant Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialized services like Fraud / Accounting etc." @@ -788,6 +794,9 @@ pub struct MerchantConnectorUpdate { pub connector_webhook_details: Option, pub pm_auth_config: Option, + + #[schema(value_type = ConnectorStatus, example = "inactive")] + pub status: Option, } ///Details of FrmConfigs are mentioned here... it should be passed in payment connector create api call, and stored in merchant_connector_table diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 8b1437fa8926..cf3c398f8f48 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -1857,3 +1857,25 @@ pub enum ApplePayFlow { Simplified, Manual, } + +#[derive( + Clone, + Copy, + Debug, + Eq, + PartialEq, + strum::Display, + strum::EnumString, + serde::Deserialize, + serde::Serialize, + ToSchema, + Default, +)] +#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum ConnectorStatus { + #[default] + Inactive, + Active, +} diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index ec021f0f51a5..817fee633190 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -3,9 +3,10 @@ pub mod diesel_exports { pub use super::{ DbAttemptStatus as AttemptStatus, DbAuthenticationType as AuthenticationType, DbCaptureMethod as CaptureMethod, DbCaptureStatus as CaptureStatus, - DbConnectorType as ConnectorType, DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency, - DbDisputeStage as DisputeStage, DbDisputeStatus as DisputeStatus, - DbEventClass as EventClass, DbEventObjectType as EventObjectType, DbEventType as EventType, + DbConnectorStatus as ConnectorStatus, DbConnectorType as ConnectorType, + DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency, DbDisputeStage as DisputeStage, + DbDisputeStatus as DisputeStatus, DbEventClass as EventClass, + DbEventObjectType as EventObjectType, DbEventType as EventType, DbFraudCheckStatus as FraudCheckStatus, DbFraudCheckType as FraudCheckType, DbFutureUsage as FutureUsage, DbIntentStatus as IntentStatus, DbMandateStatus as MandateStatus, DbMandateType as MandateType, diff --git a/crates/diesel_models/src/merchant_connector_account.rs b/crates/diesel_models/src/merchant_connector_account.rs index a4faa45ce4bc..e45ef0026261 100644 --- a/crates/diesel_models/src/merchant_connector_account.rs +++ b/crates/diesel_models/src/merchant_connector_account.rs @@ -42,6 +42,7 @@ pub struct MerchantConnectorAccount { #[diesel(deserialize_as = super::OptionalDieselArray)] pub applepay_verified_domains: Option>, pub pm_auth_config: Option, + pub status: storage_enums::ConnectorStatus, } #[derive(Clone, Debug, Insertable, router_derive::DebugAsDisplay)] @@ -70,6 +71,7 @@ pub struct MerchantConnectorAccountNew { #[diesel(deserialize_as = super::OptionalDieselArray)] pub applepay_verified_domains: Option>, pub pm_auth_config: Option, + pub status: storage_enums::ConnectorStatus, } #[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] @@ -93,6 +95,7 @@ pub struct MerchantConnectorAccountUpdateInternal { #[diesel(deserialize_as = super::OptionalDieselArray)] pub applepay_verified_domains: Option>, pub pm_auth_config: Option, + pub status: Option, } impl MerchantConnectorAccountUpdateInternal { @@ -115,6 +118,7 @@ impl MerchantConnectorAccountUpdateInternal { frm_config: self.frm_config, modified_at: self.modified_at.unwrap_or(source.modified_at), pm_auth_config: self.pm_auth_config, + status: self.status.unwrap_or(source.status), ..source } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index e9db5714bed8..190a123185e4 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -492,6 +492,7 @@ diesel::table! { profile_id -> Nullable, applepay_verified_domains -> Nullable>>, pm_auth_config -> Nullable, + status -> ConnectorStatus, } } diff --git a/crates/kgraph_utils/benches/evaluation.rs b/crates/kgraph_utils/benches/evaluation.rs index ecea12203f8a..6105dc85d7e6 100644 --- a/crates/kgraph_utils/benches/evaluation.rs +++ b/crates/kgraph_utils/benches/evaluation.rs @@ -65,6 +65,7 @@ fn build_test_data<'a>(total_enabled: usize, total_pm_types: usize) -> graph::Kn profile_id: None, applepay_verified_domains: None, pm_auth_config: None, + status: api_enums::ConnectorStatus::Inactive, }; kgraph_utils::mca::make_mca_graph(vec![stripe_account]).expect("Failed graph construction") diff --git a/crates/kgraph_utils/src/mca.rs b/crates/kgraph_utils/src/mca.rs index 34babd7a02bd..deea51bd8808 100644 --- a/crates/kgraph_utils/src/mca.rs +++ b/crates/kgraph_utils/src/mca.rs @@ -410,6 +410,7 @@ mod tests { profile_id: None, applepay_verified_domains: None, pm_auth_config: None, + status: api_enums::ConnectorStatus::Inactive, }; make_mca_graph(vec![stripe_account]).expect("Failed graph construction") diff --git a/crates/router/src/connector/square/transformers.rs b/crates/router/src/connector/square/transformers.rs index 54a7c461dbfc..dfb49e8e6775 100644 --- a/crates/router/src/connector/square/transformers.rs +++ b/crates/router/src/connector/square/transformers.rs @@ -334,6 +334,7 @@ impl TryFrom<&types::ConnectorAuthType> for SquareAuthType { | types::ConnectorAuthType::SignatureKey { .. } | types::ConnectorAuthType::MultiAuthKey { .. } | types::ConnectorAuthType::CurrencyAuthKey { .. } + | types::ConnectorAuthType::TemporaryAuth { .. } | types::ConnectorAuthType::NoKey { .. } => { Err(errors::ConnectorError::FailedToObtainAuthType.into()) } diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 39b4749535b7..c921a9164cb0 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -868,6 +868,15 @@ pub async fn create_payment_connector( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("error updating the merchant account when creating payment connector")?; + let (connector_status, disabled) = validate_status_and_disabled( + req.status, + req.disabled, + auth, + // The validate_status_and_disabled function will use this value only + // when the status can be active. So we are passing this as fallback. + api_enums::ConnectorStatus::Active, + )?; + let merchant_connector_account = domain::MerchantConnectorAccount { merchant_id: merchant_id.to_string(), connector_type: req.connector_type, @@ -886,7 +895,7 @@ pub async fn create_payment_connector( .attach_printable("Unable to encrypt connector account details")?, payment_methods_enabled, test_mode: req.test_mode, - disabled: req.disabled, + disabled, metadata: req.metadata, frm_configs, connector_label: Some(connector_label), @@ -911,6 +920,7 @@ pub async fn create_payment_connector( profile_id: Some(profile_id.clone()), applepay_verified_domains: None, pm_auth_config: req.pm_auth_config.clone(), + status: connector_status, }; let mut default_routing_config = @@ -1083,6 +1093,19 @@ pub async fn update_payment_connector( let frm_configs = get_frm_config_as_secret(req.frm_configs); + let auth: types::ConnectorAuthType = req + .connector_account_details + .clone() + .unwrap_or(mca.connector_account_details.clone().into_inner()) + .parse_value("ConnectorAuthType") + .change_context(errors::ApiErrorResponse::InvalidDataFormat { + field_name: "connector_account_details".to_string(), + expected_format: "auth_type and api_key".to_string(), + })?; + + let (connector_status, disabled) = + validate_status_and_disabled(req.status, req.disabled, auth, mca.status)?; + let payment_connector = storage::MerchantConnectorAccountUpdate::Update { merchant_id: None, connector_type: Some(req.connector_type), @@ -1098,7 +1121,7 @@ pub async fn update_payment_connector( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed while encrypting data")?, test_mode: req.test_mode, - disabled: req.disabled, + disabled, payment_methods_enabled, metadata: req.metadata, frm_configs, @@ -1115,6 +1138,7 @@ pub async fn update_payment_connector( }, applepay_verified_domains: None, pm_auth_config: req.pm_auth_config, + status: Some(connector_status), }; let updated_mca = db @@ -1722,3 +1746,37 @@ pub async fn validate_dummy_connector_enabled( Ok(()) } } + +pub fn validate_status_and_disabled( + status: Option, + disabled: Option, + auth: types::ConnectorAuthType, + current_status: api_enums::ConnectorStatus, +) -> RouterResult<(api_enums::ConnectorStatus, Option)> { + let connector_status = match (status, auth) { + (Some(common_enums::ConnectorStatus::Active), types::ConnectorAuthType::TemporaryAuth) => { + return Err(errors::ApiErrorResponse::InvalidRequestData { + message: "Connector status cannot be active when using TemporaryAuth".to_string(), + } + .into()); + } + (Some(status), _) => status, + (None, types::ConnectorAuthType::TemporaryAuth) => common_enums::ConnectorStatus::Inactive, + (None, _) => current_status, + }; + + let disabled = match (disabled, connector_status) { + (Some(true), common_enums::ConnectorStatus::Inactive) => { + return Err(errors::ApiErrorResponse::InvalidRequestData { + message: "Connector cannot be enabled when connector_status is inactive or when using TemporaryAuth" + .to_string(), + } + .into()); + } + (Some(disabled), _) => Some(disabled), + (None, common_enums::ConnectorStatus::Inactive) => Some(true), + (None, _) => None, + }; + + Ok((connector_status, disabled)) +} diff --git a/crates/router/src/core/verification/utils.rs b/crates/router/src/core/verification/utils.rs index 433430507fb1..56960d3cb480 100644 --- a/crates/router/src/core/verification/utils.rs +++ b/crates/router/src/core/verification/utils.rs @@ -60,6 +60,7 @@ pub async fn check_existence_and_add_domain_to_db( applepay_verified_domains: Some(already_verified_domains.clone()), pm_auth_config: None, connector_label: None, + status: None, }; state .store diff --git a/crates/router/src/db/merchant_connector_account.rs b/crates/router/src/db/merchant_connector_account.rs index ecf52531f28a..4fbb8f19ccff 100644 --- a/crates/router/src/db/merchant_connector_account.rs +++ b/crates/router/src/db/merchant_connector_account.rs @@ -643,6 +643,7 @@ impl MerchantConnectorAccountInterface for MockDb { profile_id: t.profile_id, applepay_verified_domains: t.applepay_verified_domains, pm_auth_config: t.pm_auth_config, + status: t.status, }; accounts.push(account.clone()); account @@ -839,6 +840,7 @@ mod merchant_connector_account_cache_tests { profile_id: Some(profile_id.to_string()), applepay_verified_domains: None, pm_auth_config: None, + status: common_enums::ConnectorStatus::Inactive, }; db.insert_merchant_connector_account(mca.clone(), &merchant_key) diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index 095e1f45f93f..04ef90546cfa 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -174,6 +174,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::enums::AttemptStatus, api_models::enums::CaptureStatus, api_models::enums::ReconStatus, + api_models::enums::ConnectorStatus, api_models::admin::MerchantConnectorCreate, api_models::admin::MerchantConnectorUpdate, api_models::admin::PrimaryBusinessDetails, diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 7cf8f6b71fa5..ceeb93f69763 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -900,6 +900,7 @@ pub struct ResponseRouterData { #[derive(Default, Debug, Clone, serde::Deserialize)] #[serde(tag = "auth_type")] pub enum ConnectorAuthType { + TemporaryAuth, HeaderKey { api_key: Secret, }, diff --git a/crates/router/src/types/domain/merchant_connector_account.rs b/crates/router/src/types/domain/merchant_connector_account.rs index 58c2e018316c..c84abbefc381 100644 --- a/crates/router/src/types/domain/merchant_connector_account.rs +++ b/crates/router/src/types/domain/merchant_connector_account.rs @@ -35,6 +35,7 @@ pub struct MerchantConnectorAccount { pub profile_id: Option, pub applepay_verified_domains: Option>, pub pm_auth_config: Option, + pub status: enums::ConnectorStatus, } #[derive(Debug)] @@ -54,6 +55,7 @@ pub enum MerchantConnectorAccountUpdate { applepay_verified_domains: Option>, pm_auth_config: Option, connector_label: Option, + status: Option, }, } @@ -89,6 +91,7 @@ impl behaviour::Conversion for MerchantConnectorAccount { profile_id: self.profile_id, applepay_verified_domains: self.applepay_verified_domains, pm_auth_config: self.pm_auth_config, + status: self.status, }, ) } @@ -128,6 +131,7 @@ impl behaviour::Conversion for MerchantConnectorAccount { profile_id: other.profile_id, applepay_verified_domains: other.applepay_verified_domains, pm_auth_config: other.pm_auth_config, + status: other.status, }) } @@ -155,6 +159,7 @@ impl behaviour::Conversion for MerchantConnectorAccount { profile_id: self.profile_id, applepay_verified_domains: self.applepay_verified_domains, pm_auth_config: self.pm_auth_config, + status: self.status, }) } } @@ -177,6 +182,7 @@ impl From for MerchantConnectorAccountUpdateInte applepay_verified_domains, pm_auth_config, connector_label, + status, } => Self { merchant_id, connector_type, @@ -194,6 +200,7 @@ impl From for MerchantConnectorAccountUpdateInte applepay_verified_domains, pm_auth_config, connector_label, + status, }, } } diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 3ffba5aff50a..2b7ea86cf51d 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -852,6 +852,7 @@ impl TryFrom for api_models::admin::MerchantCo profile_id: item.profile_id, applepay_verified_domains: item.applepay_verified_domains, pm_auth_config: item.pm_auth_config, + status: item.status, }) } } diff --git a/migrations/2023-11-12-131143_connector-status-column/down.sql b/migrations/2023-11-12-131143_connector-status-column/down.sql new file mode 100644 index 000000000000..9463f4d77135 --- /dev/null +++ b/migrations/2023-11-12-131143_connector-status-column/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE merchant_connector_account DROP COLUMN IF EXISTS status; +DROP TYPE IF EXISTS "ConnectorStatus"; diff --git a/migrations/2023-11-12-131143_connector-status-column/up.sql b/migrations/2023-11-12-131143_connector-status-column/up.sql new file mode 100644 index 000000000000..7a992d142d6f --- /dev/null +++ b/migrations/2023-11-12-131143_connector-status-column/up.sql @@ -0,0 +1,11 @@ +-- Your SQL goes here +CREATE TYPE "ConnectorStatus" AS ENUM ('active', 'inactive'); + +ALTER TABLE merchant_connector_account +ADD COLUMN status "ConnectorStatus"; + +UPDATE merchant_connector_account SET status='active'; + +ALTER TABLE merchant_connector_account +ALTER COLUMN status SET NOT NULL, +ALTER COLUMN status SET DEFAULT 'inactive'; diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index be66a1bff92c..7d94f13dd125 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -4147,6 +4147,13 @@ } } }, + "ConnectorStatus": { + "type": "string", + "enum": [ + "inactive", + "active" + ] + }, "ConnectorType": { "type": "string", "enum": [ @@ -6871,7 +6878,8 @@ "description": "Create a new Merchant Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialized services like Fraud / Accounting etc.\"", "required": [ "connector_type", - "connector_name" + "connector_name", + "status" ], "properties": { "connector_type": { @@ -7002,6 +7010,9 @@ }, "pm_auth_config": { "nullable": true + }, + "status": { + "$ref": "#/components/schemas/ConnectorStatus" } } }, @@ -7087,7 +7098,8 @@ "required": [ "connector_type", "connector_name", - "merchant_connector_id" + "merchant_connector_id", + "status" ], "properties": { "connector_type": { @@ -7230,6 +7242,9 @@ }, "pm_auth_config": { "nullable": true + }, + "status": { + "$ref": "#/components/schemas/ConnectorStatus" } } }, @@ -7237,7 +7252,8 @@ "type": "object", "description": "Create a new Merchant Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialized services like Fraud / Accounting etc.\"", "required": [ - "connector_type" + "connector_type", + "status" ], "properties": { "connector_type": { @@ -7335,6 +7351,9 @@ }, "pm_auth_config": { "nullable": true + }, + "status": { + "$ref": "#/components/schemas/ConnectorStatus" } } }, From 644709d95f6ecaab497cf0cf3788b9e2ed88b855 Mon Sep 17 00:00:00 2001 From: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Date: Mon, 20 Nov 2023 13:19:02 +0530 Subject: [PATCH 039/146] fix(connector): [fiserv] fix metadata deserialization in merchant_connector_account (#2746) --- .../src/connector/fiserv/transformers.rs | 48 +++++++++++++------ crates/router/src/core/admin.rs | 1 + 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/crates/router/src/connector/fiserv/transformers.rs b/crates/router/src/connector/fiserv/transformers.rs index 2d07da7f47a4..f8d88d08c6ba 100644 --- a/crates/router/src/connector/fiserv/transformers.rs +++ b/crates/router/src/connector/fiserv/transformers.rs @@ -1,4 +1,4 @@ -use common_utils::ext_traits::ValueExt; +use common_utils::{ext_traits::ValueExt, pii}; use error_stack::ResultExt; use serde::{Deserialize, Serialize}; @@ -150,9 +150,11 @@ impl TryFrom<&FiservRouterData<&types::PaymentsAuthorizeRouterData>> for FiservP merchant_transaction_id: item.router_data.connector_request_reference_id.clone(), }; let metadata = item.router_data.get_connector_meta()?; - let session: SessionObject = metadata - .parse_value("SessionObject") - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let session: FiservSessionObject = metadata + .parse_value("FiservSessionObject") + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "Merchant connector account metadata", + })?; let merchant_details = MerchantDetails { merchant_id: auth.merchant_account, @@ -230,9 +232,11 @@ impl TryFrom<&types::PaymentsCancelRouterData> for FiservCancelRequest { fn try_from(item: &types::PaymentsCancelRouterData) -> Result { let auth: FiservAuthType = FiservAuthType::try_from(&item.connector_auth_type)?; let metadata = item.get_connector_meta()?; - let session: SessionObject = metadata - .parse_value("SessionObject") - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let session: FiservSessionObject = metadata + .parse_value("FiservSessionObject") + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "Merchant connector account metadata", + })?; Ok(Self { merchant_details: MerchantDetails { merchant_id: auth.merchant_account, @@ -418,11 +422,21 @@ pub struct ReferenceTransactionDetails { } #[derive(Debug, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SessionObject { +pub struct FiservSessionObject { pub terminal_id: String, } +impl TryFrom<&Option> for FiservSessionObject { + type Error = error_stack::Report; + fn try_from(meta_data: &Option) -> Result { + let metadata: Self = utils::to_connector_meta_from_secret::(meta_data.clone()) + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "metadata", + })?; + Ok(metadata) + } +} + impl TryFrom<&FiservRouterData<&types::PaymentsCaptureRouterData>> for FiservCaptureRequest { type Error = error_stack::Report; fn try_from( @@ -434,9 +448,11 @@ impl TryFrom<&FiservRouterData<&types::PaymentsCaptureRouterData>> for FiservCap .connector_meta_data .clone() .ok_or(errors::ConnectorError::RequestEncodingFailed)?; - let session: SessionObject = metadata - .parse_value("SessionObject") - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let session: FiservSessionObject = metadata + .parse_value("FiservSessionObject") + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "Merchant connector account metadata", + })?; Ok(Self { amount: Amount { total: item.amount.clone(), @@ -527,9 +543,11 @@ impl TryFrom<&FiservRouterData<&types::RefundsRouterData>> for FiservRefun .connector_meta_data .clone() .ok_or(errors::ConnectorError::RequestEncodingFailed)?; - let session: SessionObject = metadata - .parse_value("SessionObject") - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let session: FiservSessionObject = metadata + .parse_value("FiservSessionObject") + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "Merchant connector account metadata", + })?; Ok(Self { amount: Amount { total: item.amount.clone(), diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index c921a9164cb0..3a0c938c32b4 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1589,6 +1589,7 @@ pub(crate) fn validate_auth_and_metadata_type( } api_enums::Connector::Fiserv => { fiserv::transformers::FiservAuthType::try_from(val)?; + fiserv::transformers::FiservSessionObject::try_from(connector_meta_data)?; Ok(()) } api_enums::Connector::Forte => { From efeebc0f2365f0900de3dd3e10a1539621c9933d Mon Sep 17 00:00:00 2001 From: Shanks Date: Mon, 20 Nov 2023 16:12:06 +0530 Subject: [PATCH 040/146] fix(router): associate parent payment token with `payment_method_id` as hyperswitch token for saved cards (#2130) Co-authored-by: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> --- crates/router/src/core/errors.rs | 2 +- crates/router/src/core/payment_methods.rs | 77 ++++++- .../router/src/core/payment_methods/cards.rs | 77 ++++--- crates/router/src/core/payments/helpers.rs | 213 ++++++++++++------ crates/router/src/routes/payment_methods.rs | 13 +- .../src/types/storage/payment_method.rs | 40 ++++ 6 files changed, 320 insertions(+), 102 deletions(-) diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index 810c079987eb..03bb9a41b5b5 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -19,7 +19,7 @@ use storage_impl::errors as storage_impl_errors; pub use user::*; pub use self::{ - api_error_response::ApiErrorResponse, + api_error_response::{ApiErrorResponse, NotImplementedMessage}, customers_error_response::CustomersErrorResponse, sch_errors::*, storage_errors::*, diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index b19b381af507..0628d301796e 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -9,13 +9,17 @@ pub use api_models::{ pub use common_utils::request::RequestBody; use data_models::payments::{payment_attempt::PaymentAttempt, PaymentIntent}; use diesel_models::enums; +use error_stack::IntoReport; use crate::{ - core::{errors::RouterResult, payments::helpers}, + core::{ + errors::{self, RouterResult}, + payments::helpers, + }, routes::AppState, types::{ api::{self, payments}, - domain, + domain, storage, }, }; @@ -30,6 +34,14 @@ pub trait PaymentMethodRetrieve { payment_attempt: &PaymentAttempt, merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<(Option, Option)>; + + async fn retrieve_payment_method_with_token( + state: &AppState, + key_store: &domain::MerchantKeyStore, + token: &storage::PaymentTokenData, + payment_intent: &PaymentIntent, + card_cvc: Option>, + ) -> RouterResult>; } #[async_trait::async_trait] @@ -105,4 +117,65 @@ impl PaymentMethodRetrieve for Oss { _ => Ok((None, None)), } } + + async fn retrieve_payment_method_with_token( + state: &AppState, + merchant_key_store: &domain::MerchantKeyStore, + token_data: &storage::PaymentTokenData, + payment_intent: &PaymentIntent, + card_cvc: Option>, + ) -> RouterResult> { + match token_data { + storage::PaymentTokenData::TemporaryGeneric(generic_token) => { + helpers::retrieve_payment_method_with_temporary_token( + state, + &generic_token.token, + payment_intent, + card_cvc, + merchant_key_store, + ) + .await + } + + storage::PaymentTokenData::Temporary(generic_token) => { + helpers::retrieve_payment_method_with_temporary_token( + state, + &generic_token.token, + payment_intent, + card_cvc, + merchant_key_store, + ) + .await + } + + storage::PaymentTokenData::Permanent(card_token) => { + helpers::retrieve_card_with_permanent_token( + state, + &card_token.token, + payment_intent, + card_cvc, + ) + .await + .map(|card| Some((card, enums::PaymentMethod::Card))) + } + + storage::PaymentTokenData::PermanentCard(card_token) => { + helpers::retrieve_card_with_permanent_token( + state, + &card_token.token, + payment_intent, + card_cvc, + ) + .await + .map(|card| Some((card, enums::PaymentMethod::Card))) + } + + storage::PaymentTokenData::AuthBankDebit(_) => { + Err(errors::ApiErrorResponse::NotImplemented { + message: errors::NotImplementedMessage::Default, + }) + .into_report() + } + } + } } diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 80daf66a6926..f2eeedf5388f 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -50,7 +50,7 @@ use crate::{ self, types::{decrypt, encrypt_optional, AsyncLift}, }, - storage::{self, enums}, + storage::{self, enums, PaymentTokenData}, transformers::ForeignFrom, }, utils::{self, ConnectorResponseExt, OptionExt}, @@ -2103,23 +2103,32 @@ pub async fn list_customer_payment_method( let mut customer_pms = Vec::new(); for pm in resp.into_iter() { let parent_payment_method_token = generate_id(consts::ID_LENGTH, "token"); - let hyperswitch_token = generate_id(consts::ID_LENGTH, "token"); - let card = if pm.payment_method == enums::PaymentMethod::Card { - get_card_details(&pm, key, state, &hyperswitch_token, &key_store).await? - } else { - None - }; + let (card, pmd, hyperswitch_token_data) = match pm.payment_method { + enums::PaymentMethod::Card => ( + Some(get_card_details(&pm, key, state).await?), + None, + PaymentTokenData::permanent_card(pm.payment_method_id.clone()), + ), - #[cfg(feature = "payouts")] - let pmd = if pm.payment_method == enums::PaymentMethod::BankTransfer { - Some( - get_lookup_key_for_payout_method(state, &key_store, &hyperswitch_token, &pm) - .await?, - ) - } else { - None + #[cfg(feature = "payouts")] + enums::PaymentMethod::BankTransfer => { + let token = generate_id(consts::ID_LENGTH, "token"); + let token_data = PaymentTokenData::temporary_generic(token.clone()); + ( + None, + Some(get_lookup_key_for_payout_method(state, &key_store, &token, &pm).await?), + token_data, + ) + } + + _ => ( + None, + None, + PaymentTokenData::temporary_generic(generate_id(consts::ID_LENGTH, "token")), + ), }; + //Need validation for enabled payment method ,querying MCA let pma = api::CustomerPaymentMethod { payment_token: parent_payment_method_token.to_owned(), @@ -2134,10 +2143,7 @@ pub async fn list_customer_payment_method( installment_payment_enabled: false, payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), created: Some(pm.created_at), - #[cfg(feature = "payouts")] bank_transfer: pmd, - #[cfg(not(feature = "payouts"))] - bank_transfer: None, requires_cvv, }; customer_pms.push(pma.to_owned()); @@ -2153,7 +2159,7 @@ pub async fn list_customer_payment_method( &parent_payment_method_token, pma.payment_method, )) - .insert(intent_created, hyperswitch_token, state) + .insert(intent_created, hyperswitch_token_data, state) .await?; if let Some(metadata) = pma.metadata { @@ -2200,10 +2206,8 @@ async fn get_card_details( pm: &payment_method::PaymentMethod, key: &[u8], state: &routes::AppState, - hyperswitch_token: &str, - key_store: &domain::MerchantKeyStore, -) -> errors::RouterResult> { - let mut _card_decrypted = +) -> errors::RouterResult { + let card_decrypted = decrypt::(pm.payment_method_data.clone(), key) .await .change_context(errors::StorageError::DecryptionError) @@ -2217,16 +2221,17 @@ async fn get_card_details( _ => None, }); - Ok(Some( - get_lookup_key_from_locker(state, hyperswitch_token, pm, key_store).await?, - )) + Ok(if let Some(mut crd) = card_decrypted { + crd.scheme = pm.scheme.clone(); + crd + } else { + get_card_details_from_locker(state, pm).await? + }) } -pub async fn get_lookup_key_from_locker( +pub async fn get_card_details_from_locker( state: &routes::AppState, - payment_token: &str, pm: &storage::PaymentMethod, - merchant_key_store: &domain::MerchantKeyStore, ) -> errors::RouterResult { let card = get_card_from_locker( state, @@ -2237,9 +2242,19 @@ pub async fn get_lookup_key_from_locker( .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Error getting card from card vault")?; - let card_detail = payment_methods::get_card_detail(pm, card) + + payment_methods::get_card_detail(pm, card) .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Get Card Details Failed")?; + .attach_printable("Get Card Details Failed") +} + +pub async fn get_lookup_key_from_locker( + state: &routes::AppState, + payment_token: &str, + pm: &storage::PaymentMethod, + merchant_key_store: &domain::MerchantKeyStore, +) -> errors::RouterResult { + let card_detail = get_card_details_from_locker(state, pm).await?; let card = card_detail.clone(); let resp = TempLockerCardSupport::create_payment_method_data_in_temp_locker( diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index cd056f81ebb4..fb74006a0671 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -55,7 +55,7 @@ use crate::{ utils::{ self, crypto::{self, SignMessage}, - OptionExt, + OptionExt, StringExt, }, }; @@ -1326,6 +1326,114 @@ pub async fn create_customer_if_not_exist<'a, F: Clone, R, Ctx>( )) } +pub async fn retrieve_payment_method_with_temporary_token( + state: &AppState, + token: &str, + payment_intent: &PaymentIntent, + card_cvc: Option>, + merchant_key_store: &domain::MerchantKeyStore, +) -> RouterResult> { + let (pm, supplementary_data) = + vault::Vault::get_payment_method_data_from_locker(state, token, merchant_key_store) + .await + .attach_printable( + "Payment method for given token not found or there was a problem fetching it", + )?; + + utils::when( + supplementary_data + .customer_id + .ne(&payment_intent.customer_id), + || { + Err(errors::ApiErrorResponse::PreconditionFailed { message: "customer associated with payment method and customer passed in payment are not same".into() }) + }, + )?; + + Ok::<_, error_stack::Report>(match pm { + Some(api::PaymentMethodData::Card(card)) => { + if let Some(cvc) = card_cvc { + let mut updated_card = card; + updated_card.card_cvc = cvc; + let updated_pm = api::PaymentMethodData::Card(updated_card); + vault::Vault::store_payment_method_data_in_locker( + state, + Some(token.to_owned()), + &updated_pm, + payment_intent.customer_id.to_owned(), + enums::PaymentMethod::Card, + merchant_key_store, + ) + .await?; + + Some((updated_pm, enums::PaymentMethod::Card)) + } else { + Some(( + api::PaymentMethodData::Card(card), + enums::PaymentMethod::Card, + )) + } + } + + Some(the_pm @ api::PaymentMethodData::Wallet(_)) => { + Some((the_pm, enums::PaymentMethod::Wallet)) + } + + Some(the_pm @ api::PaymentMethodData::BankTransfer(_)) => { + Some((the_pm, enums::PaymentMethod::BankTransfer)) + } + + Some(the_pm @ api::PaymentMethodData::BankRedirect(_)) => { + Some((the_pm, enums::PaymentMethod::BankRedirect)) + } + + Some(_) => Err(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Payment method received from locker is unsupported by locker")?, + + None => None, + }) +} + +pub async fn retrieve_card_with_permanent_token( + state: &AppState, + token: &str, + payment_intent: &PaymentIntent, + card_cvc: Option>, +) -> RouterResult { + let customer_id = payment_intent + .customer_id + .as_ref() + .get_required_value("customer_id") + .change_context(errors::ApiErrorResponse::UnprocessableEntity { + message: "no customer id provided for the payment".to_string(), + })?; + + let card = cards::get_card_from_locker(state, customer_id, &payment_intent.merchant_id, token) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to fetch card information from the permanent locker")?; + + let api_card = api::Card { + card_number: card.card_number, + card_holder_name: card + .name_on_card + .get_required_value("name_on_card") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("card holder name was not saved in permanent locker")?, + card_exp_month: card.card_exp_month, + card_exp_year: card.card_exp_year, + card_cvc: card_cvc.unwrap_or_default(), + card_issuer: card.card_brand, + nick_name: card.nick_name.map(masking::Secret::new), + card_network: None, + card_type: None, + card_issuing_country: None, + bank_code: None, + }; + + Ok(api::PaymentMethodData::Card(api_card)) +} + pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( operation: BoxedOperation<'a, F, R, Ctx>, state: &'a AppState, @@ -1339,7 +1447,7 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( let token = payment_data.token.clone(); let hyperswitch_token = match payment_data.mandate_id { - Some(_) => token, + Some(_) => token.map(storage::PaymentTokenData::temporary_generic), None => { if let Some(token) = token { let redis_conn = state @@ -1358,7 +1466,7 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( .get_required_value("payment_method")?, ); - let key = redis_conn + let token_data_string = redis_conn .get_key::>(&key) .await .change_context(errors::ApiErrorResponse::InternalServerError) @@ -1369,7 +1477,26 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( }, ))?; - Some(key) + let token_data_result = token_data_string + .clone() + .parse_struct("PaymentTokenData") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to deserialize hyperswitch token data"); + + let token_data = match token_data_result { + Ok(data) => data, + Err(e) => { + // The purpose of this logic is backwards compatibility to support tokens + // in redis that might be following the old format. + if token_data_string.starts_with('{') { + return Err(e); + } else { + storage::PaymentTokenData::temporary_generic(token_data_string) + } + } + }; + + Some(token_data) } else { None } @@ -1381,72 +1508,24 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( // TODO: Handle case where payment method and token both are present in request properly. let payment_method = match (request, hyperswitch_token) { (_, Some(hyperswitch_token)) => { - let (pm, supplementary_data) = vault::Vault::get_payment_method_data_from_locker( + let payment_method_details = Ctx::retrieve_payment_method_with_token( state, - &hyperswitch_token, merchant_key_store, + &hyperswitch_token, + &payment_data.payment_intent, + card_cvc, ) .await - .attach_printable( - "Payment method for given token not found or there was a problem fetching it", - )?; + .attach_printable("in 'make_pm_data'")?; - utils::when( - supplementary_data - .customer_id - .ne(&payment_data.payment_intent.customer_id), - || { - Err(errors::ApiErrorResponse::PreconditionFailed { message: "customer associated with payment method and customer passed in payment are not same".into() }) + Ok::<_, error_stack::Report>( + if let Some((payment_method_data, payment_method)) = payment_method_details { + payment_data.payment_attempt.payment_method = Some(payment_method); + Some(payment_method_data) + } else { + None }, - )?; - - Ok::<_, error_stack::Report>(match pm.clone() { - Some(api::PaymentMethodData::Card(card)) => { - payment_data.payment_attempt.payment_method = - Some(storage_enums::PaymentMethod::Card); - if let Some(cvc) = card_cvc { - let mut updated_card = card; - updated_card.card_cvc = cvc; - let updated_pm = api::PaymentMethodData::Card(updated_card); - vault::Vault::store_payment_method_data_in_locker( - state, - Some(hyperswitch_token), - &updated_pm, - payment_data.payment_intent.customer_id.to_owned(), - enums::PaymentMethod::Card, - merchant_key_store, - ) - .await?; - Some(updated_pm) - } else { - pm - } - } - - Some(api::PaymentMethodData::Wallet(_)) => { - payment_data.payment_attempt.payment_method = - Some(storage_enums::PaymentMethod::Wallet); - pm - } - - Some(api::PaymentMethodData::BankTransfer(_)) => { - payment_data.payment_attempt.payment_method = - Some(storage_enums::PaymentMethod::BankTransfer); - pm - } - Some(api::PaymentMethodData::BankRedirect(_)) => { - payment_data.payment_attempt.payment_method = - Some(storage_enums::PaymentMethod::BankRedirect); - pm - } - Some(_) => Err(errors::ApiErrorResponse::InternalServerError) - .into_report() - .attach_printable( - "Payment method received from locker is unsupported by locker", - )?, - - None => None, - }) + ) } (Some(_), _) => { @@ -1495,7 +1574,11 @@ pub async fn store_in_vault_and_generate_ppmt( }); if let Some(key_for_hyperswitch_token) = key_for_hyperswitch_token { key_for_hyperswitch_token - .insert(Some(payment_intent.created_at), router_token, state) + .insert( + Some(payment_intent.created_at), + storage::PaymentTokenData::temporary_generic(router_token), + state, + ) .await?; }; Ok(parent_payment_method_token) diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index 83d4c7f96611..43a7272a4435 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -9,7 +9,11 @@ use super::app::AppState; use crate::{ core::{api_locking, errors, payment_methods::cards}, services::{api, authentication as auth}, - types::api::payment_methods::{self, PaymentMethodId}, + types::{ + api::payment_methods::{self, PaymentMethodId}, + storage::payment_method::PaymentTokenData, + }, + utils::Encode, }; /// PaymentMethods - Create @@ -379,9 +383,12 @@ impl ParentPaymentMethodToken { pub async fn insert( &self, intent_created_at: Option, - token: String, + token: PaymentTokenData, state: &AppState, ) -> CustomResult<(), errors::ApiErrorResponse> { + let token_json_str = Encode::::encode_to_string_of_json(&token) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to serialize hyperswitch token to json")?; let redis_conn = state .store .get_redis_conn() @@ -392,7 +399,7 @@ impl ParentPaymentMethodToken { redis_conn .set_key_with_expiry( &self.key_for_token, - token, + token_json_str, TOKEN_TTL - time_elapsed.whole_seconds(), ) .await diff --git a/crates/router/src/types/storage/payment_method.rs b/crates/router/src/types/storage/payment_method.rs index 737e6f66076a..096303446dc5 100644 --- a/crates/router/src/types/storage/payment_method.rs +++ b/crates/router/src/types/storage/payment_method.rs @@ -1,4 +1,44 @@ +use api_models::payment_methods; pub use diesel_models::payment_method::{ PaymentMethod, PaymentMethodNew, PaymentMethodUpdate, PaymentMethodUpdateInternal, TokenizeCoreWorkflow, }; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum PaymentTokenKind { + Temporary, + Permanent, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct CardTokenData { + pub token: String, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct GenericTokenData { + pub token: String, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum PaymentTokenData { + // The variants 'Temporary' and 'Permanent' are added for backwards compatibility + // with any tokenized data present in Redis at the time of deployment of this change + Temporary(GenericTokenData), + TemporaryGeneric(GenericTokenData), + Permanent(CardTokenData), + PermanentCard(CardTokenData), + AuthBankDebit(payment_methods::BankAccountConnectorDetails), +} + +impl PaymentTokenData { + pub fn permanent_card(token: String) -> Self { + Self::PermanentCard(CardTokenData { token }) + } + + pub fn temporary_generic(token: String) -> Self { + Self::TemporaryGeneric(GenericTokenData { token }) + } +} From 39540015fde476ad8492a9142c2c1bfda8444a27 Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Mon, 20 Nov 2023 17:35:07 +0530 Subject: [PATCH 041/146] feat(router): add unified_code, unified_message in payments response (#2918) --- crates/api_models/src/gsm.rs | 6 ++ crates/api_models/src/payments.rs | 10 +++ .../src/payments/payment_attempt.rs | 8 +++ crates/diesel_models/src/gsm.rs | 12 ++++ crates/diesel_models/src/payment_attempt.rs | 20 ++++++ crates/diesel_models/src/schema.rs | 8 +++ crates/router/src/core/gsm.rs | 4 ++ crates/router/src/core/payments/helpers.rs | 45 ++++++++++++++ .../payments/operations/payment_response.rs | 17 +++++- crates/router/src/core/payments/retry.rs | 61 ++++++------------- .../router/src/core/payments/transformers.rs | 4 ++ crates/router/src/types/transformers.rs | 6 ++ crates/router/src/workflows/payment_sync.rs | 2 + .../src/mock_db/payment_attempt.rs | 2 + .../src/payments/payment_attempt.rs | 26 ++++++++ .../down.sql | 3 + .../up.sql | 3 + .../down.sql | 3 + .../up.sql | 3 + openapi/openapi_spec.json | 44 +++++++++++++ 20 files changed, 242 insertions(+), 45 deletions(-) create mode 100644 migrations/2023-11-17-061003_add-unified-error-code-mssg-gsm/down.sql create mode 100644 migrations/2023-11-17-061003_add-unified-error-code-mssg-gsm/up.sql create mode 100644 migrations/2023-11-17-084413_add-unified-error-code-mssg-payment-attempt/down.sql create mode 100644 migrations/2023-11-17-084413_add-unified-error-code-mssg-payment-attempt/up.sql diff --git a/crates/api_models/src/gsm.rs b/crates/api_models/src/gsm.rs index 254981b1f8f7..81798d05178b 100644 --- a/crates/api_models/src/gsm.rs +++ b/crates/api_models/src/gsm.rs @@ -13,6 +13,8 @@ pub struct GsmCreateRequest { pub router_error: Option, pub decision: GsmDecision, pub step_up_possible: bool, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] @@ -57,6 +59,8 @@ pub struct GsmUpdateRequest { pub router_error: Option, pub decision: Option, pub step_up_possible: Option, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] @@ -88,4 +92,6 @@ pub struct GsmResponse { pub router_error: Option, pub decision: String, pub step_up_possible: bool, + pub unified_code: Option, + pub unified_message: Option, } diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index b479f4442ba6..9f4f151c2228 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -391,6 +391,10 @@ pub struct PaymentAttemptResponse { /// reference to the payment at connector side #[schema(value_type = Option, example = "993672945374576J")] pub reference_id: Option, + /// error code unified across the connectors is received here if there was an error while calling connector + pub unified_code: Option, + /// error message unified across the connectors is received here if there was an error while calling connector + pub unified_message: Option, } #[derive( @@ -2089,6 +2093,12 @@ pub struct PaymentsResponse { #[schema(example = "Failed while verifying the card")] pub error_message: Option, + /// error code unified across the connectors is received here if there was an error while calling connector + pub unified_code: Option, + + /// error message unified across the connectors is received here if there was an error while calling connector + pub unified_message: Option, + /// Payment Experience for the current payment #[schema(value_type = Option, example = "redirect_to_url")] pub payment_experience: Option, diff --git a/crates/data_models/src/payments/payment_attempt.rs b/crates/data_models/src/payments/payment_attempt.rs index 88fc7b3b524a..1b43177feb56 100644 --- a/crates/data_models/src/payments/payment_attempt.rs +++ b/crates/data_models/src/payments/payment_attempt.rs @@ -145,6 +145,8 @@ pub struct PaymentAttempt { pub authentication_data: Option, pub encoded_data: Option, pub merchant_connector_id: Option, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -207,6 +209,8 @@ pub struct PaymentAttemptNew { pub authentication_data: Option, pub encoded_data: Option, pub merchant_connector_id: Option, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -292,6 +296,8 @@ pub enum PaymentAttemptUpdate { updated_by: String, authentication_data: Option, encoded_data: Option, + unified_code: Option>, + unified_message: Option>, }, UnresolvedResponseUpdate { status: storage_enums::AttemptStatus, @@ -316,6 +322,8 @@ pub enum PaymentAttemptUpdate { error_reason: Option>, amount_capturable: Option, updated_by: String, + unified_code: Option>, + unified_message: Option>, }, MultipleCaptureCountUpdate { multiple_capture_count: i16, diff --git a/crates/diesel_models/src/gsm.rs b/crates/diesel_models/src/gsm.rs index 2e824758aa5a..39bd880cd6c2 100644 --- a/crates/diesel_models/src/gsm.rs +++ b/crates/diesel_models/src/gsm.rs @@ -34,6 +34,8 @@ pub struct GatewayStatusMap { #[serde(with = "custom_serde::iso8601")] pub last_modified: PrimitiveDateTime, pub step_up_possible: bool, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Clone, Debug, Eq, PartialEq, Insertable)] @@ -48,6 +50,8 @@ pub struct GatewayStatusMappingNew { pub router_error: Option, pub decision: String, pub step_up_possible: bool, + pub unified_code: Option, + pub unified_message: Option, } #[derive( @@ -71,6 +75,8 @@ pub struct GatewayStatusMapperUpdateInternal { pub router_error: Option>, pub decision: Option, pub step_up_possible: Option, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Debug)] @@ -79,6 +85,8 @@ pub struct GatewayStatusMappingUpdate { pub router_error: Option>, pub decision: Option, pub step_up_possible: Option, + pub unified_code: Option, + pub unified_message: Option, } impl From for GatewayStatusMapperUpdateInternal { @@ -88,12 +96,16 @@ impl From for GatewayStatusMapperUpdateInternal { status, router_error, step_up_possible, + unified_code, + unified_message, } = value; Self { status, router_error, decision, step_up_possible, + unified_code, + unified_message, ..Default::default() } } diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index cd976b9e19db..bb8f2b60bbb7 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -61,6 +61,8 @@ pub struct PaymentAttempt { pub merchant_connector_id: Option, pub authentication_data: Option, pub encoded_data: Option, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Clone, Debug, Eq, PartialEq, Queryable, Serialize, Deserialize)] @@ -124,6 +126,8 @@ pub struct PaymentAttemptNew { pub merchant_connector_id: Option, pub authentication_data: Option, pub encoded_data: Option, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -209,6 +213,8 @@ pub enum PaymentAttemptUpdate { updated_by: String, authentication_data: Option, encoded_data: Option, + unified_code: Option>, + unified_message: Option>, }, UnresolvedResponseUpdate { status: storage_enums::AttemptStatus, @@ -233,6 +239,8 @@ pub enum PaymentAttemptUpdate { error_reason: Option>, amount_capturable: Option, updated_by: String, + unified_code: Option>, + unified_message: Option>, }, MultipleCaptureCountUpdate { multiple_capture_count: i16, @@ -298,6 +306,8 @@ pub struct PaymentAttemptUpdateInternal { merchant_connector_id: Option, authentication_data: Option, encoded_data: Option, + unified_code: Option>, + unified_message: Option>, } impl PaymentAttemptUpdate { @@ -352,6 +362,8 @@ impl PaymentAttemptUpdate { merchant_connector_id: pa_update.merchant_connector_id, authentication_data: pa_update.authentication_data.or(source.authentication_data), encoded_data: pa_update.encoded_data.or(source.encoded_data), + unified_code: pa_update.unified_code.unwrap_or(source.unified_code), + unified_message: pa_update.unified_message.unwrap_or(source.unified_message), ..source } } @@ -488,6 +500,8 @@ impl From for PaymentAttemptUpdateInternal { updated_by, authentication_data, encoded_data, + unified_code, + unified_message, } => Self { status: Some(status), connector, @@ -508,6 +522,8 @@ impl From for PaymentAttemptUpdateInternal { tax_amount, authentication_data, encoded_data, + unified_code, + unified_message, ..Default::default() }, PaymentAttemptUpdate::ErrorUpdate { @@ -518,6 +534,8 @@ impl From for PaymentAttemptUpdateInternal { error_reason, amount_capturable, updated_by, + unified_code, + unified_message, } => Self { connector, status: Some(status), @@ -527,6 +545,8 @@ impl From for PaymentAttemptUpdateInternal { error_reason, amount_capturable, updated_by, + unified_code, + unified_message, ..Default::default() }, PaymentAttemptUpdate::StatusUpdate { status, updated_by } => Self { diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 190a123185e4..ce974e409a2c 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -331,6 +331,10 @@ diesel::table! { created_at -> Timestamp, last_modified -> Timestamp, step_up_possible -> Bool, + #[max_length = 255] + unified_code -> Nullable, + #[max_length = 1024] + unified_message -> Nullable, } } @@ -585,6 +589,10 @@ diesel::table! { merchant_connector_id -> Nullable, authentication_data -> Nullable, encoded_data -> Nullable, + #[max_length = 255] + unified_code -> Nullable, + #[max_length = 1024] + unified_message -> Nullable, } } diff --git a/crates/router/src/core/gsm.rs b/crates/router/src/core/gsm.rs index ed72275a73ab..611a35d63632 100644 --- a/crates/router/src/core/gsm.rs +++ b/crates/router/src/core/gsm.rs @@ -65,6 +65,8 @@ pub async fn update_gsm_rule( status, router_error, step_up_possible, + unified_code, + unified_message, } = gsm_request; GsmInterface::update_gsm_rule( db, @@ -78,6 +80,8 @@ pub async fn update_gsm_rule( status, router_error: Some(router_error), step_up_possible, + unified_code, + unified_message, }, ) .await diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index fb74006a0671..ae729ff8fa25 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -3026,6 +3026,8 @@ impl AttemptType { authentication_data: None, encoded_data: None, merchant_connector_id: None, + unified_code: None, + unified_message: None, } } @@ -3516,3 +3518,46 @@ pub fn validate_payment_link_request( } Ok(()) } + +pub async fn get_gsm_record( + state: &AppState, + error_code: Option, + error_message: Option, + connector_name: String, + flow: String, +) -> Option { + let get_gsm = || async { + state.store.find_gsm_rule( + connector_name.clone(), + flow.clone(), + "sub_flow".to_string(), + error_code.clone().unwrap_or_default(), // TODO: make changes in connector to get a mandatory code in case of success or error response + error_message.clone().unwrap_or_default(), + ) + .await + .map_err(|err| { + if err.current_context().is_db_not_found() { + logger::warn!( + "GSM miss for connector - {}, flow - {}, error_code - {:?}, error_message - {:?}", + connector_name, + flow, + error_code, + error_message + ); + metrics::AUTO_RETRY_GSM_MISS_COUNT.add(&metrics::CONTEXT, 1, &[]); + } else { + metrics::AUTO_RETRY_GSM_FETCH_FAILURE_COUNT.add(&metrics::CONTEXT, 1, &[]); + }; + err.change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to fetch decision from gsm") + }) + }; + get_gsm() + .await + .map_err(|err| { + // warn log should suffice here because we are not propagating this error + logger::warn!(get_gsm_decision_fetch_error=?err, "error fetching gsm decision"); + err + }) + .ok() +} diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 1cfc37efa449..083d1bb030dd 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -16,7 +16,7 @@ use crate::{ errors::{self, RouterResult, StorageErrorExt}, mandate, payment_methods::PaymentMethodRetrieve, - payments::{types::MultipleCaptureData, PaymentData}, + payments::{helpers as payments_helpers, types::MultipleCaptureData, PaymentData}, utils as core_utils, }, routes::{metrics, AppState}, @@ -331,7 +331,16 @@ async fn payment_response_update_tracker( (Some((multiple_capture_data, capture_update_list)), None) } None => { + let connector_name = router_data.connector.to_string(); let flow_name = core_utils::get_flow_name::()?; + let option_gsm = payments_helpers::get_gsm_record( + state, + Some(err.code.clone()), + Some(err.message.clone()), + connector_name, + flow_name.clone(), + ) + .await; let status = // mark previous attempt status for technical failures in PSync flow if flow_name == "PSync" { @@ -364,6 +373,8 @@ async fn payment_response_update_tracker( None }, updated_by: storage_scheme.to_string(), + unified_code: option_gsm.clone().map(|gsm| gsm.unified_code), + unified_message: option_gsm.map(|gsm| gsm.unified_message), }), ) } @@ -470,7 +481,9 @@ async fn payment_response_update_tracker( payment_token: None, error_code: error_status.clone(), error_message: error_status.clone(), - error_reason: error_status, + error_reason: error_status.clone(), + unified_code: error_status.clone(), + unified_message: error_status, connector_response_reference_id, amount_capturable: if router_data.status.is_terminal_status() || router_data diff --git a/crates/router/src/core/payments/retry.rs b/crates/router/src/core/payments/retry.rs index 788e83b05e37..3c0106206e1d 100644 --- a/crates/router/src/core/payments/retry.rs +++ b/crates/router/src/core/payments/retry.rs @@ -55,7 +55,7 @@ where metrics::AUTO_RETRY_ELIGIBLE_REQUEST_COUNT.add(&metrics::CONTEXT, 1, &[]); - let mut initial_gsm = get_gsm(state, &router_data).await; + let mut initial_gsm = get_gsm(state, &router_data).await?; //Check if step-up to threeDS is possible and merchant has enabled let step_up_possible = initial_gsm @@ -99,7 +99,7 @@ where // Use initial_gsm for first time alone let gsm = match initial_gsm.as_ref() { Some(gsm) => Some(gsm.clone()), - None => get_gsm(state, &router_data).await, + None => get_gsm(state, &router_data).await?, }; match get_gsm_decision(gsm) { @@ -214,46 +214,16 @@ pub async fn get_retries( pub async fn get_gsm( state: &app::AppState, router_data: &types::RouterData, -) -> Option { +) -> RouterResult> { let error_response = router_data.response.as_ref().err(); let error_code = error_response.map(|err| err.code.to_owned()); let error_message = error_response.map(|err| err.message.to_owned()); - let get_gsm = || async { - let connector_name = router_data.connector.to_string(); - let flow = get_flow_name::()?; - state.store.find_gsm_rule( - connector_name.clone(), - flow.clone(), - "sub_flow".to_string(), - error_code.clone().unwrap_or_default(), // TODO: make changes in connector to get a mandatory code in case of success or error response - error_message.clone().unwrap_or_default(), - ) - .await - .map_err(|err| { - if err.current_context().is_db_not_found() { - logger::warn!( - "GSM miss for connector - {}, flow - {}, error_code - {:?}, error_message - {:?}", - connector_name, - flow, - error_code, - error_message - ); - metrics::AUTO_RETRY_GSM_MISS_COUNT.add(&metrics::CONTEXT, 1, &[]); - } else { - metrics::AUTO_RETRY_GSM_FETCH_FAILURE_COUNT.add(&metrics::CONTEXT, 1, &[]); - }; - err.change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("failed to fetch decision from gsm") - }) - }; - get_gsm() - .await - .map_err(|err| { - // warn log should suffice here because we are not propagating this error - logger::warn!(get_gsm_decision_fetch_error=?err, "error fetching gsm decision"); - err - }) - .ok() + let connector_name = router_data.connector.to_string(); + let flow = get_flow_name::()?; + Ok( + payments::helpers::get_gsm_record(state, error_code, error_message, connector_name, flow) + .await, + ) } #[instrument(skip_all)] @@ -417,6 +387,8 @@ where updated_by: storage_scheme.to_string(), authentication_data, encoded_data, + unified_code: None, + unified_message: None, }, storage_scheme, ) @@ -427,17 +399,20 @@ where logger::error!("unexpected response: this response was not expected in Retry flow"); return Ok(()); } - Err(error_response) => { + Err(ref error_response) => { + let option_gsm = get_gsm(state, &router_data).await?; db.update_payment_attempt_with_attempt_id( payment_data.payment_attempt.clone(), storage::PaymentAttemptUpdate::ErrorUpdate { connector: None, - error_code: Some(Some(error_response.code)), - error_message: Some(Some(error_response.message)), + error_code: Some(Some(error_response.code.clone())), + error_message: Some(Some(error_response.message.clone())), status: storage_enums::AttemptStatus::Failure, - error_reason: Some(error_response.reason), + error_reason: Some(error_response.reason.clone()), amount_capturable: Some(0), updated_by: storage_scheme.to_string(), + unified_code: option_gsm.clone().map(|gsm| gsm.unified_code), + unified_message: option_gsm.map(|gsm| gsm.unified_message), }, storage_scheme, ) diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 6c6b4ae9339f..f395c023128c 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -685,6 +685,8 @@ where .set_profile_id(payment_intent.profile_id) .set_attempt_count(payment_intent.attempt_count) .set_merchant_connector_id(payment_attempt.merchant_connector_id) + .set_unified_code(payment_attempt.unified_code) + .set_unified_message(payment_attempt.unified_message) .to_owned(), headers, )) @@ -745,6 +747,8 @@ where attempt_count: payment_intent.attempt_count, payment_link: payment_link_data, surcharge_details, + unified_code: payment_attempt.unified_code, + unified_message: payment_attempt.unified_message, ..Default::default() }, headers, diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 2b7ea86cf51d..b73ba0964fbf 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -878,6 +878,8 @@ impl ForeignFrom for api_models::payments::PaymentAttem payment_experience: payment_attempt.payment_experience, payment_method_type: payment_attempt.payment_method_type, reference_id: payment_attempt.connector_response_reference_id, + unified_code: payment_attempt.unified_code, + unified_message: payment_attempt.unified_message, } } } @@ -1055,6 +1057,8 @@ impl ForeignFrom for storage::GatewayStatusMapp status: value.status, router_error: value.router_error, step_up_possible: value.step_up_possible, + unified_code: value.unified_code, + unified_message: value.unified_message, } } } @@ -1071,6 +1075,8 @@ impl ForeignFrom for gsm_api_types::GsmResponse { status: value.status, router_error: value.router_error, step_up_possible: value.step_up_possible, + unified_code: value.unified_code, + unified_message: value.unified_message, } } } diff --git a/crates/router/src/workflows/payment_sync.rs b/crates/router/src/workflows/payment_sync.rs index 00e7357d896f..43e327559a0c 100644 --- a/crates/router/src/workflows/payment_sync.rs +++ b/crates/router/src/workflows/payment_sync.rs @@ -136,6 +136,8 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow { )), amount_capturable: Some(0), updated_by: merchant_account.storage_scheme.to_string(), + unified_code: None, + unified_message: None, }; payment_data.payment_attempt = db diff --git a/crates/storage_impl/src/mock_db/payment_attempt.rs b/crates/storage_impl/src/mock_db/payment_attempt.rs index cb2f81daa797..fe244b10325f 100644 --- a/crates/storage_impl/src/mock_db/payment_attempt.rs +++ b/crates/storage_impl/src/mock_db/payment_attempt.rs @@ -144,6 +144,8 @@ impl PaymentAttemptInterface for MockDb { authentication_data: payment_attempt.authentication_data, encoded_data: payment_attempt.encoded_data, merchant_connector_id: payment_attempt.merchant_connector_id, + unified_code: payment_attempt.unified_code, + unified_message: payment_attempt.unified_message, }; payment_attempts.push(payment_attempt.clone()); Ok(payment_attempt) diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index 3d00e2f2bf7a..cb74c981ea71 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -364,6 +364,8 @@ impl PaymentAttemptInterface for KVRouterStore { authentication_data: payment_attempt.authentication_data.clone(), encoded_data: payment_attempt.encoded_data.clone(), merchant_connector_id: payment_attempt.merchant_connector_id.clone(), + unified_code: payment_attempt.unified_code.clone(), + unified_message: payment_attempt.unified_message.clone(), }; let field = format!("pa_{}", created_attempt.attempt_id); @@ -966,6 +968,8 @@ impl DataModelExt for PaymentAttempt { authentication_data: self.authentication_data, encoded_data: self.encoded_data, merchant_connector_id: self.merchant_connector_id, + unified_code: self.unified_code, + unified_message: self.unified_message, } } @@ -1018,6 +1022,8 @@ impl DataModelExt for PaymentAttempt { authentication_data: storage_model.authentication_data, encoded_data: storage_model.encoded_data, merchant_connector_id: storage_model.merchant_connector_id, + unified_code: storage_model.unified_code, + unified_message: storage_model.unified_message, } } } @@ -1070,6 +1076,8 @@ impl DataModelExt for PaymentAttemptNew { authentication_data: self.authentication_data, encoded_data: self.encoded_data, merchant_connector_id: self.merchant_connector_id, + unified_code: self.unified_code, + unified_message: self.unified_message, } } @@ -1120,6 +1128,8 @@ impl DataModelExt for PaymentAttemptNew { authentication_data: storage_model.authentication_data, encoded_data: storage_model.encoded_data, merchant_connector_id: storage_model.merchant_connector_id, + unified_code: storage_model.unified_code, + unified_message: storage_model.unified_message, } } } @@ -1255,6 +1265,8 @@ impl DataModelExt for PaymentAttemptUpdate { tax_amount, authentication_data, encoded_data, + unified_code, + unified_message, } => DieselPaymentAttemptUpdate::ResponseUpdate { status, connector, @@ -1274,6 +1286,8 @@ impl DataModelExt for PaymentAttemptUpdate { tax_amount, authentication_data, encoded_data, + unified_code, + unified_message, }, Self::UnresolvedResponseUpdate { status, @@ -1307,6 +1321,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_reason, amount_capturable, updated_by, + unified_code, + unified_message, } => DieselPaymentAttemptUpdate::ErrorUpdate { connector, status, @@ -1315,6 +1331,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_reason, amount_capturable, updated_by, + unified_code, + unified_message, }, Self::MultipleCaptureCountUpdate { multiple_capture_count, @@ -1504,6 +1522,8 @@ impl DataModelExt for PaymentAttemptUpdate { tax_amount, authentication_data, encoded_data, + unified_code, + unified_message, } => Self::ResponseUpdate { status, connector, @@ -1523,6 +1543,8 @@ impl DataModelExt for PaymentAttemptUpdate { tax_amount, authentication_data, encoded_data, + unified_code, + unified_message, }, DieselPaymentAttemptUpdate::UnresolvedResponseUpdate { status, @@ -1556,6 +1578,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_reason, amount_capturable, updated_by, + unified_code, + unified_message, } => Self::ErrorUpdate { connector, status, @@ -1564,6 +1588,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_reason, amount_capturable, updated_by, + unified_code, + unified_message, }, DieselPaymentAttemptUpdate::MultipleCaptureCountUpdate { multiple_capture_count, diff --git a/migrations/2023-11-17-061003_add-unified-error-code-mssg-gsm/down.sql b/migrations/2023-11-17-061003_add-unified-error-code-mssg-gsm/down.sql new file mode 100644 index 000000000000..9561c8509b69 --- /dev/null +++ b/migrations/2023-11-17-061003_add-unified-error-code-mssg-gsm/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE gateway_status_map DROP COLUMN IF EXISTS unified_code; +ALTER TABLE gateway_status_map DROP COLUMN IF EXISTS unified_message; \ No newline at end of file diff --git a/migrations/2023-11-17-061003_add-unified-error-code-mssg-gsm/up.sql b/migrations/2023-11-17-061003_add-unified-error-code-mssg-gsm/up.sql new file mode 100644 index 000000000000..a4b1250a032a --- /dev/null +++ b/migrations/2023-11-17-061003_add-unified-error-code-mssg-gsm/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE gateway_status_map ADD COLUMN IF NOT EXISTS unified_code VARCHAR(255); +ALTER TABLE gateway_status_map ADD COLUMN IF NOT EXISTS unified_message VARCHAR(1024); \ No newline at end of file diff --git a/migrations/2023-11-17-084413_add-unified-error-code-mssg-payment-attempt/down.sql b/migrations/2023-11-17-084413_add-unified-error-code-mssg-payment-attempt/down.sql new file mode 100644 index 000000000000..83609093e136 --- /dev/null +++ b/migrations/2023-11-17-084413_add-unified-error-code-mssg-payment-attempt/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_attempt DROP COLUMN IF EXISTS unified_code; +ALTER TABLE payment_attempt DROP COLUMN IF EXISTS unified_message; \ No newline at end of file diff --git a/migrations/2023-11-17-084413_add-unified-error-code-mssg-payment-attempt/up.sql b/migrations/2023-11-17-084413_add-unified-error-code-mssg-payment-attempt/up.sql new file mode 100644 index 000000000000..5e390d51f760 --- /dev/null +++ b/migrations/2023-11-17-084413_add-unified-error-code-mssg-payment-attempt/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE payment_attempt ADD COLUMN IF NOT EXISTS unified_code VARCHAR(255); +ALTER TABLE payment_attempt ADD COLUMN IF NOT EXISTS unified_message VARCHAR(1024); \ No newline at end of file diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 7d94f13dd125..65280c187142 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -5934,6 +5934,14 @@ }, "step_up_possible": { "type": "boolean" + }, + "unified_code": { + "type": "string", + "nullable": true + }, + "unified_message": { + "type": "string", + "nullable": true } } }, @@ -6039,6 +6047,14 @@ }, "step_up_possible": { "type": "boolean" + }, + "unified_code": { + "type": "string", + "nullable": true + }, + "unified_message": { + "type": "string", + "nullable": true } } }, @@ -6113,6 +6129,14 @@ "step_up_possible": { "type": "boolean", "nullable": true + }, + "unified_code": { + "type": "string", + "nullable": true + }, + "unified_message": { + "type": "string", + "nullable": true } } }, @@ -8155,6 +8179,16 @@ "description": "reference to the payment at connector side", "example": "993672945374576J", "nullable": true + }, + "unified_code": { + "type": "string", + "description": "error code unified across the connectors is received here if there was an error while calling connector", + "nullable": true + }, + "unified_message": { + "type": "string", + "description": "error message unified across the connectors is received here if there was an error while calling connector", + "nullable": true } } }, @@ -10041,6 +10075,16 @@ "example": "Failed while verifying the card", "nullable": true }, + "unified_code": { + "type": "string", + "description": "error code unified across the connectors is received here if there was an error while calling connector", + "nullable": true + }, + "unified_message": { + "type": "string", + "description": "error message unified across the connectors is received here if there was an error while calling connector", + "nullable": true + }, "payment_experience": { "allOf": [ { From 44deeb7e7605cb5320b84c0fac1fd551877803a4 Mon Sep 17 00:00:00 2001 From: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com> Date: Mon, 20 Nov 2023 18:19:19 +0530 Subject: [PATCH 042/146] refactor(core): query business profile only once (#2830) --- crates/router/src/core/payments.rs | 64 +++--- crates/router/src/core/payments/operations.rs | 19 +- .../payments/operations/payment_approve.rs | 114 ++++++---- .../payments/operations/payment_cancel.rs | 100 +++++---- .../payments/operations/payment_capture.rs | 99 ++++---- .../operations/payment_complete_authorize.rs | 113 ++++++---- .../payments/operations/payment_confirm.rs | 212 +++++++++++------- .../payments/operations/payment_create.rs | 103 +++++---- .../payments/operations/payment_reject.rs | 103 +++++---- .../payments/operations/payment_session.rs | 99 ++++---- .../core/payments/operations/payment_start.rs | 99 ++++---- .../payments/operations/payment_status.rs | 127 ++++++----- .../payments/operations/payment_update.rs | 99 ++++---- 13 files changed, 772 insertions(+), 579 deletions(-) diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 000cadec0091..0259c48ee827 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -31,9 +31,8 @@ use scheduler::{db::process_tracker::ProcessTrackerExt, errors as sch_errors, ut use time; pub use self::operations::{ - PaymentApprove, PaymentCancel, PaymentCapture, PaymentConfirm, PaymentCreate, - PaymentMethodValidate, PaymentReject, PaymentResponse, PaymentSession, PaymentStatus, - PaymentUpdate, + PaymentApprove, PaymentCancel, PaymentCapture, PaymentConfirm, PaymentCreate, PaymentReject, + PaymentResponse, PaymentSession, PaymentStatus, PaymentUpdate, }; use self::{ flows::{ConstructFlowSpecificData, Feature}, @@ -112,7 +111,12 @@ where tracing::Span::current().record("payment_id", &format!("{}", validate_result.payment_id)); - let (operation, mut payment_data, customer_details) = operation + let operations::GetTrackerResponse { + operation, + customer_details, + mut payment_data, + business_profile, + } = operation .to_get_tracker()? .get_trackers( state, @@ -142,6 +146,7 @@ where state, &req, &merchant_account, + &business_profile, &key_store, &mut payment_data, eligible_connectors, @@ -1998,11 +2003,13 @@ where Ok(()) } +#[allow(clippy::too_many_arguments)] pub async fn get_connector_choice( operation: &BoxedOperation<'_, F, Req, Ctx>, state: &AppState, req: &Req, merchant_account: &domain::MerchantAccount, + business_profile: &storage::business_profile::BusinessProfile, key_store: &domain::MerchantKeyStore, payment_data: &mut PaymentData, eligible_connectors: Option>, @@ -2040,6 +2047,7 @@ where connector_selection( state, merchant_account, + business_profile, key_store, payment_data, Some(straight_through), @@ -2052,6 +2060,7 @@ where connector_selection( state, merchant_account, + business_profile, key_store, payment_data, None, @@ -2075,6 +2084,7 @@ where pub async fn connector_selection( state: &AppState, merchant_account: &domain::MerchantAccount, + business_profile: &storage::business_profile::BusinessProfile, key_store: &domain::MerchantKeyStore, payment_data: &mut PaymentData, request_straight_through: Option, @@ -2114,6 +2124,7 @@ where let decided_connector = decide_connector( state.clone(), merchant_account, + business_profile, key_store, payment_data, request_straight_through, @@ -2141,9 +2152,11 @@ where Ok(decided_connector) } +#[allow(clippy::too_many_arguments)] pub async fn decide_connector( state: AppState, merchant_account: &domain::MerchantAccount, + business_profile: &storage::business_profile::BusinessProfile, key_store: &domain::MerchantKeyStore, payment_data: &mut PaymentData, request_straight_through: Option, @@ -2345,6 +2358,7 @@ where route_connector_v1( &state, merchant_account, + business_profile, key_store, payment_data, routing_data, @@ -2480,6 +2494,7 @@ where pub async fn route_connector_v1( state: &AppState, merchant_account: &domain::MerchantAccount, + business_profile: &storage::business_profile::BusinessProfile, key_store: &domain::MerchantKeyStore, payment_data: &mut PaymentData, routing_data: &mut storage::RoutingData, @@ -2488,44 +2503,19 @@ pub async fn route_connector_v1( where F: Send + Clone, { - #[cfg(not(feature = "business_profile_routing"))] - let algorithm_ref: api::routing::RoutingAlgorithmRef = merchant_account - .routing_algorithm - .clone() - .map(|ra| ra.parse_value("RoutingAlgorithmRef")) + let routing_algorithm = if cfg!(feature = "business_profile_routing") { + business_profile.routing_algorithm.clone() + } else { + merchant_account.routing_algorithm.clone() + }; + + let algorithm_ref = routing_algorithm + .map(|ra| ra.parse_value::("RoutingAlgorithmRef")) .transpose() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Could not decode merchant routing algorithm ref")? .unwrap_or_default(); - #[cfg(feature = "business_profile_routing")] - let algorithm_ref: api::routing::RoutingAlgorithmRef = { - let profile_id = payment_data - .payment_intent - .profile_id - .as_ref() - .get_required_value("profile_id") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("'profile_id' not set in payment intent")?; - - let business_profile = state - .store - .find_business_profile_by_profile_id(profile_id) - .await - .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { - id: profile_id.to_string(), - })?; - - business_profile - .routing_algorithm - .clone() - .map(|ra| ra.parse_value("RoutingAlgorithmRef")) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Could not decode merchant routing algorithm ref")? - .unwrap_or_default() - }; - let connectors = routing::perform_static_routing_v1( state, &merchant_account.merchant_id, diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index f65e65459e00..6f01c653084f 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -4,7 +4,6 @@ pub mod payment_capture; pub mod payment_complete_authorize; pub mod payment_confirm; pub mod payment_create; -pub mod payment_method_validate; pub mod payment_reject; pub mod payment_response; pub mod payment_session; @@ -20,10 +19,9 @@ use router_env::{instrument, tracing}; pub use self::{ payment_approve::PaymentApprove, payment_cancel::PaymentCancel, payment_capture::PaymentCapture, payment_confirm::PaymentConfirm, - payment_create::PaymentCreate, payment_method_validate::PaymentMethodValidate, - payment_reject::PaymentReject, payment_response::PaymentResponse, - payment_session::PaymentSession, payment_start::PaymentStart, payment_status::PaymentStatus, - payment_update::PaymentUpdate, + payment_create::PaymentCreate, payment_reject::PaymentReject, + payment_response::PaymentResponse, payment_session::PaymentSession, + payment_start::PaymentStart, payment_status::PaymentStatus, payment_update::PaymentUpdate, }; use super::{helpers, CustomerDetails, PaymentData}; use crate::{ @@ -91,8 +89,15 @@ pub trait ValidateRequest { ) -> RouterResult<(BoxedOperation<'b, F, R, Ctx>, ValidateResult<'a>)>; } +pub struct GetTrackerResponse<'a, F: Clone, R, Ctx> { + pub operation: BoxedOperation<'a, F, R, Ctx>, + pub customer_details: Option, + pub payment_data: PaymentData, + pub business_profile: storage::business_profile::BusinessProfile, +} + #[async_trait] -pub trait GetTracker: Send { +pub trait GetTracker: Send { #[allow(clippy::too_many_arguments)] async fn get_trackers<'a>( &'a self, @@ -103,7 +108,7 @@ pub trait GetTracker: Send { merchant_account: &domain::MerchantAccount, mechant_key_store: &domain::MerchantKeyStore, auth_flow: services::AuthFlow, - ) -> RouterResult<(BoxedOperation<'a, F, R, Ctx>, D, Option)>; + ) -> RouterResult>; } #[async_trait] diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index 538e65e4b22e..78eb3fb1f10d 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -45,11 +45,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; @@ -76,6 +72,21 @@ impl "confirm", )?; + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = state + .store + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + let ( token, payment_method, @@ -207,50 +218,57 @@ impl format!("Error while retrieving frm_response, merchant_id: {}, payment_id: {attempt_id}", &merchant_account.merchant_id) }); - Ok(( - Box::new(self), - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: request.email.clone(), - mandate_id: None, - mandate_connector, - setup_mandate, - token, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: request.confirm, - payment_method_data: request.payment_method_data.clone(), - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, - sessions_token: vec![], - card_cvc: request.card_cvc.clone(), - creds_identifier: None, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response, - surcharge_details: None, - frm_message: frm_response.ok(), - payment_link_data: None, + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: request.email.clone(), + mandate_id: None, + mandate_connector, + setup_mandate, + token, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - Some(CustomerDetails { - customer_id: request.customer_id.clone(), - name: request.name.clone(), - email: request.email.clone(), - phone: request.phone.clone(), - phone_country_code: request.phone_country_code.clone(), - }), - )) + confirm: request.confirm, + payment_method_data: request.payment_method_data.clone(), + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: request.card_cvc.clone(), + creds_identifier: None, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response, + surcharge_details: None, + frm_message: frm_response.ok(), + payment_link_data: None, + }; + + let customer_details = Some(CustomerDetails { + customer_id: request.customer_id.clone(), + name: request.name.clone(), + email: request.email.clone(), + phone: request.phone.clone(), + phone_country_code: request.phone_country_code.clone(), + }); + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details, + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } diff --git a/crates/router/src/core/payments/operations/payment_cancel.rs b/crates/router/src/core/payments/operations/payment_cancel.rs index 535edf736ca6..096f900e7195 100644 --- a/crates/router/src/core/payments/operations/payment_cancel.rs +++ b/crates/router/src/core/payments/operations/payment_cancel.rs @@ -12,7 +12,7 @@ use crate::{ core::{ errors::{self, RouterResult, StorageErrorExt}, payment_methods::PaymentMethodRetrieve, - payments::{helpers, operations, CustomerDetails, PaymentAddress, PaymentData}, + payments::{helpers, operations, PaymentAddress, PaymentData}, }, routes::AppState, services, @@ -42,11 +42,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsCancelRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; @@ -128,45 +124,63 @@ impl .await .transpose()?; - Ok(( - Box::new(self), - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: None, - mandate_id: None, - mandate_connector: None, - setup_mandate: None, - token: None, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: None, - payment_method_data: None, - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; - sessions_token: vec![], - card_cvc: None, - creds_identifier, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data: None, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response: None, - surcharge_details: None, - frm_message: None, - payment_link_data: None, + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: None, + mandate_id: None, + mandate_connector: None, + setup_mandate: None, + token: None, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - None, - )) + confirm: None, + payment_method_data: None, + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: None, + creds_identifier, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data: None, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response: None, + surcharge_details: None, + frm_message: None, + payment_link_data: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: None, + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index ff51a2c49d77..09e79064dc69 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -41,11 +41,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsCaptureRequest, Ctx>, - payments::PaymentData, - Option, - )> { + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; @@ -172,44 +168,63 @@ impl .await .transpose()?; - Ok(( - Box::new(self), - payments::PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - force_sync: None, - amount, - email: None, - mandate_id: None, - mandate_connector: None, - setup_mandate: None, - token: None, - address: payments::PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: None, - payment_method_data: None, - refunds: vec![], - disputes: vec![], - attempts: None, - sessions_token: vec![], - card_cvc: None, - creds_identifier, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data: None, - ephemeral_key: None, - multiple_capture_data, - redirect_response: None, - surcharge_details: None, - frm_message: None, - payment_link_data: None, + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + let payment_data = payments::PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + force_sync: None, + amount, + email: None, + mandate_id: None, + mandate_connector: None, + setup_mandate: None, + token: None, + address: payments::PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - None, - )) + confirm: None, + payment_method_data: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: None, + creds_identifier, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data: None, + ephemeral_key: None, + multiple_capture_data, + redirect_response: None, + surcharge_details: None, + frm_message: None, + payment_link_data: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: None, + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index c648d95a4950..7cc1edf17fd1 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -44,11 +44,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; @@ -202,50 +198,71 @@ impl // The operation merges mandate data from both request and payment_attempt let setup_mandate = setup_mandate.map(Into::into); - Ok(( - Box::new(self), - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: request.email.clone(), - mandate_id: None, - mandate_connector, - setup_mandate, - token, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: request.confirm, - payment_method_data: request.payment_method_data.clone(), - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, - sessions_token: vec![], - card_cvc: request.card_cvc.clone(), - creds_identifier: None, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response, - surcharge_details: None, - frm_message: None, - payment_link_data: None, + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: request.email.clone(), + mandate_id: None, + mandate_connector, + setup_mandate, + token, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - Some(CustomerDetails { - customer_id: request.customer_id.clone(), - name: request.name.clone(), - email: request.email.clone(), - phone: request.phone.clone(), - phone_country_code: request.phone_country_code.clone(), - }), - )) + confirm: request.confirm, + payment_method_data: request.payment_method_data.clone(), + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: request.card_cvc.clone(), + creds_identifier: None, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response, + surcharge_details: None, + frm_message: None, + payment_link_data: None, + }; + + let customer_details = Some(CustomerDetails { + customer_id: request.customer_id.clone(), + name: request.name.clone(), + email: request.email.clone(), + phone: request.phone.clone(), + phone_country_code: request.phone_country_code.clone(), + }); + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details, + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index afb7f110ed5d..a040782d83cd 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -50,11 +50,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; @@ -65,7 +61,6 @@ impl .change_context(errors::ApiErrorResponse::PaymentNotFound)?; // Stage 1 - let store = state.clone().store; let m_merchant_id = merchant_id.clone(); let payment_intent_fut = tokio::spawn( @@ -137,8 +132,29 @@ impl let customer_details = helpers::get_customer_details_from_request(request); // Stage 2 - let attempt_id = payment_intent.active_attempt.get_id(); + let profile_id = payment_intent + .profile_id + .clone() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let store = state.store.clone(); + + let business_profile_fut = tokio::spawn(async move { + store + .find_business_profile_by_profile_id(&profile_id) + .map(|business_profile_result| { + business_profile_result.to_not_found_response( + errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + }, + ) + }) + .await + }); + let store = state.clone().store; let m_payment_id = payment_intent.payment_id.clone(); let m_merchant_id = merchant_id.clone(); @@ -235,48 +251,72 @@ impl .in_current_span(), ); - let (mut payment_attempt, shipping_address, billing_address) = match payment_intent.status { - api_models::enums::IntentStatus::RequiresCustomerAction - | api_models::enums::IntentStatus::RequiresMerchantAction - | api_models::enums::IntentStatus::RequiresPaymentMethod - | api_models::enums::IntentStatus::RequiresConfirmation => { - let (payment_attempt, shipping_address, billing_address, _) = tokio::try_join!( - utils::flatten_join_error(payment_attempt_fut), - utils::flatten_join_error(shipping_address_fut), - utils::flatten_join_error(billing_address_fut), - utils::flatten_join_error(config_update_fut) - )?; - - (payment_attempt, shipping_address, billing_address) - } - _ => { - let (mut payment_attempt, shipping_address, billing_address, _) = tokio::try_join!( - utils::flatten_join_error(payment_attempt_fut), - utils::flatten_join_error(shipping_address_fut), - utils::flatten_join_error(billing_address_fut), - utils::flatten_join_error(config_update_fut) - )?; - - let attempt_type = helpers::get_attempt_type( - &payment_intent, - &payment_attempt, - request, - "confirm", - )?; - - (payment_intent, payment_attempt) = attempt_type - .modify_payment_intent_and_payment_attempt( - request, - payment_intent, + // Based on whether a retry can be performed or not, fetch relevant entities + let (mut payment_attempt, shipping_address, billing_address, business_profile) = + match payment_intent.status { + api_models::enums::IntentStatus::RequiresCustomerAction + | api_models::enums::IntentStatus::RequiresMerchantAction + | api_models::enums::IntentStatus::RequiresPaymentMethod + | api_models::enums::IntentStatus::RequiresConfirmation => { + // Normal payment + let (payment_attempt, shipping_address, billing_address, business_profile, _) = + tokio::try_join!( + utils::flatten_join_error(payment_attempt_fut), + utils::flatten_join_error(shipping_address_fut), + utils::flatten_join_error(billing_address_fut), + utils::flatten_join_error(business_profile_fut), + utils::flatten_join_error(config_update_fut) + )?; + + ( payment_attempt, - &*state.store, - storage_scheme, + shipping_address, + billing_address, + business_profile, ) - .await?; + } + _ => { + // Retry payment + let ( + mut payment_attempt, + shipping_address, + billing_address, + business_profile, + _, + ) = tokio::try_join!( + utils::flatten_join_error(payment_attempt_fut), + utils::flatten_join_error(shipping_address_fut), + utils::flatten_join_error(billing_address_fut), + utils::flatten_join_error(business_profile_fut), + utils::flatten_join_error(config_update_fut) + )?; + + let attempt_type = helpers::get_attempt_type( + &payment_intent, + &payment_attempt, + request, + "confirm", + )?; + + // 3 + (payment_intent, payment_attempt) = attempt_type + .modify_payment_intent_and_payment_attempt( + request, + payment_intent, + payment_attempt, + &*state.store, + storage_scheme, + ) + .await?; - (payment_attempt, shipping_address, billing_address) - } - }; + ( + payment_attempt, + shipping_address, + billing_address, + business_profile, + ) + } + }; payment_intent.order_details = request .get_order_details_as_value() @@ -382,6 +422,7 @@ impl sm.mandate_type = payment_attempt.mandate_details.clone().or(sm.mandate_type); sm }); + Self::validate_request_surcharge_details_with_session_surcharge_details( state, &payment_attempt, @@ -394,44 +435,49 @@ impl &payment_attempt, ); - Ok(( - Box::new(self), - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: request.email.clone(), - mandate_id: None, - mandate_connector, - setup_mandate, - token, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: request.confirm, - payment_method_data: request.payment_method_data.clone(), - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, - sessions_token: vec![], - card_cvc: request.card_cvc.clone(), - creds_identifier, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response: None, - surcharge_details, - frm_message: None, - payment_link_data: None, + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: request.email.clone(), + mandate_id: None, + mandate_connector, + setup_mandate, + token, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - Some(customer_details), - )) + confirm: request.confirm, + payment_method_data: request.payment_method_data.clone(), + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: request.card_cvc.clone(), + creds_identifier, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response: None, + surcharge_details, + frm_message: None, + payment_link_data: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: Some(customer_details), + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 1fd4c7014c35..526b03137bea 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -53,11 +53,7 @@ impl merchant_account: &domain::MerchantAccount, merchant_key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let db = &*state.store; let ephemeral_key = Self::get_ephemeral_key(request, state, merchant_account).await; let merchant_id = &merchant_account.merchant_id; @@ -196,6 +192,20 @@ impl payment_id: payment_id.clone(), })?; + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + let mandate_id = request .mandate_id .as_ref() @@ -246,6 +256,7 @@ impl request.confirm, self, ); + let creds_identifier = request .merchant_connector_details .as_ref() @@ -265,9 +276,8 @@ impl .transpose()?; // The operation merges mandate data from both request and payment_attempt - let setup_mandate: Option = setup_mandate.map(Into::into); + let setup_mandate = setup_mandate.map(MandateData::from); - // populate payment_data.surcharge_details from request let surcharge_details = request.surcharge_details.map(|surcharge_details| { payment_methods::SurchargeDetailsResponse { surcharge: payment_methods::Surcharge::Fixed(surcharge_details.surcharge_amount), @@ -280,44 +290,49 @@ impl } }); - Ok(( - operation, - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: request.email.clone(), - mandate_id, - mandate_connector, - setup_mandate, - token, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: request.confirm, - payment_method_data: request.payment_method_data.clone(), - refunds: vec![], - disputes: vec![], - attempts: None, - force_sync: None, - sessions_token: vec![], - card_cvc: request.card_cvc.clone(), - creds_identifier, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data, - ephemeral_key, - multiple_capture_data: None, - redirect_response: None, - surcharge_details, - frm_message: None, - payment_link_data, + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: request.email.clone(), + mandate_id, + mandate_connector, + setup_mandate, + token, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - Some(customer_details), - )) + confirm: request.confirm, + payment_method_data: request.payment_method_data.clone(), + refunds: vec![], + disputes: vec![], + attempts: None, + force_sync: None, + sessions_token: vec![], + card_cvc: request.card_cvc.clone(), + creds_identifier, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data, + ephemeral_key, + multiple_capture_data: None, + redirect_response: None, + surcharge_details, + frm_message: None, + payment_link_data, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation, + customer_details: Some(customer_details), + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } diff --git a/crates/router/src/core/payments/operations/payment_reject.rs b/crates/router/src/core/payments/operations/payment_reject.rs index 16d264c001ec..ae02dde4bc06 100644 --- a/crates/router/src/core/payments/operations/payment_reject.rs +++ b/crates/router/src/core/payments/operations/payment_reject.rs @@ -11,7 +11,7 @@ use crate::{ core::{ errors::{self, RouterResult, StorageErrorExt}, payment_methods::PaymentMethodRetrieve, - payments::{helpers, operations, CustomerDetails, PaymentAddress, PaymentData}, + payments::{helpers, operations, PaymentAddress, PaymentData}, }, routes::AppState, services, @@ -41,11 +41,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, PaymentsRejectRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; @@ -114,45 +110,64 @@ impl format!("Error while retrieving frm_response, merchant_id: {}, payment_id: {attempt_id}", &merchant_account.merchant_id) }); - Ok(( - Box::new(self), - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: None, - mandate_id: None, - mandate_connector: None, - setup_mandate: None, - token: None, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: None, - payment_method_data: None, - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, - - sessions_token: vec![], - card_cvc: None, - creds_identifier: None, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data: None, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response: None, - surcharge_details: None, - frm_message: frm_response.ok(), - payment_link_data: None, + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = state + .store + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: None, + mandate_id: None, + mandate_connector: None, + setup_mandate: None, + token: None, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - None, - )) + confirm: None, + payment_method_data: None, + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: None, + creds_identifier: None, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data: None, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response: None, + surcharge_details: None, + frm_message: frm_response.ok(), + payment_link_data: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: None, + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index 3abde60c2e9b..cea6eb176672 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -43,11 +43,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsSessionRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let payment_id = payment_id .get_payment_intent_id() .change_context(errors::ApiErrorResponse::PaymentNotFound)?; @@ -152,44 +148,63 @@ impl .await .transpose()?; - Ok(( - Box::new(self), - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: None, - mandate_id: None, - mandate_connector: None, - token: None, - setup_mandate: None, - address: payments::PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: None, - payment_method_data: None, - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, - sessions_token: vec![], - card_cvc: None, - creds_identifier, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data: None, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response: None, - surcharge_details: None, - frm_message: None, - payment_link_data: None, + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: None, + mandate_id: None, + mandate_connector: None, + token: None, + setup_mandate: None, + address: payments::PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - Some(customer_details), - )) + confirm: None, + payment_method_data: None, + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: None, + creds_identifier, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data: None, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response: None, + surcharge_details: None, + frm_message: None, + payment_link_data: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: Some(customer_details), + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index 17f39d5150bb..6d4281216b4f 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -42,11 +42,7 @@ impl merchant_account: &domain::MerchantAccount, mechant_key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsStartRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let (mut payment_intent, payment_attempt, currency, amount); let db = &*state.store; @@ -126,44 +122,63 @@ impl ..CustomerDetails::default() }; - Ok(( - Box::new(self), - PaymentData { - flow: PhantomData, - payment_intent, - currency, - amount, - email: None, - mandate_id: None, - mandate_connector: None, - setup_mandate: None, - token: payment_attempt.payment_token.clone(), - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: Some(payment_attempt.confirm), - payment_attempt, - payment_method_data: None, - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, - sessions_token: vec![], - card_cvc: None, - creds_identifier: None, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data: None, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response: None, - surcharge_details: None, - frm_message: None, - payment_link_data: None, + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + currency, + amount, + email: None, + mandate_id: None, + mandate_connector: None, + setup_mandate: None, + token: payment_attempt.payment_token.clone(), + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - Some(customer_details), - )) + confirm: Some(payment_attempt.confirm), + payment_attempt, + payment_method_data: None, + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: None, + creds_identifier: None, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data: None, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response: None, + surcharge_details: None, + frm_message: None, + payment_link_data: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: Some(customer_details), + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index fb58aeb34e07..b31c406f0ecd 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -190,11 +190,8 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRetrieveRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> + { get_tracker_for_sync( payment_id, merchant_account, @@ -221,12 +218,8 @@ async fn get_tracker_for_sync< request: &api::PaymentsRetrieveRequest, operation: Op, storage_scheme: enums::MerchantStorageScheme, -) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRetrieveRequest, Ctx>, - PaymentData, - Option, -)> { - let (payment_intent, mut payment_attempt, currency, amount); +) -> RouterResult> { + let (payment_intent, payment_attempt, currency, amount); (payment_intent, payment_attempt) = get_payment_intent_payment_attempt( db, @@ -250,7 +243,6 @@ async fn get_tracker_for_sync< let payment_id_str = payment_attempt.payment_id.clone(); - payment_attempt.encoded_data = request.param.clone(); currency = payment_attempt.currency.get_required_value("currency")?; amount = payment_attempt.amount.into(); @@ -357,53 +349,74 @@ async fn get_tracker_for_sync< }) .await .transpose()?; - Ok(( - Box::new(operation), - PaymentData { - flow: PhantomData, - payment_intent, - currency, - amount, - email: None, - mandate_id: payment_attempt.mandate_id.clone().map(|id| { - api_models::payments::MandateIds { - mandate_id: id, - mandate_reference_id: None, - } + + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + currency, + amount, + email: None, + mandate_id: payment_attempt + .mandate_id + .clone() + .map(|id| api_models::payments::MandateIds { + mandate_id: id, + mandate_reference_id: None, }), - mandate_connector: None, - setup_mandate: None, - token: None, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: Some(request.force_sync), - payment_method_data: None, - force_sync: Some( - request.force_sync - && (helpers::check_force_psync_precondition(&payment_attempt.status) - || contains_encoded_data), - ), - payment_attempt, - refunds, - disputes, - attempts, - sessions_token: vec![], - card_cvc: None, - creds_identifier, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data: None, - ephemeral_key: None, - multiple_capture_data, - redirect_response: None, - payment_link_data: None, - surcharge_details: None, - frm_message: frm_response.ok(), + mandate_connector: None, + setup_mandate: None, + token: None, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - None, - )) + confirm: Some(request.force_sync), + payment_method_data: None, + force_sync: Some( + request.force_sync + && (helpers::check_force_psync_precondition(&payment_attempt.status) + || contains_encoded_data), + ), + payment_attempt, + refunds, + disputes, + attempts, + sessions_token: vec![], + card_cvc: None, + creds_identifier, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data: None, + ephemeral_key: None, + multiple_capture_data, + redirect_response: None, + payment_link_data: None, + surcharge_details: None, + frm_message: frm_response.ok(), + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(operation), + customer_details: None, + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } impl diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 53a768f26810..6833a6a392e7 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -44,11 +44,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let (mut payment_intent, mut payment_attempt, currency): (_, _, storage_enums::Currency); let payment_id = payment_id @@ -304,48 +300,67 @@ impl // The operation merges mandate data from both request and payment_attempt let setup_mandate = setup_mandate.map(Into::into); + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + let surcharge_details = request.surcharge_details.map(|request_surcharge_details| { request_surcharge_details.get_surcharge_details_object(payment_attempt.amount) }); - Ok(( - next_operation, - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: request.email.clone(), - mandate_id, - mandate_connector, - token, - setup_mandate, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: request.confirm, - payment_method_data: request.payment_method_data.clone(), - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, - sessions_token: vec![], - card_cvc: request.card_cvc.clone(), - creds_identifier, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response: None, - surcharge_details, - frm_message: None, - payment_link_data: None, + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: request.email.clone(), + mandate_id, + mandate_connector, + token, + setup_mandate, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - Some(customer_details), - )) + confirm: request.confirm, + payment_method_data: request.payment_method_data.clone(), + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: request.card_cvc.clone(), + creds_identifier, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response: None, + surcharge_details, + frm_message: None, + payment_link_data: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: next_operation, + customer_details: Some(customer_details), + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } From 922dc90019deeab4afcd65e6bc1d1c749bdaa09d Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Mon, 20 Nov 2023 19:29:51 +0530 Subject: [PATCH 043/146] ci(hotfix-pr-check): use env input from GitHub to read PR body (#2923) --- .github/workflows/hotfix-pr-check.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/hotfix-pr-check.yml b/.github/workflows/hotfix-pr-check.yml index 59e0bbee3cb4..7a724b602586 100644 --- a/.github/workflows/hotfix-pr-check.yml +++ b/.github/workflows/hotfix-pr-check.yml @@ -19,8 +19,9 @@ jobs: - name: Get hotfix pull request body shell: bash - run: | - echo '${{ github.event.pull_request.body }}' > hotfix_pr_body.txt + env: + PR_BODY: ${{ github.event.pull_request.body }} + run: echo $PR_BODY > hotfix_pr_body.txt - name: Get a list of all original PR numbers shell: bash From cfabfa60db4d275066be72ee64153a34d38f13b8 Mon Sep 17 00:00:00 2001 From: Kartikeya Hegde Date: Mon, 20 Nov 2023 20:52:56 +0530 Subject: [PATCH 044/146] fix: api lock on PaymentsCreate (#2916) --- crates/api_models/src/payments.rs | 14 +++++++ .../payments/operations/payment_approve.rs | 20 ++++------ .../operations/payment_complete_authorize.rs | 19 ++++----- .../payments/operations/payment_confirm.rs | 23 +++++------ .../payments/operations/payment_create.rs | 15 ++----- .../payments/operations/payment_update.rs | 19 ++++----- crates/router/src/routes/payments.rs | 39 +++++++++++++++++-- 7 files changed, 87 insertions(+), 62 deletions(-) diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 9f4f151c2228..c427088d688d 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -1679,6 +1679,20 @@ impl std::fmt::Display for PaymentIdType { } } +impl PaymentIdType { + pub fn and_then(self, f: F) -> Result + where + F: FnOnce(String) -> Result, + { + match self { + Self::PaymentIntentId(s) => f(s).map(Self::PaymentIntentId), + Self::ConnectorTransactionId(s) => f(s).map(Self::ConnectorTransactionId), + Self::PaymentAttemptId(s) => f(s).map(Self::PaymentAttemptId), + Self::PreprocessingId(s) => f(s).map(Self::PreprocessingId), + } + } +} + impl Default for PaymentIdType { fn default() -> Self { Self::PaymentIntentId(Default::default()) diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index 78eb3fb1f10d..af52105c85d5 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -3,7 +3,7 @@ use std::marker::PhantomData; use api_models::enums::FrmSuggestion; use async_trait::async_trait; use data_models::mandates::MandateData; -use error_stack::ResultExt; +use error_stack::{report, IntoReport, ResultExt}; use router_derive::PaymentOperation; use router_env::{instrument, tracing}; @@ -399,15 +399,6 @@ impl ValidateRequest, operations::ValidateResult<'a>, )> { - let given_payment_id = match &request.payment_id { - Some(id_type) => Some( - id_type - .get_payment_intent_id() - .change_context(errors::ApiErrorResponse::PaymentNotFound)?, - ), - None => None, - }; - let request_merchant_id = request.merchant_id.as_deref(); helpers::validate_merchant_id(&merchant_account.merchant_id, request_merchant_id) .change_context(errors::ApiErrorResponse::InvalidDataFormat { @@ -419,13 +410,18 @@ impl ValidateRequest ValidateRequest, operations::ValidateResult<'a>, )> { - let given_payment_id = match &request.payment_id { - Some(id_type) => Some( - id_type - .get_payment_intent_id() - .change_context(errors::ApiErrorResponse::PaymentNotFound)?, - ), - None => None, - }; + let payment_id = request + .payment_id + .clone() + .ok_or(report!(errors::ApiErrorResponse::PaymentNotFound))?; let request_merchant_id = request.merchant_id.as_deref(); helpers::validate_merchant_id(&merchant_account.merchant_id, request_merchant_id) @@ -394,13 +390,14 @@ impl ValidateRequest ValidateRequest, )> { helpers::validate_customer_details_in_request(request)?; - let given_payment_id = match &request.payment_id { - Some(id_type) => Some( - id_type - .get_payment_intent_id() - .change_context(errors::ApiErrorResponse::PaymentNotFound)?, - ), - None => None, - }; let request_merchant_id = request.merchant_id.as_deref(); helpers::validate_merchant_id(&merchant_account.merchant_id, request_merchant_id) @@ -840,14 +832,19 @@ impl ValidateRequest ValidateRequest Some( - id_type - .get_payment_intent_id() - .change_context(errors::ApiErrorResponse::PaymentNotFound)?, - ), - None => None, - }; + let payment_id = request.payment_id.clone().ok_or(error_stack::report!( + errors::ApiErrorResponse::PaymentNotFound + ))?; let request_merchant_id = request.merchant_id.as_deref(); helpers::validate_merchant_id(&merchant_account.merchant_id, request_merchant_id) @@ -555,8 +550,6 @@ impl ValidateRequest ValidateRequest ValidateRequest, )> { helpers::validate_customer_details_in_request(request)?; - let given_payment_id = match &request.payment_id { - Some(id_type) => Some( - id_type - .get_payment_intent_id() - .change_context(errors::ApiErrorResponse::PaymentNotFound)?, - ), - None => None, - }; + let payment_id = request + .payment_id + .clone() + .ok_or(report!(errors::ApiErrorResponse::PaymentNotFound))?; let request_merchant_id = request.merchant_id.as_deref(); helpers::validate_merchant_id(&merchant_account.merchant_id, request_merchant_id) @@ -635,13 +631,14 @@ impl ValidateRequest, ) -> impl Responder { let flow = Flow::PaymentsCreate; - let payload = json_payload.into_inner(); + let mut payload = json_payload.into_inner(); if let Some(api_enums::CaptureMethod::Scheduled) = payload.capture_method { return http_not_implemented(); }; + if let Err(err) = get_or_generate_payment_id(&mut payload) { + return api::log_and_return_error_response(err); + } + let locking_action = payload.get_locking_input(flow.clone()); Box::pin(api::server_wrap( @@ -959,6 +967,29 @@ where } } +pub fn get_or_generate_payment_id( + payload: &mut payment_types::PaymentsRequest, +) -> errors::RouterResult<()> { + let given_payment_id = payload + .payment_id + .clone() + .map(|payment_id| { + payment_id + .get_payment_intent_id() + .map_err(|err| err.change_context(errors::ApiErrorResponse::PaymentNotFound)) + }) + .transpose()?; + + let payment_id = + core_utils::get_or_generate_id("payment_id", &given_payment_id, "pay").into_report()?; + + payload.payment_id = Some(api_models::payments::PaymentIdType::PaymentIntentId( + payment_id, + )); + + Ok(()) +} + impl GetLockingInput for payment_types::PaymentsRequest { fn get_locking_input(&self, flow: F) -> api_locking::LockAction where From 5c4e7c9031f62d63af35da2dcab79eac948e7dbb Mon Sep 17 00:00:00 2001 From: Kashif <46213975+kashif-m@users.noreply.github.com> Date: Tue, 21 Nov 2023 13:04:22 +0530 Subject: [PATCH 045/146] refactor: add mapping for ConnectorError in payouts flow (#2608) Co-authored-by: Kashif Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Kashif --- crates/api_models/src/payouts.rs | 2 +- crates/diesel_models/src/payout_attempt.rs | 4 +- crates/diesel_models/src/schema.rs | 2 +- .../src/connector/adyen/transformers.rs | 8 +- crates/router/src/core/errors/utils.rs | 5 + crates/router/src/core/payouts.rs | 31 +- crates/router/src/core/payouts/validator.rs | 18 +- crates/router/src/core/utils.rs | 26 +- .../down.sql | 4 + .../up.sql | 6 + openapi/openapi_spec.json | 6 +- .../collection-dir/adyen_uk/.variable.json | 15 + .../Flow Testcases/Happy Cases/.meta.json | 21 +- .../Flow Testcases/QuickStart/.meta.json | 5 +- .../Merchant Account - Create/request.json | 4 + .../Payment Connector - Create/event.test.js | 13 + .../.event.meta.json | 3 + .../event.prerequest.js | 0 .../Payout Connector - Create/event.test.js | 52 +++ .../Payout Connector - Create/request.json | 293 +++++++++++++++ .../Payout Connector - Create/response.json | 1 + .../Payouts - Create/.event.meta.json | 3 + .../Payouts - Create/event.prerequest.js | 0 .../QuickStart/Payouts - Create/event.test.js | 60 ++++ .../QuickStart/Payouts - Create/request.json | 72 ++++ .../QuickStart/Payouts - Create/response.json | 1 + .../Payouts - Retrieve/.event.meta.json | 3 + .../Payouts - Retrieve/event.test.js | 48 +++ .../Payouts - Retrieve/request.json | 22 ++ .../Payouts - Retrieve/response.json | 1 + .../Flow Testcases/Variation Cases/.meta.json | 3 +- .../.meta.json | 3 + .../ACH Payouts - Create/.event.meta.json | 3 + .../ACH Payouts - Create/event.prerequest.js | 0 .../ACH Payouts - Create/event.test.js | 48 +++ .../ACH Payouts - Create/request.json | 99 ++++++ .../ACH Payouts - Create/response.json | 1 + .../Bacs Payouts - Create/.event.meta.json | 3 + .../Bacs Payouts - Create/event.prerequest.js | 0 .../Bacs Payouts - Create/event.test.js | 48 +++ .../Bacs Payouts - Create/request.json | 74 ++++ .../Bacs Payouts - Create/response.json | 1 + .../adyen_uk/event.prerequest.js | 35 ++ postman/collection-dir/wise/.auth.json | 22 ++ postman/collection-dir/wise/.event.meta.json | 3 + postman/collection-dir/wise/.info.json | 8 + postman/collection-dir/wise/.meta.json | 3 + postman/collection-dir/wise/.variable.json | 100 ++++++ .../wise/Flow Testcases/.meta.json | 3 + .../Flow Testcases/Happy Cases/.meta.json | 6 + .../.meta.json | 3 + .../Payouts - Create/.event.meta.json | 3 + .../Payouts - Create/event.prerequest.js | 0 .../Payouts - Create/event.test.js | 48 +++ .../Payouts - Create/request.json | 72 ++++ .../Payouts - Create/response.json | 1 + .../.meta.json | 3 + .../Payouts - Create/.event.meta.json | 3 + .../Payouts - Create/event.prerequest.js | 0 .../Payouts - Create/event.test.js | 48 +++ .../Payouts - Create/request.json | 72 ++++ .../Payouts - Create/response.json | 1 + .../wise/Flow Testcases/QuickStart/.meta.json | 8 + .../API Key - Create/.event.meta.json | 3 + .../QuickStart/API Key - Create/event.test.js | 46 +++ .../QuickStart/API Key - Create/request.json | 47 +++ .../QuickStart/API Key - Create/response.json | 1 + .../.event.meta.json | 3 + .../event.prerequest.js | 0 .../Merchant Account - Create/event.test.js | 56 +++ .../Merchant Account - Create/request.json | 91 +++++ .../Merchant Account - Create/response.json | 1 + .../.event.meta.json | 3 + .../event.prerequest.js | 0 .../Payout Connector - Create/event.test.js | 39 ++ .../Payout Connector - Create/request.json | 333 ++++++++++++++++++ .../Payout Connector - Create/response.json | 1 + .../Payouts - Create/.event.meta.json | 3 + .../Payouts - Create/event.prerequest.js | 0 .../QuickStart/Payouts - Create/event.test.js | 48 +++ .../QuickStart/Payouts - Create/request.json | 72 ++++ .../QuickStart/Payouts - Create/response.json | 1 + .../Flow Testcases/Variation Cases/.meta.json | 3 + .../.meta.json | 3 + .../Payouts - Create/.event.meta.json | 3 + .../Payouts - Create/event.prerequest.js | 0 .../Payouts - Create/event.test.js | 48 +++ .../Payouts - Create/request.json | 72 ++++ .../Payouts - Create/response.json | 1 + .../wise/Health check/.meta.json | 3 + .../wise/Health check/Health/.event.meta.json | 3 + .../wise/Health check/Health/event.test.js | 4 + .../wise/Health check/Health/request.json | 16 + .../wise/Health check/Health/response.json | 1 + .../collection-dir/wise/event.prerequest.js | 0 postman/collection-dir/wise/event.test.js | 13 + 96 files changed, 2310 insertions(+), 62 deletions(-) create mode 100644 migrations/2023-10-27-064512_alter_payout_profile_id/down.sql create mode 100644 migrations/2023-10-27-064512_alter_payout_profile_id/up.sql create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/.event.meta.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/event.prerequest.js create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/event.test.js create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/request.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/response.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/.event.meta.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/event.prerequest.js create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/event.test.js create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/request.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/response.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/.event.meta.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/event.test.js create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/request.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/response.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/.meta.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/.event.meta.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/event.prerequest.js create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/event.test.js create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/request.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/response.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/.event.meta.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/event.prerequest.js create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/event.test.js create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/request.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/response.json create mode 100644 postman/collection-dir/wise/.auth.json create mode 100644 postman/collection-dir/wise/.event.meta.json create mode 100644 postman/collection-dir/wise/.info.json create mode 100644 postman/collection-dir/wise/.meta.json create mode 100644 postman/collection-dir/wise/.variable.json create mode 100644 postman/collection-dir/wise/Flow Testcases/.meta.json create mode 100644 postman/collection-dir/wise/Flow Testcases/Happy Cases/.meta.json create mode 100644 postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/.meta.json create mode 100644 postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/.event.meta.json create mode 100644 postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/event.prerequest.js create mode 100644 postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/event.test.js create mode 100644 postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/request.json create mode 100644 postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/response.json create mode 100644 postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/.meta.json create mode 100644 postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/.event.meta.json create mode 100644 postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/event.prerequest.js create mode 100644 postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/event.test.js create mode 100644 postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/request.json create mode 100644 postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/response.json create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/.meta.json create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/.event.meta.json create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/event.test.js create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/request.json create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/response.json create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/.event.meta.json create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/event.prerequest.js create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/event.test.js create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/request.json create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/response.json create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/.event.meta.json create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/event.prerequest.js create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/event.test.js create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/request.json create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/response.json create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/.event.meta.json create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/event.prerequest.js create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/event.test.js create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/request.json create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/response.json create mode 100644 postman/collection-dir/wise/Flow Testcases/Variation Cases/.meta.json create mode 100644 postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/.meta.json create mode 100644 postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/.event.meta.json create mode 100644 postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/event.prerequest.js create mode 100644 postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/event.test.js create mode 100644 postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/request.json create mode 100644 postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/response.json create mode 100644 postman/collection-dir/wise/Health check/.meta.json create mode 100644 postman/collection-dir/wise/Health check/Health/.event.meta.json create mode 100644 postman/collection-dir/wise/Health check/Health/event.test.js create mode 100644 postman/collection-dir/wise/Health check/Health/request.json create mode 100644 postman/collection-dir/wise/Health check/Health/response.json create mode 100644 postman/collection-dir/wise/event.prerequest.js create mode 100644 postman/collection-dir/wise/event.test.js diff --git a/crates/api_models/src/payouts.rs b/crates/api_models/src/payouts.rs index 5cc5e5118166..f7dba2446e91 100644 --- a/crates/api_models/src/payouts.rs +++ b/crates/api_models/src/payouts.rs @@ -382,7 +382,7 @@ pub struct PayoutCreateResponse { pub error_code: Option, /// The business profile that is associated with this payment - pub profile_id: Option, + pub profile_id: String, } #[derive(Default, Debug, Clone, Deserialize, ToSchema)] diff --git a/crates/diesel_models/src/payout_attempt.rs b/crates/diesel_models/src/payout_attempt.rs index d87ed5319a91..7a2c83061877 100644 --- a/crates/diesel_models/src/payout_attempt.rs +++ b/crates/diesel_models/src/payout_attempt.rs @@ -26,7 +26,7 @@ pub struct PayoutAttempt { pub created_at: PrimitiveDateTime, #[serde(with = "common_utils::custom_serde::iso8601")] pub last_modified_at: PrimitiveDateTime, - pub profile_id: Option, + pub profile_id: String, pub merchant_connector_id: Option, } @@ -51,7 +51,7 @@ impl Default for PayoutAttempt { business_label: None, created_at: now, last_modified_at: now, - profile_id: None, + profile_id: String::default(), merchant_connector_id: None, } } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index ce974e409a2c..2ce4f2b6d9d4 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -755,7 +755,7 @@ diesel::table! { created_at -> Timestamp, last_modified_at -> Timestamp, #[max_length = 64] - profile_id -> Nullable, + profile_id -> Varchar, #[max_length = 32] merchant_connector_id -> Nullable, } diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index ec21c9baa5e9..0243dc085f83 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -4010,8 +4010,12 @@ impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutC iban: Some(b.iban), tax_id: None, }, - _ => Err(errors::ConnectorError::NotSupported { - message: "Bank transfers via ACH or Bacs are not supported".to_string(), + payouts::BankPayout::Ach(..) => Err(errors::ConnectorError::NotSupported { + message: "Bank transfer via ACH is not supported".to_string(), + connector: "Adyen", + })?, + payouts::BankPayout::Bacs(..) => Err(errors::ConnectorError::NotSupported { + message: "Bank transfer via Bacs is not supported".to_string(), connector: "Adyen", })?, }; diff --git a/crates/router/src/core/errors/utils.rs b/crates/router/src/core/errors/utils.rs index c3cdf95b87bd..869a5b6bde95 100644 --- a/crates/router/src/core/errors/utils.rs +++ b/crates/router/src/core/errors/utils.rs @@ -400,6 +400,11 @@ impl ConnectorErrorExt for error_stack::Result field_names: field_names.to_vec(), } } + errors::ConnectorError::NotSupported { message, connector } => { + errors::ApiErrorResponse::NotSupported { + message: format!("{} by {}", message, connector), + } + } _ => errors::ApiErrorResponse::InternalServerError, }; err.change_context(error) diff --git a/crates/router/src/core/payouts.rs b/crates/router/src/core/payouts.rs index f1136a35a65a..debc9d124448 100644 --- a/crates/router/src/core/payouts.rs +++ b/crates/router/src/core/payouts.rs @@ -35,6 +35,7 @@ pub struct PayoutData { pub payout_attempt: storage::PayoutAttempt, pub payout_method_data: Option, pub merchant_connector_account: Option, + pub profile_id: String, } // ********************************************** CORE FLOWS ********************************************** @@ -96,9 +97,7 @@ pub async fn payouts_create_core( merchant_account: domain::MerchantAccount, key_store: domain::MerchantKeyStore, req: payouts::PayoutCreateRequest, -) -> RouterResponse -where -{ +) -> RouterResponse { // Form connector data let connector_data = get_connector_data( &state, @@ -111,7 +110,7 @@ where .await?; // Validate create request - let (payout_id, payout_method_data) = + let (payout_id, payout_method_data, profile_id) = validator::validate_create_request(&state, &merchant_account, &req, &key_store).await?; // Create DB entries @@ -121,6 +120,7 @@ where &key_store, &req, &payout_id, + &profile_id, &connector_data.connector_name, payout_method_data.as_ref(), ) @@ -561,18 +561,8 @@ pub async fn create_recipient( let customer_details = payout_data.customer_details.to_owned(); let connector_name = connector_data.connector_name.to_string(); - let profile_id = core_utils::get_profile_id_from_business_details( - payout_data.payout_attempt.business_country, - payout_data.payout_attempt.business_label.as_ref(), - merchant_account, - payout_data.payout_attempt.profile_id.as_ref(), - &*state.store, - false, - ) - .await?; - // Create the connector label using {profile_id}_{connector_name} - let connector_label = format!("{profile_id}_{}", connector_name); + let connector_label = format!("{}_{}", payout_data.profile_id, connector_name); let (should_call_connector, _connector_customer_id) = helpers::should_call_payout_connector_create_customer( @@ -1124,6 +1114,7 @@ pub async fn response_handler( } // DB entries +#[allow(clippy::too_many_arguments)] #[cfg(feature = "payouts")] pub async fn payout_create_db_entries( state: &AppState, @@ -1131,6 +1122,7 @@ pub async fn payout_create_db_entries( key_store: &domain::MerchantKeyStore, req: &payouts::PayoutCreateRequest, payout_id: &String, + profile_id: &String, connector_name: &api_enums::PayoutConnectors, stored_payout_method_data: Option<&payouts::PayoutMethodData>, ) -> RouterResult { @@ -1231,8 +1223,7 @@ pub async fn payout_create_db_entries( } else { storage_enums::PayoutStatus::RequiresPayoutMethodData }; - let _id = core_utils::get_or_generate_uuid("payout_attempt_id", None)?; - let payout_attempt_id = format!("{}_{}", merchant_id.to_owned(), payout_id.to_owned()); + let payout_attempt_id = utils::get_payment_attempt_id(payout_id, 1); let payout_attempt_req = storage::PayoutAttemptNew::default() .set_payout_attempt_id(payout_attempt_id.to_string()) @@ -1247,7 +1238,7 @@ pub async fn payout_create_db_entries( .set_payout_token(req.payout_token.to_owned()) .set_created_at(Some(common_utils::date_time::now())) .set_last_modified_at(Some(common_utils::date_time::now())) - .set_profile_id(req.profile_id.to_owned()) + .set_profile_id(Some(profile_id.to_string())) .to_owned(); let payout_attempt = db .insert_payout_attempt(payout_attempt_req) @@ -1269,6 +1260,7 @@ pub async fn payout_create_db_entries( .cloned() .or(stored_payout_method_data.cloned()), merchant_connector_account: None, + profile_id: profile_id.to_owned(), }) } @@ -1318,6 +1310,8 @@ pub async fn make_payout_data( .await .map_or(None, |c| c); + let profile_id = payout_attempt.profile_id.clone(); + Ok(PayoutData { billing_address, customer_details, @@ -1325,5 +1319,6 @@ pub async fn make_payout_data( payout_attempt, payout_method_data: None, merchant_connector_account: None, + profile_id, }) } diff --git a/crates/router/src/core/payouts/validator.rs b/crates/router/src/core/payouts/validator.rs index 3793ee523dc3..90e3bca9de1d 100644 --- a/crates/router/src/core/payouts/validator.rs +++ b/crates/router/src/core/payouts/validator.rs @@ -8,7 +8,6 @@ use crate::{ utils as core_utils, }, db::StorageInterface, - logger, routes::AppState, types::{api::payouts, domain, storage}, utils, @@ -24,8 +23,6 @@ pub async fn validate_uniqueness_of_payout_id_against_merchant_id( let payout = db .find_payout_by_merchant_id_payout_id(merchant_id, payout_id) .await; - - logger::debug!(?payout); match payout { Err(err) => { if err.current_context().is_db_not_found() { @@ -58,7 +55,7 @@ pub async fn validate_create_request( merchant_account: &domain::MerchantAccount, req: &payouts::PayoutCreateRequest, merchant_key_store: &domain::MerchantKeyStore, -) -> RouterResult<(String, Option)> { +) -> RouterResult<(String, Option, String)> { let merchant_id = &merchant_account.merchant_id; // Merchant ID @@ -111,5 +108,16 @@ pub async fn validate_create_request( None => None, }; - Ok((payout_id, payout_method_data)) + // Profile ID + let profile_id = core_utils::get_profile_id_from_business_details( + req.business_country, + req.business_label.as_ref(), + merchant_account, + req.profile_id.as_ref(), + &*state.store, + false, + ) + .await?; + + Ok((payout_id, payout_method_data, profile_id)) } diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index fb3dc3e7d281..5ffc85fe6709 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -48,33 +48,21 @@ pub async fn get_mca_for_payout<'a>( merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, payout_data: &PayoutData, -) -> RouterResult<(helpers::MerchantConnectorAccountType, String)> { - let payout_attempt = &payout_data.payout_attempt; - let profile_id = get_profile_id_from_business_details( - payout_attempt.business_country, - payout_attempt.business_label.as_ref(), - merchant_account, - payout_attempt.profile_id.as_ref(), - &*state.store, - false, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("profile_id is not set in payout_attempt")?; +) -> RouterResult { match payout_data.merchant_connector_account.to_owned() { - Some(mca) => Ok((mca, profile_id)), + Some(mca) => Ok(mca), None => { let merchant_connector_account = helpers::get_merchant_connector_account( state, merchant_account.merchant_id.as_str(), None, key_store, - &profile_id, + &payout_data.profile_id, connector_id, - payout_attempt.merchant_connector_id.as_ref(), + payout_data.payout_attempt.merchant_connector_id.as_ref(), ) .await?; - Ok((merchant_connector_account, profile_id)) + Ok(merchant_connector_account) } } } @@ -89,7 +77,7 @@ pub async fn construct_payout_router_data<'a, F>( _request: &api_models::payouts::PayoutRequest, payout_data: &mut PayoutData, ) -> RouterResult> { - let (merchant_connector_account, profile_id) = get_mca_for_payout( + let merchant_connector_account = get_mca_for_payout( state, connector_id, merchant_account, @@ -135,7 +123,7 @@ pub async fn construct_payout_router_data<'a, F>( let payouts = &payout_data.payouts; let payout_attempt = &payout_data.payout_attempt; let customer_details = &payout_data.customer_details; - let connector_label = format!("{profile_id}_{}", payout_attempt.connector); + let connector_label = format!("{}_{}", payout_data.profile_id, payout_attempt.connector); let connector_customer_id = customer_details .as_ref() .and_then(|c| c.connector_customer.as_ref()) diff --git a/migrations/2023-10-27-064512_alter_payout_profile_id/down.sql b/migrations/2023-10-27-064512_alter_payout_profile_id/down.sql new file mode 100644 index 000000000000..a9e789429ec7 --- /dev/null +++ b/migrations/2023-10-27-064512_alter_payout_profile_id/down.sql @@ -0,0 +1,4 @@ +ALTER TABLE + payout_attempt +ALTER COLUMN + profile_id DROP NOT NULL; \ No newline at end of file diff --git a/migrations/2023-10-27-064512_alter_payout_profile_id/up.sql b/migrations/2023-10-27-064512_alter_payout_profile_id/up.sql new file mode 100644 index 000000000000..33355bb9d29c --- /dev/null +++ b/migrations/2023-10-27-064512_alter_payout_profile_id/up.sql @@ -0,0 +1,6 @@ +ALTER TABLE + payout_attempt +ALTER COLUMN + profile_id +SET + NOT NULL; \ No newline at end of file diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 65280c187142..9ca4dea4a1a8 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -10570,7 +10570,8 @@ "entity_type", "status", "error_message", - "error_code" + "error_code", + "profile_id" ], "properties": { "payout_id": { @@ -10702,8 +10703,7 @@ }, "profile_id": { "type": "string", - "description": "The business profile that is associated with this payment", - "nullable": true + "description": "The business profile that is associated with this payment" } } }, diff --git a/postman/collection-dir/adyen_uk/.variable.json b/postman/collection-dir/adyen_uk/.variable.json index 514fd88dee71..57b4c958c53f 100644 --- a/postman/collection-dir/adyen_uk/.variable.json +++ b/postman/collection-dir/adyen_uk/.variable.json @@ -39,6 +39,11 @@ "key": "refund_id", "value": "" }, + { + "key": "payout_id", + "value": "", + "type": "string" + }, { "key": "merchant_connector_id", "value": "" @@ -90,6 +95,16 @@ "key": "connector_api_secret", "value": "", "type": "string" + }, + { + "key": "payment_profile_id", + "value": "", + "type": "string" + }, + { + "key": "payout_profile_id", + "value": "", + "type": "string" } ] } diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/.meta.json index d99a886e8edb..773ed0638cbf 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/.meta.json +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/.meta.json @@ -8,14 +8,17 @@ "Scenario6-Create 3DS payment", "Scenario7-Create 3DS payment with confrm false", "Scenario9-Refund full payment", - "Scenario10-Partial refund", - "Scenario11-Create a mandate and recurring payment", - "Scenario11-Refund recurring payment", - "Scenario16-Bank Redirect-sofort", - "Scenario17-Bank Redirect-eps", - "Scenario18-Bank Redirect-giropay", - "Scenario19-Bank Redirect-Trustly", - "Scenario19-Bank debit-ach", - "Scenario19-Bank debit-Bacs" + "Scenario10-Create a mandate and recurring payment", + "Scenario11-Partial refund", + "Scenario12-Bank Redirect-sofort", + "Scenario13-Bank Redirect-eps", + "Scenario14-Refund recurring payment", + "Scenario15-Bank Redirect-giropay", + "Scenario16-Bank debit-ach", + "Scenario17-Bank debit-Bacs", + "Scenario18-Bank Redirect-Trustly", + "Scenario19-Add card flow", + "Scenario20-Pass Invalid CVV for save card flow and verify failed payment", + "Scenario21-Don't Pass CVV for save card flow and verify failed payment Copy" ] } diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/.meta.json index c4939d7ab913..45785cf7a484 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/.meta.json +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/.meta.json @@ -3,9 +3,12 @@ "Merchant Account - Create", "API Key - Create", "Payment Connector - Create", + "Payout Connector - Create", "Payments - Create", "Payments - Retrieve", "Refunds - Create", - "Refunds - Retrieve" + "Refunds - Retrieve", + "Payouts - Create", + "Payouts - Retrieve" ] } diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Merchant Account - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Merchant Account - Create/request.json index dcbf46ee5382..5603ff553ba0 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Merchant Account - Create/request.json +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Merchant Account - Create/request.json @@ -45,6 +45,10 @@ { "country": "US", "business": "default" + }, + { + "country": "GB", + "business": "payouts" } ], "merchant_details": { diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/event.test.js index 88e92d8d84a2..96b088be1361 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/event.test.js +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/event.test.js @@ -37,3 +37,16 @@ if (jsonData?.merchant_connector_id) { "INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.", ); } + +// pm.collectionVariables - Set profile_id as variable for jsonData.payment_profile_id +if (jsonData?.profile_id) { + pm.collectionVariables.set("payment_profile_id", jsonData.profile_id); + console.log( + "- use {{payment_profile_id}} as collection variable for value", + jsonData.profile_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_profile_id}}, as jsonData.profile_id is undefined.", + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/.event.meta.json new file mode 100644 index 000000000000..220b1a6723d5 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js", "event.prerequest.js"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/event.prerequest.js b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/event.test.js new file mode 100644 index 000000000000..7d0996a0732e --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/event.test.js @@ -0,0 +1,52 @@ +// Validate status 2xx +pm.test( + "[POST]::/account/:account_id/connectors - Status code is 2xx", + function () { + pm.response.to.be.success; + }, +); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/account/:account_id/connectors - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id +if (jsonData?.merchant_connector_id) { + pm.collectionVariables.set( + "merchant_connector_id", + jsonData.merchant_connector_id, + ); + console.log( + "- use {{merchant_connector_id}} as collection variable for value", + jsonData.merchant_connector_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.", + ); +} + +// pm.collectionVariables - Set profile_id as variable for jsonData.payout_profile_id +if (jsonData?.profile_id) { + pm.collectionVariables.set("payout_profile_id", jsonData.profile_id); + console.log( + "- use {{payout_profile_id}} as collection variable for value", + jsonData.profile_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payout_profile_id}}, as jsonData.profile_id is undefined.", + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/request.json new file mode 100644 index 000000000000..0ba1b1689c38 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/request.json @@ -0,0 +1,293 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "connector_type": "payout_processor", + "connector_name": "adyen", + "connector_account_details": { + "auth_type": "SignatureKey", + "api_key": "{{connector_api_key}}", + "key1": "{{connector_key1}}", + "api_secret": "{{connector_api_secret}}" + }, + "test_mode": false, + "disabled": false, + "business_country": "GB", + "business_label": "payouts", + "payment_methods_enabled": [ + { + "payment_method": "card", + "payment_method_types": [ + { + "payment_method_type": "credit", + "card_networks": ["Visa", "Mastercard"], + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "debit", + "card_networks": ["Visa", "Mastercard"], + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "pay_later", + "payment_method_types": [ + { + "payment_method_type": "klarna", + "payment_experience": "redirect_to_url", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "affirm", + "payment_experience": "redirect_to_url", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "afterpay_clearpay", + "payment_experience": "redirect_to_url", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "pay_bright", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "walley", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "wallet", + "payment_method_types": [ + { + "payment_method_type": "paypal", + "payment_experience": "redirect_to_url", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "google_pay", + "payment_experience": "invoke_sdk_client", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "apple_pay", + "payment_experience": "invoke_sdk_client", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "mobile_pay", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "ali_pay", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "we_chat_pay", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "mb_way", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "bank_redirect", + "payment_method_types": [ + { + "payment_method_type": "giropay", + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "eps", + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "sofort", + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "blik", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "trustly", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "online_banking_czech_republic", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "online_banking_finland", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "online_banking_poland", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "online_banking_slovakia", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "bancontact_card", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "bank_debit", + "payment_method_types": [ + { + "payment_method_type": "ach", + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "bacs", + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + } + ] + } + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": ["{{baseUrl}}"], + "path": ["account", ":account_id", "connectors"], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/.event.meta.json new file mode 100644 index 000000000000..220b1a6723d5 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js", "event.prerequest.js"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/event.prerequest.js b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/event.test.js new file mode 100644 index 000000000000..f641cf040d46 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/event.test.js @@ -0,0 +1,60 @@ +// Validate status 2xx +pm.test("[POST]::/payouts/create - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/payouts/create - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Validate if response has JSON Body +pm.test("[POST]::/payouts/create - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Validate if status is successful +// if (jsonData?.status) { +// pm.test("[POST]::/payouts/create - Content check if value for 'status' matches 'success'", +// function () { +// pm.expect(jsonData.status).to.eql("success"); +// }, +// ); +// } + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) { } + +// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id +if (jsonData?.payout_id) { + pm.collectionVariables.set("payout_id", jsonData.payout_id); + console.log( + "- use {{payout_id}} as collection variable for value", + jsonData.payout_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/request.json new file mode 100644 index 000000000000..d8ad685ec764 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/request.json @@ -0,0 +1,72 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 1, + "currency": "EUR", + "customer_id": "payout_customer", + "email": "payout_customer@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payout request", + "connector": ["adyen"], + "payout_type": "bank", + "payout_method_data": { + "bank": { + "iban": "NL46TEST0136169112", + "bic": "ABNANL2A", + "bank_name": "Deutsche Bank", + "bank_country_code": "NL", + "bank_city": "Amsterdam" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "CA", + "zip": "94122", + "country": "US", + "first_name": "John", + "last_name": "Doe" + }, + "phone": { + "number": "8056594427", + "country_code": "+91" + } + }, + "entity_type": "Individual", + "recurring": true, + "metadata": { + "ref": "123" + }, + "confirm": true, + "auto_fulfill": true + } + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": ["{{baseUrl}}"], + "path": ["payouts", "create"] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/event.test.js new file mode 100644 index 000000000000..e822780ee1e2 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/event.test.js @@ -0,0 +1,48 @@ +// Validate status 2xx +pm.test("[GET]::/payouts/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/payouts/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// Validate if response has JSON Body +pm.test("[GET]::/payouts/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id +if (jsonData?.payout_id) { + pm.collectionVariables.set("payout_id", jsonData.payout_id); + console.log( + "- use {{payout_id}} as collection variable for value", + jsonData.payout_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/request.json new file mode 100644 index 000000000000..b7deba38ab27 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/request.json @@ -0,0 +1,22 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payouts/:id", + "host": ["{{baseUrl}}"], + "path": ["payouts", ":id"], + "variable": [ + { + "key": "id", + "value": "{{payout_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/.meta.json index fe295640093e..9cbb319a2ae0 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/.meta.json +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/.meta.json @@ -8,6 +8,7 @@ "Scenario6-Create 3DS payment with greater capture", "Scenario7-Refund exceeds amount", "Scenario8-Refund for unsuccessful payment", - "Scenario9-Create a recurring payment with greater mandate amount" + "Scenario9-Create a recurring payment with greater mandate amount", + "Scenario10-Create payouts using unsupported methods" ] } diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/.meta.json new file mode 100644 index 000000000000..b40f94c032ff --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["ACH Payouts - Create", "Bacs Payouts - Create"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/.event.meta.json new file mode 100644 index 000000000000..220b1a6723d5 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js", "event.prerequest.js"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/event.prerequest.js b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/event.test.js new file mode 100644 index 000000000000..7cf9090d6c5e --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/event.test.js @@ -0,0 +1,48 @@ +// Validate status 4xx +pm.test("[POST]::/payouts/create - Status code is 4xx", function () { + pm.response.to.be.clientError; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payouts/create - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payouts/create - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id +if (jsonData?.payout_id) { + pm.collectionVariables.set("payout_id", jsonData.payout_id); + console.log( + "- use {{payout_id}} as collection variable for value", + jsonData.payout_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/request.json new file mode 100644 index 000000000000..a2b65418ab75 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/request.json @@ -0,0 +1,99 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 10000, + "currency": "USD", + "customer_id": "payout_customer", + "email": "payout_customer@example.com", + "name": "Doest John", + "phone": "6168205366", + "phone_country_code": "+1", + "description": "Its my first payout request", + "connector": ["adyen"], + "payout_type": "bank", + "payout_method_data": { + "bank": { + "bank_routing_number": "110000000", + "bank_account_number": "000123456789", + "bank_name": "Stripe Test Bank", + "bank_country_code": "US", + "bank_city": "California" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "CA", + "zip": "94122", + "country": "US", + "first_name": "Doest", + "last_name": "John" + }, + "phone": { + "number": "6168205366", + "country_code": "1" + } + }, + "entity_type": "Individual", + "recurring": false, + "metadata": { + "ref": "123", + "vendor_details": { + "account_type": "custom", + "business_type": "individual", + "business_profile_mcc": 5045, + "business_profile_url": "https://www.pastebin.com", + "business_profile_name": "pT", + "company_address_line1": "address_full_match", + "company_address_line2": "Kimberly Way", + "company_address_postal_code": "31062", + "company_address_city": "Milledgeville", + "company_address_state": "GA", + "company_phone": "+16168205366", + "company_tax_id": "000000000", + "company_owners_provided": false, + "capabilities_card_payments": true, + "capabilities_transfers": true + }, + "individual_details": { + "tos_acceptance_date": 1680581051, + "tos_acceptance_ip": "103.159.11.202", + "individual_dob_day": "01", + "individual_dob_month": "01", + "individual_dob_year": "1901", + "individual_id_number": "000000000", + "individual_ssn_last_4": "0000", + "external_account_account_holder_type": "individual" + } + }, + "confirm": true, + "auto_fulfill": true + } + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": ["{{baseUrl}}"], + "path": ["payouts", "create"] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/.event.meta.json new file mode 100644 index 000000000000..220b1a6723d5 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js", "event.prerequest.js"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/event.prerequest.js b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/event.test.js new file mode 100644 index 000000000000..7cf9090d6c5e --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/event.test.js @@ -0,0 +1,48 @@ +// Validate status 4xx +pm.test("[POST]::/payouts/create - Status code is 4xx", function () { + pm.response.to.be.clientError; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payouts/create - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payouts/create - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id +if (jsonData?.payout_id) { + pm.collectionVariables.set("payout_id", jsonData.payout_id); + console.log( + "- use {{payout_id}} as collection variable for value", + jsonData.payout_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/request.json new file mode 100644 index 000000000000..ea00d9e048f8 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/request.json @@ -0,0 +1,74 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 1, + "currency": "GBP", + "customer_id": "payout_customer", + "email": "payout_customer@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payout request", + "payout_type": "bank", + "payout_method_data": { + "bank": { + "bank_sort_code": "231470", + "bank_account_number": "28821822", + "bank_name": "Deutsche Bank", + "bank_country_code": "NL", + "bank_city": "Amsterdam" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "CA", + "zip": "94122", + "country": "US", + "first_name": "John", + "last_name": "Doe" + }, + "phone": { + "number": "8056594427", + "country_code": "+91" + } + }, + "entity_type": "Individual", + "recurring": true, + "metadata": { + "ref": "123" + }, + "confirm": true, + "auto_fulfill": true, + "connector": ["adyen"], + "business_label": "abcd", + "business_country": "US" + } + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": ["{{baseUrl}}"], + "path": ["payouts", "create"] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/adyen_uk/event.prerequest.js b/postman/collection-dir/adyen_uk/event.prerequest.js index e69de29bb2d1..98e1d0e5a27f 100644 --- a/postman/collection-dir/adyen_uk/event.prerequest.js +++ b/postman/collection-dir/adyen_uk/event.prerequest.js @@ -0,0 +1,35 @@ +// Add appropriate profile_id for relevant requests +const path = pm.request.url.toString(); +const isPostRequest = pm.request.method.toString() === "POST"; +const isPaymentCreation = path.match(/\/payments$/) && isPostRequest; +const isPayoutCreation = path.match(/\/payouts\/create$/) && isPostRequest; + +if (isPaymentCreation || isPayoutCreation) { + try { + const request = JSON.parse(pm.request.body.toJSON().raw); + + // Attach profile_id + const profile_id = isPaymentCreation + ? pm.collectionVariables.get("payment_profile_id") + : pm.collectionVariables.get("payout_profile_id"); + request["profile_id"] = profile_id; + + // Attach routing + const routing = { type: "single", data: "adyen" }; + request["routing"] = routing; + + let updatedRequest = { + mode: "raw", + raw: JSON.stringify(request), + options: { + raw: { + language: "json", + }, + }, + }; + pm.request.body.update(updatedRequest); + } catch (error) { + console.error("Failed to inject profile_id in the request"); + console.error(error); + } +} diff --git a/postman/collection-dir/wise/.auth.json b/postman/collection-dir/wise/.auth.json new file mode 100644 index 000000000000..915a28357900 --- /dev/null +++ b/postman/collection-dir/wise/.auth.json @@ -0,0 +1,22 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + } +} diff --git a/postman/collection-dir/wise/.event.meta.json b/postman/collection-dir/wise/.event.meta.json new file mode 100644 index 000000000000..eb871bbcb9bb --- /dev/null +++ b/postman/collection-dir/wise/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.prerequest.js", "event.test.js"] +} diff --git a/postman/collection-dir/wise/.info.json b/postman/collection-dir/wise/.info.json new file mode 100644 index 000000000000..188afe443517 --- /dev/null +++ b/postman/collection-dir/wise/.info.json @@ -0,0 +1,8 @@ +{ + "info": { + "_postman_id": "b5107328-6e3c-4ef0-b575-4072bc64462a", + "name": "wise", + "description": "## Get started\n\nJuspay Router provides a collection of APIs that enable you to process and manage payments. Our APIs accept and return JSON in the HTTP body, and return standard HTTP response codes. \nYou can consume the APIs directly using your favorite HTTP/REST library. \nWe have a testing environment referred to \"sandbox\", which you can setup to test API calls without affecting production data.\n\n### Base URLs\n\nUse the following base URLs when making requests to the APIs:\n\n| Environment | Base URL |\n| --- | --- |\n| Sandbox | [https://sandbox.hyperswitch.io](https://sandbox.hyperswitch.io) |\n| Production | [https://router.juspay.io](https://router.juspay.io) |\n\n# Authentication\n\nWhen you sign up for an account, you are given a secret key (also referred as api-key). You may authenticate all API requests with Juspay server by providing the appropriate key in the request Authorization header. \nNever share your secret api keys. Keep them guarded and secure.\n\nContact Support: \nName: Juspay Support \nEmail: [support@juspay.in](mailto:support@juspay.in)", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + } +} diff --git a/postman/collection-dir/wise/.meta.json b/postman/collection-dir/wise/.meta.json new file mode 100644 index 000000000000..d513035ce2d6 --- /dev/null +++ b/postman/collection-dir/wise/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["Health check", "Flow Testcases"] +} diff --git a/postman/collection-dir/wise/.variable.json b/postman/collection-dir/wise/.variable.json new file mode 100644 index 000000000000..7ac96230fcb0 --- /dev/null +++ b/postman/collection-dir/wise/.variable.json @@ -0,0 +1,100 @@ +{ + "variable": [ + { + "key": "baseUrl", + "value": "", + "type": "string" + }, + { + "key": "admin_api_key", + "value": "", + "type": "string" + }, + { + "key": "api_key", + "value": "", + "type": "string" + }, + { + "key": "merchant_id", + "value": "" + }, + { + "key": "payment_id", + "value": "" + }, + { + "key": "customer_id", + "value": "" + }, + { + "key": "mandate_id", + "value": "" + }, + { + "key": "payment_method_id", + "value": "" + }, + { + "key": "refund_id", + "value": "" + }, + { + "key": "payout_id", + "value": "", + "type": "string" + }, + { + "key": "merchant_connector_id", + "value": "" + }, + { + "key": "client_secret", + "value": "", + "type": "string" + }, + { + "key": "connector_api_key", + "value": "", + "type": "string" + }, + { + "key": "connector_key1", + "value": "" + }, + { + "key": "publishable_key", + "value": "", + "type": "string" + }, + { + "key": "payment_token", + "value": "", + "type": "string" + }, + { + "key": "gateway_merchant_id", + "value": "", + "type": "string" + }, + { + "key": "certificate", + "value": "", + "type": "string" + }, + { + "key": "certificate_keys", + "value": "", + "type": "string" + }, + { + "key": "api_key_id", + "value": "" + }, + { + "key": "connector_api_secret", + "value": "", + "type": "string" + } + ] +} diff --git a/postman/collection-dir/wise/Flow Testcases/.meta.json b/postman/collection-dir/wise/Flow Testcases/.meta.json new file mode 100644 index 000000000000..023989e1e494 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["QuickStart", "Happy Cases", "Variation Cases"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/.meta.json b/postman/collection-dir/wise/Flow Testcases/Happy Cases/.meta.json new file mode 100644 index 000000000000..67c98ebd314a --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/.meta.json @@ -0,0 +1,6 @@ +{ + "childrenOrder": [ + "Scenario1 - Process Bacs Payout", + "Scenario2 - Process SEPA Payout" + ] +} diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/.meta.json b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/.meta.json new file mode 100644 index 000000000000..c6b765ca0b04 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["Payouts - Create"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/.event.meta.json b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/.event.meta.json new file mode 100644 index 000000000000..220b1a6723d5 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js", "event.prerequest.js"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/event.prerequest.js b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/event.test.js b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/event.test.js new file mode 100644 index 000000000000..4ddb0243d6c6 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/event.test.js @@ -0,0 +1,48 @@ +// Validate status 2xx +pm.test("[POST]::/payouts/create - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payouts/create - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payouts/create - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id +if (jsonData?.payout_id) { + pm.collectionVariables.set("payout_id", jsonData.payout_id); + console.log( + "- use {{payout_id}} as collection variable for value", + jsonData.payout_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/request.json b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/request.json new file mode 100644 index 000000000000..9189968ecf7d --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/request.json @@ -0,0 +1,72 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 1, + "currency": "GBP", + "customer_id": "wise_customer", + "email": "payout_customer@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payout request", + "payout_type": "bank", + "payout_method_data": { + "bank": { + "bank_sort_code": "231470", + "bank_account_number": "28821822", + "bank_name": "Deutsche Bank", + "bank_country_code": "NL", + "bank_city": "Amsterdam" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "CA", + "zip": "94122", + "country": "US", + "first_name": "John", + "last_name": "Doe" + }, + "phone": { + "number": "8056594427", + "country_code": "+91" + } + }, + "entity_type": "Individual", + "recurring": true, + "metadata": { + "ref": "123" + }, + "confirm": true, + "auto_fulfill": true, + "connector": ["wise"] + } + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": ["{{baseUrl}}"], + "path": ["payouts", "create"] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/response.json b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/.meta.json b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/.meta.json new file mode 100644 index 000000000000..c6b765ca0b04 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["Payouts - Create"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/.event.meta.json b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/.event.meta.json new file mode 100644 index 000000000000..220b1a6723d5 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js", "event.prerequest.js"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/event.prerequest.js b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/event.test.js b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/event.test.js new file mode 100644 index 000000000000..4ddb0243d6c6 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/event.test.js @@ -0,0 +1,48 @@ +// Validate status 2xx +pm.test("[POST]::/payouts/create - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payouts/create - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payouts/create - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id +if (jsonData?.payout_id) { + pm.collectionVariables.set("payout_id", jsonData.payout_id); + console.log( + "- use {{payout_id}} as collection variable for value", + jsonData.payout_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/request.json b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/request.json new file mode 100644 index 000000000000..fbaf31c36a37 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/request.json @@ -0,0 +1,72 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 1, + "currency": "EUR", + "customer_id": "wise_customer", + "email": "payout_customer@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payout request", + "connector": ["wise"], + "payout_type": "bank", + "payout_method_data": { + "bank": { + "iban": "NL46TEST0136169112", + "bic": "ABNANL2A", + "bank_name": "Deutsche Bank", + "bank_country_code": "NL", + "bank_city": "Amsterdam" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "CA", + "zip": "94122", + "country": "US", + "first_name": "John", + "last_name": "Doe" + }, + "phone": { + "number": "8056594427", + "country_code": "+91" + } + }, + "entity_type": "Individual", + "recurring": true, + "metadata": { + "ref": "123" + }, + "confirm": true, + "auto_fulfill": true + } + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": ["{{baseUrl}}"], + "path": ["payouts", "create"] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/response.json b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/.meta.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/.meta.json new file mode 100644 index 000000000000..935df6d4e112 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/.meta.json @@ -0,0 +1,8 @@ +{ + "childrenOrder": [ + "Merchant Account - Create", + "API Key - Create", + "Payout Connector - Create", + "Payouts - Create" + ] +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/.event.meta.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/event.test.js b/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/event.test.js new file mode 100644 index 000000000000..4e27c5a50253 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/event.test.js @@ -0,0 +1,46 @@ +// Validate status 2xx +pm.test("[POST]::/api_keys/:merchant_id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/api_keys/:merchant_id - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id +if (jsonData?.key_id) { + pm.collectionVariables.set("api_key_id", jsonData.key_id); + console.log( + "- use {{api_key_id}} as collection variable for value", + jsonData.key_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.", + ); +} + +// pm.collectionVariables - Set api_key as variable for jsonData.api_key +if (jsonData?.api_key) { + pm.collectionVariables.set("api_key", jsonData.api_key); + console.log( + "- use {{api_key}} as collection variable for value", + jsonData.api_key, + ); +} else { + console.log( + "INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.", + ); +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/request.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/request.json new file mode 100644 index 000000000000..6ceefe5d24cd --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/request.json @@ -0,0 +1,47 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw_json_formatted": { + "name": "API Key 1", + "description": null, + "expiration": "2069-09-23T01:02:03.000Z" + } + }, + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id", + "host": ["{{baseUrl}}"], + "path": ["api_keys", ":merchant_id"], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + } + ] + } +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/response.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/.event.meta.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/.event.meta.json new file mode 100644 index 000000000000..220b1a6723d5 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js", "event.prerequest.js"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/event.prerequest.js b/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/event.test.js b/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/event.test.js new file mode 100644 index 000000000000..7de0d5beb316 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/event.test.js @@ -0,0 +1,56 @@ +// Validate status 2xx +pm.test("[POST]::/accounts - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/accounts - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id +if (jsonData?.merchant_id) { + pm.collectionVariables.set("merchant_id", jsonData.merchant_id); + console.log( + "- use {{merchant_id}} as collection variable for value", + jsonData.merchant_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.", + ); +} + +// pm.collectionVariables - Set api_key as variable for jsonData.api_key +if (jsonData?.api_key) { + pm.collectionVariables.set("api_key", jsonData.api_key); + console.log( + "- use {{api_key}} as collection variable for value", + jsonData.api_key, + ); +} else { + console.log( + "INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.", + ); +} + +// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key +if (jsonData?.publishable_key) { + pm.collectionVariables.set("publishable_key", jsonData.publishable_key); + console.log( + "- use {{publishable_key}} as collection variable for value", + jsonData.publishable_key, + ); +} else { + console.log( + "INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.", + ); +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/request.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/request.json new file mode 100644 index 000000000000..dcbf46ee5382 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/request.json @@ -0,0 +1,91 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "merchant_id": "postman_merchant_GHAction_{{$guid}}", + "locker_id": "m0010", + "merchant_name": "NewAge Retailer", + "primary_business_details": [ + { + "country": "US", + "business": "default" + } + ], + "merchant_details": { + "primary_contact_person": "John Test", + "primary_email": "JohnTest@test.com", + "primary_phone": "sunt laborum", + "secondary_contact_person": "John Test2", + "secondary_email": "JohnTest2@test.com", + "secondary_phone": "cillum do dolor id", + "website": "www.example.com", + "about_business": "Online Retail with a wide selection of organic products for North America", + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US" + } + }, + "return_url": "https://duck.com", + "webhook_details": { + "webhook_version": "1.0.1", + "webhook_username": "ekart_retail", + "webhook_password": "password_ekart@123", + "payment_created_enabled": true, + "payment_succeeded_enabled": true, + "payment_failed_enabled": true + }, + "sub_merchants_enabled": false, + "metadata": { + "city": "NY", + "unit": "245" + } + } + }, + "url": { + "raw": "{{baseUrl}}/accounts", + "host": ["{{baseUrl}}"], + "path": ["accounts"] + }, + "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/response.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/.event.meta.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/.event.meta.json new file mode 100644 index 000000000000..220b1a6723d5 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js", "event.prerequest.js"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/event.prerequest.js b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/event.test.js b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/event.test.js new file mode 100644 index 000000000000..88e92d8d84a2 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/event.test.js @@ -0,0 +1,39 @@ +// Validate status 2xx +pm.test( + "[POST]::/account/:account_id/connectors - Status code is 2xx", + function () { + pm.response.to.be.success; + }, +); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/account/:account_id/connectors - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id +if (jsonData?.merchant_connector_id) { + pm.collectionVariables.set( + "merchant_connector_id", + jsonData.merchant_connector_id, + ); + console.log( + "- use {{merchant_connector_id}} as collection variable for value", + jsonData.merchant_connector_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.", + ); +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/request.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/request.json new file mode 100644 index 000000000000..817114b426a7 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/request.json @@ -0,0 +1,333 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "connector_type": "payout_processor", + "connector_name": "wise", + "connector_account_details": { + "auth_type": "BodyKey", + "api_key": "{{connector_api_key}}", + "key1": "{{connector_key1}}" + }, + "test_mode": false, + "disabled": false, + "business_country": "US", + "business_label": "default", + "payment_methods_enabled": [ + { + "payment_method": "card", + "payment_method_types": [ + { + "payment_method_type": "credit", + "card_networks": ["Visa", "Mastercard"], + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "debit", + "card_networks": ["Visa", "Mastercard"], + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "pay_later", + "payment_method_types": [ + { + "payment_method_type": "klarna", + "payment_experience": "redirect_to_url", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "affirm", + "payment_experience": "redirect_to_url", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "afterpay_clearpay", + "payment_experience": "redirect_to_url", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "pay_bright", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "walley", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "wallet", + "payment_method_types": [ + { + "payment_method_type": "paypal", + "payment_experience": "redirect_to_url", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "google_pay", + "payment_experience": "invoke_sdk_client", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "apple_pay", + "payment_experience": "invoke_sdk_client", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "mobile_pay", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "ali_pay", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "we_chat_pay", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "mb_way", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "bank_redirect", + "payment_method_types": [ + { + "payment_method_type": "giropay", + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "eps", + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "sofort", + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "blik", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "trustly", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "online_banking_czech_republic", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "online_banking_finland", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "online_banking_poland", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "online_banking_slovakia", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "bancontact_card", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "bank_debit", + "payment_method_types": [ + { + "payment_method_type": "ach", + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "bacs", + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + } + ], + "metadata": { + "google_pay": { + "allowed_payment_methods": [ + { + "type": "CARD", + "parameters": { + "allowed_auth_methods": ["PAN_ONLY", "CRYPTOGRAM_3DS"], + "allowed_card_networks": [ + "AMEX", + "DISCOVER", + "INTERAC", + "JCB", + "MASTERCARD", + "VISA" + ] + }, + "tokenization_specification": { + "type": "PAYMENT_GATEWAY" + } + } + ], + "merchant_info": { + "merchant_name": "Narayan Bhat" + } + }, + "apple_pay": { + "session_token_data": { + "initiative": "web", + "certificate": "{{certificate}}", + "display_name": "applepay", + "certificate_keys": "{{certificate_keys}}", + "initiative_context": "hyperswitch-sdk-test.netlify.app", + "merchant_identifier": "merchant.com.stripe.sang" + }, + "payment_request_data": { + "label": "applepay pvt.ltd", + "supported_networks": ["visa", "masterCard", "amex", "discover"], + "merchant_capabilities": ["supports3DS"] + } + } + } + } + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": ["{{baseUrl}}"], + "path": ["account", ":account_id", "connectors"], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/response.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/.event.meta.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/.event.meta.json new file mode 100644 index 000000000000..220b1a6723d5 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js", "event.prerequest.js"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/event.prerequest.js b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/event.test.js b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/event.test.js new file mode 100644 index 000000000000..4ddb0243d6c6 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/event.test.js @@ -0,0 +1,48 @@ +// Validate status 2xx +pm.test("[POST]::/payouts/create - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payouts/create - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payouts/create - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id +if (jsonData?.payout_id) { + pm.collectionVariables.set("payout_id", jsonData.payout_id); + console.log( + "- use {{payout_id}} as collection variable for value", + jsonData.payout_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/request.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/request.json new file mode 100644 index 000000000000..fbaf31c36a37 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/request.json @@ -0,0 +1,72 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 1, + "currency": "EUR", + "customer_id": "wise_customer", + "email": "payout_customer@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payout request", + "connector": ["wise"], + "payout_type": "bank", + "payout_method_data": { + "bank": { + "iban": "NL46TEST0136169112", + "bic": "ABNANL2A", + "bank_name": "Deutsche Bank", + "bank_country_code": "NL", + "bank_city": "Amsterdam" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "CA", + "zip": "94122", + "country": "US", + "first_name": "John", + "last_name": "Doe" + }, + "phone": { + "number": "8056594427", + "country_code": "+91" + } + }, + "entity_type": "Individual", + "recurring": true, + "metadata": { + "ref": "123" + }, + "confirm": true, + "auto_fulfill": true + } + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": ["{{baseUrl}}"], + "path": ["payouts", "create"] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/response.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/wise/Flow Testcases/Variation Cases/.meta.json b/postman/collection-dir/wise/Flow Testcases/Variation Cases/.meta.json new file mode 100644 index 000000000000..972765b13ea5 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Variation Cases/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["Scenario1 - Create ACH payout with invalid data"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/.meta.json b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/.meta.json new file mode 100644 index 000000000000..c6b765ca0b04 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["Payouts - Create"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/.event.meta.json b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/.event.meta.json new file mode 100644 index 000000000000..220b1a6723d5 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js", "event.prerequest.js"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/event.prerequest.js b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/event.test.js b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/event.test.js new file mode 100644 index 000000000000..7cf9090d6c5e --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/event.test.js @@ -0,0 +1,48 @@ +// Validate status 4xx +pm.test("[POST]::/payouts/create - Status code is 4xx", function () { + pm.response.to.be.clientError; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payouts/create - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payouts/create - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id +if (jsonData?.payout_id) { + pm.collectionVariables.set("payout_id", jsonData.payout_id); + console.log( + "- use {{payout_id}} as collection variable for value", + jsonData.payout_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} diff --git a/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/request.json b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/request.json new file mode 100644 index 000000000000..02e8169b787b --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/request.json @@ -0,0 +1,72 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 10000, + "currency": "USD", + "customer_id": "wise_customer", + "email": "payout_customer@example.com", + "name": "Doest John", + "phone": "6168205366", + "phone_country_code": "+1", + "description": "Its my first payout request", + "connector": ["wise"], + "payout_type": "bank", + "payout_method_data": { + "bank": { + "bank_routing_number": "110000000", + "bank_account_number": "000123456789", + "bank_name": "Stripe Test Bank", + "bank_country_code": "US", + "bank_city": "California" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "CA", + "zip": "94122", + "country": "US", + "first_name": "Doest", + "last_name": "John" + }, + "phone": { + "number": "6168205366", + "country_code": "1" + } + }, + "entity_type": "Individual", + "recurring": false, + "metadata": { + "ref": "123" + }, + "confirm": true, + "auto_fulfill": true + } + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": ["{{baseUrl}}"], + "path": ["payouts", "create"] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/response.json b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/wise/Health check/.meta.json b/postman/collection-dir/wise/Health check/.meta.json new file mode 100644 index 000000000000..f5da236cd01f --- /dev/null +++ b/postman/collection-dir/wise/Health check/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["Health"] +} diff --git a/postman/collection-dir/wise/Health check/Health/.event.meta.json b/postman/collection-dir/wise/Health check/Health/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/wise/Health check/Health/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/wise/Health check/Health/event.test.js b/postman/collection-dir/wise/Health check/Health/event.test.js new file mode 100644 index 000000000000..b490b8be090f --- /dev/null +++ b/postman/collection-dir/wise/Health check/Health/event.test.js @@ -0,0 +1,4 @@ +// Validate status 2xx +pm.test("[POST]::/accounts - Status code is 2xx", function () { + pm.response.to.be.success; +}); diff --git a/postman/collection-dir/wise/Health check/Health/request.json b/postman/collection-dir/wise/Health check/Health/request.json new file mode 100644 index 000000000000..e40e93961785 --- /dev/null +++ b/postman/collection-dir/wise/Health check/Health/request.json @@ -0,0 +1,16 @@ +{ + "method": "GET", + "header": [ + { + "key": "x-feature", + "value": "router-custom", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{baseUrl}}/health", + "host": ["{{baseUrl}}"], + "path": ["health"] + } +} diff --git a/postman/collection-dir/wise/Health check/Health/response.json b/postman/collection-dir/wise/Health check/Health/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/wise/Health check/Health/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/wise/event.prerequest.js b/postman/collection-dir/wise/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/wise/event.test.js b/postman/collection-dir/wise/event.test.js new file mode 100644 index 000000000000..fb52caec30fc --- /dev/null +++ b/postman/collection-dir/wise/event.test.js @@ -0,0 +1,13 @@ +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log("[LOG]::payment_id - " + jsonData.payment_id); +} + +console.log("[LOG]::x-request-id - " + pm.response.headers.get("x-request-id")); From be4aa3b913819698c6c22ddedafe1d90fbe02add Mon Sep 17 00:00:00 2001 From: Sarthak Soni <76486416+Sarthak1799@users.noreply.github.com> Date: Tue, 21 Nov 2023 14:53:19 +0530 Subject: [PATCH 046/146] refactor(payment_methods): Added support for pm_auth_connector field in pm list response (#2667) Co-authored-by: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Co-authored-by: Shanks --- crates/api_models/src/payment_methods.rs | 3 +++ crates/router/src/core/payment_methods/cards.rs | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 755acbf7f425..c40dffe4cf31 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -325,6 +325,9 @@ pub struct ResponsePaymentMethodTypes { } "#)] pub surcharge_details: Option, + + /// auth service connector label for this payment method type, if exists + pub pm_auth_connector: Option, } #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] #[serde(rename_all = "snake_case")] diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index f2eeedf5388f..2fe3a75d80ee 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -1384,6 +1384,7 @@ pub async fn list_payment_methods( .and_then(|inner_hm| inner_hm.get(payment_method_types_hm.0)) .cloned(), surcharge_details: None, + pm_auth_connector: None, }) } @@ -1418,6 +1419,7 @@ pub async fn list_payment_methods( .and_then(|inner_hm| inner_hm.get(payment_method_types_hm.0)) .cloned(), surcharge_details: None, + pm_auth_connector: None, }) } @@ -1447,6 +1449,7 @@ pub async fn list_payment_methods( .and_then(|inner_hm| inner_hm.get(key.0)) .cloned(), surcharge_details: None, + pm_auth_connector: None, } }) } @@ -1479,6 +1482,7 @@ pub async fn list_payment_methods( .and_then(|inner_hm| inner_hm.get(key.0)) .cloned(), surcharge_details: None, + pm_auth_connector: None, } }) } @@ -1511,6 +1515,7 @@ pub async fn list_payment_methods( .and_then(|inner_hm| inner_hm.get(key.0)) .cloned(), surcharge_details: None, + pm_auth_connector: None, } }) } From e566a4eff2270c2a56ec90966f42ccfd79906068 Mon Sep 17 00:00:00 2001 From: Kartikeya Hegde Date: Tue, 21 Nov 2023 15:41:35 +0530 Subject: [PATCH 047/146] fix: merchant_connector_id null in KV flow (#2810) Co-authored-by: preetamrevankar <132073736+preetamrevankar@users.noreply.github.com> --- crates/diesel_models/src/payment_attempt.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index bb8f2b60bbb7..f77e75491d86 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -359,7 +359,9 @@ impl PaymentAttemptUpdate { .amount_capturable .unwrap_or(source.amount_capturable), updated_by: pa_update.updated_by, - merchant_connector_id: pa_update.merchant_connector_id, + merchant_connector_id: pa_update + .merchant_connector_id + .or(source.merchant_connector_id), authentication_data: pa_update.authentication_data.or(source.authentication_data), encoded_data: pa_update.encoded_data.or(source.encoded_data), unified_code: pa_update.unified_code.unwrap_or(source.unified_code), From 938b63a1fceb87b4aae4211dac4d051e024028b1 Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Tue, 21 Nov 2023 18:41:07 +0530 Subject: [PATCH 048/146] fix(connector): [CASHTOCODE] Fix Error Response Handling (#2926) --- crates/router/src/connector/cashtocode/transformers.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/router/src/connector/cashtocode/transformers.rs b/crates/router/src/connector/cashtocode/transformers.rs index 2caef69db92c..42e47c077e8c 100644 --- a/crates/router/src/connector/cashtocode/transformers.rs +++ b/crates/router/src/connector/cashtocode/transformers.rs @@ -289,7 +289,7 @@ impl #[derive(Debug, Deserialize)] pub struct CashtocodeErrorResponse { - pub error: String, + pub error: serde_json::Value, pub error_description: String, pub errors: Option>, } From d8fcd3c9712480c1230590c4f23b35da79df784d Mon Sep 17 00:00:00 2001 From: Swangi Kumari <85639103+swangi-kumari@users.noreply.github.com> Date: Tue, 21 Nov 2023 19:44:40 +0530 Subject: [PATCH 049/146] refactor(connector): [Paypal] Add support for both BodyKey and SignatureKey (#2633) Co-authored-by: Mani Chandra Dulam Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> --- crates/router/src/connector/paypal.rs | 90 ++++-- .../src/connector/paypal/transformers.rs | 291 ++++++++++++++++-- 2 files changed, 325 insertions(+), 56 deletions(-) diff --git a/crates/router/src/connector/paypal.rs b/crates/router/src/connector/paypal.rs index e514ebbed2fc..0e8cff8c0569 100644 --- a/crates/router/src/connector/paypal.rs +++ b/crates/router/src/connector/paypal.rs @@ -5,10 +5,10 @@ use base64::Engine; use common_utils::ext_traits::ByteSliceExt; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; -use masking::PeekInterface; +use masking::{ExposeInterface, PeekInterface, Secret}; use transformers as paypal; -use self::transformers::{PaypalAuthResponse, PaypalMeta, PaypalWebhookEventType}; +use self::transformers::{auth_headers, PaypalAuthResponse, PaypalMeta, PaypalWebhookEventType}; use super::utils::PaymentsCompleteAuthorizeRequestData; use crate::{ configs::settings, @@ -31,7 +31,7 @@ use crate::{ self, api::{self, CompleteAuthorize, ConnectorCommon, ConnectorCommonExt, VerifyWebhookSource}, transformers::ForeignFrom, - ErrorResponse, Response, + ConnectorAuthType, ErrorResponse, Response, }, utils::{self, BytesExt}, }; @@ -110,8 +110,8 @@ where .clone() .ok_or(errors::ConnectorError::FailedToObtainAuthType)?; let key = &req.attempt_id; - - Ok(vec![ + let auth = paypal::PaypalAuthType::try_from(&req.connector_auth_type)?; + let mut headers = vec![ ( headers::CONTENT_TYPE.to_string(), self.get_content_type().to_string().into(), @@ -121,17 +121,57 @@ where format!("Bearer {}", access_token.token.peek()).into_masked(), ), ( - "Prefer".to_string(), + auth_headers::PREFER.to_string(), "return=representation".to_string().into(), ), ( - "PayPal-Request-Id".to_string(), + auth_headers::PAYPAL_REQUEST_ID.to_string(), key.to_string().into_masked(), ), - ]) + ]; + if let Ok(paypal::PaypalConnectorCredentials::PartnerIntegration(credentials)) = + auth.get_credentials() + { + let auth_assertion_header = + construct_auth_assertion_header(&credentials.payer_id, &credentials.client_id); + headers.extend(vec![ + ( + auth_headers::PAYPAL_AUTH_ASSERTION.to_string(), + auth_assertion_header.to_string().into_masked(), + ), + ( + auth_headers::PAYPAL_PARTNER_ATTRIBUTION_ID.to_string(), + "HyperSwitchPPCP_SP".to_string().into(), + ), + ]) + } else { + headers.extend(vec![( + auth_headers::PAYPAL_PARTNER_ATTRIBUTION_ID.to_string(), + "HyperSwitchlegacy_Ecom".to_string().into(), + )]) + } + Ok(headers) } } +fn construct_auth_assertion_header( + payer_id: &Secret, + client_id: &Secret, +) -> String { + let algorithm = consts::BASE64_ENGINE + .encode("{\"alg\":\"none\"}") + .to_string(); + let merchant_credentials = format!( + "{{\"iss\":\"{}\",\"payer_id\":\"{}\"}}", + client_id.clone().expose(), + payer_id.clone().expose() + ); + let encoded_credentials = consts::BASE64_ENGINE + .encode(merchant_credentials) + .to_string(); + format!("{algorithm}.{encoded_credentials}.") +} + impl ConnectorCommon for Paypal { fn id(&self) -> &'static str { "paypal" @@ -151,14 +191,14 @@ impl ConnectorCommon for Paypal { fn get_auth_header( &self, - auth_type: &types::ConnectorAuthType, + auth_type: &ConnectorAuthType, ) -> CustomResult)>, errors::ConnectorError> { - let auth: paypal::PaypalAuthType = auth_type - .try_into() - .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let auth = paypal::PaypalAuthType::try_from(auth_type)?; + let credentials = auth.get_credentials()?; + Ok(vec![( headers::AUTHORIZATION.to_string(), - auth.api_key.into_masked(), + credentials.get_client_secret().into_masked(), )]) } @@ -260,15 +300,9 @@ impl ConnectorIntegration CustomResult)>, errors::ConnectorError> { - let auth: paypal::PaypalAuthType = (&req.connector_auth_type) - .try_into() - .change_context(errors::ConnectorError::FailedToObtainAuthType)?; - - let auth_id = auth - .key1 - .zip(auth.api_key) - .map(|(key1, api_key)| format!("{}:{}", key1, api_key)); - let auth_val = format!("Basic {}", consts::BASE64_ENGINE.encode(auth_id.peek())); + let auth = paypal::PaypalAuthType::try_from(&req.connector_auth_type)?; + let credentials = auth.get_credentials()?; + let auth_val = credentials.generate_authorization_value(); Ok(vec![ ( @@ -998,15 +1032,9 @@ impl >, _connectors: &settings::Connectors, ) -> CustomResult)>, errors::ConnectorError> { - let auth: paypal::PaypalAuthType = (&req.connector_auth_type) - .try_into() - .change_context(errors::ConnectorError::FailedToObtainAuthType)?; - - let auth_id = auth - .key1 - .zip(auth.api_key) - .map(|(key1, api_key)| format!("{}:{}", key1, api_key)); - let auth_val = format!("Basic {}", consts::BASE64_ENGINE.encode(auth_id.peek())); + let auth = paypal::PaypalAuthType::try_from(&req.connector_auth_type)?; + let credentials = auth.get_credentials()?; + let auth_val = credentials.generate_authorization_value(); Ok(vec![ ( diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs index 5468c6bb8061..d023077ff008 100644 --- a/crates/router/src/connector/paypal/transformers.rs +++ b/crates/router/src/connector/paypal/transformers.rs @@ -1,7 +1,8 @@ use api_models::{enums, payments::BankRedirectData}; +use base64::Engine; use common_utils::errors::CustomResult; use error_stack::{IntoReport, ResultExt}; -use masking::Secret; +use masking::{ExposeInterface, Secret}; use serde::{Deserialize, Serialize}; use time::PrimitiveDateTime; use url::Url; @@ -11,10 +12,11 @@ use crate::{ self, to_connector_meta, AccessTokenRequestInfo, AddressDetailsData, BankRedirectBillingData, CardData, PaymentsAuthorizeRequestData, }, + consts, core::errors, services, types::{ - self, api, storage::enums as storage_enums, transformers::ForeignFrom, + self, api, storage::enums as storage_enums, transformers::ForeignFrom, ConnectorAuthType, VerifyWebhookSourceResponseData, }, }; @@ -57,6 +59,12 @@ mod webhook_headers { pub const PAYPAL_CERT_URL: &str = "paypal-cert-url"; pub const PAYPAL_AUTH_ALGO: &str = "paypal-auth-algo"; } +pub mod auth_headers { + pub const PAYPAL_PARTNER_ATTRIBUTION_ID: &str = "PayPal-Partner-Attribution-Id"; + pub const PREFER: &str = "Prefer"; + pub const PAYPAL_REQUEST_ID: &str = "PayPal-Request-Id"; + pub const PAYPAL_AUTH_ASSERTION: &str = "PayPal-Auth-Assertion"; +} #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "UPPERCASE")] @@ -72,19 +80,111 @@ pub struct OrderAmount { pub value: String, } +#[derive(Default, Debug, Serialize, Deserialize, Eq, PartialEq)] +pub struct OrderRequestAmount { + pub currency_code: storage_enums::Currency, + pub value: String, + pub breakdown: AmountBreakdown, +} + +impl From<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for OrderRequestAmount { + fn from(item: &PaypalRouterData<&types::PaymentsAuthorizeRouterData>) -> Self { + Self { + currency_code: item.router_data.request.currency, + value: item.amount.to_owned(), + breakdown: AmountBreakdown { + item_total: OrderAmount { + currency_code: item.router_data.request.currency, + value: item.amount.to_owned(), + }, + }, + } + } +} + +#[derive(Default, Debug, Serialize, Deserialize, Eq, PartialEq)] +pub struct AmountBreakdown { + item_total: OrderAmount, +} + #[derive(Default, Debug, Serialize, Eq, PartialEq)] pub struct PurchaseUnitRequest { reference_id: Option, //reference for an item in purchase_units invoice_id: Option, //The API caller-provided external invoice number for this order. Appears in both the payer's transaction history and the emails that the payer receives. custom_id: Option, //Used to reconcile client transactions with PayPal transactions. - amount: OrderAmount, + amount: OrderRequestAmount, + #[serde(skip_serializing_if = "Option::is_none")] + payee: Option, + shipping: Option, + items: Vec, } -#[derive(Debug, Serialize)] +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct Payee { + merchant_id: Secret, +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct ItemDetails { + name: String, + quantity: u16, + unit_amount: OrderAmount, +} + +impl From<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for ItemDetails { + fn from(item: &PaypalRouterData<&types::PaymentsAuthorizeRouterData>) -> Self { + Self { + name: format!( + "Payment for invoice {}", + item.router_data.connector_request_reference_id + ), + quantity: 1, + unit_amount: OrderAmount { + currency_code: item.router_data.request.currency, + value: item.amount.to_string(), + }, + } + } +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] pub struct Address { address_line_1: Option>, postal_code: Option>, country_code: api_models::enums::CountryAlpha2, + admin_area_2: Option, +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct ShippingAddress { + address: Option
, + name: Option, +} + +impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for ShippingAddress { + type Error = error_stack::Report; + + fn try_from( + item: &PaypalRouterData<&types::PaymentsAuthorizeRouterData>, + ) -> Result { + Ok(Self { + address: get_address_info(item.router_data.address.shipping.as_ref())?, + name: Some(ShippingName { + full_name: item + .router_data + .address + .shipping + .as_ref() + .and_then(|inner_data| inner_data.address.as_ref()) + .and_then(|inner_data| inner_data.first_name.clone()), + }), + }) + } +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct ShippingName { + full_name: Option>, } #[derive(Debug, Serialize)] @@ -124,6 +224,22 @@ pub struct RedirectRequest { pub struct ContextStruct { return_url: Option, cancel_url: Option, + user_action: Option, + shipping_preference: ShippingPreference, +} + +#[derive(Debug, Serialize)] +pub enum UserAction { + #[serde(rename = "PAY_NOW")] + PayNow, +} + +#[derive(Debug, Serialize)] +pub enum ShippingPreference { + #[serde(rename = "SET_PROVIDED_ADDRESS")] + SetProvidedAddress, + #[serde(rename = "GET_FROM_FILE")] + GetFromFile, } #[derive(Debug, Serialize)] @@ -158,6 +274,7 @@ fn get_address_info( country_code: address.get_country()?.to_owned(), address_line_1: address.line1.clone(), postal_code: address.zip.clone(), + admin_area_2: address.city.clone(), }), None => None, }; @@ -180,6 +297,12 @@ fn get_payment_source( experience_context: ContextStruct { return_url: item.request.complete_authorize_url.clone(), cancel_url: item.request.complete_authorize_url.clone(), + shipping_preference: if item.address.shipping.is_some() { + ShippingPreference::SetProvidedAddress + } else { + ShippingPreference::GetFromFile + }, + user_action: Some(UserAction::PayNow), }, })), BankRedirectData::Giropay { @@ -194,6 +317,12 @@ fn get_payment_source( experience_context: ContextStruct { return_url: item.request.complete_authorize_url.clone(), cancel_url: item.request.complete_authorize_url.clone(), + shipping_preference: if item.address.shipping.is_some() { + ShippingPreference::SetProvidedAddress + } else { + ShippingPreference::GetFromFile + }, + user_action: Some(UserAction::PayNow), }, })), BankRedirectData::Ideal { @@ -208,6 +337,12 @@ fn get_payment_source( experience_context: ContextStruct { return_url: item.request.complete_authorize_url.clone(), cancel_url: item.request.complete_authorize_url.clone(), + shipping_preference: if item.address.shipping.is_some() { + ShippingPreference::SetProvidedAddress + } else { + ShippingPreference::GetFromFile + }, + user_action: Some(UserAction::PayNow), }, })), BankRedirectData::Sofort { @@ -220,6 +355,12 @@ fn get_payment_source( experience_context: ContextStruct { return_url: item.request.complete_authorize_url.clone(), cancel_url: item.request.complete_authorize_url.clone(), + shipping_preference: if item.address.shipping.is_some() { + ShippingPreference::SetProvidedAddress + } else { + ShippingPreference::GetFromFile + }, + user_action: Some(UserAction::PayNow), }, })), BankRedirectData::BancontactCard { .. } @@ -247,11 +388,24 @@ fn get_payment_source( } } +fn get_payee(auth_type: &PaypalAuthType) -> Option { + auth_type + .get_credentials() + .ok() + .and_then(|credentials| credentials.get_payer_id()) + .map(|payer_id| Payee { + merchant_id: payer_id, + }) +} + impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalPaymentsRequest { type Error = error_stack::Report; fn try_from( item: &PaypalRouterData<&types::PaymentsAuthorizeRouterData>, ) -> Result { + let paypal_auth: PaypalAuthType = + PaypalAuthType::try_from(&item.router_data.connector_auth_type)?; + let payee = get_payee(&paypal_auth); match item.router_data.request.payment_method_data { api_models::payments::PaymentMethodData::Card(ref ccard) => { let intent = if item.router_data.request.is_auto_capture()? { @@ -259,18 +413,20 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP } else { PaypalPaymentIntent::Authorize }; - let amount = OrderAmount { - currency_code: item.router_data.request.currency, - value: item.amount.to_owned(), - }; + let amount = OrderRequestAmount::from(item); let connector_request_reference_id = item.router_data.connector_request_reference_id.clone(); + let shipping_address = ShippingAddress::try_from(item)?; + let item_details = vec![ItemDetails::from(item)]; let purchase_units = vec![PurchaseUnitRequest { reference_id: Some(connector_request_reference_id.clone()), custom_id: Some(connector_request_reference_id.clone()), invoice_id: Some(connector_request_reference_id), amount, + payee, + shipping: Some(shipping_address), + items: item_details, }]; let card = item.router_data.request.get_card()?; let expiry = Some(card.get_expiry_date_as_yyyymm("-")); @@ -306,25 +462,29 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP } else { PaypalPaymentIntent::Authorize }; - let amount = OrderAmount { - currency_code: item.router_data.request.currency, - value: item.amount.to_owned(), - }; + let amount = OrderRequestAmount::from(item); let connector_req_reference_id = item.router_data.connector_request_reference_id.clone(); + let shipping_address = ShippingAddress::try_from(item)?; + let item_details = vec![ItemDetails::from(item)]; let purchase_units = vec![PurchaseUnitRequest { reference_id: Some(connector_req_reference_id.clone()), custom_id: Some(connector_req_reference_id.clone()), invoice_id: Some(connector_req_reference_id), amount, + payee, + shipping: Some(shipping_address), + items: item_details, }]; let payment_source = Some(PaymentSourceItem::Paypal(PaypalRedirectionRequest { experience_context: ContextStruct { return_url: item.router_data.request.complete_authorize_url.clone(), cancel_url: item.router_data.request.complete_authorize_url.clone(), + shipping_preference: ShippingPreference::SetProvidedAddress, + user_action: Some(UserAction::PayNow), }, })); @@ -374,18 +534,20 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP connector: "Paypal".to_string(), })? }; - let amount = OrderAmount { - currency_code: item.router_data.request.currency, - value: item.amount.to_owned(), - }; + let amount = OrderRequestAmount::from(item); let connector_req_reference_id = item.router_data.connector_request_reference_id.clone(); + let shipping_address = ShippingAddress::try_from(item)?; + let item_details = vec![ItemDetails::from(item)]; let purchase_units = vec![PurchaseUnitRequest { reference_id: Some(connector_req_reference_id.clone()), custom_id: Some(connector_req_reference_id.clone()), invoice_id: Some(connector_req_reference_id), amount, + payee, + shipping: Some(shipping_address), + items: item_details, }]; let payment_source = Some(get_payment_source(item.router_data, bank_redirection_data)?); @@ -604,19 +766,98 @@ impl TryFrom, - pub(super) key1: Secret, +pub enum PaypalAuthType { + TemporaryAuth, + AuthWithDetails(PaypalConnectorCredentials), +} + +#[derive(Debug)] +pub enum PaypalConnectorCredentials { + StandardIntegration(StandardFlowCredentials), + PartnerIntegration(PartnerFlowCredentials), } -impl TryFrom<&types::ConnectorAuthType> for PaypalAuthType { +impl PaypalConnectorCredentials { + pub fn get_client_id(&self) -> Secret { + match self { + Self::StandardIntegration(item) => item.client_id.clone(), + Self::PartnerIntegration(item) => item.client_id.clone(), + } + } + + pub fn get_client_secret(&self) -> Secret { + match self { + Self::StandardIntegration(item) => item.client_secret.clone(), + Self::PartnerIntegration(item) => item.client_secret.clone(), + } + } + + pub fn get_payer_id(&self) -> Option> { + match self { + Self::StandardIntegration(_) => None, + Self::PartnerIntegration(item) => Some(item.payer_id.clone()), + } + } + + pub fn generate_authorization_value(&self) -> String { + let auth_id = format!( + "{}:{}", + self.get_client_id().expose(), + self.get_client_secret().expose(), + ); + format!("Basic {}", consts::BASE64_ENGINE.encode(auth_id)) + } +} + +#[derive(Debug)] +pub struct StandardFlowCredentials { + pub(super) client_id: Secret, + pub(super) client_secret: Secret, +} + +#[derive(Debug)] +pub struct PartnerFlowCredentials { + pub(super) client_id: Secret, + pub(super) client_secret: Secret, + pub(super) payer_id: Secret, +} + +impl PaypalAuthType { + pub fn get_credentials( + &self, + ) -> CustomResult<&PaypalConnectorCredentials, errors::ConnectorError> { + match self { + Self::TemporaryAuth => Err(errors::ConnectorError::InvalidConnectorConfig { + config: "TemporaryAuth found in connector_account_details", + } + .into()), + Self::AuthWithDetails(credentials) => Ok(credentials), + } + } +} + +impl TryFrom<&ConnectorAuthType> for PaypalAuthType { type Error = error_stack::Report; - fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + fn try_from(auth_type: &ConnectorAuthType) -> Result { match auth_type { - types::ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self { - api_key: api_key.to_owned(), - key1: key1.to_owned(), - }), + types::ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self::AuthWithDetails( + PaypalConnectorCredentials::StandardIntegration(StandardFlowCredentials { + client_id: key1.to_owned(), + client_secret: api_key.to_owned(), + }), + )), + types::ConnectorAuthType::SignatureKey { + api_key, + key1, + api_secret, + } => Ok(Self::AuthWithDetails( + PaypalConnectorCredentials::PartnerIntegration(PartnerFlowCredentials { + client_id: key1.to_owned(), + client_secret: api_key.to_owned(), + payer_id: api_secret.to_owned(), + }), + )), + types::ConnectorAuthType::TemporaryAuth => Ok(Self::TemporaryAuth), _ => Err(errors::ConnectorError::FailedToObtainAuthType)?, } } From ce725ef8c680eea3fe03671c989fd4572cfc0640 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 21 Nov 2023 14:59:18 +0000 Subject: [PATCH 050/146] test(postman): update postman collection files --- .../adyen_uk.postman_collection.json | 6612 +++++++++-------- .../wise.postman_collection.json | 1025 +++ 2 files changed, 4642 insertions(+), 2995 deletions(-) create mode 100644 postman/collection-json/wise.postman_collection.json diff --git a/postman/collection-json/adyen_uk.postman_collection.json b/postman/collection-json/adyen_uk.postman_collection.json index 716b6d9d0699..ad916657948f 100644 --- a/postman/collection-json/adyen_uk.postman_collection.json +++ b/postman/collection-json/adyen_uk.postman_collection.json @@ -4,6 +4,41 @@ "listen": "prerequest", "script": { "exec": [ + "// Add appropriate profile_id for relevant requests", + "const path = pm.request.url.toString();", + "const isPostRequest = pm.request.method.toString() === \"POST\";", + "const isPaymentCreation = path.match(/\\/payments$/) && isPostRequest;", + "const isPayoutCreation = path.match(/\\/payouts\\/create$/) && isPostRequest;", + "", + "if (isPaymentCreation || isPayoutCreation) {", + " try {", + " const request = JSON.parse(pm.request.body.toJSON().raw);", + "", + " // Attach profile_id", + " const profile_id = isPaymentCreation", + " ? pm.collectionVariables.get(\"payment_profile_id\")", + " : pm.collectionVariables.get(\"payout_profile_id\");", + " request[\"profile_id\"] = profile_id;", + "", + " // Attach routing", + " const routing = { type: \"single\", data: \"adyen\" };", + " request[\"routing\"] = routing;", + "", + " let updatedRequest = {", + " mode: \"raw\",", + " raw: JSON.stringify(request),", + " options: {", + " raw: {", + " language: \"json\",", + " },", + " },", + " };", + " pm.request.body.update(updatedRequest);", + " } catch (error) {", + " console.error(\"Failed to inject profile_id in the request\");", + " console.error(error);", + " }", + "}", "" ], "type": "text/javascript" @@ -200,7 +235,7 @@ "language": "json" } }, - "raw": "{\"merchant_id\":\"postman_merchant_GHAction_{{$guid}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"}],\"merchant_details\":{\"primary_contact_person\":\"John Test\",\"primary_email\":\"JohnTest@test.com\",\"primary_phone\":\"sunt laborum\",\"secondary_contact_person\":\"John Test2\",\"secondary_email\":\"JohnTest2@test.com\",\"secondary_phone\":\"cillum do dolor id\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" + "raw": "{\"merchant_id\":\"postman_merchant_GHAction_{{$guid}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"},{\"country\":\"GB\",\"business\":\"payouts\"}],\"merchant_details\":{\"primary_contact_person\":\"John Test\",\"primary_email\":\"JohnTest@test.com\",\"primary_phone\":\"sunt laborum\",\"secondary_contact_person\":\"John Test2\",\"secondary_email\":\"JohnTest2@test.com\",\"secondary_phone\":\"cillum do dolor id\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" }, "url": { "raw": "{{baseUrl}}/accounts", @@ -370,6 +405,19 @@ " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", " );", "}", + "", + "// pm.collectionVariables - Set profile_id as variable for jsonData.payment_profile_id", + "if (jsonData?.profile_id) {", + " pm.collectionVariables.set(\"payment_profile_id\", jsonData.profile_id);", + " console.log(", + " \"- use {{payment_profile_id}} as collection variable for value\",", + " jsonData.profile_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_profile_id}}, as jsonData.profile_id is undefined.\",", + " );", + "}", "" ], "type": "text/javascript" @@ -448,6 +496,143 @@ }, "response": [] }, + { + "name": "Payout Connector - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[POST]::/account/:account_id/connectors - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/account/:account_id/connectors - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set profile_id as variable for jsonData.payout_profile_id", + "if (jsonData?.profile_id) {", + " pm.collectionVariables.set(\"payout_profile_id\", jsonData.profile_id);", + " console.log(", + " \"- use {{payout_profile_id}} as collection variable for value\",", + " jsonData.profile_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_profile_id}}, as jsonData.profile_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"connector_type\":\"payout_processor\",\"connector_name\":\"adyen\",\"connector_account_details\":{\"auth_type\":\"SignatureKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\",\"api_secret\":\"{{connector_api_secret}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"GB\",\"business_label\":\"payouts\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"pay_bright\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"walley\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mobile_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"ali_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mb_way\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"giropay\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"blik\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"trustly\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_czech_republic\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_finland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_poland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_slovakia\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bancontact_card\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}]}" + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." + }, + "response": [] + }, { "name": "Payments - Create", "event": [ @@ -816,180 +1001,243 @@ "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - } - ] - }, - { - "name": "Happy Cases", - "item": [ + }, { - "name": "Scenario10-Create a mandate and recurring payment", - "item": [ + "name": "Payouts - Create", + "event": [ { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payouts/create - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payouts/create - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payouts/create - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Validate if status is successful", + "// if (jsonData?.status) {", + "// pm.test(\"[POST]::/payouts/create - Content check if value for 'status' matches 'success'\",", + "// function () {", + "// pm.expect(jsonData.status).to.eql(\"success\");", + "// },", + "// );", + "// }", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) { }", + "", + "// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id", + "if (jsonData?.payout_id) {", + " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", + " console.log(", + " \"- use {{payout_id}} as collection variable for value\",", + " jsonData.payout_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" } + }, + "raw": "{\"amount\":1,\"currency\":\"EUR\",\"customer_id\":\"payout_customer\",\"email\":\"payout_customer@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payout request\",\"connector\":[\"adyen\"],\"payout_type\":\"bank\",\"payout_method_data\":{\"bank\":{\"iban\":\"NL46TEST0136169112\",\"bic\":\"ABNANL2A\",\"bank_name\":\"Deutsche Bank\",\"bank_country_code\":\"NL\",\"bank_city\":\"Amsterdam\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"CA\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"entity_type\":\"Individual\",\"recurring\":true,\"metadata\":{\"ref\":\"123\"},\"confirm\":true,\"auto_fulfill\":true}" + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": [ + "{{baseUrl}}" ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } + "path": [ + "payouts", + "create" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payouts - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payouts/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payouts/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payouts/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id", + "if (jsonData?.payout_id) {", + " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", + " console.log(", + " \"- use {{payout_id}} as collection variable for value\",", + " jsonData.payout_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"setup_future_usage\":\"off_session\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"127.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"single_use\":{\"amount\":7000,\"currency\":\"USD\"}}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payouts/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payouts", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{payout_id}}", + "description": "(Required) unique payment id" + } + ] }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Happy Cases", + "item": [ + { + "name": "Scenario1-Create payment with confirm true", + "item": [ { - "name": "Payments - Retrieve", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -1038,31 +1286,15 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", " function () {", " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", "" ], "type": "text/javascript" @@ -1070,61 +1302,60 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] }, { - "name": "Recurring Payments - Create", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -1173,39 +1404,15 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"payment_method_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'payment_method_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.payment_method_data !== \"undefined\").to.be.true;", - " },", - ");", "" ], "type": "text/javascript" @@ -1213,60 +1420,66 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario2-Create payment with confirm false", + "item": [ { - "name": "Payments - Retrieve-copy", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -1315,31 +1528,15 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"requires_confirmation\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", " },", " );", "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", "" ], "type": "text/javascript" @@ -1347,66 +1544,63 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] - } - ] - }, - { - "name": "Scenario11-Partial refund", - "item": [ + }, { - "name": "Payments - Create", + "name": "Payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -1458,7 +1652,7 @@ "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", " function () {", " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", @@ -1471,6 +1665,26 @@ } ], "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, "method": "POST", "header": [ { @@ -1489,18 +1703,27 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"client_secret\":\"{{client_secret}}\"}" }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] }, @@ -1573,10 +1796,10 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", @@ -1622,61 +1845,87 @@ "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario3-Create payment without PMD", + "item": [ { - "name": "Refunds - Create", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", - " },", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", " );", "}", "", - "// Response body should have value \"540\" for \"amount\"", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(540);", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", " },", " );", "}", @@ -1705,38 +1954,46 @@ "language": "json" } }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"RETURN\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/refunds", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "refunds" + "payments" ] }, - "description": "To create a refund against an already processed payment" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] }, { - "name": "Refunds - Retrieve", + "name": "Payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", @@ -1745,35 +2002,51 @@ " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", - " },", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", " );", "}", "", - "// Response body should have value \"6540\" for \"amount\"", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " \"[POST]::/payments:id/confirm - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(540);", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", @@ -1784,88 +2057,143 @@ } ], "request": { - "method": "GET", + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}}}" + }, "url": { - "raw": "{{baseUrl}}/refunds/:id", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ - "refunds", - ":id" + "payments", + ":id", + "confirm" ], "variable": [ { "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" + "value": "{{payment_id}}", + "description": "(Required) unique payment id" } ] }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] }, { - "name": "Refunds - Create-copy", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", - " },", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", " );", "}", "", - "// Response body should have value \"1000\" for \"amount\"", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(1000);", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", @@ -1876,93 +2204,120 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":1000,\"reason\":\"FRAUD\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, "url": { - "raw": "{{baseUrl}}/refunds", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "refunds" + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To create a refund against an already processed payment" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, - { - "name": "Refunds - Retrieve-copy", + } + ] + }, + { + "name": "Scenario4-Create payment with Manual capture", + "item": [ + { + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", - " },", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", " );", "}", "", - "// Response body should have value \"6540\" for \"amount\"", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_capture\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(1000);", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", " },", " );", "}", @@ -1973,55 +2328,63 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, "url": { - "raw": "{{baseUrl}}/refunds/:id", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } + "payments" ] }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] }, { - "name": "Payments - Retrieve-copy", + "name": "Payments - Capture", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", "", "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -2070,20 +2433,35 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'processing'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"processing\");", " },", " );", "}", "", - "// Response body should have \"refunds\"", - "pm.test(\"[POST]::/payments - Content check if 'refunds' exists\", function () {", - " pm.expect(typeof jsonData.refunds !== \"undefined\").to.be.true;", - "});", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6000);", + " },", + " );", + "}", "" ], "type": "text/javascript" @@ -2091,27 +2469,35 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments/:id/capture", "host": [ "{{baseUrl}}" ], "path": [ "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } + ":id", + "capture" ], "variable": [ { @@ -2121,36 +2507,31 @@ } ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To capture the funds for an uncaptured payment" }, "response": [] - } - ] - }, - { - "name": "Scenario12-Bank Redirect-sofort", - "item": [ + }, { - "name": "Payments - Create", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -2199,12 +2580,12 @@ " );", "}", "", - "// Response body should have value \"requires_payment_method\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " pm.expect(jsonData.status).to.eql(\"processing\");", " },", " );", "}", @@ -2215,63 +2596,60 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/payments/:id", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario5-Void the payment", + "item": [ { - "name": "Payments - Confirm", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -2320,41 +2698,120 @@ " );", "}", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", + "// Response body should have value \"requires_capture\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", " },", " );", "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Cancel", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/cancel - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", " },", ");", "", - "// Response body should have value \"sofort\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'sofort'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"sofort\");", - " },", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", " );", "}", "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"cancelled\" for \"status\"", + "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " \"[POST]::/payments/:id/cancel - Content check if value for 'status' matches 'cancelled'\",", " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " pm.expect(jsonData.status).to.eql(\"cancelled\");", " },", " );", "}", @@ -2365,26 +2822,6 @@ } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, "method": "POST", "header": [ { @@ -2403,17 +2840,17 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"sofort\",\"payment_method_data\":{\"bank_redirect\":{\"sofort\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" + "raw": "{\"cancellation_reason\":\"requested_by_customer\"}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/payments/:id/cancel", "host": [ "{{baseUrl}}" ], "path": [ "payments", ":id", - "confirm" + "cancel" ], "variable": [ { @@ -2423,7 +2860,7 @@ } ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" }, "response": [] }, @@ -2496,12 +2933,12 @@ " );", "}", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", + "// Response body should have value \"cancelled\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'cancelled'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.status).to.eql(\"cancelled\");", " },", " );", "}", @@ -2549,7 +2986,7 @@ ] }, { - "name": "Scenario13-Bank Redirect-eps", + "name": "Scenario6-Create 3DS payment", "item": [ { "name": "Payments - Create", @@ -2620,15 +3057,24 @@ " );", "}", "", - "// Response body should have value \"requires_payment_method\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", "" ], "type": "text/javascript" @@ -2654,7 +3100,7 @@ "language": "json" } }, - "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"AT\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"AT\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4917610000000000\",\"card_exp_month\":\"03\",\"card_exp_year\":\"30\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"737\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -2670,29 +3116,26 @@ "response": [] }, { - "name": "Payments - Confirm", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -2744,41 +3187,12 @@ "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "", - "// Response body should have value \"eps\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'eps'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"eps\");", - " },", - " );", - "}", - "", - "// Response body should have value \"adyen\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - " },", - " );", - "}", "" ], "type": "text/javascript" @@ -2786,89 +3200,66 @@ } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"eps\",\"payment_method_data\":{\"bank_redirect\":{\"eps\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"AT\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" - }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ "payments", - ":id", - "confirm" + ":id" ], - "variable": [ + "query": [ { - "key": "id", - "value": "{{payment_id}}", + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", "description": "(Required) unique payment id" } ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario7-Create 3DS payment with confrm false", + "item": [ { - "name": "Payments - Retrieve", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -2917,12 +3308,12 @@ " );", "}", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", + "// Response body should have value \"requires_confirmation\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", " },", " );", "}", @@ -2933,66 +3324,63 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4917610000000000\",\"card_exp_month\":\"03\",\"card_exp_year\":\"30\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"737\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] - } - ] - }, - { - "name": "Scenario14-Refund recurring payment", - "item": [ + }, { - "name": "Payments - Create", + "name": "Payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -3041,39 +3429,22 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", + "// Response body should have \"next_action.redirect_to_url\"", "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " \"[POST]::/payments/:id/confirm - Content check if 'next_action.redirect_to_url' exists\",", " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", " },", ");", "" @@ -3083,6 +3454,26 @@ } ], "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, "method": "POST", "header": [ { @@ -3101,18 +3492,27 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"setup_future_usage\":\"off_session\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"127.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"single_use\":{\"amount\":7000,\"currency\":\"USD\"}}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] }, @@ -3185,31 +3585,15 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", "" ], "type": "text/javascript" @@ -3250,9 +3634,14 @@ "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario9-Refund full payment", + "item": [ { - "name": "Recurring Payments - Create", + "name": "Payments - Create", "event": [ { "listen": "test", @@ -3329,40 +3718,6 @@ " },", " );", "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"payment_method_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'payment_method_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.payment_method_data !== \"undefined\").to.be.true;", - " },", - ");", "" ], "type": "text/javascript" @@ -3388,7 +3743,7 @@ "language": "json" } }, - "raw": "{\"amount\":6570,\"currency\":\"USD\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -3404,7 +3759,7 @@ "response": [] }, { - "name": "Payments - Retrieve-copy", + "name": "Payments - Retrieve", "event": [ { "listen": "test", @@ -3481,22 +3836,6 @@ " },", " );", "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", "" ], "type": "text/javascript" @@ -3539,7 +3878,7 @@ "response": [] }, { - "name": "Refunds - Create Copy", + "name": "Refunds - Create", "event": [ { "listen": "test", @@ -3636,7 +3975,7 @@ "response": [] }, { - "name": "Refunds - Retrieve Copy", + "name": "Refunds - Retrieve", "event": [ { "listen": "test", @@ -3730,7 +4069,7 @@ ] }, { - "name": "Scenario15-Bank Redirect-giropay", + "name": "Scenario10-Create a mandate and recurring payment", "item": [ { "name": "Payments - Create", @@ -3801,15 +4140,41 @@ " );", "}", "", - "// Response body should have value \"requires_payment_method\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", "" ], "type": "text/javascript" @@ -3835,7 +4200,7 @@ "language": "json" } }, - "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"setup_future_usage\":\"off_session\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"127.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"single_use\":{\"amount\":7000,\"currency\":\"USD\"}}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -3851,29 +4216,26 @@ "response": [] }, { - "name": "Payments - Confirm", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -3922,44 +4284,31 @@ " );", "}", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", "", - "// Response body should have \"next_action.redirect_to_url\"", + "// Response body should have \"mandate_id\"", "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", " },", ");", "", - "// Response body should have value \"giropay\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'giropay'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"giropay\");", - " },", - " );", - "}", - "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - " },", - " );", - "}", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", "" ], "type": "text/javascript" @@ -3967,55 +4316,27 @@ } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"giropay\",\"payment_method_data\":{\"bank_redirect\":{\"giropay\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" - }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ "payments", - ":id", - "confirm" + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } ], "variable": [ { @@ -4025,31 +4346,31 @@ } ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] }, { - "name": "Payments - Retrieve", + "name": "Recurring Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -4098,15 +4419,39 @@ " );", "}", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"payment_method_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'payment_method_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.payment_method_data !== \"undefined\").to.be.true;", + " },", + ");", "" ], "type": "text/javascript" @@ -4114,66 +4459,60 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] - } - ] - }, - { - "name": "Scenario16-Bank debit-ach", - "item": [ + }, { - "name": "Payments - Create", + "name": "Payments - Retrieve-copy", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -4222,15 +4561,31 @@ " );", "}", "", - "// Response body should have value \"requires_payment_method\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", "" ], "type": "text/javascript" @@ -4238,63 +4593,66 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":10000,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"off_session\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario11-Partial refund", + "item": [ { - "name": "Payments - Confirm", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -4343,35 +4701,15 @@ " );", "}", "", - "// Response body should have value \"ach\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'ach'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"ach\");", - " },", - " );", - "}", - "", "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", " function () {", " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", - "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - " },", - " );", - "}", "" ], "type": "text/javascript" @@ -4379,26 +4717,6 @@ } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, "method": "POST", "header": [ { @@ -4417,27 +4735,18 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_debit\",\"payment_method_type\":\"ach\",\"payment_method_data\":{\"bank_debit\":{\"ach_bank_debit\":{\"account_number\":\"40308669\",\"routing_number\":\"121000358\",\"sort_code\":\"560036\",\"shopper_email\":\"example@gmail.com\",\"card_holder_name\":\"joseph Doe\",\"bank_account_holder_name\":\"David Archer\",\"billing_details\":{\"houseNumberOrName\":\"50\",\"street\":\"Test Street\",\"city\":\"Amsterdam\",\"stateOrProvince\":\"NY\",\"postalCode\":\"12010\",\"country\":\"US\",\"name\":\"A. Klaassen\",\"email\":\"example@gmail.com\"},\"reference\":\"daslvcgbaieh\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] }, @@ -4510,10 +4819,10 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", @@ -4559,87 +4868,61 @@ "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - } - ] - }, - { - "name": "Scenario17-Bank debit-Bacs", - "item": [ + }, { - "name": "Payments - Create", + "name": "Refunds - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", " );", "}", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"pending\");", + " },", " );", "}", "", - "// Response body should have value \"requires_payment_method\" for \"status\"", + "// Response body should have value \"540\" for \"amount\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " pm.expect(jsonData.amount).to.eql(540);", " },", " );", "}", @@ -4668,46 +4951,38 @@ "language": "json" } }, - "raw": "{\"amount\":100,\"currency\":\"GBP\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"off_session\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"GB\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"RETURN\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/refunds", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "refunds" ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "To create a refund against an already processed payment" }, "response": [] }, { - "name": "Payments - Confirm", + "name": "Refunds - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", "});", "", "// Set response object as internal variable", @@ -4716,71 +4991,127 @@ " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", " );", "}", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"pending\");", + " },", " );", "}", "", - "// Response body should have value \"processing\" for \"status\"", + "// Response body should have value \"6540\" for \"amount\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", + " pm.expect(jsonData.amount).to.eql(540);", " },", " );", "}", - "", - "// Response body should have value \"bacs\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create-copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'bacs'\",", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"bacs\");", + " pm.expect(jsonData.status).to.eql(\"pending\");", " },", " );", "}", "", - "// Response body should have value \"adyen\" for \"connector\"", - "if (jsonData?.connector) {", + "// Response body should have value \"1000\" for \"amount\"", + "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " pm.expect(jsonData.amount).to.eql(1000);", " },", " );", "}", @@ -4791,26 +5122,6 @@ } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, "method": "POST", "header": [ { @@ -4829,32 +5140,115 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_debit\",\"payment_method_type\":\"bacs\",\"payment_method_data\":{\"bank_debit\":{\"bacs_bank_debit\":{\"account_number\":\"40308669\",\"routing_number\":\"121000358\",\"sort_code\":\"560036\",\"shopper_email\":\"example@gmail.com\",\"card_holder_name\":\"joseph Doe\",\"bank_account_holder_name\":\"David Archer\",\"billing_details\":{\"houseNumberOrName\":\"50\",\"street\":\"Test Street\",\"city\":\"Amsterdam\",\"stateOrProvince\":\"NY\",\"postalCode\":\"12010\",\"country\":\"GB\",\"name\":\"A. Klaassen\",\"email\":\"abcd@gmail.com\"},\"reference\":\"daslvcgbaieh\"}}}}" + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":1000,\"reason\":\"FRAUD\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/refunds", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "confirm" + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve-copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"pending\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(1000);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" ], "variable": [ { "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" + "value": "{{refund_id}}", + "description": "(Required) unique refund id" } ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] }, { - "name": "Payments - Retrieve", + "name": "Payments - Retrieve-copy", "event": [ { "listen": "test", @@ -4922,15 +5316,20 @@ " );", "}", "", - "// Response body should have value \"processing\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'processing'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", + "", + "// Response body should have \"refunds\"", + "pm.test(\"[POST]::/payments - Content check if 'refunds' exists\", function () {", + " pm.expect(typeof jsonData.refunds !== \"undefined\").to.be.true;", + "});", "" ], "type": "text/javascript" @@ -4975,7 +5374,7 @@ ] }, { - "name": "Scenario18-Bank Redirect-Trustly", + "name": "Scenario12-Bank Redirect-sofort", "item": [ { "name": "Payments - Create", @@ -5080,7 +5479,7 @@ "language": "json" } }, - "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"FI\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -5186,12 +5585,12 @@ " },", ");", "", - "// Response body should have value \"giropay\" for \"payment_method_type\"", + "// Response body should have value \"sofort\" for \"payment_method_type\"", "if (jsonData?.payment_method_type) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'trustly'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'sofort'\",", " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"trustly\");", + " pm.expect(jsonData.payment_method_type).to.eql(\"sofort\");", " },", " );", "}", @@ -5250,7 +5649,7 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"trustly\",\"payment_method_data\":{\"bank_redirect\":{\"trustly\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"FI\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"sofort\",\"payment_method_data\":{\"bank_redirect\":{\"sofort\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", @@ -5396,7 +5795,7 @@ ] }, { - "name": "Scenario19-Add card flow", + "name": "Scenario13-Bank Redirect-eps", "item": [ { "name": "Payments - Create", @@ -5405,256 +5804,78 @@ "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", + "// Validate status 2xx", "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", "});", "", - "// Validate if response has JSON Body ", + "// Validate if response has JSON Body", "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "List payment methods for a Customer", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "if (jsonData?.customer_payment_methods[0]?.payment_token) {", - " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", - " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "customers", - ":customer_id", - "payment_methods" - ], - "query": [ - { - "key": "accepted_country", - "value": "co", - "disabled": true - }, - { - "key": "accepted_country", - "value": "pa", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "voluptate ea", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "exercitation", - "disabled": true - }, - { - "key": "minimum_amount", - "value": "100", - "disabled": true - }, - { - "key": "maximum_amount", - "value": "10000000", - "disabled": true - }, - { - "key": "recurring_payment_enabled", - "value": "true", - "disabled": true - }, - { - "key": "installment_payment_enabled", - "value": "true", - "disabled": true - } - ], - "variable": [ - { - "key": "customer_id", - "value": "{{customer_id}}", - "description": "//Pass the customer id" - } - ] - }, - "description": "To filter and list the applicable payment methods for a particular Customer ID" - }, - "response": [] - }, - { - "name": "Save card payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" ], "type": "text/javascript" } @@ -5679,7 +5900,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"AT\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"AT\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -5695,7 +5916,7 @@ "response": [] }, { - "name": "Save card payments - Confirm", + "name": "Payments - Confirm", "event": [ { "listen": "test", @@ -5766,16 +5987,34 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "", + "// Response body should have value \"eps\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'eps'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"eps\");", + " },", + " );", + "}", "", "// Response body should have value \"adyen\" for \"connector\"", "if (jsonData?.connector) {", @@ -5831,7 +6070,7 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"7373\"}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"eps\",\"payment_method_data\":{\"bank_redirect\":{\"eps\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"AT\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", @@ -5856,7 +6095,7 @@ "response": [] }, { - "name": "Payments - Retrieve Copy", + "name": "Payments - Retrieve", "event": [ { "listen": "test", @@ -5924,47 +6163,12 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", @@ -6008,69 +6212,116 @@ "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario14-Refund recurring payment", + "item": [ { - "name": "Refunds - Create", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", " );", "}", "", "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", "", - "// Response body should have value \"540\" for \"amount\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(540);", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - "});", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", "" ], "type": "text/javascript" @@ -6096,78 +6347,115 @@ "language": "json" } }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"FRAUD\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"setup_future_usage\":\"off_session\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"127.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"single_use\":{\"amount\":7000,\"currency\":\"USD\"}}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/refunds", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "refunds" + "payments" ] }, - "description": "To create a refund against an already processed payment" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] }, { - "name": "Refunds - Retrieve", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", - " },", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", " );", "}", "", - "// Response body should have value \"6540\" for \"amount\"", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(540);", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", "" ], "type": "text/javascript" @@ -6183,88 +6471,145 @@ } ], "url": { - "raw": "{{baseUrl}}/refunds/:id", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "refunds", + "payments", ":id" ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], "variable": [ { "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" + "value": "{{payment_id}}", + "description": "(Required) unique payment id" } ] }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - } - ] - }, - { - "name": "Scenario20-Pass Invalid CVV for save card flow and verify failed payment", - "item": [ + }, { - "name": "Payments - Create", + "name": "Recurring Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", + "// Validate status 2xx", "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", "});", "", - "// Validate if response has JSON Body ", + "// Validate if response has JSON Body", "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"payment_method_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'payment_method_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.payment_method_data !== \"undefined\").to.be.true;", + " },", + ");", + "" ], "type": "text/javascript" } @@ -6289,7 +6634,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6570,\"currency\":\"USD\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -6305,32 +6650,100 @@ "response": [] }, { - "name": "List payment methods for a Customer", + "name": "Payments - Retrieve-copy", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", - "if (jsonData?.customer_payment_methods[0]?.payment_token) {", - " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", - " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", - "}" + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "" ], "type": "text/javascript" } @@ -6345,126 +6758,90 @@ } ], "url": { - "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "customers", - ":customer_id", - "payment_methods" + "payments", + ":id" ], "query": [ { - "key": "accepted_country", - "value": "co", - "disabled": true - }, - { - "key": "accepted_country", - "value": "pa", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "voluptate ea", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "exercitation", - "disabled": true - }, - { - "key": "minimum_amount", - "value": "100", - "disabled": true - }, - { - "key": "maximum_amount", - "value": "10000000", - "disabled": true - }, - { - "key": "recurring_payment_enabled", - "value": "true", - "disabled": true - }, - { - "key": "installment_payment_enabled", - "value": "true", - "disabled": true + "key": "force_sync", + "value": "true" } ], "variable": [ { - "key": "customer_id", - "value": "{{customer_id}}", - "description": "//Pass the customer id" - } + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To filter and list the applicable payment methods for a particular Customer ID" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] }, { - "name": "Save card payments - Create", + "name": "Refunds - Create Copy", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"pending\");", + " },", + " );", + "}", "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "" ], "type": "text/javascript" } @@ -6489,46 +6866,38 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"DUPLICATE\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/refunds", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "refunds" ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "To create a refund against an already processed payment" }, "response": [] }, { - "name": "Save card payments - Confirm", + "name": "Refunds - Retrieve Copy", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", "});", "", "// Set response object as internal variable", @@ -6537,172 +6906,99 @@ " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", " );", "}", "", - "// Response body should have value \"failed\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'failed'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"failed\");", - " },", - " );", - "}", - "// Response body should have value \"adyen\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " pm.expect(jsonData.status).to.eql(\"pending\");", " },", " );", "}", "", - "// Response body should have value \"24\" for \"error_code\"", - "if (jsonData?.error_code) {", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error_code' matches '24'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", " function () {", - " pm.expect(jsonData.error_code).to.eql(\"24\");", + " pm.expect(jsonData.amount).to.eql(6540);", " },", " );", "}", - "", - "// Response body should have value \"24\" for \"error_message\"", - "if (jsonData?.error_message) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error_message' matches 'CVC Declined'\",", - " function () {", - " pm.expect(jsonData.error_message).to.eql(\"CVC Declined\");", - " },", - " );", - "}" + "" ], "type": "text/javascript" } } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"737\"}" - }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/refunds/:id", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "confirm" + "refunds", + ":id" ], "variable": [ { "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" + "value": "{{refund_id}}", + "description": "(Required) unique refund id" } ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario15-Bank Redirect-giropay", + "item": [ { - "name": "Payments - Retrieve", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -6751,47 +7047,12 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"requires_payment_method\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"failed\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", " },", " );", "}", @@ -6802,108 +7063,176 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] - } - ] - }, - { - "name": "Scenario21-Don't Pass CVV for save card flow and verify failed payment Copy", - "item": [ + }, { - "name": "Payments - Create", + "name": "Payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "", + "// Response body should have value \"giropay\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'giropay'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"giropay\");", + " },", + " );", + "}", + "", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " },", + " );", + "}", + "" ], "type": "text/javascript" } } ], "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, "method": "POST", "header": [ { @@ -6922,48 +7251,109 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"giropay\",\"payment_method_data\":{\"bank_redirect\":{\"giropay\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] }, { - "name": "List payment methods for a Customer", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", - "if (jsonData?.customer_payment_methods[0]?.payment_token) {", - " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", - " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", - "}" + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" ], "type": "text/javascript" } @@ -6975,129 +7365,119 @@ { "key": "Accept", "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "customers", - ":customer_id", - "payment_methods" - ], - "query": [ - { - "key": "accepted_country", - "value": "co", - "disabled": true - }, - { - "key": "accepted_country", - "value": "pa", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "voluptate ea", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "exercitation", - "disabled": true - }, - { - "key": "minimum_amount", - "value": "100", - "disabled": true - }, - { - "key": "maximum_amount", - "value": "10000000", - "disabled": true - }, - { - "key": "recurring_payment_enabled", - "value": "true", - "disabled": true - }, + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ { - "key": "installment_payment_enabled", - "value": "true", - "disabled": true + "key": "force_sync", + "value": "true" } ], "variable": [ { - "key": "customer_id", - "value": "{{customer_id}}", - "description": "//Pass the customer id" + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" } ] }, - "description": "To filter and list the applicable payment methods for a particular Customer ID" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario16-Bank debit-ach", + "item": [ { - "name": "Save card payments - Create", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", + "// Validate status 2xx", "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", "});", "", - "// Validate if response has JSON Body ", + "// Validate if response has JSON Body", "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" ], "type": "text/javascript" } @@ -7122,7 +7502,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":10000,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"off_session\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -7138,7 +7518,7 @@ "response": [] }, { - "name": "Save card payments - Confirm", + "name": "Payments - Confirm", "event": [ { "listen": "test", @@ -7209,44 +7589,36 @@ " );", "}", "", - "// Response body should have value \"failed\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'failed'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"failed\");", - " },", - " );", - "}", - "// Response body should have value \"adyen\" for \"connector\"", - "if (jsonData?.connector) {", + "// Response body should have value \"ach\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'ach'\",", " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " pm.expect(jsonData.payment_method_type).to.eql(\"ach\");", " },", " );", "}", "", - "// Response body should have value \"24\" for \"error_code\"", - "if (jsonData?.error_code) {", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error_code' matches '24'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.error_code).to.eql(\"24\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", "", - "// Response body should have value \"24\" for \"error_message\"", - "if (jsonData?.error_message) {", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error_message' matches 'CVC Declined'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", " function () {", - " pm.expect(jsonData.error_message).to.eql(\"CVC Declined\");", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", " },", " );", - "}" + "}", + "" ], "type": "text/javascript" } @@ -7291,17 +7663,136 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\"}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_debit\",\"payment_method_type\":\"ach\",\"payment_method_data\":{\"bank_debit\":{\"ach_bank_debit\":{\"account_number\":\"40308669\",\"routing_number\":\"121000358\",\"sort_code\":\"560036\",\"shopper_email\":\"example@gmail.com\",\"card_holder_name\":\"joseph Doe\",\"bank_account_holder_name\":\"David Archer\",\"billing_details\":{\"houseNumberOrName\":\"50\",\"street\":\"Test Street\",\"city\":\"Amsterdam\",\"stateOrProvince\":\"NY\",\"postalCode\":\"12010\",\"country\":\"US\",\"name\":\"A. Klaassen\",\"email\":\"example@gmail.com\"},\"reference\":\"daslvcgbaieh\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ "payments", - ":id", - "confirm" + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } ], "variable": [ { @@ -7311,31 +7802,36 @@ } ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario17-Bank debit-Bacs", + "item": [ { - "name": "Payments - Retrieve", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -7384,47 +7880,12 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"requires_payment_method\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"failed\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", " },", " );", "}", @@ -7435,66 +7896,63 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":100,\"currency\":\"GBP\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"off_session\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"GB\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] - } - ] - }, - { - "name": "Scenario1-Create payment with confirm true", - "item": [ + }, { - "name": "Payments - Create", + "name": "Payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -7543,12 +8001,32 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"processing\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// Response body should have value \"bacs\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'bacs'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"bacs\");", + " },", + " );", + "}", + "", + "// Response body should have value \"adyen\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", " },", " );", "}", @@ -7559,6 +8037,26 @@ } ], "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, "method": "POST", "header": [ { @@ -7577,18 +8075,27 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_debit\",\"payment_method_type\":\"bacs\",\"payment_method_data\":{\"bank_debit\":{\"bacs_bank_debit\":{\"account_number\":\"40308669\",\"routing_number\":\"121000358\",\"sort_code\":\"560036\",\"shopper_email\":\"example@gmail.com\",\"card_holder_name\":\"joseph Doe\",\"bank_account_holder_name\":\"David Archer\",\"billing_details\":{\"houseNumberOrName\":\"50\",\"street\":\"Test Street\",\"city\":\"Amsterdam\",\"stateOrProvince\":\"NY\",\"postalCode\":\"12010\",\"country\":\"GB\",\"name\":\"A. Klaassen\",\"email\":\"abcd@gmail.com\"},\"reference\":\"daslvcgbaieh\"}}}}" }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] }, @@ -7661,12 +8168,12 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"processing\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'processing'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"processing\");", " },", " );", "}", @@ -7714,7 +8221,7 @@ ] }, { - "name": "Scenario2-Create payment with confirm false", + "name": "Scenario18-Bank Redirect-Trustly", "item": [ { "name": "Payments - Create", @@ -7785,12 +8292,12 @@ " );", "}", "", - "// Response body should have value \"requires_confirmation\" for \"status\"", + "// Response body should have value \"requires_payment_method\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", " },", " );", "}", @@ -7819,7 +8326,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"FI\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -7906,12 +8413,41 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "", + "// Response body should have value \"giropay\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'trustly'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"trustly\");", + " },", + " );", + "}", + "", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", " },", " );", "}", @@ -7960,7 +8496,7 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\"}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"trustly\",\"payment_method_data\":{\"bank_redirect\":{\"trustly\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"FI\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", @@ -8053,12 +8589,12 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", @@ -8106,7 +8642,7 @@ ] }, { - "name": "Scenario3-Create payment without PMD", + "name": "Scenario19-Add card flow", "item": [ { "name": "Payments - Create", @@ -8115,78 +8651,56 @@ "listen": "test", "script": { "exec": [ - "// Validate status 2xx", + "// Validate status 2xx ", "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", - "// Validate if response has JSON Body", + "// Validate if response has JSON Body ", "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", + "try {jsonData = pm.response.json();}catch(e){}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" ], "type": "text/javascript" } @@ -8211,7 +8725,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -8227,300 +8741,229 @@ "response": [] }, { - "name": "Payments - Confirm", + "name": "List payment methods for a Customer", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", + "try {jsonData = pm.response.json();}catch(e){}", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", + "if (jsonData?.customer_payment_methods[0]?.payment_token) {", + " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", + " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id/confirm - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" + " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", + "}" ], "type": "text/javascript" } } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}}}" - }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "confirm" + "customers", + ":customer_id", + "payment_methods" + ], + "query": [ + { + "key": "accepted_country", + "value": "co", + "disabled": true + }, + { + "key": "accepted_country", + "value": "pa", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "voluptate ea", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "exercitation", + "disabled": true + }, + { + "key": "minimum_amount", + "value": "100", + "disabled": true + }, + { + "key": "maximum_amount", + "value": "10000000", + "disabled": true + }, + { + "key": "recurring_payment_enabled", + "value": "true", + "disabled": true + }, + { + "key": "installment_payment_enabled", + "value": "true", + "disabled": true + } ], "variable": [ { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" + "key": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" } ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To filter and list the applicable payment methods for a particular Customer ID" }, "response": [] }, { - "name": "Payments - Retrieve", + "name": "Save card payments - Create", "event": [ { "listen": "test", "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", + "try {jsonData = pm.response.json();}catch(e){}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" ], "type": "text/javascript" } } ], "request": { - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] - } - ] - }, - { - "name": "Scenario4-Create payment with Manual capture", - "item": [ + }, { - "name": "Payments - Create", + "name": "Save card payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -8569,12 +9012,23 @@ " );", "}", "", - "// Response body should have value \"requires_capture\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "", + "// Response body should have value \"adyen\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", " },", " );", "}", @@ -8585,6 +9039,26 @@ } ], "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, "method": "POST", "header": [ { @@ -8603,45 +9077,51 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"7373\"}" }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] }, { - "name": "Payments - Capture", + "name": "Payments - Retrieve Copy", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -8690,16 +9170,21 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'processing'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + "});", + "", "// Response body should have value \"6540\" for \"amount\"", "if (jsonData?.amount) {", " pm.test(", @@ -8715,7 +9200,17 @@ " pm.test(", " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", " },", " );", "}", @@ -8726,35 +9221,27 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" - }, "url": { - "raw": "{{baseUrl}}/payments/:id/capture", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ "payments", - ":id", - "capture" + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } ], "variable": [ { @@ -8764,88 +9251,72 @@ } ] }, - "description": "To capture the funds for an uncaptured payment" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] }, { - "name": "Payments - Retrieve", + "name": "Refunds - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", " );", "}", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"pending\");", + " },", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"540\" for \"amount\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", + " pm.expect(jsonData.amount).to.eql(540);", " },", " );", "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + "});", "" ], "type": "text/javascript" @@ -8853,114 +9324,93 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"FRAUD\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id", + "raw": "{{baseUrl}}/refunds", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "refunds" ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To create a refund against an already processed payment" }, "response": [] - } - ] - }, - { - "name": "Scenario5-Void the payment", - "item": [ + }, { - "name": "Payments - Create", + "name": "Refunds - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", " );", "}", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"pending\");", + " },", " );", "}", "", - "// Response body should have value \"requires_capture\" for \"status\"", + "// Response body should have value \"6540\" for \"amount\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " pm.expect(jsonData.amount).to.eql(540);", " },", " );", "}", @@ -8971,108 +9421,96 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/refunds/:id", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario20-Pass Invalid CVV for save card flow and verify failed payment", + "item": [ { - "name": "Payments - Cancel", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/cancel - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", + "try {jsonData = pm.response.json();}catch(e){}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", "", - "// Response body should have value \"cancelled\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'status' matches 'cancelled'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"cancelled\");", - " },", - " );", - "}", - "" + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" ], "type": "text/javascript" } @@ -9097,109 +9535,48 @@ "language": "json" } }, - "raw": "{\"cancellation_reason\":\"requested_by_customer\"}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/cancel", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "cancel" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] }, { - "name": "Payments - Retrieve", + "name": "List payment methods for a Customer", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", + "try {jsonData = pm.response.json();}catch(e){}", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", + "if (jsonData?.customer_payment_methods[0]?.payment_token) {", + " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", + " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"cancelled\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'cancelled'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"cancelled\");", - " },", - " );", - "}", - "" + " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", + "}" ], "type": "text/javascript" } @@ -9214,125 +9591,126 @@ } ], "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" + "customers", + ":customer_id", + "payment_methods" ], "query": [ { - "key": "force_sync", - "value": "true" + "key": "accepted_country", + "value": "co", + "disabled": true + }, + { + "key": "accepted_country", + "value": "pa", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "voluptate ea", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "exercitation", + "disabled": true + }, + { + "key": "minimum_amount", + "value": "100", + "disabled": true + }, + { + "key": "maximum_amount", + "value": "10000000", + "disabled": true + }, + { + "key": "recurring_payment_enabled", + "value": "true", + "disabled": true + }, + { + "key": "installment_payment_enabled", + "value": "true", + "disabled": true } ], "variable": [ { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" + "key": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" } ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To filter and list the applicable payment methods for a particular Customer ID" }, "response": [] - } - ] - }, - { - "name": "Scenario6-Create 3DS payment", - "item": [ + }, { - "name": "Payments - Create", + "name": "Save card payments - Create", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx", + "// Validate status 2xx ", "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", - "// Validate if response has JSON Body", + "// Validate if response has JSON Body ", "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", + "try {jsonData = pm.response.json();}catch(e){}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "" + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" ], "type": "text/javascript" } @@ -9357,7 +9735,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4917610000000000\",\"card_exp_month\":\"03\",\"card_exp_year\":\"30\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"737\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -9373,26 +9751,29 @@ "response": [] }, { - "name": "Payments - Retrieve", + "name": "Save card payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", "", "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -9441,43 +9822,99 @@ " );", "}", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", + "// Response body should have value \"failed\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'failed'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.status).to.eql(\"failed\");", " },", " );", "}", - "" + "// Response body should have value \"adyen\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " },", + " );", + "}", + "", + "// Response body should have value \"24\" for \"error_code\"", + "if (jsonData?.error_code) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error_code' matches '24'\",", + " function () {", + " pm.expect(jsonData.error_code).to.eql(\"24\");", + " },", + " );", + "}", + "", + "// Response body should have value \"24\" for \"error_message\"", + "if (jsonData?.error_message) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error_message' matches 'CVC Declined'\",", + " function () {", + " pm.expect(jsonData.error_message).to.eql(\"CVC Declined\");", + " },", + " );", + "}" ], "type": "text/javascript" } } ], "request": { - "method": "GET", + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"737\"}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } + ":id", + "confirm" ], "variable": [ { @@ -9487,36 +9924,31 @@ } ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] - } - ] - }, - { - "name": "Scenario7-Create 3DS payment with confrm false", - "item": [ + }, { - "name": "Payments - Create", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -9565,12 +9997,47 @@ " );", "}", "", - "// Response body should have value \"requires_confirmation\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " pm.expect(jsonData.status).to.eql(\"failed\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", " },", " );", "}", @@ -9581,156 +10048,108 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4917610000000000\",\"card_exp_month\":\"03\",\"card_exp_year\":\"30\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"737\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario21-Don't Pass CVV for save card flow and verify failed payment Copy", + "item": [ { - "name": "Payments - Confirm", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", + "try {jsonData = pm.response.json();}catch(e){}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "" + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" ], "type": "text/javascript" } } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, "method": "POST", "header": [ { @@ -9749,109 +10168,48 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] }, { - "name": "Payments - Retrieve", + "name": "List payment methods for a Customer", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", + "try {jsonData = pm.response.json();}catch(e){}", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", + "if (jsonData?.customer_payment_methods[0]?.payment_token) {", + " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", + " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" + " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", + "}" ], "type": "text/javascript" } @@ -9866,116 +10224,126 @@ } ], "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" + "customers", + ":customer_id", + "payment_methods" ], "query": [ { - "key": "force_sync", - "value": "true" + "key": "accepted_country", + "value": "co", + "disabled": true + }, + { + "key": "accepted_country", + "value": "pa", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "voluptate ea", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "exercitation", + "disabled": true + }, + { + "key": "minimum_amount", + "value": "100", + "disabled": true + }, + { + "key": "maximum_amount", + "value": "10000000", + "disabled": true + }, + { + "key": "recurring_payment_enabled", + "value": "true", + "disabled": true + }, + { + "key": "installment_payment_enabled", + "value": "true", + "disabled": true } ], "variable": [ { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" + "key": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" } ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To filter and list the applicable payment methods for a particular Customer ID" }, "response": [] - } - ] - }, - { - "name": "Scenario9-Refund full payment", - "item": [ + }, { - "name": "Payments - Create", + "name": "Save card payments - Create", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx", + "// Validate status 2xx ", "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", - "// Validate if response has JSON Body", + "// Validate if response has JSON Body ", "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", + "try {jsonData = pm.response.json();}catch(e){}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" ], "type": "text/javascript" } @@ -10000,7 +10368,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -10016,26 +10384,29 @@ "response": [] }, { - "name": "Payments - Retrieve", + "name": "Save card payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", "", "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -10084,120 +10455,70 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"failed\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'failed'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"failed\");", " },", " );", "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + "// Response body should have value \"adyen\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " },", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", + "// Response body should have value \"24\" for \"error_code\"", + "if (jsonData?.error_code) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error_code' matches '24'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", + " pm.expect(jsonData.error_code).to.eql(\"24\");", " },", " );", "}", "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", + "// Response body should have value \"24\" for \"error_message\"", + "if (jsonData?.error_message) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error_message' matches 'CVC Declined'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", + " pm.expect(jsonData.error_message).to.eql(\"CVC Declined\");", " },", " );", - "}", - "" + "}" ], "type": "text/javascript" } } ], "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, "method": "POST", "header": [ { @@ -10216,78 +10537,143 @@ "language": "json" } }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"DUPLICATE\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\"}" }, "url": { - "raw": "{{baseUrl}}/refunds", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ - "refunds" + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To create a refund against an already processed payment" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] }, { - "name": "Refunds - Retrieve", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", + " pm.expect(jsonData.status).to.eql(\"failed\");", " },", " );", "}", "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + "});", + "", "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", + "if (jsonData?.amount) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", " function () {", " pm.expect(jsonData.amount).to.eql(6540);", " },", " );", "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", "" ], "type": "text/javascript" @@ -10303,23 +10689,29 @@ } ], "url": { - "raw": "{{baseUrl}}/refunds/:id", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "refunds", + "payments", ":id" ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], "variable": [ { "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" + "value": "{{payment_id}}", + "description": "(Required) unique payment id" } ] }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] } @@ -13542,6 +13934,221 @@ "response": [] } ] + }, + { + "name": "Scenario10-Create payouts using unsupported methods", + "item": [ + { + "name": "ACH Payouts - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payouts/create - Status code is 4xx\", function () {", + " pm.response.to.be.clientError;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payouts/create - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payouts/create - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id", + "if (jsonData?.payout_id) {", + " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", + " console.log(", + " \"- use {{payout_id}} as collection variable for value\",", + " jsonData.payout_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":10000,\"currency\":\"USD\",\"customer_id\":\"payout_customer\",\"email\":\"payout_customer@example.com\",\"name\":\"Doest John\",\"phone\":\"6168205366\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payout request\",\"connector\":[\"adyen\"],\"payout_type\":\"bank\",\"payout_method_data\":{\"bank\":{\"bank_routing_number\":\"110000000\",\"bank_account_number\":\"000123456789\",\"bank_name\":\"Stripe Test Bank\",\"bank_country_code\":\"US\",\"bank_city\":\"California\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"CA\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"Doest\",\"last_name\":\"John\"},\"phone\":{\"number\":\"6168205366\",\"country_code\":\"1\"}},\"entity_type\":\"Individual\",\"recurring\":false,\"metadata\":{\"ref\":\"123\",\"vendor_details\":{\"account_type\":\"custom\",\"business_type\":\"individual\",\"business_profile_mcc\":5045,\"business_profile_url\":\"https://www.pastebin.com\",\"business_profile_name\":\"pT\",\"company_address_line1\":\"address_full_match\",\"company_address_line2\":\"Kimberly Way\",\"company_address_postal_code\":\"31062\",\"company_address_city\":\"Milledgeville\",\"company_address_state\":\"GA\",\"company_phone\":\"+16168205366\",\"company_tax_id\":\"000000000\",\"company_owners_provided\":false,\"capabilities_card_payments\":true,\"capabilities_transfers\":true},\"individual_details\":{\"tos_acceptance_date\":1680581051,\"tos_acceptance_ip\":\"103.159.11.202\",\"individual_dob_day\":\"01\",\"individual_dob_month\":\"01\",\"individual_dob_year\":\"1901\",\"individual_id_number\":\"000000000\",\"individual_ssn_last_4\":\"0000\",\"external_account_account_holder_type\":\"individual\"}},\"confirm\":true,\"auto_fulfill\":true}" + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payouts", + "create" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Bacs Payouts - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payouts/create - Status code is 4xx\", function () {", + " pm.response.to.be.clientError;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payouts/create - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payouts/create - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id", + "if (jsonData?.payout_id) {", + " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", + " console.log(", + " \"- use {{payout_id}} as collection variable for value\",", + " jsonData.payout_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":1,\"currency\":\"GBP\",\"customer_id\":\"payout_customer\",\"email\":\"payout_customer@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payout request\",\"payout_type\":\"bank\",\"payout_method_data\":{\"bank\":{\"bank_sort_code\":\"231470\",\"bank_account_number\":\"28821822\",\"bank_name\":\"Deutsche Bank\",\"bank_country_code\":\"NL\",\"bank_city\":\"Amsterdam\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"CA\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"entity_type\":\"Individual\",\"recurring\":true,\"metadata\":{\"ref\":\"123\"},\"confirm\":true,\"auto_fulfill\":true,\"connector\":[\"adyen\"],\"business_label\":\"abcd\",\"business_country\":\"US\"}" + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payouts", + "create" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + } + ] } ] } @@ -13614,6 +14221,11 @@ "key": "refund_id", "value": "" }, + { + "key": "payout_id", + "value": "", + "type": "string" + }, { "key": "merchant_connector_id", "value": "" @@ -13665,6 +14277,16 @@ "key": "connector_api_secret", "value": "", "type": "string" + }, + { + "key": "payment_profile_id", + "value": "", + "type": "string" + }, + { + "key": "payout_profile_id", + "value": "", + "type": "string" } ] } diff --git a/postman/collection-json/wise.postman_collection.json b/postman/collection-json/wise.postman_collection.json new file mode 100644 index 000000000000..dc4d9395d3ac --- /dev/null +++ b/postman/collection-json/wise.postman_collection.json @@ -0,0 +1,1025 @@ +{ + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"[LOG]::payment_id - \" + jsonData.payment_id);", + "}", + "", + "console.log(\"[LOG]::x-request-id - \" + pm.response.headers.get(\"x-request-id\"));", + "" + ], + "type": "text/javascript" + } + } + ], + "item": [ + { + "name": "Health check", + "item": [ + { + "name": "Health", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "x-feature", + "value": "router-custom", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{baseUrl}}/health", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "health" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Flow Testcases", + "item": [ + { + "name": "QuickStart", + "item": [ + { + "name": "Merchant Account - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/accounts - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", + "if (jsonData?.merchant_id) {", + " pm.collectionVariables.set(\"merchant_id\", jsonData.merchant_id);", + " console.log(", + " \"- use {{merchant_id}} as collection variable for value\",", + " jsonData.merchant_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", + "if (jsonData?.publishable_key) {", + " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", + " console.log(", + " \"- use {{publishable_key}} as collection variable for value\",", + " jsonData.publishable_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"merchant_id\":\"postman_merchant_GHAction_{{$guid}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"}],\"merchant_details\":{\"primary_contact_person\":\"John Test\",\"primary_email\":\"JohnTest@test.com\",\"primary_phone\":\"sunt laborum\",\"secondary_contact_person\":\"John Test2\",\"secondary_email\":\"JohnTest2@test.com\",\"secondary_phone\":\"cillum do dolor id\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" + }, + "url": { + "raw": "{{baseUrl}}/accounts", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts" + ] + }, + "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." + }, + "response": [] + }, + { + "name": "API Key - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/api_keys/:merchant_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/api_keys/:merchant_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", + "if (jsonData?.key_id) {", + " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", + " console.log(", + " \"- use {{api_key_id}} as collection variable for value\",", + " jsonData.key_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"name\":\"API Key 1\",\"description\":null,\"expiration\":\"2069-09-23T01:02:03.000Z\"}" + }, + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Payout Connector - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[POST]::/account/:account_id/connectors - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/account/:account_id/connectors - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"connector_type\":\"payout_processor\",\"connector_name\":\"wise\",\"connector_account_details\":{\"auth_type\":\"BodyKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"pay_bright\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"walley\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mobile_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"ali_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mb_way\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"giropay\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"blik\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"trustly\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_czech_republic\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_finland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_poland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_slovakia\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bancontact_card\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\"}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"{{certificate}}\",\"display_name\":\"applepay\",\"certificate_keys\":\"{{certificate_keys}}\",\"initiative_context\":\"hyperswitch-sdk-test.netlify.app\",\"merchant_identifier\":\"merchant.com.stripe.sang\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." + }, + "response": [] + }, + { + "name": "Payouts - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payouts/create - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payouts/create - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payouts/create - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id", + "if (jsonData?.payout_id) {", + " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", + " console.log(", + " \"- use {{payout_id}} as collection variable for value\",", + " jsonData.payout_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":1,\"currency\":\"EUR\",\"customer_id\":\"wise_customer\",\"email\":\"payout_customer@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payout request\",\"connector\":[\"wise\"],\"payout_type\":\"bank\",\"payout_method_data\":{\"bank\":{\"iban\":\"NL46TEST0136169112\",\"bic\":\"ABNANL2A\",\"bank_name\":\"Deutsche Bank\",\"bank_country_code\":\"NL\",\"bank_city\":\"Amsterdam\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"CA\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"entity_type\":\"Individual\",\"recurring\":true,\"metadata\":{\"ref\":\"123\"},\"confirm\":true,\"auto_fulfill\":true}" + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payouts", + "create" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + } + ] + }, + { + "name": "Happy Cases", + "item": [ + { + "name": "Scenario1 - Process Bacs Payout", + "item": [ + { + "name": "Payouts - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payouts/create - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payouts/create - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payouts/create - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id", + "if (jsonData?.payout_id) {", + " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", + " console.log(", + " \"- use {{payout_id}} as collection variable for value\",", + " jsonData.payout_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":1,\"currency\":\"GBP\",\"customer_id\":\"wise_customer\",\"email\":\"payout_customer@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payout request\",\"payout_type\":\"bank\",\"payout_method_data\":{\"bank\":{\"bank_sort_code\":\"231470\",\"bank_account_number\":\"28821822\",\"bank_name\":\"Deutsche Bank\",\"bank_country_code\":\"NL\",\"bank_city\":\"Amsterdam\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"CA\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"entity_type\":\"Individual\",\"recurring\":true,\"metadata\":{\"ref\":\"123\"},\"confirm\":true,\"auto_fulfill\":true,\"connector\":[\"wise\"]}" + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payouts", + "create" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + } + ] + }, + { + "name": "Scenario2 - Process SEPA Payout", + "item": [ + { + "name": "Payouts - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payouts/create - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payouts/create - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payouts/create - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id", + "if (jsonData?.payout_id) {", + " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", + " console.log(", + " \"- use {{payout_id}} as collection variable for value\",", + " jsonData.payout_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":1,\"currency\":\"EUR\",\"customer_id\":\"wise_customer\",\"email\":\"payout_customer@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payout request\",\"connector\":[\"wise\"],\"payout_type\":\"bank\",\"payout_method_data\":{\"bank\":{\"iban\":\"NL46TEST0136169112\",\"bic\":\"ABNANL2A\",\"bank_name\":\"Deutsche Bank\",\"bank_country_code\":\"NL\",\"bank_city\":\"Amsterdam\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"CA\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"entity_type\":\"Individual\",\"recurring\":true,\"metadata\":{\"ref\":\"123\"},\"confirm\":true,\"auto_fulfill\":true}" + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payouts", + "create" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + } + ] + } + ] + }, + { + "name": "Variation Cases", + "item": [ + { + "name": "Scenario1 - Create ACH payout with invalid data", + "item": [ + { + "name": "Payouts - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payouts/create - Status code is 4xx\", function () {", + " pm.response.to.be.clientError;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payouts/create - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payouts/create - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id", + "if (jsonData?.payout_id) {", + " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", + " console.log(", + " \"- use {{payout_id}} as collection variable for value\",", + " jsonData.payout_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":10000,\"currency\":\"USD\",\"customer_id\":\"wise_customer\",\"email\":\"payout_customer@example.com\",\"name\":\"Doest John\",\"phone\":\"6168205366\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payout request\",\"connector\":[\"wise\"],\"payout_type\":\"bank\",\"payout_method_data\":{\"bank\":{\"bank_routing_number\":\"110000000\",\"bank_account_number\":\"000123456789\",\"bank_name\":\"Stripe Test Bank\",\"bank_country_code\":\"US\",\"bank_city\":\"California\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"CA\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"Doest\",\"last_name\":\"John\"},\"phone\":{\"number\":\"6168205366\",\"country_code\":\"1\"}},\"entity_type\":\"Individual\",\"recurring\":false,\"metadata\":{\"ref\":\"123\"},\"confirm\":true,\"auto_fulfill\":true}" + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payouts", + "create" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + } + ] + } + ] + } + ] + } + ], + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "info": { + "_postman_id": "b5107328-6e3c-4ef0-b575-4072bc64462a", + "name": "wise", + "description": "## Get started\n\nJuspay Router provides a collection of APIs that enable you to process and manage payments. Our APIs accept and return JSON in the HTTP body, and return standard HTTP response codes. \nYou can consume the APIs directly using your favorite HTTP/REST library. \nWe have a testing environment referred to \"sandbox\", which you can setup to test API calls without affecting production data.\n\n### Base URLs\n\nUse the following base URLs when making requests to the APIs:\n\n| Environment | Base URL |\n| --- | --- |\n| Sandbox | [https://sandbox.hyperswitch.io](https://sandbox.hyperswitch.io) |\n| Production | [https://router.juspay.io](https://router.juspay.io) |\n\n# Authentication\n\nWhen you sign up for an account, you are given a secret key (also referred as api-key). You may authenticate all API requests with Juspay server by providing the appropriate key in the request Authorization header. \nNever share your secret api keys. Keep them guarded and secure.\n\nContact Support: \nName: Juspay Support \nEmail: [support@juspay.in](mailto:support@juspay.in)", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "variable": [ + { + "key": "baseUrl", + "value": "", + "type": "string" + }, + { + "key": "admin_api_key", + "value": "", + "type": "string" + }, + { + "key": "api_key", + "value": "", + "type": "string" + }, + { + "key": "merchant_id", + "value": "" + }, + { + "key": "payment_id", + "value": "" + }, + { + "key": "customer_id", + "value": "" + }, + { + "key": "mandate_id", + "value": "" + }, + { + "key": "payment_method_id", + "value": "" + }, + { + "key": "refund_id", + "value": "" + }, + { + "key": "payout_id", + "value": "", + "type": "string" + }, + { + "key": "merchant_connector_id", + "value": "" + }, + { + "key": "client_secret", + "value": "", + "type": "string" + }, + { + "key": "connector_api_key", + "value": "", + "type": "string" + }, + { + "key": "connector_key1", + "value": "" + }, + { + "key": "publishable_key", + "value": "", + "type": "string" + }, + { + "key": "payment_token", + "value": "", + "type": "string" + }, + { + "key": "gateway_merchant_id", + "value": "", + "type": "string" + }, + { + "key": "certificate", + "value": "", + "type": "string" + }, + { + "key": "certificate_keys", + "value": "", + "type": "string" + }, + { + "key": "api_key_id", + "value": "" + }, + { + "key": "connector_api_secret", + "value": "", + "type": "string" + } + ] +} From 8f610f4cf13256ee6c0ef534a97d735089fd8f33 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 21 Nov 2023 14:59:19 +0000 Subject: [PATCH 051/146] chore(version): v1.85.0 --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 141bfd40ac5d..bbe558180021 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,38 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.85.0 (2023-11-21) + +### Features + +- **mca:** Add new `auth_type` and a status field for mca ([#2883](https://github.com/juspay/hyperswitch/pull/2883)) ([`25cef38`](https://github.com/juspay/hyperswitch/commit/25cef386b8876b43893f20b93cd68ece6e68412d)) +- **router:** Add unified_code, unified_message in payments response ([#2918](https://github.com/juspay/hyperswitch/pull/2918)) ([`3954001`](https://github.com/juspay/hyperswitch/commit/39540015fde476ad8492a9142c2c1bfda8444a27)) + +### Bug Fixes + +- **connector:** + - [fiserv] fix metadata deserialization in merchant_connector_account ([#2746](https://github.com/juspay/hyperswitch/pull/2746)) ([`644709d`](https://github.com/juspay/hyperswitch/commit/644709d95f6ecaab497cf0cf3788b9e2ed88b855)) + - [CASHTOCODE] Fix Error Response Handling ([#2926](https://github.com/juspay/hyperswitch/pull/2926)) ([`938b63a`](https://github.com/juspay/hyperswitch/commit/938b63a1fceb87b4aae4211dac4d051e024028b1)) +- **router:** Associate parent payment token with `payment_method_id` as hyperswitch token for saved cards ([#2130](https://github.com/juspay/hyperswitch/pull/2130)) ([`efeebc0`](https://github.com/juspay/hyperswitch/commit/efeebc0f2365f0900de3dd3e10a1539621c9933d)) +- Api lock on PaymentsCreate ([#2916](https://github.com/juspay/hyperswitch/pull/2916)) ([`cfabfa6`](https://github.com/juspay/hyperswitch/commit/cfabfa60db4d275066be72ee64153a34d38f13b8)) +- Merchant_connector_id null in KV flow ([#2810](https://github.com/juspay/hyperswitch/pull/2810)) ([`e566a4e`](https://github.com/juspay/hyperswitch/commit/e566a4eff2270c2a56ec90966f42ccfd79906068)) + +### Refactors + +- **connector:** [Paypal] Add support for both BodyKey and SignatureKey ([#2633](https://github.com/juspay/hyperswitch/pull/2633)) ([`d8fcd3c`](https://github.com/juspay/hyperswitch/commit/d8fcd3c9712480c1230590c4f23b35da79df784d)) +- **core:** Query business profile only once ([#2830](https://github.com/juspay/hyperswitch/pull/2830)) ([`44deeb7`](https://github.com/juspay/hyperswitch/commit/44deeb7e7605cb5320b84c0fac1fd551877803a4)) +- **payment_methods:** Added support for pm_auth_connector field in pm list response ([#2667](https://github.com/juspay/hyperswitch/pull/2667)) ([`be4aa3b`](https://github.com/juspay/hyperswitch/commit/be4aa3b913819698c6c22ddedafe1d90fbe02add)) +- Add mapping for ConnectorError in payouts flow ([#2608](https://github.com/juspay/hyperswitch/pull/2608)) ([`5c4e7c9`](https://github.com/juspay/hyperswitch/commit/5c4e7c9031f62d63af35da2dcab79eac948e7dbb)) + +### Testing + +- **postman:** Update postman collection files ([`ce725ef`](https://github.com/juspay/hyperswitch/commit/ce725ef8c680eea3fe03671c989fd4572cfc0640)) + +**Full Changelog:** [`v1.84.0...v1.85.0`](https://github.com/juspay/hyperswitch/compare/v1.84.0...v1.85.0) + +- - - + + ## 1.84.0 (2023-11-17) ### Features From 3f3b797dc65c1bc6f710b122ef00d5bcb409e600 Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Tue, 21 Nov 2023 20:25:38 +0530 Subject: [PATCH 052/146] fix: status goes from pending to partially captured in psync (#2915) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../src/payments/payment_attempt.rs | 7 ++++-- crates/diesel_models/src/payment_attempt.rs | 17 ++++++++++---- crates/router/src/connector/utils.rs | 20 ++++++++++++---- crates/router/src/core/payments/helpers.rs | 23 +++++++++++++++++++ .../payments/operations/payment_capture.rs | 23 +++++++++++++------ .../payments/operations/payment_create.rs | 14 ++++------- .../payments/operations/payment_response.rs | 2 ++ crates/router/src/core/payments/retry.rs | 2 ++ crates/router/src/types.rs | 11 +++++++-- crates/router/src/workflows/payment_sync.rs | 2 ++ .../src/payments/payment_attempt.rs | 20 ++++++++++++---- .../Payments - Create/request.json | 2 +- .../Recurring Payments - Create/request.json | 2 +- .../Payments - Create/request.json | 2 +- .../Payments - Create/request.json | 2 +- 15 files changed, 112 insertions(+), 37 deletions(-) diff --git a/crates/data_models/src/payments/payment_attempt.rs b/crates/data_models/src/payments/payment_attempt.rs index 1b43177feb56..80ae283be85b 100644 --- a/crates/data_models/src/payments/payment_attempt.rs +++ b/crates/data_models/src/payments/payment_attempt.rs @@ -321,12 +321,15 @@ pub enum PaymentAttemptUpdate { error_message: Option>, error_reason: Option>, amount_capturable: Option, + surcharge_amount: Option, + tax_amount: Option, updated_by: String, unified_code: Option>, unified_message: Option>, }, - MultipleCaptureCountUpdate { - multiple_capture_count: i16, + CaptureUpdate { + amount_to_capture: Option, + multiple_capture_count: Option, updated_by: String, }, AmountToCaptureUpdate { diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index f77e75491d86..82ab9a1c02e1 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -238,12 +238,15 @@ pub enum PaymentAttemptUpdate { error_message: Option>, error_reason: Option>, amount_capturable: Option, + surcharge_amount: Option, + tax_amount: Option, updated_by: String, unified_code: Option>, unified_message: Option>, }, - MultipleCaptureCountUpdate { - multiple_capture_count: i16, + CaptureUpdate { + amount_to_capture: Option, + multiple_capture_count: Option, updated_by: String, }, AmountToCaptureUpdate { @@ -535,6 +538,8 @@ impl From for PaymentAttemptUpdateInternal { error_message, error_reason, amount_capturable, + surcharge_amount, + tax_amount, updated_by, unified_code, unified_message, @@ -547,6 +552,8 @@ impl From for PaymentAttemptUpdateInternal { error_reason, amount_capturable, updated_by, + surcharge_amount, + tax_amount, unified_code, unified_message, ..Default::default() @@ -618,12 +625,14 @@ impl From for PaymentAttemptUpdateInternal { updated_by, ..Default::default() }, - PaymentAttemptUpdate::MultipleCaptureCountUpdate { + PaymentAttemptUpdate::CaptureUpdate { multiple_capture_count, updated_by, + amount_to_capture, } => Self { - multiple_capture_count: Some(multiple_capture_count), + multiple_capture_count, updated_by, + amount_to_capture, ..Default::default() }, PaymentAttemptUpdate::AmountToCaptureUpdate { diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 9c19d4eed8f6..8b20332ce5ed 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -24,7 +24,10 @@ use crate::{ payments::PaymentData, }, pii::PeekInterface, - types::{self, api, transformers::ForeignTryFrom, PaymentsCancelData, ResponseId}, + types::{ + self, api, storage::payment_attempt::PaymentAttemptExt, transformers::ForeignTryFrom, + PaymentsCancelData, ResponseId, + }, utils::{OptionExt, ValueExt}, }; @@ -108,11 +111,20 @@ where } } enums::AttemptStatus::Charged => { - let captured_amount = types::Capturable::get_capture_amount(&self.request); - if Some(payment_data.payment_intent.amount) == captured_amount { - enums::AttemptStatus::Charged + let captured_amount = if self.request.is_psync() { + payment_data + .payment_attempt + .amount_to_capture + .or(Some(payment_data.payment_attempt.get_total_amount())) } else { + types::Capturable::get_capture_amount(&self.request) + }; + if Some(payment_data.payment_attempt.get_total_amount()) == captured_amount { + enums::AttemptStatus::Charged + } else if captured_amount.is_some() { enums::AttemptStatus::PartialCharged + } else { + self.status } } _ => self.status, diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index ae729ff8fa25..c823fcd4937e 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -600,6 +600,29 @@ pub fn validate_request_amount_and_amount_to_capture( } } +/// if confirm = true and capture method = automatic, amount_to_capture(if provided) must be equal to amount +#[instrument(skip_all)] +pub fn validate_amount_to_capture_in_create_call_request( + request: &api_models::payments::PaymentsRequest, +) -> CustomResult<(), errors::ApiErrorResponse> { + if request.capture_method.unwrap_or_default() == api_enums::CaptureMethod::Automatic + && request.confirm.unwrap_or(false) + { + if let Some((amount_to_capture, amount)) = request.amount_to_capture.zip(request.amount) { + let amount_int: i64 = amount.into(); + utils::when(amount_to_capture != amount_int, || { + Err(report!(errors::ApiErrorResponse::PreconditionFailed { + message: "amount_to_capture must be equal to amount when confirm = true and capture_method = automatic".into() + })) + }) + } else { + Ok(()) + } + } else { + Ok(()) + } +} + #[instrument(skip_all)] pub fn validate_card_data( payment_method_data: Option, diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index 09e79064dc69..ef8e2b0153d4 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -251,20 +251,29 @@ impl where F: 'b + Send, { - payment_data.payment_attempt = match &payment_data.multiple_capture_data { - Some(multiple_capture_data) => db - .store + payment_data.payment_attempt = if payment_data.multiple_capture_data.is_some() + || payment_data.payment_attempt.amount_to_capture.is_some() + { + let multiple_capture_count = payment_data + .multiple_capture_data + .as_ref() + .map(|multiple_capture_data| multiple_capture_data.get_captures_count()) + .transpose()?; + let amount_to_capture = payment_data.payment_attempt.amount_to_capture; + db.store .update_payment_attempt_with_attempt_id( payment_data.payment_attempt, - storage::PaymentAttemptUpdate::MultipleCaptureCountUpdate { - multiple_capture_count: multiple_capture_data.get_captures_count()?, + storage::PaymentAttemptUpdate::CaptureUpdate { + amount_to_capture, + multiple_capture_count, updated_by: storage_scheme.to_string(), }, storage_scheme, ) .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?, - None => payment_data.payment_attempt, + .to_not_found_response(errors::ApiErrorResponse::InternalServerError)? + } else { + payment_data.payment_attempt }; Ok((Box::new(self), payment_data)) } diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 10c237cc8ab7..845915cc332c 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use api_models::{enums::FrmSuggestion, payment_methods}; +use api_models::enums::FrmSuggestion; use async_trait::async_trait; use common_utils::ext_traits::{AsyncExt, Encode, ValueExt}; use data_models::{mandates::MandateData, payments::payment_attempt::PaymentAttempt}; @@ -279,15 +279,7 @@ impl let setup_mandate = setup_mandate.map(MandateData::from); let surcharge_details = request.surcharge_details.map(|surcharge_details| { - payment_methods::SurchargeDetailsResponse { - surcharge: payment_methods::Surcharge::Fixed(surcharge_details.surcharge_amount), - tax_on_surcharge: None, - surcharge_amount: surcharge_details.surcharge_amount, - tax_on_surcharge_amount: surcharge_details.tax_amount.unwrap_or(0), - final_amount: payment_attempt.amount - + surcharge_details.surcharge_amount - + surcharge_details.tax_amount.unwrap_or(0), - } + surcharge_details.get_surcharge_details_object(payment_attempt.amount) }); let payment_data = PaymentData { @@ -546,6 +538,8 @@ impl ValidateRequest( } else { None }, + surcharge_amount: router_data.request.get_surcharge_amount(), + tax_amount: router_data.request.get_tax_on_surcharge_amount(), updated_by: storage_scheme.to_string(), unified_code: option_gsm.clone().map(|gsm| gsm.unified_code), unified_message: option_gsm.map(|gsm| gsm.unified_message), diff --git a/crates/router/src/core/payments/retry.rs b/crates/router/src/core/payments/retry.rs index 3c0106206e1d..f16f7629578b 100644 --- a/crates/router/src/core/payments/retry.rs +++ b/crates/router/src/core/payments/retry.rs @@ -410,6 +410,8 @@ where status: storage_enums::AttemptStatus::Failure, error_reason: Some(error_response.reason.clone()), amount_capturable: Some(0), + surcharge_amount: None, + tax_amount: None, updated_by: storage_scheme.to_string(), unified_code: option_gsm.clone().map(|gsm| gsm.unified_code), unified_message: option_gsm.map(|gsm| gsm.unified_message), diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index ceeb93f69763..203d4e30bf9a 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -545,7 +545,7 @@ pub struct AccessTokenRequestData { pub trait Capturable { fn get_capture_amount(&self) -> Option { - Some(0) + None } fn get_surcharge_amount(&self) -> Option { None @@ -553,6 +553,9 @@ pub trait Capturable { fn get_tax_on_surcharge_amount(&self) -> Option { None } + fn is_psync(&self) -> bool { + false + } } impl Capturable for PaymentsAuthorizeData { @@ -591,7 +594,11 @@ impl Capturable for PaymentsCancelData {} impl Capturable for PaymentsApproveData {} impl Capturable for PaymentsRejectData {} impl Capturable for PaymentsSessionData {} -impl Capturable for PaymentsSyncData {} +impl Capturable for PaymentsSyncData { + fn is_psync(&self) -> bool { + true + } +} pub struct AddAccessTokenResult { pub access_token_result: Result, ErrorResponse>, diff --git a/crates/router/src/workflows/payment_sync.rs b/crates/router/src/workflows/payment_sync.rs index 43e327559a0c..c4b35cd6301a 100644 --- a/crates/router/src/workflows/payment_sync.rs +++ b/crates/router/src/workflows/payment_sync.rs @@ -135,6 +135,8 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow { consts::REQUEST_TIMEOUT_ERROR_MESSAGE_FROM_PSYNC.to_string(), )), amount_capturable: Some(0), + surcharge_amount: None, + tax_amount: None, updated_by: merchant_account.storage_scheme.to_string(), unified_code: None, unified_message: None, diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index cb74c981ea71..238a2d75087c 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -1320,6 +1320,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_message, error_reason, amount_capturable, + tax_amount, + surcharge_amount, updated_by, unified_code, unified_message, @@ -1330,16 +1332,20 @@ impl DataModelExt for PaymentAttemptUpdate { error_message, error_reason, amount_capturable, + surcharge_amount, + tax_amount, updated_by, unified_code, unified_message, }, - Self::MultipleCaptureCountUpdate { + Self::CaptureUpdate { multiple_capture_count, updated_by, - } => DieselPaymentAttemptUpdate::MultipleCaptureCountUpdate { + amount_to_capture, + } => DieselPaymentAttemptUpdate::CaptureUpdate { multiple_capture_count, updated_by, + amount_to_capture, }, Self::PreprocessingUpdate { status, @@ -1577,6 +1583,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_message, error_reason, amount_capturable, + surcharge_amount, + tax_amount, updated_by, unified_code, unified_message, @@ -1588,13 +1596,17 @@ impl DataModelExt for PaymentAttemptUpdate { error_reason, amount_capturable, updated_by, + surcharge_amount, + tax_amount, unified_code, unified_message, }, - DieselPaymentAttemptUpdate::MultipleCaptureCountUpdate { + DieselPaymentAttemptUpdate::CaptureUpdate { + amount_to_capture, multiple_capture_count, updated_by, - } => Self::MultipleCaptureCountUpdate { + } => Self::CaptureUpdate { + amount_to_capture, multiple_capture_count, updated_by, }, diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json index fe57a7698926..550880583066 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json @@ -25,7 +25,7 @@ "business_label": "default", "capture_method": "automatic", "capture_on": "2022-09-10T10:11:12Z", - "amount_to_capture": 1, + "amount_to_capture": 6540, "customer_id": "bernard123", "email": "guest@example.com", "name": "John Doe", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Recurring Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Recurring Payments - Create/request.json index 90c966e10f1f..304d03350584 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Recurring Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Recurring Payments - Create/request.json @@ -23,7 +23,7 @@ "confirm": true, "capture_method": "automatic", "capture_on": "2022-09-10T10:11:12Z", - "amount_to_capture": 6540, + "amount_to_capture": 6570, "customer_id": "StripeCustomer", "email": "guest@example.com", "name": "John Doe", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Create/request.json index 150139b8e104..6542d21542da 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Create/request.json @@ -25,7 +25,7 @@ "business_label": "default", "capture_method": "automatic", "capture_on": "2022-09-10T10:11:12Z", - "amount_to_capture": 1, + "amount_to_capture": 6540, "customer_id": "bernard123", "email": "guest@example.com", "name": "John Doe", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/request.json index 21f054843897..e37391b78b5c 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/request.json @@ -25,7 +25,7 @@ "business_label": "default", "capture_method": "automatic", "capture_on": "2022-09-10T10:11:12Z", - "amount_to_capture": 1, + "amount_to_capture": 6540, "customer_id": "bernard123", "email": "guest@example.com", "name": "John Doe", From f8618e077065d94aa27d7153fc5ea6f93870bd81 Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Tue, 21 Nov 2023 20:25:50 +0530 Subject: [PATCH 053/146] feat: add support for 3ds and surcharge decision through routing rules (#2869) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- crates/api_models/src/conditional_configs.rs | 113 +++++++ crates/api_models/src/lib.rs | 2 + .../src/surcharge_decision_configs.rs | 77 +++++ crates/router/src/core.rs | 2 + crates/router/src/core/conditional_config.rs | 204 ++++++++++++ crates/router/src/core/errors.rs | 20 ++ crates/router/src/core/payment_methods.rs | 1 + .../router/src/core/payment_methods/cards.rs | 78 ++++- .../surcharge_decision_configs.rs | 301 ++++++++++++++++++ crates/router/src/core/payments.rs | 223 +++++++++++-- .../src/core/payments/conditional_configs.rs | 118 +++++++ .../conditional_configs/transformers.rs | 22 ++ crates/router/src/core/payments/helpers.rs | 101 ++++++ crates/router/src/core/payments/operations.rs | 10 + .../payments/operations/payment_confirm.rs | 50 +-- crates/router/src/core/payments/routing.rs | 58 ++++ .../src/core/surcharge_decision_config.rs | 190 +++++++++++ crates/router/src/core/utils.rs | 2 + crates/router/src/routes/app.rs | 14 + crates/router/src/routes/lock_utils.rs | 5 +- crates/router/src/routes/routing.rs | 168 +++++++++- .../src/types/storage/payment_attempt.rs | 10 +- crates/router_env/src/logger/types.rs | 6 + 23 files changed, 1717 insertions(+), 58 deletions(-) create mode 100644 crates/api_models/src/conditional_configs.rs create mode 100644 crates/api_models/src/surcharge_decision_configs.rs create mode 100644 crates/router/src/core/conditional_config.rs create mode 100644 crates/router/src/core/payment_methods/surcharge_decision_configs.rs create mode 100644 crates/router/src/core/payments/conditional_configs.rs create mode 100644 crates/router/src/core/payments/conditional_configs/transformers.rs create mode 100644 crates/router/src/core/surcharge_decision_config.rs diff --git a/crates/api_models/src/conditional_configs.rs b/crates/api_models/src/conditional_configs.rs new file mode 100644 index 000000000000..f8ed13421ac4 --- /dev/null +++ b/crates/api_models/src/conditional_configs.rs @@ -0,0 +1,113 @@ +use common_utils::events; +use euclid::{ + dssa::types::EuclidAnalysable, + enums, + frontend::{ + ast::Program, + dir::{DirKeyKind, DirValue, EuclidDirFilter}, + }, + types::Metadata, +}; +use serde::{Deserialize, Serialize}; + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + Serialize, + Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum AuthenticationType { + ThreeDs, + NoThreeDs, +} +impl AuthenticationType { + pub fn to_dir_value(&self) -> DirValue { + match self { + Self::ThreeDs => DirValue::AuthenticationType(enums::AuthenticationType::ThreeDs), + Self::NoThreeDs => DirValue::AuthenticationType(enums::AuthenticationType::NoThreeDs), + } + } +} + +impl EuclidAnalysable for AuthenticationType { + fn get_dir_value_for_analysis(&self, rule_name: String) -> Vec<(DirValue, Metadata)> { + let auth = self.to_string(); + + vec![( + self.to_dir_value(), + std::collections::HashMap::from_iter([( + "AUTHENTICATION_TYPE".to_string(), + serde_json::json!({ + "rule_name":rule_name, + "Authentication_type": auth, + }), + )]), + )] + } +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct ConditionalConfigs { + pub override_3ds: Option, +} +impl EuclidDirFilter for ConditionalConfigs { + const ALLOWED: &'static [DirKeyKind] = &[ + DirKeyKind::PaymentMethod, + DirKeyKind::CardType, + DirKeyKind::CardNetwork, + DirKeyKind::MetaData, + DirKeyKind::PaymentAmount, + DirKeyKind::PaymentCurrency, + DirKeyKind::CaptureMethod, + DirKeyKind::BillingCountry, + DirKeyKind::BusinessCountry, + ]; +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct DecisionManagerRecord { + pub name: String, + pub program: Program, + pub created_at: i64, + pub modified_at: i64, +} +impl events::ApiEventMetric for DecisionManagerRecord { + fn get_api_event_type(&self) -> Option { + Some(events::ApiEventsType::Routing) + } +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ConditionalConfigReq { + pub name: Option, + pub algorithm: Option>, +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] + +pub struct DecisionManagerRequest { + pub name: Option, + pub program: Option>, +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +pub enum DecisionManager { + DecisionManagerv0(ConditionalConfigReq), + DecisionManagerv1(DecisionManagerRequest), +} + +impl events::ApiEventMetric for DecisionManager { + fn get_api_event_type(&self) -> Option { + Some(events::ApiEventsType::Routing) + } +} + +pub type DecisionManagerResponse = DecisionManagerRecord; diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index 40faa6b3e81d..1abeff7b6ddb 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -4,6 +4,7 @@ pub mod analytics; pub mod api_keys; pub mod bank_accounts; pub mod cards_info; +pub mod conditional_configs; pub mod customers; pub mod disputes; pub mod enums; @@ -22,6 +23,7 @@ pub mod payments; pub mod payouts; pub mod refunds; pub mod routing; +pub mod surcharge_decision_configs; pub mod user; pub mod verifications; pub mod webhooks; diff --git a/crates/api_models/src/surcharge_decision_configs.rs b/crates/api_models/src/surcharge_decision_configs.rs new file mode 100644 index 000000000000..3ebf8f42744e --- /dev/null +++ b/crates/api_models/src/surcharge_decision_configs.rs @@ -0,0 +1,77 @@ +use common_utils::{consts::SURCHARGE_PERCENTAGE_PRECISION_LENGTH, events, types::Percentage}; +use euclid::frontend::{ + ast::Program, + dir::{DirKeyKind, EuclidDirFilter}, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct SurchargeDetails { + pub surcharge: Surcharge, + pub tax_on_surcharge: Option>, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case", tag = "type", content = "value")] +pub enum Surcharge { + Fixed(i64), + Rate(Percentage), +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct SurchargeDecisionConfigs { + pub surcharge_details: Option, +} +impl EuclidDirFilter for SurchargeDecisionConfigs { + const ALLOWED: &'static [DirKeyKind] = &[ + DirKeyKind::PaymentMethod, + DirKeyKind::MetaData, + DirKeyKind::PaymentAmount, + DirKeyKind::PaymentCurrency, + DirKeyKind::BillingCountry, + DirKeyKind::CardType, + DirKeyKind::CardNetwork, + DirKeyKind::PayLaterType, + DirKeyKind::WalletType, + DirKeyKind::BankTransferType, + DirKeyKind::BankRedirectType, + DirKeyKind::BankDebitType, + DirKeyKind::CryptoType, + ]; +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct SurchargeDecisionManagerRecord { + pub name: String, + pub merchant_surcharge_configs: MerchantSurchargeConfigs, + pub algorithm: Program, + pub created_at: i64, + pub modified_at: i64, +} + +impl events::ApiEventMetric for SurchargeDecisionManagerRecord { + fn get_api_event_type(&self) -> Option { + Some(events::ApiEventsType::Routing) + } +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SurchargeDecisionConfigReq { + pub name: Option, + pub merchant_surcharge_configs: MerchantSurchargeConfigs, + pub algorithm: Option>, +} + +impl events::ApiEventMetric for SurchargeDecisionConfigReq { + fn get_api_event_type(&self) -> Option { + Some(events::ApiEventsType::Routing) + } +} + +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +pub struct MerchantSurchargeConfigs { + pub show_surcharge_breakup_screen: Option, +} + +pub type SurchargeDecisionManagerResponse = SurchargeDecisionManagerRecord; diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index 8cc85eef60d6..a429cab482b4 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -3,6 +3,7 @@ pub mod api_keys; pub mod api_locking; pub mod cache; pub mod cards_info; +pub mod conditional_config; pub mod configs; pub mod customers; pub mod disputes; @@ -19,6 +20,7 @@ pub mod payments; pub mod payouts; pub mod refunds; pub mod routing; +pub mod surcharge_decision_config; #[cfg(feature = "olap")] pub mod user; pub mod utils; diff --git a/crates/router/src/core/conditional_config.rs b/crates/router/src/core/conditional_config.rs new file mode 100644 index 000000000000..e30d11ef6f2b --- /dev/null +++ b/crates/router/src/core/conditional_config.rs @@ -0,0 +1,204 @@ +use api_models::{ + conditional_configs::{DecisionManager, DecisionManagerRecord, DecisionManagerResponse}, + routing::{self}, +}; +use common_utils::ext_traits::{StringExt, ValueExt}; +use diesel_models::configs; +use error_stack::{IntoReport, ResultExt}; +use euclid::frontend::ast; + +use super::routing::helpers::{ + get_payment_config_routing_id, update_merchant_active_algorithm_ref, +}; +use crate::{ + core::errors::{self, RouterResponse}, + routes::AppState, + services::api as service_api, + types::domain, + utils::{self, OptionExt}, +}; + +pub async fn upsert_conditional_config( + state: AppState, + key_store: domain::MerchantKeyStore, + merchant_account: domain::MerchantAccount, + request: DecisionManager, +) -> RouterResponse { + let db = state.store.as_ref(); + let (name, prog) = match request { + DecisionManager::DecisionManagerv0(ccr) => { + let name = ccr.name; + + let prog = ccr + .algorithm + .get_required_value("algorithm") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "algorithm", + }) + .attach_printable("Algorithm for config not given")?; + (name, prog) + } + DecisionManager::DecisionManagerv1(dmr) => { + let name = dmr.name; + + let prog = dmr + .program + .get_required_value("program") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "program", + }) + .attach_printable("Program for config not given")?; + (name, prog) + } + }; + let timestamp = common_utils::date_time::now_unix_timestamp(); + let mut algo_id: routing::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|val| val.parse_value("routing algorithm")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode the routing algorithm")? + .unwrap_or_default(); + + let key = get_payment_config_routing_id(merchant_account.merchant_id.as_str()); + let read_config_key = db.find_config_by_key(&key).await; + + ast::lowering::lower_program(prog.clone()) + .into_report() + .change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid Request Data".to_string(), + }) + .attach_printable("The Request has an Invalid Comparison")?; + + match read_config_key { + Ok(config) => { + let previous_record: DecisionManagerRecord = config + .config + .parse_struct("DecisionManagerRecord") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("The Payment Config Key Not Found")?; + + let new_algo = DecisionManagerRecord { + name: previous_record.name, + program: prog, + modified_at: timestamp, + created_at: previous_record.created_at, + }; + + let serialize_updated_str = + utils::Encode::::encode_to_string_of_json(&new_algo) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to serialize config to string")?; + + let updated_config = configs::ConfigUpdate::Update { + config: Some(serialize_updated_str), + }; + + db.update_config_by_key(&key, updated_config) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error serializing the config")?; + + algo_id.update_conditional_config_id(key); + update_merchant_active_algorithm_ref(db, &key_store, algo_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update routing algorithm ref")?; + + Ok(service_api::ApplicationResponse::Json(new_algo)) + } + Err(e) if e.current_context().is_db_not_found() => { + let new_rec = DecisionManagerRecord { + name: name + .get_required_value("name") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "name", + }) + .attach_printable("name of the config not found")?, + program: prog, + modified_at: timestamp, + created_at: timestamp, + }; + + let serialized_str = + utils::Encode::::encode_to_string_of_json(&new_rec) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error serializing the config")?; + let new_config = configs::ConfigNew { + key: key.clone(), + config: serialized_str, + }; + + db.insert_config(new_config) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error fetching the config")?; + + algo_id.update_conditional_config_id(key); + update_merchant_active_algorithm_ref(db, &key_store, algo_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update routing algorithm ref")?; + + Ok(service_api::ApplicationResponse::Json(new_rec)) + } + Err(e) => Err(e) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error fetching payment config"), + } +} + +pub async fn delete_conditional_config( + state: AppState, + key_store: domain::MerchantKeyStore, + merchant_account: domain::MerchantAccount, +) -> RouterResponse<()> { + let db = state.store.as_ref(); + let key = get_payment_config_routing_id(&merchant_account.merchant_id); + let mut algo_id: routing::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|value| value.parse_value("routing algorithm")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode the conditional_config algorithm")? + .unwrap_or_default(); + algo_id.config_algo_id = None; + update_merchant_active_algorithm_ref(db, &key_store, algo_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update deleted algorithm ref")?; + + db.delete_config_by_key(&key) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to delete routing config from DB")?; + Ok(service_api::ApplicationResponse::StatusOk) +} + +pub async fn retrieve_conditional_config( + state: AppState, + merchant_account: domain::MerchantAccount, +) -> RouterResponse { + let db = state.store.as_ref(); + let algorithm_id = get_payment_config_routing_id(merchant_account.merchant_id.as_str()); + let algo_config = db + .find_config_by_key(&algorithm_id) + .await + .change_context(errors::ApiErrorResponse::ResourceIdNotFound) + .attach_printable("The conditional config was not found in the DB")?; + let record: DecisionManagerRecord = algo_config + .config + .parse_struct("ConditionalConfigRecord") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("The Conditional Config Record was not found")?; + + let response = DecisionManagerRecord { + name: record.name, + program: record.program, + created_at: record.created_at, + modified_at: record.modified_at, + }; + Ok(service_api::ApplicationResponse::Json(response)) +} diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index 03bb9a41b5b5..054f4053504e 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -375,3 +375,23 @@ pub enum RoutingError { #[error("Unable to parse metadata")] MetadataParsingError, } + +#[derive(Debug, Clone, thiserror::Error)] +pub enum ConditionalConfigError { + #[error("failed to fetch the fallback config for the merchant")] + FallbackConfigFetchFailed, + #[error("The lock on the DSL cache is most probably poisoned")] + DslCachePoisoned, + #[error("Merchant routing algorithm not found in cache")] + CacheMiss, + #[error("Expected DSL to be saved in DB but did not find")] + DslMissingInDb, + #[error("Unable to parse DSL from JSON")] + DslParsingError, + #[error("Failed to initialize DSL backend")] + DslBackendInitError, + #[error("Error executing the DSL")] + DslExecutionError, + #[error("Error constructing the Input")] + InputConstructionError, +} diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index 0628d301796e..80cec01e9166 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -1,4 +1,5 @@ pub mod cards; +pub mod surcharge_decision_configs; pub mod transformers; pub mod vault; diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 2fe3a75d80ee..9736edc73987 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -12,6 +12,7 @@ use api_models::{ ResponsePaymentMethodTypes, ResponsePaymentMethodsEnabled, }, payments::BankCodeResponse, + surcharge_decision_configs as api_surcharge_decision_configs, }; use common_utils::{ consts, @@ -23,6 +24,7 @@ use error_stack::{report, IntoReport, ResultExt}; use masking::Secret; use router_env::{instrument, tracing}; +use super::surcharge_decision_configs::perform_surcharge_decision_management_for_payment_method_list; use crate::{ configs::settings, core::{ @@ -35,6 +37,7 @@ use crate::{ helpers, routing::{self, SessionFlowRoutingInput}, }, + utils::persist_individual_surcharge_details_in_redis, }, db, logger, pii::prelude::*, @@ -1527,6 +1530,21 @@ pub async fn list_payment_methods( }); } + let merchant_surcharge_configs = + if let Some((attempt, payment_intent)) = payment_attempt.as_ref().zip(payment_intent) { + Box::pin(call_surcharge_decision_management( + state, + &merchant_account, + attempt, + payment_intent, + billing_address, + &mut payment_method_responses, + )) + .await? + } else { + api_surcharge_decision_configs::MerchantSurchargeConfigs::default() + }; + Ok(services::ApplicationResponse::Json( api::PaymentMethodListResponse { redirect_url: merchant_account.return_url, @@ -1558,11 +1576,69 @@ pub async fn list_payment_methods( } }, ), - show_surcharge_breakup_screen: false, + show_surcharge_breakup_screen: merchant_surcharge_configs + .show_surcharge_breakup_screen + .unwrap_or_default(), }, )) } +pub async fn call_surcharge_decision_management( + state: routes::AppState, + merchant_account: &domain::MerchantAccount, + payment_attempt: &storage::PaymentAttempt, + payment_intent: storage::PaymentIntent, + billing_address: Option, + response_payment_method_types: &mut [ResponsePaymentMethodsEnabled], +) -> errors::RouterResult { + if payment_attempt.surcharge_amount.is_some() { + Ok(api_surcharge_decision_configs::MerchantSurchargeConfigs::default()) + } else { + let algorithm_ref: routing_types::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|val| val.parse_value("routing algorithm")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode the routing algorithm")? + .unwrap_or_default(); + let (surcharge_results, merchant_sucharge_configs) = + perform_surcharge_decision_management_for_payment_method_list( + &state, + algorithm_ref, + payment_attempt, + &payment_intent, + billing_address.as_ref().map(Into::into), + response_payment_method_types, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error performing surcharge decision operation")?; + if !surcharge_results.is_empty_result() { + persist_individual_surcharge_details_in_redis( + &state, + merchant_account, + &surcharge_results, + ) + .await?; + let _ = state + .store + .update_payment_intent( + payment_intent, + storage::PaymentIntentUpdate::SurchargeApplicableUpdate { + surcharge_applicable: true, + updated_by: merchant_account.storage_scheme.to_string(), + }, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) + .attach_printable("Failed to update surcharge_applicable in Payment Intent"); + } + Ok(merchant_sucharge_configs) + } +} + #[allow(clippy::too_many_arguments)] pub async fn filter_payment_methods( payment_methods: Vec, diff --git a/crates/router/src/core/payment_methods/surcharge_decision_configs.rs b/crates/router/src/core/payment_methods/surcharge_decision_configs.rs new file mode 100644 index 000000000000..9a65ec76f2a5 --- /dev/null +++ b/crates/router/src/core/payment_methods/surcharge_decision_configs.rs @@ -0,0 +1,301 @@ +use api_models::{ + payment_methods::{self, SurchargeDetailsResponse, SurchargeMetadata}, + payments::Address, + routing, + surcharge_decision_configs::{ + self, SurchargeDecisionConfigs, SurchargeDecisionManagerRecord, SurchargeDetails, + }, +}; +use common_utils::{ext_traits::StringExt, static_cache::StaticCache}; +use error_stack::{self, IntoReport, ResultExt}; +use euclid::{ + backend, + backend::{inputs as dsl_inputs, EuclidBackend}, +}; +use router_env::{instrument, tracing}; + +use crate::{core::payments::PaymentData, db::StorageInterface, types::storage as oss_storage}; +static CONF_CACHE: StaticCache = StaticCache::new(); +use crate::{ + core::{ + errors::ConditionalConfigError as ConfigError, + payments::{ + conditional_configs::ConditionalConfigResult, routing::make_dsl_input_for_surcharge, + }, + }, + AppState, +}; + +struct VirInterpreterBackendCacheWrapper { + cached_alogorith: backend::VirInterpreterBackend, + merchant_surcharge_configs: surcharge_decision_configs::MerchantSurchargeConfigs, +} + +impl TryFrom for VirInterpreterBackendCacheWrapper { + type Error = error_stack::Report; + + fn try_from(value: SurchargeDecisionManagerRecord) -> Result { + let cached_alogorith = backend::VirInterpreterBackend::with_program(value.algorithm) + .into_report() + .change_context(ConfigError::DslBackendInitError) + .attach_printable("Error initializing DSL interpreter backend")?; + let merchant_surcharge_configs = value.merchant_surcharge_configs; + Ok(Self { + cached_alogorith, + merchant_surcharge_configs, + }) + } +} + +pub async fn perform_surcharge_decision_management_for_payment_method_list( + state: &AppState, + algorithm_ref: routing::RoutingAlgorithmRef, + payment_attempt: &oss_storage::PaymentAttempt, + payment_intent: &oss_storage::PaymentIntent, + billing_address: Option
, + response_payment_method_types: &mut [api_models::payment_methods::ResponsePaymentMethodsEnabled], +) -> ConditionalConfigResult<( + SurchargeMetadata, + surcharge_decision_configs::MerchantSurchargeConfigs, +)> { + let mut surcharge_metadata = SurchargeMetadata::new(payment_attempt.attempt_id.clone()); + let algorithm_id = if let Some(id) = algorithm_ref.surcharge_config_algo_id { + id + } else { + return Ok(( + surcharge_metadata, + surcharge_decision_configs::MerchantSurchargeConfigs::default(), + )); + }; + + let key = ensure_algorithm_cached( + &*state.store, + &payment_attempt.merchant_id, + algorithm_ref.timestamp, + algorithm_id.as_str(), + ) + .await?; + let cached_algo = CONF_CACHE + .retrieve(&key) + .into_report() + .change_context(ConfigError::CacheMiss) + .attach_printable("Unable to retrieve cached routing algorithm even after refresh")?; + let mut backend_input = + make_dsl_input_for_surcharge(payment_attempt, payment_intent, billing_address) + .change_context(ConfigError::InputConstructionError)?; + let interpreter = &cached_algo.cached_alogorith; + let merchant_surcharge_configs = cached_algo.merchant_surcharge_configs.clone(); + + for payment_methods_enabled in response_payment_method_types.iter_mut() { + for payment_method_type_response in + &mut payment_methods_enabled.payment_method_types.iter_mut() + { + let payment_method_type = payment_method_type_response.payment_method_type; + backend_input.payment_method.payment_method_type = Some(payment_method_type); + backend_input.payment_method.payment_method = + Some(payment_methods_enabled.payment_method); + + if let Some(card_network_list) = &mut payment_method_type_response.card_networks { + for card_network_type in card_network_list.iter_mut() { + backend_input.payment_method.card_network = + Some(card_network_type.card_network.clone()); + let surcharge_output = + execute_dsl_and_get_conditional_config(backend_input.clone(), interpreter)?; + card_network_type.surcharge_details = surcharge_output + .surcharge_details + .map(|surcharge_details| { + get_surcharge_details_response(surcharge_details, payment_attempt).map( + |surcharge_details_response| { + surcharge_metadata.insert_surcharge_details( + &payment_methods_enabled.payment_method, + &payment_method_type_response.payment_method_type, + Some(&card_network_type.card_network), + surcharge_details_response.clone(), + ); + surcharge_details_response + }, + ) + }) + .transpose()?; + } + } else { + let surcharge_output = + execute_dsl_and_get_conditional_config(backend_input.clone(), interpreter)?; + payment_method_type_response.surcharge_details = surcharge_output + .surcharge_details + .map(|surcharge_details| { + get_surcharge_details_response(surcharge_details, payment_attempt).map( + |surcharge_details_response| { + surcharge_metadata.insert_surcharge_details( + &payment_methods_enabled.payment_method, + &payment_method_type_response.payment_method_type, + None, + surcharge_details_response.clone(), + ); + surcharge_details_response + }, + ) + }) + .transpose()?; + } + } + } + Ok((surcharge_metadata, merchant_surcharge_configs)) +} + +pub async fn perform_surcharge_decision_management_for_session_flow( + state: &AppState, + algorithm_ref: routing::RoutingAlgorithmRef, + payment_data: &mut PaymentData, + payment_method_type_list: &Vec, +) -> ConditionalConfigResult +where + O: Send + Clone, +{ + let mut surcharge_metadata = + SurchargeMetadata::new(payment_data.payment_attempt.attempt_id.clone()); + let algorithm_id = if let Some(id) = algorithm_ref.surcharge_config_algo_id { + id + } else { + return Ok(surcharge_metadata); + }; + + let key = ensure_algorithm_cached( + &*state.store, + &payment_data.payment_attempt.merchant_id, + algorithm_ref.timestamp, + algorithm_id.as_str(), + ) + .await?; + let cached_algo = CONF_CACHE + .retrieve(&key) + .into_report() + .change_context(ConfigError::CacheMiss) + .attach_printable("Unable to retrieve cached routing algorithm even after refresh")?; + let mut backend_input = make_dsl_input_for_surcharge( + &payment_data.payment_attempt, + &payment_data.payment_intent, + payment_data.address.billing.clone(), + ) + .change_context(ConfigError::InputConstructionError)?; + let interpreter = &cached_algo.cached_alogorith; + for payment_method_type in payment_method_type_list { + backend_input.payment_method.payment_method_type = Some(*payment_method_type); + // in case of session flow, payment_method will always be wallet + backend_input.payment_method.payment_method = Some(payment_method_type.to_owned().into()); + let surcharge_output = + execute_dsl_and_get_conditional_config(backend_input.clone(), interpreter)?; + if let Some(surcharge_details) = surcharge_output.surcharge_details { + let surcharge_details_response = + get_surcharge_details_response(surcharge_details, &payment_data.payment_attempt)?; + surcharge_metadata.insert_surcharge_details( + &payment_method_type.to_owned().into(), + payment_method_type, + None, + surcharge_details_response, + ); + } + } + Ok(surcharge_metadata) +} + +fn get_surcharge_details_response( + surcharge_details: SurchargeDetails, + payment_attempt: &oss_storage::PaymentAttempt, +) -> ConditionalConfigResult { + let surcharge_amount = match surcharge_details.surcharge.clone() { + surcharge_decision_configs::Surcharge::Fixed(value) => value, + surcharge_decision_configs::Surcharge::Rate(percentage) => percentage + .apply_and_ceil_result(payment_attempt.amount) + .change_context(ConfigError::DslExecutionError) + .attach_printable("Failed to Calculate surcharge amount by applying percentage")?, + }; + let tax_on_surcharge_amount = surcharge_details + .tax_on_surcharge + .clone() + .map(|tax_on_surcharge| { + tax_on_surcharge + .apply_and_ceil_result(surcharge_amount) + .change_context(ConfigError::DslExecutionError) + .attach_printable("Failed to Calculate tax amount") + }) + .transpose()? + .unwrap_or(0); + Ok(SurchargeDetailsResponse { + surcharge: match surcharge_details.surcharge { + surcharge_decision_configs::Surcharge::Fixed(surcharge_amount) => { + payment_methods::Surcharge::Fixed(surcharge_amount) + } + surcharge_decision_configs::Surcharge::Rate(percentage) => { + payment_methods::Surcharge::Rate(percentage) + } + }, + tax_on_surcharge: surcharge_details.tax_on_surcharge, + surcharge_amount, + tax_on_surcharge_amount, + final_amount: payment_attempt.amount + surcharge_amount + tax_on_surcharge_amount, + }) +} + +#[instrument(skip_all)] +pub async fn ensure_algorithm_cached( + store: &dyn StorageInterface, + merchant_id: &str, + timestamp: i64, + algorithm_id: &str, +) -> ConditionalConfigResult { + let key = format!("surcharge_dsl_{merchant_id}"); + let present = CONF_CACHE + .present(&key) + .into_report() + .change_context(ConfigError::DslCachePoisoned) + .attach_printable("Error checking presence of DSL")?; + let expired = CONF_CACHE + .expired(&key, timestamp) + .into_report() + .change_context(ConfigError::DslCachePoisoned) + .attach_printable("Error checking presence of DSL")?; + + if !present || expired { + refresh_surcharge_algorithm_cache(store, key.clone(), algorithm_id, timestamp).await? + } + Ok(key) +} + +#[instrument(skip_all)] +pub async fn refresh_surcharge_algorithm_cache( + store: &dyn StorageInterface, + key: String, + algorithm_id: &str, + timestamp: i64, +) -> ConditionalConfigResult<()> { + let config = store + .find_config_by_key(algorithm_id) + .await + .change_context(ConfigError::DslMissingInDb) + .attach_printable("Error parsing DSL from config")?; + let record: SurchargeDecisionManagerRecord = config + .config + .parse_struct("Program") + .change_context(ConfigError::DslParsingError) + .attach_printable("Error parsing routing algorithm from configs")?; + let value_to_cache = VirInterpreterBackendCacheWrapper::try_from(record)?; + CONF_CACHE + .save(key, value_to_cache, timestamp) + .into_report() + .change_context(ConfigError::DslCachePoisoned) + .attach_printable("Error saving DSL to cache")?; + Ok(()) +} + +pub fn execute_dsl_and_get_conditional_config( + backend_input: dsl_inputs::BackendInput, + interpreter: &backend::VirInterpreterBackend, +) -> ConditionalConfigResult { + let routing_output = interpreter + .execute(backend_input) + .map(|out| out.connector_selection) + .into_report() + .change_context(ConfigError::DslExecutionError)?; + Ok(routing_output) +} diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 0259c48ee827..8c13b05836f1 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1,4 +1,5 @@ pub mod access_token; +pub mod conditional_configs; pub mod customers; pub mod flows; pub mod helpers; @@ -13,9 +14,9 @@ pub mod types; use std::{fmt::Debug, marker::PhantomData, ops::Deref, time::Instant, vec::IntoIter}; use api_models::{ - enums, + self, enums, payment_methods::{Surcharge, SurchargeDetailsResponse}, - payments::HeaderPayload, + payments::{self, HeaderPayload}, }; use common_utils::{ext_traits::AsyncExt, pii}; use data_models::mandates::MandateData; @@ -24,6 +25,7 @@ use error_stack::{IntoReport, ResultExt}; use futures::future::join_all; use helpers::ApplePayData; use masking::Secret; +use redis_interface::errors::RedisError; use router_env::{instrument, tracing}; #[cfg(feature = "olap")] use router_types::transformers::ForeignFrom; @@ -35,11 +37,15 @@ pub use self::operations::{ PaymentResponse, PaymentSession, PaymentStatus, PaymentUpdate, }; use self::{ + conditional_configs::perform_decision_management, flows::{ConstructFlowSpecificData, Feature}, + helpers::get_key_params_for_surcharge_details, operations::{payment_complete_authorize, BoxedOperation, Operation}, routing::{self as self_routing, SessionFlowRoutingInput}, }; -use super::errors::StorageErrorExt; +use super::{ + errors::StorageErrorExt, payment_methods::surcharge_decision_configs, utils as core_utils, +}; use crate::{ configs::settings::PaymentMethodTypeTokenFilter, core::{ @@ -55,8 +61,8 @@ use crate::{ self as router_types, api::{self, ConnectorCallType}, domain, - storage::{self, enums as storage_enums}, - transformers::ForeignTryInto, + storage::{self, enums as storage_enums, payment_attempt::PaymentAttemptExt}, + transformers::{ForeignInto, ForeignTryInto}, }, utils::{ add_apple_pay_flow_metrics, add_connector_http_status_code_metrics, Encode, OptionExt, @@ -141,6 +147,8 @@ where .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound) .attach_printable("Failed while fetching/creating customer")?; + call_decision_manager(state, &merchant_account, &mut payment_data).await?; + let connector = get_connector_choice( &operation, state, @@ -167,6 +175,10 @@ where let mut connector_http_status_code = None; let mut external_latency = None; if let Some(connector_details) = connector { + operation + .to_domain()? + .populate_payment_data(state, &mut payment_data, &req, &merchant_account) + .await?; payment_data = match connector_details { api::ConnectorCallType::PreDetermined(connector) => { let schedule_time = if should_add_task_to_process_tracker { @@ -294,8 +306,14 @@ where } api::ConnectorCallType::SessionMultiple(connectors) => { - let session_surcharge_data = - get_session_surcharge_data(&payment_data.payment_attempt); + let session_surcharge_details = + call_surcharge_decision_management_for_session_flow( + state, + &merchant_account, + &mut payment_data, + &connectors, + ) + .await?; call_multiple_connectors_service( state, &merchant_account, @@ -304,7 +322,7 @@ where &operation, payment_data, &customer, - session_surcharge_data, + session_surcharge_details, ) .await? } @@ -348,6 +366,123 @@ where )) } +#[instrument(skip_all)] +pub async fn call_decision_manager( + state: &AppState, + merchant_account: &domain::MerchantAccount, + payment_data: &mut PaymentData, +) -> RouterResult<()> +where + O: Send + Clone, +{ + let algorithm_ref: api::routing::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|val| val.parse_value("routing algorithm")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode the routing algorithm")? + .unwrap_or_default(); + + let output = perform_decision_management( + state, + algorithm_ref, + merchant_account.merchant_id.as_str(), + payment_data, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode the conditional config")?; + payment_data.payment_attempt.authentication_type = payment_data + .payment_attempt + .authentication_type + .or(output.override_3ds.map(ForeignInto::foreign_into)) + .or(Some(storage_enums::AuthenticationType::NoThreeDs)); + Ok(()) +} + +#[instrument(skip_all)] +async fn populate_surcharge_details( + state: &AppState, + payment_data: &mut PaymentData, + request: &payments::PaymentsRequest, +) -> RouterResult<()> +where + F: Send + Clone, +{ + if payment_data + .payment_intent + .surcharge_applicable + .unwrap_or(false) + { + let payment_method_data = request + .payment_method_data + .clone() + .get_required_value("payment_method_data")?; + let (payment_method, payment_method_type, card_network) = + get_key_params_for_surcharge_details(payment_method_data)?; + + let calculated_surcharge_details = match utils::get_individual_surcharge_detail_from_redis( + state, + &payment_method, + &payment_method_type, + card_network, + &payment_data.payment_attempt.attempt_id, + ) + .await + { + Ok(surcharge_details) => Some(surcharge_details), + Err(err) if err.current_context() == &RedisError::NotFound => None, + Err(err) => Err(err).change_context(errors::ApiErrorResponse::InternalServerError)?, + }; + + let request_surcharge_details = request.surcharge_details; + + match (request_surcharge_details, calculated_surcharge_details) { + (Some(request_surcharge_details), Some(calculated_surcharge_details)) => { + if calculated_surcharge_details + .is_request_surcharge_matching(request_surcharge_details) + { + payment_data.surcharge_details = Some(calculated_surcharge_details); + } else { + return Err(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid value provided: 'surcharge_details'. surcharge details provided do not match with surcharge details sent in payment_methods list response".to_string(), + } + .into()); + } + } + (None, Some(_calculated_surcharge_details)) => { + return Err(errors::ApiErrorResponse::MissingRequiredField { + field_name: "surcharge_details", + } + .into()); + } + (Some(request_surcharge_details), None) => { + if request_surcharge_details.is_surcharge_zero() { + return Ok(()); + } else { + return Err(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid value provided: 'surcharge_details'. surcharge details provided do not match with surcharge details sent in payment_methods list response".to_string(), + } + .into()); + } + } + (None, None) => return Ok(()), + }; + } else { + let surcharge_details = + payment_data + .payment_attempt + .get_surcharge_details() + .map(|surcharge_details| { + surcharge_details + .get_surcharge_details_object(payment_data.payment_attempt.amount) + }); + payment_data.surcharge_details = surcharge_details; + } + Ok(()) +} + #[inline] pub fn get_connector_data( connectors: &mut IntoIter, @@ -359,20 +494,66 @@ pub fn get_connector_data( .attach_printable("Connector not found in connectors iterator") } -pub fn get_session_surcharge_data( - payment_attempt: &data_models::payments::payment_attempt::PaymentAttempt, -) -> Option { - payment_attempt.surcharge_amount.map(|surcharge_amount| { - let tax_on_surcharge_amount = payment_attempt.tax_amount.unwrap_or(0); - let final_amount = payment_attempt.amount + surcharge_amount + tax_on_surcharge_amount; - api::SessionSurchargeDetails::PreDetermined(SurchargeDetailsResponse { - surcharge: Surcharge::Fixed(surcharge_amount), - tax_on_surcharge: None, - surcharge_amount, - tax_on_surcharge_amount, - final_amount, +#[instrument(skip_all)] +pub async fn call_surcharge_decision_management_for_session_flow( + state: &AppState, + merchant_account: &domain::MerchantAccount, + payment_data: &mut PaymentData, + session_connector_data: &[api::SessionConnectorData], +) -> RouterResult> +where + O: Send + Clone + Sync, +{ + if let Some(surcharge_amount) = payment_data.payment_attempt.surcharge_amount { + let tax_on_surcharge_amount = payment_data.payment_attempt.tax_amount.unwrap_or(0); + let final_amount = + payment_data.payment_attempt.amount + surcharge_amount + tax_on_surcharge_amount; + Ok(Some(api::SessionSurchargeDetails::PreDetermined( + SurchargeDetailsResponse { + surcharge: Surcharge::Fixed(surcharge_amount), + tax_on_surcharge: None, + surcharge_amount, + tax_on_surcharge_amount, + final_amount, + }, + ))) + } else { + let payment_method_type_list = session_connector_data + .iter() + .map(|session_connector_data| session_connector_data.payment_method_type) + .collect(); + let algorithm_ref: api::routing::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|val| val.parse_value("routing algorithm")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode the routing algorithm")? + .unwrap_or_default(); + let surcharge_results = + surcharge_decision_configs::perform_surcharge_decision_management_for_session_flow( + state, + algorithm_ref, + payment_data, + &payment_method_type_list, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error performing surcharge decision operation")?; + + core_utils::persist_individual_surcharge_details_in_redis( + state, + merchant_account, + &surcharge_results, + ) + .await?; + + Ok(if surcharge_results.is_empty_result() { + None + } else { + Some(api::SessionSurchargeDetails::Calculated(surcharge_results)) }) - }) + } } #[allow(clippy::too_many_arguments)] pub async fn payments_core( diff --git a/crates/router/src/core/payments/conditional_configs.rs b/crates/router/src/core/payments/conditional_configs.rs new file mode 100644 index 000000000000..bf1f43e2b0f9 --- /dev/null +++ b/crates/router/src/core/payments/conditional_configs.rs @@ -0,0 +1,118 @@ +mod transformers; + +use api_models::{ + conditional_configs::{ConditionalConfigs, DecisionManagerRecord}, + routing, +}; +use common_utils::{ext_traits::StringExt, static_cache::StaticCache}; +use error_stack::{IntoReport, ResultExt}; +use euclid::backend::{self, inputs as dsl_inputs, EuclidBackend}; +use router_env::{instrument, tracing}; + +use super::routing::make_dsl_input; +use crate::{ + core::{errors, errors::ConditionalConfigError as ConfigError, payments}, + routes, +}; + +static CONF_CACHE: StaticCache> = + StaticCache::new(); +pub type ConditionalConfigResult = errors::CustomResult; + +#[instrument(skip_all)] +pub async fn perform_decision_management( + state: &routes::AppState, + algorithm_ref: routing::RoutingAlgorithmRef, + merchant_id: &str, + payment_data: &mut payments::PaymentData, +) -> ConditionalConfigResult { + let algorithm_id = if let Some(id) = algorithm_ref.config_algo_id { + id + } else { + return Ok(ConditionalConfigs::default()); + }; + + let key = ensure_algorithm_cached( + state, + merchant_id, + algorithm_ref.timestamp, + algorithm_id.as_str(), + ) + .await?; + let cached_algo = CONF_CACHE + .retrieve(&key) + .into_report() + .change_context(ConfigError::CacheMiss) + .attach_printable("Unable to retrieve cached routing algorithm even after refresh")?; + let backend_input = + make_dsl_input(payment_data).change_context(ConfigError::InputConstructionError)?; + let interpreter = cached_algo.as_ref(); + execute_dsl_and_get_conditional_config(backend_input, interpreter).await +} + +#[instrument(skip_all)] +pub async fn ensure_algorithm_cached( + state: &routes::AppState, + merchant_id: &str, + timestamp: i64, + algorithm_id: &str, +) -> ConditionalConfigResult { + let key = format!("dsl_{merchant_id}"); + let present = CONF_CACHE + .present(&key) + .into_report() + .change_context(ConfigError::DslCachePoisoned) + .attach_printable("Error checking presece of DSL")?; + let expired = CONF_CACHE + .expired(&key, timestamp) + .into_report() + .change_context(ConfigError::DslCachePoisoned) + .attach_printable("Error checking presence of DSL")?; + if !present || expired { + refresh_routing_cache(state, key.clone(), algorithm_id, timestamp).await?; + }; + Ok(key) +} + +#[instrument(skip_all)] +pub async fn refresh_routing_cache( + state: &routes::AppState, + key: String, + algorithm_id: &str, + timestamp: i64, +) -> ConditionalConfigResult<()> { + let config = state + .store + .find_config_by_key(algorithm_id) + .await + .change_context(ConfigError::DslMissingInDb) + .attach_printable("Error parsing DSL from config")?; + let rec: DecisionManagerRecord = config + .config + .parse_struct("Program") + .change_context(ConfigError::DslParsingError) + .attach_printable("Error parsing routing algorithm from configs")?; + let interpreter: backend::VirInterpreterBackend = + backend::VirInterpreterBackend::with_program(rec.program) + .into_report() + .change_context(ConfigError::DslBackendInitError) + .attach_printable("Error initializing DSL interpreter backend")?; + CONF_CACHE + .save(key, interpreter, timestamp) + .into_report() + .change_context(ConfigError::DslCachePoisoned) + .attach_printable("Error saving DSL to cache")?; + Ok(()) +} + +pub async fn execute_dsl_and_get_conditional_config( + backend_input: dsl_inputs::BackendInput, + interpreter: &backend::VirInterpreterBackend, +) -> ConditionalConfigResult { + let routing_output = interpreter + .execute(backend_input) + .map(|out| out.connector_selection) + .into_report() + .change_context(ConfigError::DslExecutionError)?; + Ok(routing_output) +} diff --git a/crates/router/src/core/payments/conditional_configs/transformers.rs b/crates/router/src/core/payments/conditional_configs/transformers.rs new file mode 100644 index 000000000000..023bd65dcf41 --- /dev/null +++ b/crates/router/src/core/payments/conditional_configs/transformers.rs @@ -0,0 +1,22 @@ +use api_models::{self, conditional_configs}; +use diesel_models::enums as storage_enums; +use euclid::enums as dsl_enums; + +use crate::types::transformers::ForeignFrom; +impl ForeignFrom for conditional_configs::AuthenticationType { + fn foreign_from(from: dsl_enums::AuthenticationType) -> Self { + match from { + dsl_enums::AuthenticationType::ThreeDs => Self::ThreeDs, + dsl_enums::AuthenticationType::NoThreeDs => Self::NoThreeDs, + } + } +} + +impl ForeignFrom for storage_enums::AuthenticationType { + fn foreign_from(from: conditional_configs::AuthenticationType) -> Self { + match from { + conditional_configs::AuthenticationType::ThreeDs => Self::ThreeDs, + conditional_configs::AuthenticationType::NoThreeDs => Self::NoThreeDs, + } + } +} diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index c823fcd4937e..4d8daa1fe69d 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1,5 +1,6 @@ use std::borrow::Cow; +use api_models::payments::GetPaymentMethodType; use base64::Engine; use common_utils::{ ext_traits::{AsyncExt, ByteSliceExt, ValueExt}, @@ -3516,6 +3517,106 @@ impl ApplePayData { } } +pub fn get_key_params_for_surcharge_details( + payment_method_data: api_models::payments::PaymentMethodData, +) -> RouterResult<( + common_enums::PaymentMethod, + common_enums::PaymentMethodType, + Option, +)> { + match payment_method_data { + api_models::payments::PaymentMethodData::Card(card) => { + let card_type = card + .card_type + .get_required_value("payment_method_data.card.card_type")?; + let card_network = card + .card_network + .get_required_value("payment_method_data.card.card_network")?; + match card_type.to_lowercase().as_str() { + "credit" => Ok(( + common_enums::PaymentMethod::Card, + common_enums::PaymentMethodType::Credit, + Some(card_network), + )), + "debit" => Ok(( + common_enums::PaymentMethod::Card, + common_enums::PaymentMethodType::Debit, + Some(card_network), + )), + _ => { + logger::debug!("Invalid Card type found in payment confirm call, hence surcharge not applicable"); + Err(errors::ApiErrorResponse::InvalidDataValue { + field_name: "payment_method_data.card.card_type", + } + .into()) + } + } + } + api_models::payments::PaymentMethodData::CardRedirect(card_redirect_data) => Ok(( + common_enums::PaymentMethod::CardRedirect, + card_redirect_data.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::Wallet(wallet) => Ok(( + common_enums::PaymentMethod::Wallet, + wallet.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::PayLater(pay_later) => Ok(( + common_enums::PaymentMethod::PayLater, + pay_later.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::BankRedirect(bank_redirect) => Ok(( + common_enums::PaymentMethod::BankRedirect, + bank_redirect.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::BankDebit(bank_debit) => Ok(( + common_enums::PaymentMethod::BankDebit, + bank_debit.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::BankTransfer(bank_transfer) => Ok(( + common_enums::PaymentMethod::BankTransfer, + bank_transfer.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::Crypto(crypto) => Ok(( + common_enums::PaymentMethod::Crypto, + crypto.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::MandatePayment => { + Err(errors::ApiErrorResponse::InvalidDataValue { + field_name: "payment_method_data", + } + .into()) + } + api_models::payments::PaymentMethodData::Reward => { + Err(errors::ApiErrorResponse::InvalidDataValue { + field_name: "payment_method_data", + } + .into()) + } + api_models::payments::PaymentMethodData::Upi(_) => Ok(( + common_enums::PaymentMethod::Upi, + common_enums::PaymentMethodType::UpiCollect, + None, + )), + api_models::payments::PaymentMethodData::Voucher(voucher) => Ok(( + common_enums::PaymentMethod::Voucher, + voucher.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::GiftCard(gift_card) => Ok(( + common_enums::PaymentMethod::GiftCard, + gift_card.get_payment_method_type(), + None, + )), + } +} + pub fn validate_payment_link_request( payment_link_object: &api_models::payments::PaymentLinkObject, confirm: Option, diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index 6f01c653084f..809c9e925de0 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -152,6 +152,16 @@ pub trait Domain: Send + Sync { payment_intent: &storage::PaymentIntent, mechant_key_store: &domain::MerchantKeyStore, ) -> CustomResult; + + async fn populate_payment_data<'a>( + &'a self, + _state: &AppState, + _payment_data: &mut PaymentData, + _request: &R, + _merchant_account: &domain::MerchantAccount, + ) -> CustomResult<(), errors::ApiErrorResponse> { + Ok(()) + } } #[async_trait] diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 8b4f91b63a2e..e85531050529 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -1,9 +1,6 @@ use std::marker::PhantomData; -use api_models::{ - enums::FrmSuggestion, - payment_methods::{self, SurchargeDetailsResponse}, -}; +use api_models::enums::FrmSuggestion; use async_trait::async_trait; use common_utils::ext_traits::{AsyncExt, Encode}; use error_stack::{report, IntoReport, ResultExt}; @@ -18,7 +15,10 @@ use crate::{ core::{ errors::{self, CustomResult, RouterResult, StorageErrorExt}, payment_methods::PaymentMethodRetrieve, - payments::{self, helpers, operations, CustomerDetails, PaymentAddress, PaymentData}, + payments::{ + self, helpers, operations, populate_surcharge_details, CustomerDetails, PaymentAddress, + PaymentData, + }, utils::{self as core_utils, get_individual_surcharge_detail_from_redis}, }, db::StorageInterface, @@ -430,11 +430,6 @@ impl ) .await?; - let surcharge_details = Self::get_surcharge_details_from_payment_request_or_payment_attempt( - request, - &payment_attempt, - ); - let payment_data = PaymentData { flow: PhantomData, payment_intent, @@ -465,7 +460,7 @@ impl ephemeral_key: None, multiple_capture_data: None, redirect_response: None, - surcharge_details, + surcharge_details: None, frm_message: None, payment_link_data: None, }; @@ -574,6 +569,17 @@ impl Domain( + &'a self, + state: &AppState, + payment_data: &mut PaymentData, + request: &api::PaymentsRequest, + _merchant_account: &domain::MerchantAccount, + ) -> CustomResult<(), errors::ApiErrorResponse> { + populate_surcharge_details(state, payment_data, request).await + } } #[async_trait] @@ -921,26 +927,4 @@ impl PaymentConfirm { _ => Ok(()), } } - - fn get_surcharge_details_from_payment_request_or_payment_attempt( - payment_request: &api::PaymentsRequest, - payment_attempt: &storage::PaymentAttempt, - ) -> Option { - payment_request - .surcharge_details - .map(|surcharge_details| { - surcharge_details.get_surcharge_details_object(payment_attempt.amount) - }) // if not passed in confirm request, look inside payment_attempt - .or(payment_attempt - .surcharge_amount - .map(|surcharge_amount| SurchargeDetailsResponse { - surcharge: payment_methods::Surcharge::Fixed(surcharge_amount), - tax_on_surcharge: None, - surcharge_amount, - tax_on_surcharge_amount: payment_attempt.tax_amount.unwrap_or(0), - final_amount: payment_attempt.amount - + surcharge_amount - + payment_attempt.tax_amount.unwrap_or(0), - })) - } } diff --git a/crates/router/src/core/payments/routing.rs b/crates/router/src/core/payments/routing.rs index 3b89d4e38e4e..841b48b9444a 100644 --- a/crates/router/src/core/payments/routing.rs +++ b/crates/router/src/core/payments/routing.rs @@ -9,6 +9,7 @@ use std::{ use api_models::{ admin as admin_api, enums::{self as api_enums, CountryAlpha2}, + payments::Address, routing::ConnectorSelection, }; use common_utils::static_cache::StaticCache; @@ -996,3 +997,60 @@ async fn perform_session_routing_for_pm_type( Ok(final_choice) } + +pub fn make_dsl_input_for_surcharge( + payment_attempt: &oss_storage::PaymentAttempt, + payment_intent: &oss_storage::PaymentIntent, + billing_address: Option
, +) -> RoutingResult { + let mandate_data = dsl_inputs::MandateData { + mandate_acceptance_type: None, + mandate_type: None, + payment_type: None, + }; + let payment_input = dsl_inputs::PaymentInput { + amount: payment_attempt.amount, + // currency is always populated in payment_attempt during payment create + currency: payment_attempt + .currency + .get_required_value("currency") + .change_context(errors::RoutingError::DslMissingRequiredField { + field_name: "currency".to_string(), + })?, + authentication_type: payment_attempt.authentication_type, + card_bin: None, + capture_method: payment_attempt.capture_method, + business_country: payment_intent + .business_country + .map(api_enums::Country::from_alpha2), + billing_country: billing_address + .and_then(|bic| bic.address) + .and_then(|add| add.country) + .map(api_enums::Country::from_alpha2), + business_label: payment_intent.business_label.clone(), + setup_future_usage: payment_intent.setup_future_usage, + }; + let metadata = payment_intent + .metadata + .clone() + .map(|val| val.parse_value("routing_parameters")) + .transpose() + .change_context(errors::RoutingError::MetadataParsingError) + .attach_printable("Unable to parse routing_parameters from metadata of payment_intent") + .unwrap_or_else(|err| { + logger::error!(error=?err); + None + }); + let payment_method_input = dsl_inputs::PaymentMethodInput { + payment_method: None, + payment_method_type: None, + card_network: None, + }; + let backend_input = dsl_inputs::BackendInput { + metadata, + payment: payment_input, + payment_method: payment_method_input, + mandate: mandate_data, + }; + Ok(backend_input) +} diff --git a/crates/router/src/core/surcharge_decision_config.rs b/crates/router/src/core/surcharge_decision_config.rs new file mode 100644 index 000000000000..82615aef2845 --- /dev/null +++ b/crates/router/src/core/surcharge_decision_config.rs @@ -0,0 +1,190 @@ +use api_models::{ + routing::{self}, + surcharge_decision_configs::{ + SurchargeDecisionConfigReq, SurchargeDecisionManagerRecord, + SurchargeDecisionManagerResponse, + }, +}; +use common_utils::ext_traits::{StringExt, ValueExt}; +use diesel_models::configs; +use error_stack::{IntoReport, ResultExt}; +use euclid::frontend::ast; + +use super::routing::helpers::{ + get_payment_method_surcharge_routing_id, update_merchant_active_algorithm_ref, +}; +use crate::{ + core::errors::{self, RouterResponse}, + routes::AppState, + services::api as service_api, + types::domain, + utils::{self, OptionExt}, +}; + +pub async fn upsert_surcharge_decision_config( + state: AppState, + key_store: domain::MerchantKeyStore, + merchant_account: domain::MerchantAccount, + request: SurchargeDecisionConfigReq, +) -> RouterResponse { + let db = state.store.as_ref(); + let name = request.name; + + let program = request + .algorithm + .get_required_value("algorithm") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "algorithm", + }) + .attach_printable("Program for config not given")?; + let merchant_surcharge_configs = request.merchant_surcharge_configs; + + let timestamp = common_utils::date_time::now_unix_timestamp(); + let mut algo_id: routing::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|val| val.parse_value("routing algorithm")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode the routing algorithm")? + .unwrap_or_default(); + + let key = get_payment_method_surcharge_routing_id(merchant_account.merchant_id.as_str()); + let read_config_key = db.find_config_by_key(&key).await; + + ast::lowering::lower_program(program.clone()) + .into_report() + .change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid Request Data".to_string(), + }) + .attach_printable("The Request has an Invalid Comparison")?; + + match read_config_key { + Ok(config) => { + let previous_record: SurchargeDecisionManagerRecord = config + .config + .parse_struct("SurchargeDecisionManagerRecord") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("The Payment Config Key Not Found")?; + + let new_algo = SurchargeDecisionManagerRecord { + name: name.unwrap_or(previous_record.name), + algorithm: program, + modified_at: timestamp, + created_at: previous_record.created_at, + merchant_surcharge_configs, + }; + + let serialize_updated_str = + utils::Encode::::encode_to_string_of_json( + &new_algo, + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to serialize config to string")?; + + let updated_config = configs::ConfigUpdate::Update { + config: Some(serialize_updated_str), + }; + + db.update_config_by_key(&key, updated_config) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error serializing the config")?; + + algo_id.update_surcharge_config_id(key); + update_merchant_active_algorithm_ref(db, &key_store, algo_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update routing algorithm ref")?; + + Ok(service_api::ApplicationResponse::Json(new_algo)) + } + Err(e) if e.current_context().is_db_not_found() => { + let new_rec = SurchargeDecisionManagerRecord { + name: name + .get_required_value("name") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "name", + }) + .attach_printable("name of the config not found")?, + algorithm: program, + merchant_surcharge_configs, + modified_at: timestamp, + created_at: timestamp, + }; + + let serialized_str = + utils::Encode::::encode_to_string_of_json(&new_rec) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error serializing the config")?; + let new_config = configs::ConfigNew { + key: key.clone(), + config: serialized_str, + }; + + db.insert_config(new_config) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error fetching the config")?; + + algo_id.update_surcharge_config_id(key); + update_merchant_active_algorithm_ref(db, &key_store, algo_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update routing algorithm ref")?; + + Ok(service_api::ApplicationResponse::Json(new_rec)) + } + Err(e) => Err(e) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error fetching payment config"), + } +} + +pub async fn delete_surcharge_decision_config( + state: AppState, + key_store: domain::MerchantKeyStore, + merchant_account: domain::MerchantAccount, +) -> RouterResponse<()> { + let db = state.store.as_ref(); + let key = get_payment_method_surcharge_routing_id(&merchant_account.merchant_id); + let mut algo_id: routing::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|value| value.parse_value("routing algorithm")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode the surcharge conditional_config algorithm")? + .unwrap_or_default(); + algo_id.surcharge_config_algo_id = None; + update_merchant_active_algorithm_ref(db, &key_store, algo_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update deleted algorithm ref")?; + + db.delete_config_by_key(&key) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to delete routing config from DB")?; + Ok(service_api::ApplicationResponse::StatusOk) +} + +pub async fn retrieve_surcharge_decision_config( + state: AppState, + merchant_account: domain::MerchantAccount, +) -> RouterResponse { + let db = state.store.as_ref(); + let algorithm_id = + get_payment_method_surcharge_routing_id(merchant_account.merchant_id.as_str()); + let algo_config = db + .find_config_by_key(&algorithm_id) + .await + .change_context(errors::ApiErrorResponse::ResourceIdNotFound) + .attach_printable("The surcharge conditional config was not found in the DB")?; + let record: SurchargeDecisionManagerRecord = algo_config + .config + .parse_struct("SurchargeDecisionConfigsRecord") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("The Surcharge Decision Config Record was not found")?; + Ok(service_api::ApplicationResponse::Json(record)) +} diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 5ffc85fe6709..5207e4ba8079 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -1070,6 +1070,7 @@ pub fn get_flow_name() -> RouterResult { .to_string()) } +#[instrument(skip_all)] pub async fn persist_individual_surcharge_details_in_redis( state: &AppState, merchant_account: &domain::MerchantAccount, @@ -1109,6 +1110,7 @@ pub async fn persist_individual_surcharge_details_in_redis( Ok(()) } +#[instrument(skip_all)] pub async fn get_individual_surcharge_detail_from_redis( state: &AppState, payment_method: &euclid_enums::PaymentMethod, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 070f1eb29bf8..79801e8e64f0 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -325,6 +325,20 @@ impl Routing { web::resource("/deactivate") .route(web::post().to(cloud_routing::routing_unlink_config)), ) + .service( + web::resource("/decision") + .route(web::put().to(cloud_routing::upsert_decision_manager_config)) + .route(web::get().to(cloud_routing::retrieve_decision_manager_config)) + .route(web::delete().to(cloud_routing::delete_decision_manager_config)), + ) + .service( + web::resource("/decision/surcharge") + .route(web::put().to(cloud_routing::upsert_surcharge_decision_manager_config)) + .route(web::get().to(cloud_routing::retrieve_surcharge_decision_manager_config)) + .route( + web::delete().to(cloud_routing::delete_surcharge_decision_manager_config), + ), + ) .service( web::resource("/{algorithm_id}") .route(web::get().to(cloud_routing::routing_retrieve_config)), diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index c093523d455a..84b00867b98d 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -46,7 +46,10 @@ impl From for ApiIdentifier { | Flow::RoutingRetrieveDictionary | Flow::RoutingUpdateConfig | Flow::RoutingUpdateDefaultConfig - | Flow::RoutingDeleteConfig => Self::Routing, + | Flow::RoutingDeleteConfig + | Flow::DecisionManagerDeleteConfig + | Flow::DecisionManagerRetrieveConfig + | Flow::DecisionManagerUpsertConfig => Self::Routing, Flow::MerchantConnectorsCreate | Flow::MerchantConnectorsRetrieve diff --git a/crates/router/src/routes/routing.rs b/crates/router/src/routes/routing.rs index 606111a88818..1d2549bb047a 100644 --- a/crates/router/src/routes/routing.rs +++ b/crates/router/src/routes/routing.rs @@ -12,7 +12,7 @@ use router_env::{ }; use crate::{ - core::{api_locking, routing}, + core::{api_locking, conditional_config, routing, surcharge_decision_config}, routes::AppState, services::{api as oss_api, authentication as auth}, }; @@ -248,6 +248,172 @@ pub async fn routing_retrieve_default_config( .await } +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn upsert_surcharge_decision_manager_config( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let flow = Flow::DecisionManagerUpsertConfig; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: auth::AuthenticationData, update_decision| { + surcharge_decision_config::upsert_surcharge_decision_config( + state, + auth.key_store, + auth.merchant_account, + update_decision, + ) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn delete_surcharge_decision_manager_config( + state: web::Data, + req: HttpRequest, +) -> impl Responder { + let flow = Flow::DecisionManagerDeleteConfig; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + (), + |state, auth: auth::AuthenticationData, ()| { + surcharge_decision_config::delete_surcharge_decision_config( + state, + auth.key_store, + auth.merchant_account, + ) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn retrieve_surcharge_decision_manager_config( + state: web::Data, + req: HttpRequest, +) -> impl Responder { + let flow = Flow::DecisionManagerRetrieveConfig; + oss_api::server_wrap( + flow, + state, + &req, + (), + |state, auth: auth::AuthenticationData, _| { + surcharge_decision_config::retrieve_surcharge_decision_config( + state, + auth.merchant_account, + ) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + ) + .await +} + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn upsert_decision_manager_config( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let flow = Flow::DecisionManagerUpsertConfig; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: auth::AuthenticationData, update_decision| { + conditional_config::upsert_conditional_config( + state, + auth.key_store, + auth.merchant_account, + update_decision, + ) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn delete_decision_manager_config( + state: web::Data, + req: HttpRequest, +) -> impl Responder { + let flow = Flow::DecisionManagerDeleteConfig; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + (), + |state, auth: auth::AuthenticationData, ()| { + conditional_config::delete_conditional_config( + state, + auth.key_store, + auth.merchant_account, + ) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn retrieve_decision_manager_config( + state: web::Data, + req: HttpRequest, +) -> impl Responder { + let flow = Flow::DecisionManagerRetrieveConfig; + oss_api::server_wrap( + flow, + state, + &req, + (), + |state, auth: auth::AuthenticationData, _| { + conditional_config::retrieve_conditional_config(state, auth.merchant_account) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + ) + .await +} + #[cfg(feature = "olap")] #[instrument(skip_all)] pub async fn routing_retrieve_linked_config( diff --git a/crates/router/src/types/storage/payment_attempt.rs b/crates/router/src/types/storage/payment_attempt.rs index a4fbcb022005..f94d06997ca9 100644 --- a/crates/router/src/types/storage/payment_attempt.rs +++ b/crates/router/src/types/storage/payment_attempt.rs @@ -17,6 +17,7 @@ pub trait PaymentAttemptExt { fn get_next_capture_id(&self) -> String; fn get_total_amount(&self) -> i64; + fn get_surcharge_details(&self) -> Option; } impl PaymentAttemptExt for PaymentAttempt { @@ -58,7 +59,14 @@ impl PaymentAttemptExt for PaymentAttempt { let next_sequence_number = self.multiple_capture_count.unwrap_or_default() + 1; format!("{}_{}", self.attempt_id.clone(), next_sequence_number) } - + fn get_surcharge_details(&self) -> Option { + self.surcharge_amount.map(|surcharge_amount| { + api_models::payments::RequestSurchargeDetails { + surcharge_amount, + tax_amount: self.tax_amount, + } + }) + } fn get_total_amount(&self) -> i64 { self.amount + self.surcharge_amount.unwrap_or(0) + self.tax_amount.unwrap_or(0) } diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 3bfd1ef7d9f8..f6d61f550840 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -247,6 +247,12 @@ pub enum Flow { GsmRuleDelete, /// User connect account UserConnectAccount, + /// Upsert Decision Manager Config + DecisionManagerUpsertConfig, + /// Delete Decision Manager Config + DecisionManagerDeleteConfig, + /// Retrieve Decision Manager Config + DecisionManagerRetrieveConfig, } /// From e66ccde4cf6d055b7d02c5e982d2e09364845602 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Tue, 21 Nov 2023 20:33:06 +0530 Subject: [PATCH 054/146] fix(mca): Change the check for `disabled` field in mca create and update (#2938) --- crates/router/src/core/admin.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 3a0c938c32b4..107e8f8859d6 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1767,7 +1767,7 @@ pub fn validate_status_and_disabled( }; let disabled = match (disabled, connector_status) { - (Some(true), common_enums::ConnectorStatus::Inactive) => { + (Some(false), common_enums::ConnectorStatus::Inactive) => { return Err(errors::ApiErrorResponse::InvalidRequestData { message: "Connector cannot be enabled when connector_status is inactive or when using TemporaryAuth" .to_string(), From 15a255ea60dffad9e4cf20d642636028c27c7c00 Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Tue, 21 Nov 2023 21:22:50 +0530 Subject: [PATCH 055/146] feat(connector): [Prophetpay] Save card token for Refund and remove Void flow (#2927) --- crates/router/src/connector/prophetpay.rs | 47 +-- .../src/connector/prophetpay/transformers.rs | 384 ++++++++++++------ 2 files changed, 272 insertions(+), 159 deletions(-) diff --git a/crates/router/src/connector/prophetpay.rs b/crates/router/src/connector/prophetpay.rs index e5ebe6331ba2..efe87bcefd9f 100644 --- a/crates/router/src/connector/prophetpay.rs +++ b/crates/router/src/connector/prophetpay.rs @@ -107,16 +107,15 @@ impl ConnectorCommon for Prophetpay { &self, res: Response, ) -> CustomResult { - let response: prophetpay::ProphetpayErrorResponse = res + let response: serde_json::Value = res .response - .parse_struct("ProphetpayErrorResponse") + .parse_struct("ProphetPayErrorResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - Ok(ErrorResponse { status_code: res.status_code, - code: response.status.to_string(), - message: response.title, - reason: Some(response.errors.to_string()), + code: consts::NO_ERROR_CODE.to_string(), + message: consts::NO_ERROR_MESSAGE.to_string(), + reason: Some(response.to_string()), attempt_status: None, }) } @@ -324,7 +323,7 @@ impl where types::PaymentsResponseData: Clone, { - let response: prophetpay::ProphetpayResponse = res + let response: prophetpay::ProphetpayCompleteAuthResponse = res .response .parse_struct("prophetpay ProphetpayResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -407,9 +406,9 @@ impl ConnectorIntegration CustomResult { - let response: prophetpay::ProphetpayResponse = res + let response: prophetpay::ProphetpaySyncResponse = res .response - .parse_struct("prophetpay ProphetpayResponse") + .parse_struct("prophetpay PaymentsSyncResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, @@ -431,9 +430,12 @@ impl ConnectorIntegration for Prophetpay { + /* fn get_headers( &self, req: &types::PaymentsCancelRouterData, @@ -471,33 +473,25 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - Ok(Some( - services::RequestBuilder::new() - .method(services::Method::Get) - .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) - .attach_default_headers() - .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) - .body(types::PaymentsVoidType::get_request_body( - self, req, connectors, - )?) - .build(), - )) + Err(errors::ConnectorError::NotImplemented("Void flow not implemented".to_string()).into()) } + /* fn handle_response( &self, data: &types::PaymentsCancelRouterData, res: Response, ) -> CustomResult { - let response: prophetpay::ProphetpayResponse = res + let response: prophetpay::ProphetpayVoidResponse = res .response - .parse_struct("prophetpay ProphetpayResponse") + .parse_struct("prophetpay PaymentsCancelResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, @@ -512,6 +506,7 @@ impl ConnectorIntegration CustomResult { self.build_error_response(res) } + */ } impl ConnectorIntegration @@ -652,7 +647,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { Ok(Some( services::RequestBuilder::new() - .method(services::Method::Get) + .method(services::Method::Post) .url(&types::RefundSyncType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::RefundSyncType::get_headers(self, req, connectors)?) @@ -668,7 +663,7 @@ impl ConnectorIntegration CustomResult { - let response: prophetpay::ProphetpayRefundResponse = res + let response: prophetpay::ProphetpayRefundSyncResponse = res .response .parse_struct("prophetpay ProphetpayRefundResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; diff --git a/crates/router/src/connector/prophetpay/transformers.rs b/crates/router/src/connector/prophetpay/transformers.rs index 74071d5b85cb..b8cf3e3a1f5b 100644 --- a/crates/router/src/connector/prophetpay/transformers.rs +++ b/crates/router/src/connector/prophetpay/transformers.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use url::Url; use crate::{ - connector::utils, + connector::utils::{self, to_connector_meta}, core::errors, services, types::{self, api, storage::enums}, @@ -159,7 +159,11 @@ impl TryFrom<&ProphetpayRouterData<&types::PaymentsAuthorizeRouterData>> ), } } else { - Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()) + Err(errors::ConnectorError::CurrencyNotSupported { + message: item.router_data.request.currency.to_string(), + connector: "Prophetpay", + } + .into()) } } } @@ -266,10 +270,7 @@ impl TryFrom<&ProphetpayRouterData<&types::PaymentsCompleteAuthorizeRouterData>> Ok(Self { amount: item.amount.to_owned(), ref_info: item.router_data.connector_request_reference_id.to_owned(), - inquiry_reference: format!( - "inquiry_{}", - item.router_data.connector_request_reference_id - ), + inquiry_reference: item.router_data.connector_request_reference_id.clone(), profile: auth_data.profile_id, action_type: ProphetpayActionType::get_action_type(&ProphetpayActionType::Charge), card_token, @@ -346,8 +347,8 @@ impl TryFrom<&types::PaymentsSyncRouterData> for ProphetpaySyncRequest { .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; Ok(Self { transaction_id, - ref_info: item.attempt_id.to_owned(), - inquiry_reference: format!("inquiry_{}", item.attempt_id), + ref_info: item.connector_request_reference_id.to_owned(), + inquiry_reference: item.connector_request_reference_id.clone(), profile: auth_data.profile_id, action_type: ProphetpayActionType::get_action_type(&ProphetpayActionType::Inquiry), }) @@ -355,66 +356,170 @@ impl TryFrom<&types::PaymentsSyncRouterData> for ProphetpaySyncRequest { } #[derive(Debug, Clone, Deserialize)] -pub enum ProphetpayPaymentStatus { - Success, - #[serde(rename = "Transaction Approved")] - Charged, - Failure, - #[serde(rename = "Transaction Voided")] - Voided, - #[serde(rename = "Requires a card on file.")] - CardTokenNotFound, - #[serde(rename = "RefInfo and InquiryReference are duplicated")] - DuplicateValue, - #[serde(rename = "Profile is missing")] - MissingProfile, - #[serde(rename = "RefInfo is empty.")] - EmptyRef, -} - -impl From for enums::AttemptStatus { - fn from(item: ProphetpayPaymentStatus) -> Self { - match item { - ProphetpayPaymentStatus::Success | ProphetpayPaymentStatus::Charged => Self::Charged, - ProphetpayPaymentStatus::Failure - | ProphetpayPaymentStatus::CardTokenNotFound - | ProphetpayPaymentStatus::DuplicateValue - | ProphetpayPaymentStatus::MissingProfile - | ProphetpayPaymentStatus::EmptyRef => Self::Failure, - ProphetpayPaymentStatus::Voided => Self::Voided, +#[serde(rename_all = "camelCase")] +pub struct ProphetpayCompleteAuthResponse { + pub success: bool, + pub response_text: String, + #[serde(rename = "transactionID")] + pub transaction_id: String, + pub response_code: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProphetpayCardTokenData { + card_token: Secret, +} + +impl + TryFrom< + types::ResponseRouterData< + F, + ProphetpayCompleteAuthResponse, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + ProphetpayCompleteAuthResponse, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + >, + ) -> Result { + if item.response.success { + let card_token = get_card_token(item.data.request.redirect_response.clone())?; + let card_token_data = ProphetpayCardTokenData { + card_token: Secret::from(card_token), + }; + let connector_metadata = serde_json::to_value(card_token_data).ok(); + Ok(Self { + status: enums::AttemptStatus::Charged, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + item.response.transaction_id, + ), + redirection_data: None, + mandate_reference: None, + connector_metadata, + network_txn_id: None, + connector_response_reference_id: None, + }), + ..item.data + }) + } else { + Ok(Self { + status: enums::AttemptStatus::Failure, + response: Err(types::ErrorResponse { + code: item.response.response_code, + message: item.response.response_text.clone(), + reason: Some(item.response.response_text), + status_code: item.http_code, + attempt_status: None, + }), + ..item.data + }) } } } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct ProphetpayResponse { - pub response_text: ProphetpayPaymentStatus, +pub struct ProphetpaySyncResponse { + success: bool, + pub response_text: String, #[serde(rename = "transactionID")] pub transaction_id: String, + pub response_code: String, } -impl TryFrom> +impl + TryFrom> for types::RouterData { type Error = error_stack::Report; fn try_from( - item: types::ResponseRouterData, + item: types::ResponseRouterData, ) -> Result { - Ok(Self { - status: enums::AttemptStatus::from(item.response.response_text), - response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId( - item.response.transaction_id, - ), - redirection_data: None, - mandate_reference: None, - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: None, - }), - ..item.data - }) + if item.response.success { + Ok(Self { + status: enums::AttemptStatus::Charged, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + item.response.transaction_id, + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + }), + ..item.data + }) + } else { + Ok(Self { + status: enums::AttemptStatus::Failure, + response: Err(types::ErrorResponse { + code: item.response.response_code, + message: item.response.response_text.clone(), + reason: Some(item.response.response_text), + status_code: item.http_code, + attempt_status: None, + }), + ..item.data + }) + } + } +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProphetpayVoidResponse { + pub success: bool, + pub response_text: String, + #[serde(rename = "transactionID")] + pub transaction_id: String, + pub response_code: String, +} + +impl + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData, + ) -> Result { + if item.response.success { + Ok(Self { + status: enums::AttemptStatus::Voided, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + item.response.transaction_id, + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + }), + ..item.data + }) + } else { + Ok(Self { + status: enums::AttemptStatus::VoidFailed, + response: Err(types::ErrorResponse { + code: item.response.response_code, + message: item.response.response_text.clone(), + reason: Some(item.response.response_text), + status_code: item.http_code, + attempt_status: None, + }), + ..item.data + }) + } } } @@ -435,8 +540,8 @@ impl TryFrom<&types::PaymentsCancelRouterData> for ProphetpayVoidRequest { let transaction_id = item.request.connector_transaction_id.to_owned(); Ok(Self { transaction_id, - ref_info: item.attempt_id.to_owned(), - inquiry_reference: format!("inquiry_{}", item.attempt_id), + ref_info: item.connector_request_reference_id.to_owned(), + inquiry_reference: item.connector_request_reference_id.clone(), profile: auth_data.profile_id, action_type: ProphetpayActionType::get_action_type(&ProphetpayActionType::Inquiry), }) @@ -447,6 +552,7 @@ impl TryFrom<&types::PaymentsCancelRouterData> for ProphetpayVoidRequest { #[serde(rename_all = "camelCase")] pub struct ProphetpayRefundRequest { pub amount: f64, + pub card_token: Secret, pub transaction_id: String, pub profile: Secret, pub ref_info: String, @@ -459,47 +565,26 @@ impl TryFrom<&ProphetpayRouterData<&types::RefundsRouterData>> for Prophet fn try_from( item: &ProphetpayRouterData<&types::RefundsRouterData>, ) -> Result { - let auth_data = ProphetpayAuthType::try_from(&item.router_data.connector_auth_type)?; - let transaction_id = item.router_data.request.connector_transaction_id.to_owned(); - Ok(Self { - transaction_id, - amount: item.amount.to_owned(), - profile: auth_data.profile_id, - ref_info: item.router_data.request.refund_id.to_owned(), - inquiry_reference: format!("inquiry_{}", item.router_data.request.refund_id), - action_type: ProphetpayActionType::get_action_type(&ProphetpayActionType::Refund), - }) - } -} - -#[allow(dead_code)] -#[derive(Debug, Deserialize, Clone)] -pub enum RefundStatus { - Success, - Failure, - #[serde(rename = "Transaction Voided")] - Voided, - #[serde(rename = "Requires a card on file.")] - CardTokenNotFound, - #[serde(rename = "RefInfo and InquiryReference are duplicated")] - DuplicateValue, - #[serde(rename = "Profile is missing")] - MissingProfile, - #[serde(rename = "RefInfo is empty.")] - EmptyRef, -} - -impl From for enums::RefundStatus { - fn from(item: RefundStatus) -> Self { - match item { - RefundStatus::Success - // in retrieving refund, if it is successful, it is shown as voided - | RefundStatus::Voided => Self::Success, - RefundStatus::Failure - | RefundStatus::CardTokenNotFound - | RefundStatus::DuplicateValue - | RefundStatus::MissingProfile - | RefundStatus::EmptyRef => Self::Failure, + if item.router_data.request.payment_amount == item.router_data.request.refund_amount { + let auth_data = ProphetpayAuthType::try_from(&item.router_data.connector_auth_type)?; + let transaction_id = item.router_data.request.connector_transaction_id.to_owned(); + let card_token_data: ProphetpayCardTokenData = + to_connector_meta(item.router_data.request.connector_metadata.clone())?; + + Ok(Self { + transaction_id, + amount: item.amount.to_owned(), + card_token: card_token_data.card_token, + profile: auth_data.profile_id, + ref_info: item.router_data.connector_request_reference_id.to_owned(), + inquiry_reference: item.router_data.connector_request_reference_id.clone(), + action_type: ProphetpayActionType::get_action_type(&ProphetpayActionType::Refund), + }) + } else { + Err(errors::ConnectorError::NotImplemented( + "Partial Refund is Not Supported".to_string(), + ) + .into()) } } } @@ -507,7 +592,10 @@ impl From for enums::RefundStatus { #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ProphetpayRefundResponse { - pub response_text: RefundStatus, + pub success: bool, + pub response_text: String, + pub tran_seq_number: String, + pub response_code: String, } impl TryFrom> @@ -517,20 +605,75 @@ impl TryFrom, ) -> Result { - Ok(Self { - response: Ok(types::RefundsResponseData { - // no refund id is generated, rather transaction id is used for referring to status in refund also - connector_refund_id: item.data.request.connector_transaction_id.clone(), - refund_status: enums::RefundStatus::from(item.response.response_text), - }), - ..item.data - }) + if item.response.success { + Ok(Self { + response: Ok(types::RefundsResponseData { + // no refund id is generated, tranSeqNumber is kept for future usage + connector_refund_id: item.response.tran_seq_number, + refund_status: enums::RefundStatus::Success, + }), + ..item.data + }) + } else { + Ok(Self { + status: enums::AttemptStatus::Failure, + response: Err(types::ErrorResponse { + code: item.response.response_code, + message: item.response.response_text.clone(), + reason: Some(item.response.response_text), + status_code: item.http_code, + attempt_status: None, + }), + ..item.data + }) + } } } +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProphetpayRefundSyncResponse { + pub success: bool, + pub response_text: String, + pub response_code: String, +} + +impl TryFrom> + for types::RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + if item.response.success { + Ok(Self { + response: Ok(types::RefundsResponseData { + // no refund id is generated, rather transaction id is used for referring to status in refund also + connector_refund_id: item.data.request.connector_transaction_id.clone(), + refund_status: enums::RefundStatus::Success, + }), + ..item.data + }) + } else { + Ok(Self { + status: enums::AttemptStatus::Failure, + response: Err(types::ErrorResponse { + code: item.response.response_code, + message: item.response.response_text.clone(), + reason: Some(item.response.response_text), + status_code: item.http_code, + attempt_status: None, + }), + ..item.data + }) + } + } +} #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct ProphetpayRefundSyncRequest { + transaction_id: String, + inquiry_reference: String, ref_info: String, profile: Secret, action_type: i8, @@ -541,36 +684,11 @@ impl TryFrom<&types::RefundSyncRouterData> for ProphetpayRefundSyncRequest { fn try_from(item: &types::RefundSyncRouterData) -> Result { let auth_data = ProphetpayAuthType::try_from(&item.connector_auth_type)?; Ok(Self { - ref_info: item.attempt_id.to_owned(), + transaction_id: item.request.connector_transaction_id.clone(), + ref_info: item.connector_request_reference_id.to_owned(), + inquiry_reference: item.connector_request_reference_id.clone(), profile: auth_data.profile_id, action_type: ProphetpayActionType::get_action_type(&ProphetpayActionType::Inquiry), }) } } - -impl TryFrom> - for types::RefundsRouterData -{ - type Error = error_stack::Report; - fn try_from( - item: types::RefundsResponseRouterData, - ) -> Result { - Ok(Self { - response: Ok(types::RefundsResponseData { - connector_refund_id: item.data.request.connector_transaction_id.clone(), - refund_status: enums::RefundStatus::from(item.response.response_text), - }), - ..item.data - }) - } -} - -// Error Response body is yet to be confirmed with the connector -#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct ProphetpayErrorResponse { - pub status: u16, - pub title: String, - pub trace_id: String, - pub errors: serde_json::Value, -} From 245e489d13209da19d6e9af01219056eec04e897 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 21 Nov 2023 16:21:39 +0000 Subject: [PATCH 056/146] test(postman): update postman collection files --- postman/collection-json/stripe.postman_collection.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/postman/collection-json/stripe.postman_collection.json b/postman/collection-json/stripe.postman_collection.json index 06ccae91b2c7..9c9a8a5d685c 100644 --- a/postman/collection-json/stripe.postman_collection.json +++ b/postman/collection-json/stripe.postman_collection.json @@ -8504,7 +8504,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4000000000009995\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4000000000009995\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -8784,7 +8784,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -9436,7 +9436,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -13998,7 +13998,7 @@ "language": "json" } }, - "raw": "{\"amount\":6570,\"currency\":\"USD\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":6570,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6570,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", From fcd206b6af0e0afdb8276077c61adc53f030e471 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 21 Nov 2023 16:21:40 +0000 Subject: [PATCH 057/146] chore(version): v1.86.0 --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbe558180021..7d7b6770d471 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,27 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.86.0 (2023-11-21) + +### Features + +- **connector:** [Prophetpay] Save card token for Refund and remove Void flow ([#2927](https://github.com/juspay/hyperswitch/pull/2927)) ([`15a255e`](https://github.com/juspay/hyperswitch/commit/15a255ea60dffad9e4cf20d642636028c27c7c00)) +- Add support for 3ds and surcharge decision through routing rules ([#2869](https://github.com/juspay/hyperswitch/pull/2869)) ([`f8618e0`](https://github.com/juspay/hyperswitch/commit/f8618e077065d94aa27d7153fc5ea6f93870bd81)) + +### Bug Fixes + +- **mca:** Change the check for `disabled` field in mca create and update ([#2938](https://github.com/juspay/hyperswitch/pull/2938)) ([`e66ccde`](https://github.com/juspay/hyperswitch/commit/e66ccde4cf6d055b7d02c5e982d2e09364845602)) +- Status goes from pending to partially captured in psync ([#2915](https://github.com/juspay/hyperswitch/pull/2915)) ([`3f3b797`](https://github.com/juspay/hyperswitch/commit/3f3b797dc65c1bc6f710b122ef00d5bcb409e600)) + +### Testing + +- **postman:** Update postman collection files ([`245e489`](https://github.com/juspay/hyperswitch/commit/245e489d13209da19d6e9af01219056eec04e897)) + +**Full Changelog:** [`v1.85.0...v1.86.0`](https://github.com/juspay/hyperswitch/compare/v1.85.0...v1.86.0) + +- - - + + ## 1.85.0 (2023-11-21) ### Features From f8261a96e758498a32c988191bf314aa6c752059 Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Tue, 21 Nov 2023 22:38:40 +0530 Subject: [PATCH 058/146] feat(router): migrate `payment_method_data` to rust locker only if `payment_method` is card (#2929) --- crates/router/src/core/locker_migration.rs | 61 ++++++++++++------- .../router/src/core/payment_methods/cards.rs | 15 +++-- 2 files changed, 51 insertions(+), 25 deletions(-) diff --git a/crates/router/src/core/locker_migration.rs b/crates/router/src/core/locker_migration.rs index f036a03a2f0e..3f56cddee126 100644 --- a/crates/router/src/core/locker_migration.rs +++ b/crates/router/src/core/locker_migration.rs @@ -1,6 +1,6 @@ use api_models::{enums as api_enums, locker_migration::MigrateCardResponse}; use common_utils::errors::CustomResult; -use diesel_models::PaymentMethod; +use diesel_models::{enums as storage_enums, PaymentMethod}; use error_stack::{FutureExt, ResultExt}; use futures::TryFutureExt; @@ -79,10 +79,21 @@ pub async fn call_to_locker( ) -> CustomResult { let mut cards_moved = 0; - for pm in payment_methods { + for pm in payment_methods + .into_iter() + .filter(|pm| matches!(pm.payment_method, storage_enums::PaymentMethod::Card)) + { let card = cards::get_card_from_locker(state, customer_id, merchant_id, &pm.payment_method_id) - .await?; + .await; + + let card = match card { + Ok(card) => card, + Err(err) => { + logger::error!("Failed to fetch card from Basilisk HS locker : {:?}", err); + continue; + } + }; let card_details = api::CardDetail { card_number: card.card_number, @@ -103,28 +114,36 @@ pub async fn call_to_locker( card_network: card.card_brand, }; - let (_add_card_rs_resp, _is_duplicate) = cards::add_card_hs( - state, - pm_create, - &card_details, - customer_id.to_string(), - merchant_account, - api_enums::LockerChoice::Tartarus, - Some(&pm.payment_method_id), - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable(format!( - "Card migration failed for merchant_id: {merchant_id}, customer_id: {customer_id}, payment_method_id: {} ", - pm.payment_method_id - ))?; + let add_card_result = cards::add_card_hs( + state, + pm_create, + &card_details, + customer_id.to_string(), + merchant_account, + api_enums::LockerChoice::Tartarus, + Some(&pm.payment_method_id), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable(format!( + "Card migration failed for merchant_id: {merchant_id}, customer_id: {customer_id}, payment_method_id: {} ", + pm.payment_method_id + )); + + let (_add_card_rs_resp, _is_duplicate) = match add_card_result { + Ok(output) => output, + Err(err) => { + logger::error!("Failed to add card to Rust locker : {:?}", err); + continue; + } + }; cards_moved += 1; logger::info!( - "Card migrated for merchant_id: {merchant_id}, customer_id: {customer_id}, payment_method_id: {} ", - pm.payment_method_id - ); + "Card migrated for merchant_id: {merchant_id}, customer_id: {customer_id}, payment_method_id: {} ", + pm.payment_method_id + ); } Ok(cards_moved) diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 9736edc73987..ad42a8579127 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -254,11 +254,18 @@ pub async fn add_card_to_locker( &metrics::CARD_ADD_TIME, &[], ) - .await?; - - logger::debug!("card added to rust locker"); + .await; - Ok(add_card_to_rs_resp) + match add_card_to_rs_resp { + value @ Ok(_) => { + logger::debug!("Card added successfully"); + value + } + Err(err) => { + logger::debug!(error =? err,"failed to add card"); + Ok(add_card_to_hs_resp) + } + } } pub async fn get_card_from_locker( From a701db70caff63517dde42d1d094a6b3dc39ef26 Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Wed, 22 Nov 2023 00:00:38 +0530 Subject: [PATCH 059/146] CI: update release new version workflow to not generate release notes (#2941) --- .github/git-cliff-release.toml | 89 ----------------------- .github/workflows/release-new-version.yml | 27 +------ 2 files changed, 1 insertion(+), 115 deletions(-) delete mode 100644 .github/git-cliff-release.toml diff --git a/.github/git-cliff-release.toml b/.github/git-cliff-release.toml deleted file mode 100644 index 1b82c812b5d8..000000000000 --- a/.github/git-cliff-release.toml +++ /dev/null @@ -1,89 +0,0 @@ -# configuration file for git-cliff -# see https://github.com/orhun/git-cliff#configuration-file - -[changelog] -# changelog header -header = "" -# template for the changelog body -# https://tera.netlify.app/docs/#introduction -body = """ -{% set newline = "\n" -%} -{% set commit_base_url = "https://github.com/juspay/hyperswitch/commit/" -%} -{% set compare_base_url = "https://github.com/juspay/hyperswitch/compare/" -%} -{% if version -%} - ## {{ version | trim_start_matches(pat="v") }} ({{ timestamp | date(format="%Y-%m-%d") }}) -{% else -%} - ## [unreleased] -{% endif -%} -{% for group, commits in commits | group_by(attribute="group") %} - {# The `striptags` removes the HTML comments added while grouping -#} - ### {{ group | striptags | trim | upper_first }} - {% for scope, commits in commits | group_by(attribute="scope") %} - - {{ "**" ~ scope ~ ":" ~ "**" -}} - {% for commit in commits -%} - {% if commits | length != 1 %}{{ newline ~ " - " }}{% else %}{{ " " }}{% endif -%} - {{ commit.message | upper_first | trim }} ([`{{ commit.id | truncate(length=7, end="") }}`]({{ commit_base_url ~ commit.id }})) by {{ commit.author.email -}} - {%- endfor -%} - {%- endfor -%} - {%- for commit in commits -%} - {% if commit.scope %}{% else %} - - {{ commit.message | upper_first | trim }} ([`{{ commit.id | truncate(length=7, end="") }}`]({{ commit_base_url ~ commit.id }})) by {{ commit.author.email -}} - {%- endif %} - {%- endfor %} -{% endfor %} -{% if previous and previous.commit_id and commit_id -%} - **Full Changelog:** [`{{ previous.version }}...{{ version }}`]({{ compare_base_url }}{{ previous.version }}...{{ version }})\n -{% endif %} -""" -# remove the leading and trailing whitespace from the template -trim = true -# changelog footer -footer = "" - -[git] -# parse the commits based on https://www.conventionalcommits.org -conventional_commits = true -# filter out the commits that are not conventional -filter_unconventional = false -# process each line of a commit as an individual commit -split_commits = false -# regex for preprocessing the commit messages -commit_preprocessors = [ - { pattern = "^ +", replace = "" }, # remove spaces at the beginning of the message - { pattern = " +", replace = " " }, # replace multiple spaces with a single space - { pattern = "\\(#([0-9]+)\\)", replace = "([#${1}](https://github.com/juspay/hyperswitch/pull/${1}))" }, # replace PR numbers with links - { pattern = "(\\n?Co-authored-by: .+ <.+@.+>\\n?)+", replace = "" }, # remove co-author information - { pattern = "(\\n?Signed-off-by: .+ <.+@.+>\\n?)+", replace = "" }, # remove sign-off information -] -# regex for parsing and grouping commits -# the HTML comments (``) are a workaround to get sections in custom order, since `git-cliff` sorts sections in alphabetical order -# reference: https://github.com/orhun/git-cliff/issues/9 -commit_parsers = [ - { message = "^(?i)(feat)", group = "Features" }, - { message = "^(?i)(fix)", group = "Bug Fixes" }, - { message = "^(?i)(perf)", group = "Performance" }, - { body = ".*security", group = "Security" }, - { message = "^(?i)(refactor)", group = "Refactors" }, - { message = "^(?i)(test)", group = "Testing" }, - { message = "^(?i)(docs)", group = "Documentation" }, - { message = "^(?i)(chore\\(version\\)): V[\\d]+\\.[\\d]+\\.[\\d]+", skip = true }, - { message = "^(?i)(chore)", group = "Miscellaneous Tasks" }, - { message = "^(?i)(build)", group = "Build System / Dependencies" }, - { message = "^(?i)(ci)", skip = true }, -] -# protect breaking changes from being skipped due to matching a skipping commit_parser -protect_breaking_commits = false -# filter out the commits that are not matched by commit parsers -filter_commits = false -# glob pattern for matching git tags -tag_pattern = "v[0-9]*" -# regex for skipping tags -# skip_tags = "v0.1.0-beta.1" -# regex for ignoring tags -# ignore_tags = "" -# sort the tags topologically -topo_order = true -# sort the commits inside sections by oldest/newest order -sort_commits = "oldest" -# limit the number of commits included in the changelog. -# limit_commits = 42 diff --git a/.github/workflows/release-new-version.yml b/.github/workflows/release-new-version.yml index 872c207e8aa3..eda2df05153b 100644 --- a/.github/workflows/release-new-version.yml +++ b/.github/workflows/release-new-version.yml @@ -40,19 +40,6 @@ jobs: crate: cocogitto version: 5.4.0 - - name: Install git-cliff - uses: baptiste0928/cargo-install@v2.1.0 - with: - crate: git-cliff - version: 1.2.0 - - - name: Install changelog-gh-usernames - uses: baptiste0928/cargo-install@v2.1.0 - with: - crate: changelog-gh-usernames - git: https://github.com/SanchithHegde/changelog-gh-usernames - rev: dab6da3ff99dbbff8650c114984c4d8be5161ac8 - - name: Set Git Configuration shell: bash run: | @@ -87,7 +74,7 @@ jobs: PREVIOUS_TAG="$(git tag --sort='version:refname' --merged | tail --lines 1)" if [[ "$(cog bump --auto --dry-run)" == *"No conventional commits for your repository that required a bump"* ]]; then NEW_TAG="$(cog bump --patch --dry-run)" - elif [[ "${PREVIOUS_TAG}" != "${NEW_TAG}" ]]; then + else NEW_TAG="$(cog bump --auto --dry-run)" fi echo "NEW_TAG=${NEW_TAG}" >> $GITHUB_ENV @@ -106,15 +93,3 @@ jobs: run: | git push git push --tags - - - name: Generate release notes and create GitHub release - shell: bash - if: ${{ env.NEW_TAG != env.PREVIOUS_TAG }} - env: - GITHUB_TOKEN: ${{ github.token }} - GH_TOKEN: ${{ secrets.AUTO_RELEASE_PAT }} - # Need to consider commits inclusive of previous tag to generate diff link between versions. - # This would also then require us to remove the last few lines from the changelog. - run: | - git-cliff --config .github/git-cliff-release.toml "${PREVIOUS_TAG}^..${NEW_TAG}" | changelog-gh-usernames | sed "/## ${PREVIOUS_TAG#v}/,\$d" > release-notes.md - gh release create "${NEW_TAG}" --notes-file release-notes.md --verify-tag --title "Hyperswitch ${NEW_TAG}" From 7f74ae98a1d48eed98341e4505d3801a61e69fc7 Mon Sep 17 00:00:00 2001 From: SamraatBansal <55536657+SamraatBansal@users.noreply.github.com> Date: Wed, 22 Nov 2023 00:35:40 +0530 Subject: [PATCH 060/146] fix: cybersource mandates and fiserv exp year (#2920) Co-authored-by: Arjun Karthik --- crates/router/src/connector/cybersource.rs | 94 ++++- .../src/connector/cybersource/transformers.rs | 388 +++++++++++++----- .../src/connector/fiserv/transformers.rs | 37 +- crates/router/src/connector/utils.rs | 10 + crates/router/src/core/payments.rs | 24 +- 5 files changed, 427 insertions(+), 126 deletions(-) diff --git a/crates/router/src/connector/cybersource.rs b/crates/router/src/connector/cybersource.rs index f69701f73958..ce283b12b798 100644 --- a/crates/router/src/connector/cybersource.rs +++ b/crates/router/src/connector/cybersource.rs @@ -94,7 +94,7 @@ impl ConnectorCommon for Cybersource { } fn get_currency_unit(&self) -> api::CurrencyUnit { - api::CurrencyUnit::Minor + api::CurrencyUnit::Base } fn build_error_response( @@ -252,6 +252,80 @@ impl types::PaymentsResponseData, > for Cybersource { + fn get_headers( + &self, + req: &types::SetupMandateRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + fn get_url( + &self, + _req: &types::SetupMandateRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}pts/v2/payments/", self.base_url(connectors))) + } + fn get_request_body( + &self, + req: &types::SetupMandateRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = cybersource::CybersourceZeroMandateRequest::try_from(req)?; + let cybersource_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(cybersource_req)) + } + + fn build_request( + &self, + req: &types::SetupMandateRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::SetupMandateType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::SetupMandateType::get_headers(self, req, connectors)?) + .body(types::SetupMandateType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::SetupMandateRouterData, + res: types::Response, + ) -> CustomResult { + let response: cybersource::CybersourcePaymentsResponse = res + .response + .parse_struct("CybersourceMandateResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(( + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }, + false, + )) + } + + fn get_error_response( + &self, + res: types::Response, + ) -> CustomResult { + self.build_error_response(res) + } } impl ConnectorIntegration @@ -300,7 +374,14 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_request = cybersource::CybersourcePaymentsRequest::try_from(req)?; + let connector_router_data = cybersource::CybersourceRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount_to_capture, + req, + ))?; + let connector_request = + cybersource::CybersourcePaymentsCaptureRequest::try_from(&connector_router_data)?; let cybersource_payments_request = types::RequestBody::log_and_get_request_body( &connector_request, utils::Encode::::encode_to_string_of_json, @@ -665,7 +746,14 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_request = cybersource::CybersourceRefundRequest::try_from(req)?; + let connector_router_data = cybersource::CybersourceRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.refund_amount, + req, + ))?; + let connector_request = + cybersource::CybersourceRefundRequest::try_from(&connector_router_data)?; let cybersource_refund_request = types::RequestBody::log_and_get_request_body( &connector_request, utils::Encode::::encode_to_string_of_json, diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 9233a95d7dd7..0e81b6b59dff 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -4,10 +4,12 @@ use masking::Secret; use serde::{Deserialize, Serialize}; use crate::{ - connector::utils::{self, AddressDetailsData, PhoneDetailsData, RouterData}, + connector::utils::{ + self, AddressDetailsData, PaymentsAuthorizeRequestData, PaymentsSetupMandateRequestData, + PhoneDetailsData, RouterData, + }, consts, core::errors, - pii::PeekInterface, types::{ self, api::{self, enums as api_enums}, @@ -46,7 +48,81 @@ impl } } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceZeroMandateRequest { + processing_information: ProcessingInformation, + payment_information: PaymentInformation, + order_information: OrderInformationWithBill, + client_reference_information: ClientReferenceInformation, +} + +impl TryFrom<&types::SetupMandateRouterData> for CybersourceZeroMandateRequest { + type Error = error_stack::Report; + fn try_from(item: &types::SetupMandateRouterData) -> Result { + let phone = item.get_billing_phone()?; + let number_with_code = phone.get_number_with_country_code()?; + let email = item.request.get_email()?; + let bill_to = build_bill_to(item.get_billing()?, email, number_with_code)?; + + let order_information = OrderInformationWithBill { + amount_details: Amount { + total_amount: "0".to_string(), + currency: item.request.currency.to_string(), + }, + bill_to: Some(bill_to), + }; + let (action_list, action_token_types, authorization_options) = ( + Some(vec![CybersourceActionsList::TokenCreate]), + Some(vec![CybersourceActionsTokenType::InstrumentIdentifier]), + Some(CybersourceAuthorizationOptions { + initiator: CybersourcePaymentInitiator { + initiator_type: CybersourcePaymentInitiatorTypes::Customer, + credential_stored_on_file: true, + }, + }), + ); + + let processing_information = ProcessingInformation { + capture: Some(false), + capture_options: None, + action_list, + action_token_types, + authorization_options, + commerce_indicator: CybersourceCommerceIndicator::Internet, + }; + + let client_reference_information = ClientReferenceInformation { + code: Some(item.connector_request_reference_id.clone()), + }; + + let payment_information = match item.request.payment_method_data.clone() { + api::PaymentMethodData::Card(ccard) => { + let card = CardDetails::PaymentCard(Card { + number: ccard.card_number, + expiration_month: ccard.card_exp_month, + expiration_year: ccard.card_exp_year, + security_code: ccard.card_cvc, + }); + PaymentInformation { + card, + instrument_identifier: None, + } + } + _ => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Cybersource"), + ))?, + }; + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + }) + } +} + +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CybersourcePaymentsRequest { processing_information: ProcessingInformation, @@ -55,26 +131,82 @@ pub struct CybersourcePaymentsRequest { client_reference_information: ClientReferenceInformation, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ProcessingInformation { - capture: bool, + action_list: Option>, + action_token_types: Option>, + authorization_options: Option, + commerce_indicator: CybersourceCommerceIndicator, + capture: Option, capture_options: Option, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum CybersourceActionsList { + TokenCreate, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum CybersourceActionsTokenType { + InstrumentIdentifier, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceAuthorizationOptions { + initiator: CybersourcePaymentInitiator, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourcePaymentInitiator { + #[serde(rename = "type")] + initiator_type: CybersourcePaymentInitiatorTypes, + credential_stored_on_file: bool, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum CybersourcePaymentInitiatorTypes { + Customer, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum CybersourceCommerceIndicator { + Internet, +} + +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CaptureOptions { capture_sequence_number: u32, total_capture_count: u32, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] pub struct PaymentInformation { - card: Card, + card: CardDetails, + instrument_identifier: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CybersoucreInstrumentIdentifier { + id: String, +} + +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub enum CardDetails { + PaymentCard(Card), + MandateCard(MandateCardDetails), } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct Card { number: cards::CardNumber, @@ -83,27 +215,34 @@ pub struct Card { security_code: Secret, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MandateCardDetails { + expiration_month: Secret, + expiration_year: Secret, +} + +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct OrderInformationWithBill { amount_details: Amount, - bill_to: BillTo, + bill_to: Option, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct OrderInformation { amount_details: Amount, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct Amount { total_amount: String, currency: String, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct BillTo { first_name: Secret, @@ -147,104 +286,135 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> fn try_from( item: &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, ) -> Result { - match item.router_data.request.payment_method_data.clone() { + let phone = item.router_data.get_billing_phone()?; + let number_with_code = phone.get_number_with_country_code()?; + let email = item.router_data.request.get_email()?; + let bill_to = build_bill_to(item.router_data.get_billing()?, email, number_with_code)?; + + let order_information = OrderInformationWithBill { + amount_details: Amount { + total_amount: item.amount.to_owned(), + currency: item.router_data.request.currency.to_string(), + }, + bill_to: Some(bill_to), + }; + let (action_list, action_token_types, authorization_options) = + if item.router_data.request.setup_future_usage.is_some() { + ( + Some(vec![CybersourceActionsList::TokenCreate]), + Some(vec![CybersourceActionsTokenType::InstrumentIdentifier]), + Some(CybersourceAuthorizationOptions { + initiator: CybersourcePaymentInitiator { + initiator_type: CybersourcePaymentInitiatorTypes::Customer, + credential_stored_on_file: true, + }, + }), + ) + } else { + (None, None, None) + }; + + let processing_information = ProcessingInformation { + capture: Some(matches!( + item.router_data.request.capture_method, + Some(enums::CaptureMethod::Automatic) | None + )), + capture_options: None, + action_list, + action_token_types, + authorization_options, + commerce_indicator: CybersourceCommerceIndicator::Internet, + }; + + let client_reference_information = ClientReferenceInformation { + code: Some(item.router_data.connector_request_reference_id.clone()), + }; + let payment_information = match item.router_data.request.payment_method_data.clone() { api::PaymentMethodData::Card(ccard) => { - let phone = item.router_data.get_billing_phone()?; - let phone_number = phone.get_number()?; - let country_code = phone.get_country_code()?; - let number_with_code = - Secret::new(format!("{}{}", country_code, phone_number.peek())); - let email = item - .router_data - .request - .email - .clone() - .ok_or_else(utils::missing_field_err("email"))?; - let bill_to = - build_bill_to(item.router_data.get_billing()?, email, number_with_code)?; - - let order_information = OrderInformationWithBill { - amount_details: Amount { - total_amount: item.amount.to_owned(), - currency: item.router_data.request.currency.to_string().to_uppercase(), - }, - bill_to, - }; - - let payment_information = PaymentInformation { - card: Card { + let instrument_identifier = + item.router_data + .request + .connector_mandate_id() + .map(|mandate_token_id| CybersoucreInstrumentIdentifier { + id: mandate_token_id, + }); + let card = if instrument_identifier.is_some() { + CardDetails::MandateCard(MandateCardDetails { + expiration_month: ccard.card_exp_month, + expiration_year: ccard.card_exp_year, + }) + } else { + CardDetails::PaymentCard(Card { number: ccard.card_number, expiration_month: ccard.card_exp_month, expiration_year: ccard.card_exp_year, security_code: ccard.card_cvc, - }, - }; - - let processing_information = ProcessingInformation { - capture: matches!( - item.router_data.request.capture_method, - Some(enums::CaptureMethod::Automatic) | None - ), - capture_options: None, - }; - - let client_reference_information = ClientReferenceInformation { - code: Some(item.router_data.connector_request_reference_id.clone()), + }) }; - - Ok(Self { - processing_information, - payment_information, - order_information, - client_reference_information, - }) + PaymentInformation { + card, + instrument_identifier, + } } - _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), - } + payments::PaymentMethodData::CardRedirect(_) + | payments::PaymentMethodData::Wallet(_) + | payments::PaymentMethodData::PayLater(_) + | payments::PaymentMethodData::BankRedirect(_) + | payments::PaymentMethodData::BankDebit(_) + | payments::PaymentMethodData::BankTransfer(_) + | payments::PaymentMethodData::Crypto(_) + | payments::PaymentMethodData::MandatePayment + | payments::PaymentMethodData::Reward + | payments::PaymentMethodData::Upi(_) + | payments::PaymentMethodData::Voucher(_) + | payments::PaymentMethodData::GiftCard(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Cybersource"), + ))? + } + }; + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + }) } } -impl TryFrom<&types::PaymentsCaptureRouterData> for CybersourcePaymentsRequest { +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourcePaymentsCaptureRequest { + processing_information: ProcessingInformation, + order_information: OrderInformationWithBill, +} + +impl TryFrom<&CybersourceRouterData<&types::PaymentsCaptureRouterData>> + for CybersourcePaymentsCaptureRequest +{ type Error = error_stack::Report; - fn try_from(value: &types::PaymentsCaptureRouterData) -> Result { + fn try_from( + item: &CybersourceRouterData<&types::PaymentsCaptureRouterData>, + ) -> Result { Ok(Self { processing_information: ProcessingInformation { capture_options: Some(CaptureOptions { capture_sequence_number: 1, total_capture_count: 1, }), - ..Default::default() + action_list: None, + action_token_types: None, + authorization_options: None, + capture: None, + commerce_indicator: CybersourceCommerceIndicator::Internet, }, order_information: OrderInformationWithBill { amount_details: Amount { - total_amount: value.request.amount_to_capture.to_string(), - ..Default::default() + total_amount: item.amount.clone(), + currency: item.router_data.request.currency.to_string(), }, - ..Default::default() - }, - client_reference_information: ClientReferenceInformation { - code: Some(value.connector_request_reference_id.clone()), + bill_to: None, }, - ..Default::default() - }) - } -} - -impl TryFrom<&types::RefundExecuteRouterData> for CybersourcePaymentsRequest { - type Error = error_stack::Report; - fn try_from(value: &types::RefundExecuteRouterData) -> Result { - Ok(Self { - order_information: OrderInformationWithBill { - amount_details: Amount { - total_amount: value.request.refund_amount.to_string(), - currency: value.request.currency.to_string(), - }, - ..Default::default() - }, - client_reference_information: ClientReferenceInformation { - code: Some(value.connector_request_reference_id.clone()), - }, - ..Default::default() }) } } @@ -274,7 +444,7 @@ impl TryFrom<&types::ConnectorAuthType> for CybersourceAuthType { } } } -#[derive(Debug, Default, Clone, Deserialize, Eq, PartialEq)] +#[derive(Debug, Default, Clone, Deserialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum CybersourcePaymentStatus { Authorized, @@ -318,22 +488,29 @@ impl From for enums::RefundStatus { } } -#[derive(Default, Debug, Clone, Deserialize, Eq, PartialEq)] +#[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CybersourcePaymentsResponse { id: String, status: CybersourcePaymentStatus, error_information: Option, client_reference_information: Option, + token_information: Option, } -#[derive(Default, Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ClientReferenceInformation { code: Option, } -#[derive(Default, Debug, Clone, Deserialize, Eq, PartialEq)] +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceTokenInformation { + instrument_identifier: CybersoucreInstrumentIdentifier, +} + +#[derive(Debug, Clone, Deserialize)] pub struct CybersourceErrorInformation { reason: String, message: String, @@ -359,6 +536,13 @@ impl ) -> Result { let item = data.0; let is_capture = data.1; + let mandate_reference = + item.response + .token_information + .map(|token_info| types::MandateReference { + connector_mandate_id: Some(token_info.instrument_identifier.id), + payment_method_id: None, + }); Ok(Self { status: get_payment_status(is_capture, item.response.status.into()), response: match item.response.error_information { @@ -374,7 +558,7 @@ impl item.response.id.clone(), ), redirection_data: None, - mandate_reference: None, + mandate_reference, connector_metadata: None, network_txn_id: None, connector_response_reference_id: item @@ -495,26 +679,28 @@ pub struct Details { pub reason: String, } -#[derive(Debug, Default, Deserialize)] +#[derive(Debug, Deserialize)] pub struct ErrorInformation { pub message: String, pub reason: String, } -#[derive(Default, Debug, Serialize)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CybersourceRefundRequest { order_information: OrderInformation, } -impl TryFrom<&types::RefundsRouterData> for CybersourceRefundRequest { +impl TryFrom<&CybersourceRouterData<&types::RefundsRouterData>> for CybersourceRefundRequest { type Error = error_stack::Report; - fn try_from(item: &types::RefundsRouterData) -> Result { + fn try_from( + item: &CybersourceRouterData<&types::RefundsRouterData>, + ) -> Result { Ok(Self { order_information: OrderInformation { amount_details: Amount { - total_amount: item.request.refund_amount.to_string(), - currency: item.request.currency.to_string(), + total_amount: item.amount.clone(), + currency: item.router_data.request.currency.to_string(), }, }, }) diff --git a/crates/router/src/connector/fiserv/transformers.rs b/crates/router/src/connector/fiserv/transformers.rs index f8d88d08c6ba..5add9b79e3f9 100644 --- a/crates/router/src/connector/fiserv/transformers.rs +++ b/crates/router/src/connector/fiserv/transformers.rs @@ -3,7 +3,10 @@ use error_stack::ResultExt; use serde::{Deserialize, Serialize}; use crate::{ - connector::utils::{self, PaymentsCancelRequestData, PaymentsSyncRequestData, RouterData}, + connector::utils::{ + self, CardData as CardDataUtil, PaymentsCancelRequestData, PaymentsSyncRequestData, + RouterData, + }, core::errors, pii::Secret, types::{self, api, storage::enums}, @@ -41,7 +44,7 @@ impl } } -#[derive(Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct FiservPaymentsRequest { amount: Amount, @@ -51,7 +54,7 @@ pub struct FiservPaymentsRequest { transaction_interaction: TransactionInteraction, } -#[derive(Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] #[serde(tag = "sourceType")] pub enum Source { PaymentCard { @@ -65,7 +68,7 @@ pub enum Source { }, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CardData { card_data: cards::CardNumber, @@ -74,7 +77,7 @@ pub struct CardData { security_code: Secret, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct GooglePayToken { signature: String, @@ -82,14 +85,14 @@ pub struct GooglePayToken { protocol_version: String, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize)] pub struct Amount { #[serde(serialize_with = "utils::str_to_f32")] total: String, currency: String, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct TransactionDetails { capture_flag: Option, @@ -97,14 +100,14 @@ pub struct TransactionDetails { merchant_transaction_id: String, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct MerchantDetails { merchant_id: Secret, terminal_id: Option, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct TransactionInteraction { origin: TransactionInteractionOrigin, @@ -112,19 +115,19 @@ pub struct TransactionInteraction { pos_condition_code: TransactionInteractionPosConditionCode, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize)] #[serde(rename_all = "UPPERCASE")] pub enum TransactionInteractionOrigin { #[default] Ecom, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum TransactionInteractionEciIndicator { #[default] ChannelEncrypted, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum TransactionInteractionPosConditionCode { #[default] @@ -174,7 +177,7 @@ impl TryFrom<&FiservRouterData<&types::PaymentsAuthorizeRouterData>> for FiservP let card = CardData { card_data: ccard.card_number.clone(), expiration_month: ccard.card_exp_month.clone(), - expiration_year: ccard.card_exp_year.clone(), + expiration_year: ccard.get_expiry_year_4_digit(), security_code: ccard.card_cvc.clone(), }; Source::PaymentCard { card } @@ -219,7 +222,7 @@ impl TryFrom<&types::ConnectorAuthType> for FiservAuthType { } } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct FiservCancelRequest { transaction_details: TransactionDetails, @@ -406,7 +409,7 @@ impl TryFrom> for FiservCap } } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct FiservSyncRequest { merchant_details: MerchantDetails, diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 8b20332ce5ed..a098cef5b778 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -322,6 +322,7 @@ impl PaymentsCaptureRequestData for types::PaymentsCaptureData { pub trait PaymentsSetupMandateRequestData { fn get_browser_info(&self) -> Result; + fn get_email(&self) -> Result; } impl PaymentsSetupMandateRequestData for types::SetupMandateRequestData { @@ -330,6 +331,9 @@ impl PaymentsSetupMandateRequestData for types::SetupMandateRequestData { .clone() .ok_or_else(missing_field_err("browser_info")) } + fn get_email(&self) -> Result { + self.email.clone().ok_or_else(missing_field_err("email")) + } } pub trait PaymentsAuthorizeRequestData { fn is_auto_capture(&self) -> Result; @@ -869,6 +873,7 @@ impl CryptoData for api::CryptoData { pub trait PhoneDetailsData { fn get_number(&self) -> Result, Error>; fn get_country_code(&self) -> Result; + fn get_number_with_country_code(&self) -> Result, Error>; } impl PhoneDetailsData for api::PhoneDetails { @@ -882,6 +887,11 @@ impl PhoneDetailsData for api::PhoneDetails { .clone() .ok_or_else(missing_field_err("billing.phone.number")) } + fn get_number_with_country_code(&self) -> Result, Error> { + let number = self.get_number()?; + let country_code = self.get_country_code()?; + Ok(Secret::new(format!("{}{}", country_code, number.peek()))) + } } pub trait AddressDetailsData { diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 8c13b05836f1..1c40ef81f497 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1663,10 +1663,24 @@ where .unwrap_or(false); let payment_data_and_tokenization_action = match connector { - Some(_) if is_mandate => ( - payment_data.to_owned(), - TokenizationAction::SkipConnectorTokenization, - ), + Some(connector_name) if is_mandate => { + if connector_name == *"cybersource" { + let (_operation, payment_method_data) = operation + .to_domain()? + .make_pm_data( + state, + payment_data, + validate_result.storage_scheme, + merchant_key_store, + ) + .await?; + payment_data.payment_method_data = payment_method_data; + } + ( + payment_data.to_owned(), + TokenizationAction::SkipConnectorTokenization, + ) + } Some(connector) if is_operation_confirm(&operation) => { let payment_method = &payment_data .payment_attempt @@ -1749,7 +1763,7 @@ where }; (payment_data.to_owned(), connector_tokenization_action) } - _ => ( + Some(_) | None => ( payment_data.to_owned(), TokenizationAction::SkipConnectorTokenization, ), From c6a5a8574825dc333602f4f1cee7e26969eab030 Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Wed, 22 Nov 2023 01:13:01 +0530 Subject: [PATCH 061/146] chore: address Rust 1.74 clippy lints (#2942) --- crates/router/src/configs/defaults.rs | 4 ++-- crates/router/src/connector/powertranz/transformers.rs | 4 ++-- crates/router/src/core/payment_methods/cards.rs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index b71e2aad5b5d..a0da9c88ef35 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -4356,8 +4356,8 @@ impl Default for super::settings::ApiKeys { #[cfg(feature = "kms")] kms_encrypted_hash_key: KmsValue::default(), - /// Hex-encoded 32-byte long (64 characters long when hex-encoded) key used for calculating - /// hashes of API keys + // Hex-encoded 32-byte long (64 characters long when hex-encoded) key used for calculating + // hashes of API keys #[cfg(not(feature = "kms"))] hash_key: String::new(), diff --git a/crates/router/src/connector/powertranz/transformers.rs b/crates/router/src/connector/powertranz/transformers.rs index 83bca662ec21..5a8c49bd8ee1 100644 --- a/crates/router/src/connector/powertranz/transformers.rs +++ b/crates/router/src/connector/powertranz/transformers.rs @@ -150,8 +150,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for ExtendedData { fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { Ok(Self { three_d_secure: ThreeDSecure { - /// Merchants preferred sized of challenge window presented to cardholder. - /// 5 maps to 100% of challenge window size + // Merchants preferred sized of challenge window presented to cardholder. + // 5 maps to 100% of challenge window size challenge_window_size: 5, }, merchant_response_url: item.request.get_complete_authorize_url()?, diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index ad42a8579127..60fd3f315ea6 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -156,7 +156,7 @@ pub async fn add_payment_method( .await?; } - Ok(resp).map(services::ApplicationResponse::Json) + Ok(services::ApplicationResponse::Json(resp)) } #[instrument(skip_all)] From ce10579a729fe4a7d4ab9f1a4cbd38c3ca00e90b Mon Sep 17 00:00:00 2001 From: ivor-juspay <138492857+ivor-juspay@users.noreply.github.com> Date: Wed, 22 Nov 2023 12:55:51 +0530 Subject: [PATCH 062/146] feat(api_event_errors): error field in APIEvents (#2808) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: harsh-sharma-juspay <125131007+harsh-sharma-juspay@users.noreply.github.com> --- crates/api_models/src/errors/types.rs | 6 ++++-- crates/router/src/core/webhooks.rs | 1 + crates/router/src/events/api_logs.rs | 3 +++ crates/router/src/services/api.rs | 17 +++++++++++++++-- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/crates/api_models/src/errors/types.rs b/crates/api_models/src/errors/types.rs index 365be676f167..5f303f93c56b 100644 --- a/crates/api_models/src/errors/types.rs +++ b/crates/api_models/src/errors/types.rs @@ -1,6 +1,7 @@ use std::borrow::Cow; use reqwest::StatusCode; +use serde::Serialize; #[derive(Debug, serde::Serialize)] pub enum ErrorType { @@ -78,7 +79,8 @@ pub struct Extra { pub reason: Option, } -#[derive(Debug, Clone)] +#[derive(Serialize, Debug, Clone)] +#[serde(tag = "type", content = "value")] pub enum ApiErrorResponse { Unauthorized(ApiError), ForbiddenCommonResource(ApiError), @@ -88,7 +90,7 @@ pub enum ApiErrorResponse { Unprocessable(ApiError), InternalServerError(ApiError), NotImplemented(ApiError), - ConnectorError(ApiError, StatusCode), + ConnectorError(ApiError, #[serde(skip_serializing)] StatusCode), NotFound(ApiError), MethodNotAllowed(ApiError), BadRequest(ApiError), diff --git a/crates/router/src/core/webhooks.rs b/crates/router/src/core/webhooks.rs index 9bbe35ba2a9d..67154ae33aef 100644 --- a/crates/router/src/core/webhooks.rs +++ b/crates/router/src/core/webhooks.rs @@ -913,6 +913,7 @@ pub async fn webhooks_wrapper, url_path: String, response: Option, + error: Option, #[serde(flatten)] event_type: ApiEventsType, hs_latency: Option, @@ -52,6 +53,7 @@ impl ApiEvent { response: Option, hs_latency: Option, auth_type: AuthenticationType, + error: Option, event_type: ApiEventsType, http_req: &HttpRequest, ) -> Self { @@ -64,6 +66,7 @@ impl ApiEvent { request, response, auth_type, + error, ip_addr: http_req .connection_info() .realip_remote_addr() diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 0a8b84ffd11c..aae17195517d 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -769,7 +769,7 @@ where T: Debug + Serialize + ApiEventMetric, A: AppStateInfo + Clone, E: ErrorSwitch + error_stack::Context, - OErr: ResponseError + error_stack::Context, + OErr: ResponseError + error_stack::Context + Serialize, errors::ApiErrorResponse: ErrorSwitch, { let request_id = RequestId::extract(request) @@ -826,7 +826,9 @@ where .as_millis(); let mut serialized_response = None; + let mut error = None; let mut overhead_latency = None; + let status_code = match output.as_ref() { Ok(res) => { if let ApplicationResponse::Json(data) = res { @@ -854,7 +856,17 @@ where metrics::request::track_response_status_code(res) } - Err(err) => err.current_context().status_code().as_u16().into(), + Err(err) => { + error.replace( + serde_json::to_value(err.current_context()) + .into_report() + .attach_printable("Failed to serialize json response") + .change_context(errors::ApiErrorResponse::InternalServerError.switch()) + .ok() + .into(), + ); + err.current_context().status_code().as_u16().into() + } }; let api_event = ApiEvent::new( @@ -866,6 +878,7 @@ where serialized_response, overhead_latency, auth_type, + error, event_type.unwrap_or(ApiEventsType::Miscellaneous), request, ); From 037e310aab5fac90ba33cdff2acda2f031261a6c Mon Sep 17 00:00:00 2001 From: Shanks Date: Wed, 22 Nov 2023 12:59:51 +0530 Subject: [PATCH 063/146] ci: update CODEOWNERS with hyperswitch-routing modules (#2933) --- .github/CODEOWNERS | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 638d5540d3d6..3024477bac20 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -33,6 +33,17 @@ crates/router/src/compatibility/ @juspay/hyperswitch-compatibility crates/router/src/core/ @juspay/hyperswitch-core +crates/api_models/src/routing.rs @juspay/hyperswitch-routing +crates/euclid @juspay/hyperswitch-routing +crates/euclid_macros @juspay/hyperswitch-routing +crates/euclid_wasm @juspay/hyperswitch-routing +crates/kgraph_utils @juspay/hyperswitch-routing +crates/router/src/routes/routing.rs @juspay/hyperswitch-routing +crates/router/src/core/routing @juspay/hyperswitch-routing +crates/router/src/core/routing.rs @juspay/hyperswitch-routing +crates/router/src/core/payments/routing @juspay/hyperswitch-routing +crates/router/src/core/payments/routing.rs @juspay/hyperswitch-routing + crates/router/src/scheduler/ @juspay/hyperswitch-process-tracker Dockerfile @juspay/hyperswitch-infra From b441a1f2f9d9d84601cf78a6e39145e8fb847593 Mon Sep 17 00:00:00 2001 From: Sahkal Poddar Date: Wed, 22 Nov 2023 15:37:01 +0530 Subject: [PATCH 064/146] feat(router): add list payment link support (#2805) Co-authored-by: Sahkal Poddar Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Kashif <46213975+kashif-m@users.noreply.github.com> Co-authored-by: Kashif Co-authored-by: Gnanasundari24 <118818938+Gnanasundari24@users.noreply.github.com> --- README.md | 1 - crates/api_models/src/events.rs | 1 + crates/api_models/src/payments.rs | 63 +++++++++++++++-- crates/common_utils/src/consts.rs | 3 + crates/diesel_models/src/payment_link.rs | 2 + crates/diesel_models/src/schema.rs | 2 + crates/router/src/core/payment_link.rs | 45 +++++++++++-- .../src/core/payment_link/payment_link.html | 7 +- .../payments/operations/payment_create.rs | 4 ++ crates/router/src/db/payment_link.rs | 32 ++++++++- crates/router/src/lib.rs | 2 +- crates/router/src/routes/app.rs | 10 ++- crates/router/src/routes/lock_utils.rs | 5 +- crates/router/src/routes/payment_link.rs | 43 ++++++++++++ crates/router/src/types/api.rs | 5 +- crates/router/src/types/api/payment_link.rs | 29 ++++++++ .../router/src/types/storage/payment_link.rs | 67 ++++++++++++++++++- crates/router/src/types/transformers.rs | 12 ++-- crates/router_env/src/logger/types.rs | 2 + .../down.sql | 2 +- .../down.sql | 2 + .../up.sql | 2 + openapi/openapi_spec.json | 33 +++++---- 23 files changed, 330 insertions(+), 44 deletions(-) create mode 100644 crates/router/src/types/api/payment_link.rs create mode 100644 migrations/2023-11-06-065213_add_description_to_payment_link/down.sql create mode 100644 migrations/2023-11-06-065213_add_description_to_payment_link/up.sql diff --git a/README.md b/README.md index bc528da9bbf5..edc8cae5cf8e 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,6 @@ The fastest and easiest way to try hyperswitch is via our CDK scripts    - 2. Sign-in to your AWS console. 3. Follow the instructions provided on the console to successfully deploy Hyperswitch diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index 0ce7638b5ed1..782c02be7a3a 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -36,6 +36,7 @@ impl_misc_api_event_type!( MandateResponse, MandateRevokedResponse, RetrievePaymentLinkRequest, + PaymentLinkListConstraints, MandateId, DisputeListConstraints, RetrieveApiKeyResponse, diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index c427088d688d..508eeb8d7310 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -3194,18 +3194,17 @@ pub struct PaymentLinkResponse { #[derive(Clone, Debug, serde::Serialize, ToSchema)] pub struct RetrievePaymentLinkResponse { pub payment_link_id: String, - pub payment_id: String, pub merchant_id: String, pub link_to_pay: String, pub amount: i64, - #[schema(value_type = Option, example = "USD")] - pub currency: Option, #[serde(with = "common_utils::custom_serde::iso8601")] pub created_at: PrimitiveDateTime, - #[serde(with = "common_utils::custom_serde::iso8601")] - pub last_modified_at: PrimitiveDateTime, #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub link_expiry: Option, + pub description: Option, + pub status: String, + #[schema(value_type = Option)] + pub currency: Option, } #[derive(Clone, Debug, serde::Deserialize, ToSchema, serde::Serialize)] @@ -3230,3 +3229,57 @@ pub struct PaymentLinkDetails { pub max_items_visible_after_collapse: i8, pub sdk_theme: Option, } + +#[derive(Clone, Debug, serde::Deserialize, ToSchema, serde::Serialize)] +#[serde(deny_unknown_fields)] + +pub struct PaymentLinkListConstraints { + /// limit on the number of objects to return + pub limit: Option, + + /// The time at which payment link is created + #[schema(example = "2022-09-10T10:11:12Z")] + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub created: Option, + + /// Time less than the payment link created time + #[schema(example = "2022-09-10T10:11:12Z")] + #[serde( + default, + with = "common_utils::custom_serde::iso8601::option", + rename = "created.lt" + )] + pub created_lt: Option, + + /// Time greater than the payment link created time + #[schema(example = "2022-09-10T10:11:12Z")] + #[serde( + default, + with = "common_utils::custom_serde::iso8601::option", + rename = "created.gt" + )] + pub created_gt: Option, + + /// Time less than or equals to the payment link created time + #[schema(example = "2022-09-10T10:11:12Z")] + #[serde( + default, + with = "common_utils::custom_serde::iso8601::option", + rename = "created.lte" + )] + pub created_lte: Option, + + /// Time greater than or equals to the payment link created time + #[schema(example = "2022-09-10T10:11:12Z")] + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + #[serde(rename = "created.gte")] + pub created_gte: Option, +} + +#[derive(Clone, Debug, serde::Serialize, ToSchema)] +pub struct PaymentLinkListResponse { + /// The number of payment links included in the list + pub size: usize, + // The list of payment link response objects + pub data: Vec, +} diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index 60756192d66e..7f9533d7eadd 100644 --- a/crates/common_utils/src/consts.rs +++ b/crates/common_utils/src/consts.rs @@ -24,6 +24,9 @@ pub const PAYMENTS_LIST_MAX_LIMIT_V1: u32 = 100; /// Maximum limit for payments list post api with filters pub const PAYMENTS_LIST_MAX_LIMIT_V2: u32 = 20; +/// Maximum limit for payment link list get api +pub const PAYMENTS_LINK_LIST_LIMIT: u32 = 100; + /// surcharge percentage maximum precision length pub const SURCHARGE_PERCENTAGE_PRECISION_LENGTH: u8 = 2; diff --git a/crates/diesel_models/src/payment_link.rs b/crates/diesel_models/src/payment_link.rs index 264cc915b35a..999a6767d8f3 100644 --- a/crates/diesel_models/src/payment_link.rs +++ b/crates/diesel_models/src/payment_link.rs @@ -22,6 +22,7 @@ pub struct PaymentLink { pub fulfilment_time: Option, pub custom_merchant_name: Option, pub payment_link_config: Option, + pub description: Option, } #[derive( @@ -51,4 +52,5 @@ pub struct PaymentLinkNew { pub fulfilment_time: Option, pub custom_merchant_name: Option, pub payment_link_config: Option, + pub description: Option, } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 2ce4f2b6d9d4..33400635f052 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -678,6 +678,8 @@ diesel::table! { #[max_length = 64] custom_merchant_name -> Nullable, payment_link_config -> Nullable, + #[max_length = 255] + description -> Nullable, } } diff --git a/crates/router/src/core/payment_link.rs b/crates/router/src/core/payment_link.rs index 89d345b28674..07fdf4ae4072 100644 --- a/crates/router/src/core/payment_link.rs +++ b/crates/router/src/core/payment_link.rs @@ -6,7 +6,9 @@ use common_utils::{ ext_traits::{OptionExt, ValueExt}, }; use error_stack::{IntoReport, ResultExt}; +use futures::future; use masking::{PeekInterface, Secret}; +use time::PrimitiveDateTime; use super::errors::{self, RouterResult, StorageErrorExt}; use crate::{ @@ -14,7 +16,10 @@ use crate::{ errors::RouterResponse, routes::AppState, services, - types::{domain, storage::enums as storage_enums, transformers::ForeignFrom}, + types::{ + api::payment_link::PaymentLinkResponseExt, domain, storage::enums as storage_enums, + transformers::ForeignFrom, + }, }; pub async fn retrieve_payment_link( @@ -27,8 +32,12 @@ pub async fn retrieve_payment_link( .await .to_not_found_response(errors::ApiErrorResponse::PaymentLinkNotFound)?; - let response = - api_models::payments::RetrievePaymentLinkResponse::foreign_from(payment_link_object); + let status = check_payment_link_status(payment_link_object.fulfilment_time); + + let response = api_models::payments::RetrievePaymentLinkResponse::foreign_from(( + payment_link_object, + status, + )); Ok(services::ApplicationResponse::Json(response)) } @@ -62,7 +71,7 @@ pub async fn intiate_payment_link_flow( storage_enums::IntentStatus::RequiresCapture, storage_enums::IntentStatus::RequiresMerchantAction, ], - "create payment link", + "use payment link for", )?; let payment_link = db @@ -197,6 +206,34 @@ fn validate_sdk_requirements( Ok((pub_key, currency, client_secret)) } +pub async fn list_payment_link( + state: AppState, + merchant: domain::MerchantAccount, + constraints: api_models::payments::PaymentLinkListConstraints, +) -> RouterResponse> { + let db = state.store.as_ref(); + let payment_link = db + .list_payment_link_by_merchant_id(&merchant.merchant_id, constraints) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to retrieve payment link")?; + let payment_link_list = future::try_join_all(payment_link.into_iter().map(|payment_link| { + api_models::payments::RetrievePaymentLinkResponse::from_db_payment_link(payment_link) + })) + .await?; + Ok(services::ApplicationResponse::Json(payment_link_list)) +} + +pub fn check_payment_link_status(fulfillment_time: Option) -> String { + let curr_time = Some(common_utils::date_time::now()); + + if curr_time > fulfillment_time { + "expired".to_string() + } else { + "active".to_string() + } +} + fn validate_order_details( order_details: Option>>, ) -> Result< diff --git a/crates/router/src/core/payment_link/payment_link.html b/crates/router/src/core/payment_link/payment_link.html index abacf0998f67..0ca4abd340d6 100644 --- a/crates/router/src/core/payment_link/payment_link.html +++ b/crates/router/src/core/payment_link/payment_link.html @@ -46,6 +46,7 @@ width: 100%; height: 100%; max-width: 1900px; + overflow: scroll; } #hyper-footer { @@ -418,6 +419,7 @@ margin-top: 20px; border-radius: 3px; border: 1px solid #e6e6e6; + width: 90vw; } .hyper-checkout-status-item { @@ -432,12 +434,15 @@ } .hyper-checkout-item-header { - width: 15ch; + min-width: 13ch; font-size: 12px; } .hyper-checkout-item-value { font-size: 12px; + overflow-x: hidden; + overflow-y: auto; + word-wrap: break-word; } @keyframes loading { diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 845915cc332c..eee937071d6b 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -75,6 +75,7 @@ impl db, state, amount, + request.description.clone(), ) .await? } else { @@ -779,6 +780,7 @@ pub fn payments_create_request_validation( Ok((amount, currency)) } +#[allow(clippy::too_many_arguments)] async fn create_payment_link( request: &api::PaymentsRequest, payment_link_object: api_models::payments::PaymentLinkObject, @@ -787,6 +789,7 @@ async fn create_payment_link( db: &dyn StorageInterface, state: &AppState, amount: api::Amount, + description: Option, ) -> RouterResult> { let created_at @ last_modified_at = Some(common_utils::date_time::now()); let domain = if let Some(domain_name) = payment_link_object.merchant_custom_domain_name { @@ -817,6 +820,7 @@ async fn create_payment_link( created_at, last_modified_at, fulfilment_time: payment_link_object.link_expiry, + description, payment_link_config, custom_merchant_name: payment_link_object.custom_merchant_name, }; diff --git a/crates/router/src/db/payment_link.rs b/crates/router/src/db/payment_link.rs index 38b59b1d60de..5dc9871e707e 100644 --- a/crates/router/src/db/payment_link.rs +++ b/crates/router/src/db/payment_link.rs @@ -1,10 +1,11 @@ use error_stack::IntoReport; -use super::{MockDb, Store}; use crate::{ connection, core::errors::{self, CustomResult}, - types::storage, + db::MockDb, + services::Store, + types::storage::{self, PaymentLinkDbExt}, }; #[async_trait::async_trait] @@ -18,6 +19,12 @@ pub trait PaymentLinkInterface { &self, _payment_link: storage::PaymentLinkNew, ) -> CustomResult; + + async fn list_payment_link_by_merchant_id( + &self, + merchant_id: &str, + payment_link_constraints: api_models::payments::PaymentLinkListConstraints, + ) -> CustomResult, errors::StorageError>; } #[async_trait::async_trait] @@ -44,6 +51,18 @@ impl PaymentLinkInterface for Store { .map_err(Into::into) .into_report() } + + async fn list_payment_link_by_merchant_id( + &self, + merchant_id: &str, + payment_link_constraints: api_models::payments::PaymentLinkListConstraints, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_read(self).await?; + storage::PaymentLink::filter_by_constraints(&conn, merchant_id, payment_link_constraints) + .await + .map_err(Into::into) + .into_report() + } } #[async_trait::async_trait] @@ -63,4 +82,13 @@ impl PaymentLinkInterface for MockDb { // TODO: Implement function for `MockDb`x Err(errors::StorageError::MockDbError)? } + + async fn list_payment_link_by_merchant_id( + &self, + _merchant_id: &str, + _payment_link_constraints: api_models::payments::PaymentLinkListConstraints, + ) -> CustomResult, errors::StorageError> { + // TODO: Implement function for `MockDb`x + Err(errors::StorageError::MockDbError)? + } } diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 58d77d9e02f4..0bc8e492c40c 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -133,7 +133,6 @@ pub fn mk_app( .service(routes::PaymentMethods::server(state.clone())) .service(routes::EphemeralKey::server(state.clone())) .service(routes::Webhooks::server(state.clone())) - .service(routes::PaymentLink::server(state.clone())); } #[cfg(feature = "olap")] @@ -147,6 +146,7 @@ pub fn mk_app( .service(routes::Routing::server(state.clone())) .service(routes::LockerMigrate::server(state.clone())) .service(routes::Gsm::server(state.clone())) + .service(routes::PaymentLink::server(state.clone())) .service(routes::User::server(state.clone())) } diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 79801e8e64f0..96bb47ea4e97 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -19,8 +19,11 @@ use super::routing as cloud_routing; #[cfg(all(feature = "olap", feature = "kms"))] use super::verification::{apple_pay_merchant_registration, retrieve_apple_pay_verified_domains}; #[cfg(feature = "olap")] -use super::{admin::*, api_keys::*, disputes::*, files::*, gsm::*, locker_migration, user::*}; -use super::{cache::*, health::*, payment_link::*}; +use super::{ + admin::*, api_keys::*, disputes::*, files::*, gsm::*, locker_migration, payment_link::*, + user::*, +}; +use super::{cache::*, health::*}; #[cfg(any(feature = "olap", feature = "oltp"))] use super::{configs::*, customers::*, mandates::*, payments::*, refunds::*}; #[cfg(feature = "oltp")] @@ -675,11 +678,12 @@ impl Cache { } pub struct PaymentLink; - +#[cfg(feature = "olap")] impl PaymentLink { pub fn server(state: AppState) -> Scope { web::scope("/payment_link") .app_data(web::Data::new(state)) + .service(web::resource("/list").route(web::post().to(payments_link_list))) .service( web::resource("/{payment_link_id}").route(web::get().to(payment_link_retrieve)), ) diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 84b00867b98d..a9cf7b44a73d 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -132,9 +132,12 @@ impl From for ApiIdentifier { | Flow::BusinessProfileDelete | Flow::BusinessProfileList => Self::Business, + Flow::PaymentLinkRetrieve | Flow::PaymentLinkInitiate | Flow::PaymentLinkList => { + Self::PaymentLink + } + Flow::Verification => Self::Verification, - Flow::PaymentLinkInitiate | Flow::PaymentLinkRetrieve => Self::PaymentLink, Flow::RustLockerMigration => Self::RustLockerMigration, Flow::GsmRuleCreate | Flow::GsmRuleRetrieve diff --git a/crates/router/src/routes/payment_link.rs b/crates/router/src/routes/payment_link.rs index 7d6bf1a05f09..4c26ea71f7d5 100644 --- a/crates/router/src/routes/payment_link.rs +++ b/crates/router/src/routes/payment_link.rs @@ -80,3 +80,46 @@ pub async fn initiate_payment_link( )) .await } + +/// Payment Link - List +/// +/// To list the payment links +#[utoipa::path( + get, + path = "/payment_link/list", + params( + ("limit" = Option, Query, description = "The maximum number of payment_link Objects to include in the response"), + ("connector" = Option, Query, description = "The connector linked to payment_link"), + ("created_time" = Option, Query, description = "The time at which payment_link is created"), + ("created_time.lt" = Option, Query, description = "Time less than the payment_link created time"), + ("created_time.gt" = Option, Query, description = "Time greater than the payment_link created time"), + ("created_time.lte" = Option, Query, description = "Time less than or equals to the payment_link created time"), + ("created_time.gte" = Option, Query, description = "Time greater than or equals to the payment_link created time"), + ), + responses( + (status = 200, description = "The payment link list was retrieved successfully"), + (status = 401, description = "Unauthorized request") + ), + tag = "Payment Link", + operation_id = "List all Payment links", + security(("api_key" = [])) +)] +#[instrument(skip_all, fields(flow = ?Flow::PaymentLinkList))] +pub async fn payments_link_list( + state: web::Data, + req: actix_web::HttpRequest, + payload: web::Query, +) -> impl Responder { + let flow = Flow::PaymentLinkList; + let payload = payload.into_inner(); + api::server_wrap( + flow, + state, + &req, + payload, + |state, auth, payload| list_payment_link(state, auth.merchant_account, payload), + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + api_locking::LockAction::NotApplicable, + ) + .await +} diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index dc615c4e41fa..b7d2fc8db33e 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -7,6 +7,7 @@ pub mod enums; pub mod ephemeral_key; pub mod files; pub mod mandates; +pub mod payment_link; pub mod payment_methods; pub mod payments; pub mod payouts; @@ -20,8 +21,8 @@ use api_models::payment_methods::{SurchargeDetailsResponse, SurchargeMetadata}; use error_stack::{report, IntoReport, ResultExt}; pub use self::{ - admin::*, api_keys::*, configs::*, customers::*, disputes::*, files::*, payment_methods::*, - payments::*, payouts::*, refunds::*, webhooks::*, + admin::*, api_keys::*, configs::*, customers::*, disputes::*, files::*, payment_link::*, + payment_methods::*, payments::*, payouts::*, refunds::*, webhooks::*, }; use super::ErrorResponse; use crate::{ diff --git a/crates/router/src/types/api/payment_link.rs b/crates/router/src/types/api/payment_link.rs new file mode 100644 index 000000000000..e56af6b4aec4 --- /dev/null +++ b/crates/router/src/types/api/payment_link.rs @@ -0,0 +1,29 @@ +pub use api_models::payments::RetrievePaymentLinkResponse; + +use crate::{ + core::{errors::RouterResult, payment_link}, + types::storage::{self}, +}; + +#[async_trait::async_trait] +pub(crate) trait PaymentLinkResponseExt: Sized { + async fn from_db_payment_link(payment_link: storage::PaymentLink) -> RouterResult; +} + +#[async_trait::async_trait] +impl PaymentLinkResponseExt for RetrievePaymentLinkResponse { + async fn from_db_payment_link(payment_link: storage::PaymentLink) -> RouterResult { + let status = payment_link::check_payment_link_status(payment_link.fulfilment_time); + Ok(Self { + link_to_pay: payment_link.link_to_pay, + payment_link_id: payment_link.payment_link_id, + amount: payment_link.amount, + description: payment_link.description, + created_at: payment_link.created_at, + merchant_id: payment_link.merchant_id, + link_expiry: payment_link.fulfilment_time, + currency: payment_link.currency, + status, + }) + } +} diff --git a/crates/router/src/types/storage/payment_link.rs b/crates/router/src/types/storage/payment_link.rs index 1fa2465e5131..4dd9e06b4b41 100644 --- a/crates/router/src/types/storage/payment_link.rs +++ b/crates/router/src/types/storage/payment_link.rs @@ -1 +1,66 @@ -pub use diesel_models::payment_link::{PaymentLink, PaymentLinkNew}; +use async_bb8_diesel::AsyncRunQueryDsl; +use diesel::{associations::HasTable, ExpressionMethods, QueryDsl}; +pub use diesel_models::{ + payment_link::{PaymentLink, PaymentLinkNew}, + schema::payment_link::dsl, +}; +use error_stack::{IntoReport, ResultExt}; + +use crate::{ + connection::PgPooledConn, + core::errors::{self, CustomResult}, + logger, +}; +#[async_trait::async_trait] + +pub trait PaymentLinkDbExt: Sized { + async fn filter_by_constraints( + conn: &PgPooledConn, + merchant_id: &str, + payment_link_list_constraints: api_models::payments::PaymentLinkListConstraints, + ) -> CustomResult, errors::DatabaseError>; +} + +#[async_trait::async_trait] +impl PaymentLinkDbExt for PaymentLink { + async fn filter_by_constraints( + conn: &PgPooledConn, + merchant_id: &str, + payment_link_list_constraints: api_models::payments::PaymentLinkListConstraints, + ) -> CustomResult, errors::DatabaseError> { + let mut filter = ::table() + .filter(dsl::merchant_id.eq(merchant_id.to_owned())) + .order(dsl::created_at.desc()) + .into_boxed(); + + if let Some(created_time) = payment_link_list_constraints.created { + filter = filter.filter(dsl::created_at.eq(created_time)); + } + if let Some(created_time_lt) = payment_link_list_constraints.created_lt { + filter = filter.filter(dsl::created_at.lt(created_time_lt)); + } + if let Some(created_time_gt) = payment_link_list_constraints.created_gt { + filter = filter.filter(dsl::created_at.gt(created_time_gt)); + } + if let Some(created_time_lte) = payment_link_list_constraints.created_lte { + filter = filter.filter(dsl::created_at.le(created_time_lte)); + } + if let Some(created_time_gte) = payment_link_list_constraints.created_gte { + filter = filter.filter(dsl::created_at.ge(created_time_gte)); + } + if let Some(limit) = payment_link_list_constraints.limit { + filter = filter.limit(limit); + } + + logger::debug!(query = %diesel::debug_query::(&filter).to_string()); + + filter + .get_results_async(conn) + .await + .into_report() + // The query built here returns an empty Vec when no records are found, and if any error does occur, + // it would be an internal database error, due to which we are raising a DatabaseError::Unknown error + .change_context(errors::DatabaseError::Others) + .attach_printable("Error filtering payment link by specified constraints") + } +} diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index b73ba0964fbf..45aad93371e2 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -992,18 +992,20 @@ impl } } -impl ForeignFrom for api_models::payments::RetrievePaymentLinkResponse { - fn foreign_from(payment_link_object: storage::PaymentLink) -> Self { +impl ForeignFrom<(storage::PaymentLink, String)> + for api_models::payments::RetrievePaymentLinkResponse +{ + fn foreign_from((payment_link_object, status): (storage::PaymentLink, String)) -> Self { Self { payment_link_id: payment_link_object.payment_link_id, - payment_id: payment_link_object.payment_id, merchant_id: payment_link_object.merchant_id, link_to_pay: payment_link_object.link_to_pay, amount: payment_link_object.amount, - currency: payment_link_object.currency, created_at: payment_link_object.created_at, - last_modified_at: payment_link_object.last_modified_at, link_expiry: payment_link_object.fulfilment_time, + description: payment_link_object.description, + currency: payment_link_object.currency, + status, } } } diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index f6d61f550840..178f837fce18 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -223,6 +223,8 @@ pub enum Flow { PaymentLinkRetrieve, /// payment Link Initiate flow PaymentLinkInitiate, + /// Payment Link List flow + PaymentLinkList, /// Create a business profile BusinessProfileCreate, /// Update a business profile diff --git a/migrations/2023-10-31-070509_add_payment_link_config_in_payment_link_db/down.sql b/migrations/2023-10-31-070509_add_payment_link_config_in_payment_link_db/down.sql index b5ffba726937..f16e2800598f 100644 --- a/migrations/2023-10-31-070509_add_payment_link_config_in_payment_link_db/down.sql +++ b/migrations/2023-10-31-070509_add_payment_link_config_in_payment_link_db/down.sql @@ -1,2 +1,2 @@ -- This file should undo anything in `up.sql` -ALTER TABLE payment_link DROP COLUMN IF EXISTS payment_link_config; \ No newline at end of file +ALTER TABLE payment_link DROP COLUMN IF EXISTS payment_link_config; diff --git a/migrations/2023-11-06-065213_add_description_to_payment_link/down.sql b/migrations/2023-11-06-065213_add_description_to_payment_link/down.sql new file mode 100644 index 000000000000..b184a2ce3dd7 --- /dev/null +++ b/migrations/2023-11-06-065213_add_description_to_payment_link/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_link DROP COLUMN IF EXISTS description; \ No newline at end of file diff --git a/migrations/2023-11-06-065213_add_description_to_payment_link/up.sql b/migrations/2023-11-06-065213_add_description_to_payment_link/up.sql new file mode 100644 index 000000000000..65a074063ed3 --- /dev/null +++ b/migrations/2023-11-06-065213_add_description_to_payment_link/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER table payment_link ADD COLUMN IF NOT EXISTS description VARCHAR (255); \ No newline at end of file diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 9ca4dea4a1a8..df9df43a43ee 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -11306,20 +11306,16 @@ "type": "object", "required": [ "payment_link_id", - "payment_id", "merchant_id", "link_to_pay", "amount", "created_at", - "last_modified_at" + "status" ], "properties": { "payment_link_id": { "type": "string" }, - "payment_id": { - "type": "string" - }, "merchant_id": { "type": "string" }, @@ -11330,26 +11326,29 @@ "type": "integer", "format": "int64" }, - "currency": { - "allOf": [ - { - "$ref": "#/components/schemas/Currency" - } - ], - "nullable": true - }, "created_at": { "type": "string", "format": "date-time" }, - "last_modified_at": { - "type": "string", - "format": "date-time" - }, "link_expiry": { "type": "string", "format": "date-time", "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "status": { + "type": "string" + }, + "currency": { + "allOf": [ + { + "$ref": "#/components/schemas/Currency" + } + ], + "nullable": true } } }, From 7d223ee0d1b53c02421ed6bd1b5584362d7a7456 Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Wed, 22 Nov 2023 15:37:59 +0530 Subject: [PATCH 065/146] docs(README): Update feature support link (#2894) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index edc8cae5cf8e..8c5ad9e03b2d 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ You can find the latest list of payment processors, supported methods, and features [here][supported-connectors-and-features]. -[supported-connectors-and-features]: https://docs.google.com/spreadsheets/d/e/2PACX-1vQWHLza9m5iO4Ol-tEBx22_Nnq8Mb3ISCWI53nrinIGLK8eHYmHGnvXFXUXEut8AFyGyI9DipsYaBLG/pubhtml?gid=0&single=true +[supported-connectors-and-features]: https://hyperswitch.io/pm-list ### 🌟 Hosted Version From 46e13d54759168ad7667af08d5481ab510e5706a Mon Sep 17 00:00:00 2001 From: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com> Date: Wed, 22 Nov 2023 15:46:33 +0530 Subject: [PATCH 066/146] refactor(macros): use syn2.0 (#2890) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Sanchith Hegde <22217505+SanchithHegde@users.noreply.github.com> --- Cargo.lock | 48 +-- crates/common_enums/src/enums.rs | 45 ++- crates/diesel_models/src/enums.rs | 23 +- .../payments/operations/payment_approve.rs | 2 +- .../payments/operations/payment_cancel.rs | 2 +- .../payments/operations/payment_capture.rs | 2 +- .../operations/payment_complete_authorize.rs | 2 +- .../payments/operations/payment_confirm.rs | 2 +- .../payments/operations/payment_create.rs | 2 +- .../operations/payment_method_validate.rs | 2 +- .../payments/operations/payment_reject.rs | 2 +- .../payments/operations/payment_response.rs | 4 +- .../payments/operations/payment_session.rs | 2 +- .../core/payments/operations/payment_start.rs | 2 +- .../payments/operations/payment_status.rs | 2 +- .../payments/operations/payment_update.rs | 2 +- crates/router_derive/Cargo.toml | 6 +- crates/router_derive/src/lib.rs | 28 +- crates/router_derive/src/macros.rs | 5 +- .../src/macros/api_error/helpers.rs | 20 +- crates/router_derive/src/macros/diesel.rs | 180 ++++++--- .../src/macros/generate_schema.rs | 6 +- crates/router_derive/src/macros/helpers.rs | 13 +- crates/router_derive/src/macros/operation.rs | 343 +++++++++++------- .../router_derive/src/macros/try_get_enum.rs | 107 +++--- crates/storage_impl/src/redis/kv_store.rs | 2 +- 26 files changed, 495 insertions(+), 359 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 730b08774fa3..bf0ee2d110c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,7 +114,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a0a77f836d869f700e5b47ac7c3c8b9c8bc82e4aec861954c6198abee3ebd4d" dependencies = [ - "darling 0.20.3", + "darling", "parse-size", "proc-macro2", "quote", @@ -1858,38 +1858,14 @@ dependencies = [ "typenum", ] -[[package]] -name = "darling" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" -dependencies = [ - "darling_core 0.14.4", - "darling_macro 0.14.4", -] - [[package]] name = "darling" version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" dependencies = [ - "darling_core 0.20.3", - "darling_macro 0.20.3", -] - -[[package]] -name = "darling_core" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 1.0.109", + "darling_core", + "darling_macro", ] [[package]] @@ -1906,24 +1882,13 @@ dependencies = [ "syn 2.0.38", ] -[[package]] -name = "darling_macro" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" -dependencies = [ - "darling_core 0.14.4", - "quote", - "syn 1.0.109", -] - [[package]] name = "darling_macro" version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ - "darling_core 0.20.3", + "darling_core", "quote", "syn 2.0.38", ] @@ -4861,7 +4826,6 @@ dependencies = [ name = "router_derive" version = "0.1.0" dependencies = [ - "darling 0.14.4", "diesel", "indexmap 2.0.2", "proc-macro2", @@ -4869,7 +4833,7 @@ dependencies = [ "serde", "serde_json", "strum 0.24.1", - "syn 1.0.109", + "syn 2.0.38", ] [[package]] @@ -5358,7 +5322,7 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e6be15c453eb305019bfa438b1593c731f36a289a7853f7707ee29e870b3b3c" dependencies = [ - "darling 0.20.3", + "darling", "proc-macro2", "quote", "syn 2.0.38", diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index cf3c398f8f48..063e35933c43 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -1,6 +1,5 @@ use std::num::{ParseFloatError, TryFromIntError}; -use router_derive; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; #[doc(hidden)] @@ -29,7 +28,7 @@ pub mod diesel_exports { strum::EnumString, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum AttemptStatus { @@ -107,7 +106,7 @@ impl AttemptStatus { strum::EnumString, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum AuthenticationType { @@ -132,7 +131,7 @@ pub enum AuthenticationType { ToSchema, Hash, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum CaptureStatus { @@ -163,7 +162,7 @@ pub enum CaptureStatus { strum::EnumString, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum CaptureMethod { @@ -190,7 +189,7 @@ pub enum CaptureMethod { serde::Serialize, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[strum(serialize_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum ConnectorType { @@ -231,7 +230,7 @@ pub enum ConnectorType { strum::EnumVariantNames, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] pub enum Currency { AED, ALL, @@ -789,7 +788,7 @@ impl Currency { strum::EnumString, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum EventType { @@ -825,7 +824,7 @@ pub enum EventType { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum MerchantStorageScheme { @@ -848,7 +847,7 @@ pub enum MerchantStorageScheme { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum IntentStatus { @@ -882,7 +881,7 @@ pub enum IntentStatus { strum::EnumString, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum FutureUsage { @@ -904,7 +903,7 @@ pub enum FutureUsage { strum::EnumString, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[strum(serialize_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum PaymentMethodIssuerCode { @@ -1108,7 +1107,7 @@ pub enum PaymentMethod { strum::EnumString, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum PaymentType { @@ -1132,7 +1131,7 @@ pub enum PaymentType { serde::Serialize, serde::Deserialize, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[strum(serialize_all = "snake_case")] pub enum RefundStatus { Failure, @@ -1157,7 +1156,7 @@ pub enum RefundStatus { strum::EnumString, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum MandateStatus { @@ -1211,7 +1210,7 @@ pub enum CardNetwork { strum::EnumString, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum DisputeStage { @@ -1235,7 +1234,7 @@ pub enum DisputeStage { strum::EnumString, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum DisputeStatus { @@ -1265,7 +1264,7 @@ pub enum DisputeStatus { utoipa::ToSchema, Copy )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[rustfmt::skip] pub enum CountryAlpha2 { AF, AX, AL, DZ, AS, AD, AO, AI, AQ, AG, AR, AM, AW, AU, AT, @@ -1692,7 +1691,7 @@ pub enum CanadaStatesAbbreviation { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum PayoutStatus { @@ -1720,7 +1719,7 @@ pub enum PayoutStatus { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum PayoutType { @@ -1775,7 +1774,7 @@ pub enum PayoutEntityType { ToSchema, Hash, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum PaymentSource { @@ -1842,7 +1841,7 @@ pub enum FrmSuggestion { utoipa::ToSchema, Copy, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum ReconStatus { @@ -1871,7 +1870,7 @@ pub enum ApplePayFlow { ToSchema, Default, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[strum(serialize_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum ConnectorStatus { diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index 817fee633190..dc4a7614f587 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -21,6 +21,7 @@ pub mod diesel_exports { pub use common_enums::*; use common_utils::pii; use diesel::serialize::{Output, ToSql}; +use router_derive::diesel_enum; use time::PrimitiveDateTime; #[derive( @@ -34,7 +35,7 @@ use time::PrimitiveDateTime; strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum RoutingAlgorithmKind { @@ -55,7 +56,7 @@ pub enum RoutingAlgorithmKind { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum EventClass { @@ -76,7 +77,7 @@ pub enum EventClass { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum EventObjectType { @@ -97,7 +98,7 @@ pub enum EventObjectType { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum ProcessTrackerStatus { @@ -126,7 +127,7 @@ pub enum ProcessTrackerStatus { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[diesel_enum(storage_type = "db_enum")] #[strum(serialize_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum RefundType { @@ -149,7 +150,7 @@ pub enum RefundType { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum MandateType { @@ -217,7 +218,7 @@ pub struct MandateAmountData { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "text")] +#[diesel_enum(storage_type = "text")] #[strum(serialize_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum BankNames { @@ -348,7 +349,7 @@ pub enum BankNames { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum FraudCheckType { @@ -369,7 +370,7 @@ pub enum FraudCheckType { strum::EnumString, frunk::LabelledGeneric, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[diesel_enum(storage_type = "db_enum")] #[strum(serialize_all = "snake_case")] pub enum FraudCheckStatus { Fraud, @@ -393,7 +394,7 @@ pub enum FraudCheckStatus { strum::EnumString, frunk::LabelledGeneric, )] -#[router_derive::diesel_enum(storage_type = "text")] +#[diesel_enum(storage_type = "text")] #[strum(serialize_all = "snake_case")] pub enum FraudCheckLastStep { #[default] @@ -416,7 +417,7 @@ pub enum FraudCheckLastStep { strum::EnumString, frunk::LabelledGeneric, )] -#[router_derive::diesel_enum(storage_type = "text")] +#[diesel_enum(storage_type = "text")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum UserStatus { diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index af52105c85d5..f51d7a93ee5e 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -28,7 +28,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(ops = "all", flow = "authorize")] +#[operation(operations = "all", flow = "authorize")] pub struct PaymentApprove; #[async_trait] diff --git a/crates/router/src/core/payments/operations/payment_cancel.rs b/crates/router/src/core/payments/operations/payment_cancel.rs index 096f900e7195..d4605b47c438 100644 --- a/crates/router/src/core/payments/operations/payment_cancel.rs +++ b/crates/router/src/core/payments/operations/payment_cancel.rs @@ -25,7 +25,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, router_derive::PaymentOperation)] -#[operation(ops = "all", flow = "cancel")] +#[operation(operations = "all", flow = "cancel")] pub struct PaymentCancel; #[async_trait] diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index ef8e2b0153d4..5b89cfdbcf0b 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -24,7 +24,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, router_derive::PaymentOperation)] -#[operation(ops = "all", flow = "capture")] +#[operation(operations = "all", flow = "capture")] pub struct PaymentCapture; #[async_trait] diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index 62759bd0fd9b..8b264edbb3d1 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -27,7 +27,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(ops = "all", flow = "authorize")] +#[operation(operations = "all", flow = "authorize")] pub struct CompleteAuthorize; #[async_trait] diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index e85531050529..125787e1a30f 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -34,7 +34,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(ops = "all", flow = "authorize")] +#[operation(operations = "all", flow = "authorize")] pub struct PaymentConfirm; #[async_trait] impl diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index eee937071d6b..ccf9fc3fad1c 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -34,7 +34,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(ops = "all", flow = "authorize")] +#[operation(operations = "all", flow = "authorize")] pub struct PaymentCreate; /// The `get_trackers` function for `PaymentsCreate` is an entrypoint for new payments diff --git a/crates/router/src/core/payments/operations/payment_method_validate.rs b/crates/router/src/core/payments/operations/payment_method_validate.rs index 62f12cfbc90c..693fce236846 100644 --- a/crates/router/src/core/payments/operations/payment_method_validate.rs +++ b/crates/router/src/core/payments/operations/payment_method_validate.rs @@ -29,7 +29,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(ops = "all", flow = "verify")] +#[operation(operations = "all", flow = "verify")] pub struct PaymentMethodValidate; impl ValidateRequest diff --git a/crates/router/src/core/payments/operations/payment_reject.rs b/crates/router/src/core/payments/operations/payment_reject.rs index ae02dde4bc06..ae606187a0a1 100644 --- a/crates/router/src/core/payments/operations/payment_reject.rs +++ b/crates/router/src/core/payments/operations/payment_reject.rs @@ -24,7 +24,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, router_derive::PaymentOperation)] -#[operation(ops = "all", flow = "reject")] +#[operation(operations = "all", flow = "reject")] pub struct PaymentReject; #[async_trait] diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index d68215bec7a7..3734abfc6ab5 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -35,8 +35,8 @@ use crate::{ #[derive(Debug, Clone, Copy, router_derive::PaymentOperation)] #[operation( - ops = "post_tracker", - flow = "syncdata,authorizedata,canceldata,capturedata,completeauthorizedata,approvedata,rejectdata,setupmandatedata,sessiondata" + operations = "post_update_tracker", + flow = "sync_data, authorize_data, cancel_data, capture_data, complete_authorize_data, approve_data, reject_data, setup_mandate_data, session_data" )] pub struct PaymentResponse; diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index cea6eb176672..6097a5e430ce 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -26,7 +26,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(ops = "all", flow = "session")] +#[operation(operations = "all", flow = "session")] pub struct PaymentSession; #[async_trait] diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index 6d4281216b4f..3a4ae2c2e0de 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -25,7 +25,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(ops = "all", flow = "start")] +#[operation(operations = "all", flow = "start")] pub struct PaymentStart; #[async_trait] diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index b31c406f0ecd..d0cd4b32d3c2 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -28,7 +28,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(ops = "all", flow = "sync")] +#[operation(operations = "all", flow = "sync")] pub struct PaymentStatus; impl Operation diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 817b14286809..75d3b6b82b4c 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -27,7 +27,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(ops = "all", flow = "authorize")] +#[operation(operations = "all", flow = "authorize")] pub struct PaymentUpdate; #[async_trait] diff --git a/crates/router_derive/Cargo.toml b/crates/router_derive/Cargo.toml index b4e60a8c2a33..6f598e0f0502 100644 --- a/crates/router_derive/Cargo.toml +++ b/crates/router_derive/Cargo.toml @@ -12,14 +12,14 @@ proc-macro = true doctest = false [dependencies] -darling = "0.14.4" indexmap = "2.0.0" proc-macro2 = "1.0.56" quote = "1.0.26" -syn = { version = "1.0.109", features = ["full", "extra-traits"] } # the full feature does not seem to encompass all the features +syn = { version = "2.0.5", features = ["full", "extra-traits"] } # the full feature does not seem to encompass all the features +strum = { version = "0.24.1", features = ["derive"] } [dev-dependencies] diesel = { version = "2.1.0", features = ["postgres"] } serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.96" -strum = { version = "0.24.1", features = ["derive"] } + diff --git a/crates/router_derive/src/lib.rs b/crates/router_derive/src/lib.rs index 3f34c156ae8f..109003e0cc41 100644 --- a/crates/router_derive/src/lib.rs +++ b/crates/router_derive/src/lib.rs @@ -2,6 +2,10 @@ #![forbid(unsafe_code)] #![warn(missing_docs)] +use syn::parse_macro_input; + +use crate::macros::diesel::DieselEnumMeta; + mod macros; /// Uses the [`Debug`][Debug] implementation of a type to derive its [`Display`][Display] @@ -66,7 +70,7 @@ pub fn debug_as_display_derive(input: proc_macro::TokenStream) -> proc_macro::To /// Blue, /// } /// ``` -#[proc_macro_derive(DieselEnum)] +#[proc_macro_derive(DieselEnum, attributes(storage_type))] pub fn diesel_enum_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let ast = syn::parse_macro_input!(input as syn::DeriveInput); let tokens = @@ -104,16 +108,15 @@ pub fn diesel_enum_derive_string(input: proc_macro::TokenStream) -> proc_macro:: /// Derives the boilerplate code required for using an enum with `diesel` and a PostgreSQL database. /// -/// Storage Type can either be "text" or "pg_enum" -/// Choosing text will store the enum as text in the database, whereas pg_enum will map it to the -/// database enum +/// Storage Type can either be "text" or "db_enum" +/// Choosing text will store the enum as text in the database, whereas db_enum will map it to the +/// corresponding database enum /// -/// Works in tandem with the [`DieselEnum`][DieselEnum] and [`DieselEnumText`][DieselEnumText] derive macro to achieve the desired results. +/// Works in tandem with the [`DieselEnum`][DieselEnum] derive macro to achieve the desired results. /// The enum is required to implement (or derive) the [`ToString`][ToString] and the /// [`FromStr`][FromStr] traits for the [`DieselEnum`][DieselEnum] derive macro to be used. /// /// [DieselEnum]: crate::DieselEnum -/// [DieselEnumText]: crate::DieselEnumText /// [FromStr]: ::core::str::FromStr /// [ToString]: ::std::string::ToString /// @@ -138,12 +141,12 @@ pub fn diesel_enum( args: proc_macro::TokenStream, item: proc_macro::TokenStream, ) -> proc_macro::TokenStream { - let args = syn::parse_macro_input!(args as syn::AttributeArgs); + let args_parsed = parse_macro_input!(args as DieselEnumMeta); let item = syn::parse_macro_input!(item as syn::ItemEnum); - let tokens = macros::diesel_enum_attribute_inner(&args, &item) - .unwrap_or_else(|error| error.to_compile_error()); - tokens.into() + macros::diesel::diesel_enum_attribute_macro(args_parsed, &item) + .unwrap_or_else(|error| error.to_compile_error()) + .into() } /// A derive macro which generates the setter functions for any struct with fields @@ -226,7 +229,7 @@ pub fn setter(input: proc_macro::TokenStream) -> proc_macro::TokenStream { #[inline] fn check_if_auth_based_attr_is_present(f: &syn::Field, ident: &str) -> bool { for i in f.attrs.iter() { - if i.path.is_ident(ident) { + if i.path().is_ident(ident) { return true; } } @@ -460,7 +463,8 @@ pub fn api_error_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStre #[proc_macro_derive(PaymentOperation, attributes(operation))] pub fn operation_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input = syn::parse_macro_input!(input as syn::DeriveInput); - macros::operation_derive_inner(input).unwrap_or_else(|err| err.to_compile_error().into()) + macros::operation::operation_derive_inner(input) + .unwrap_or_else(|err| err.to_compile_error().into()) } /// Generates different schemas with the ability to mark few fields as mandatory for certain schema diff --git a/crates/router_derive/src/macros.rs b/crates/router_derive/src/macros.rs index 86501f054a59..9a8e514c5c11 100644 --- a/crates/router_derive/src/macros.rs +++ b/crates/router_derive/src/macros.rs @@ -13,11 +13,8 @@ use syn::DeriveInput; pub(crate) use self::{ api_error::api_error_derive_inner, - diesel::{ - diesel_enum_attribute_inner, diesel_enum_derive_inner, diesel_enum_text_derive_inner, - }, + diesel::{diesel_enum_derive_inner, diesel_enum_text_derive_inner}, generate_schema::polymorphic_macro_derive_inner, - operation::operation_derive_inner, }; pub(crate) fn debug_as_display_inner(ast: &DeriveInput) -> syn::Result { diff --git a/crates/router_derive/src/macros/api_error/helpers.rs b/crates/router_derive/src/macros/api_error/helpers.rs index e1e2a09eacb1..5781d786ee56 100644 --- a/crates/router_derive/src/macros/api_error/helpers.rs +++ b/crates/router_derive/src/macros/api_error/helpers.rs @@ -1,3 +1,5 @@ +use proc_macro2::TokenStream; +use quote::ToTokens; use syn::{ parse::Parse, spanned::Spanned, DeriveInput, Field, Fields, LitStr, Token, TypePath, Variant, }; @@ -38,10 +40,10 @@ impl Parse for EnumMeta { } } -impl Spanned for EnumMeta { - fn span(&self) -> proc_macro2::Span { +impl ToTokens for EnumMeta { + fn to_tokens(&self, tokens: &mut TokenStream) { match self { - Self::ErrorTypeEnum { keyword, .. } => keyword.span(), + Self::ErrorTypeEnum { keyword, .. } => keyword.to_tokens(tokens), } } } @@ -143,13 +145,13 @@ impl Parse for VariantMeta { } } -impl Spanned for VariantMeta { - fn span(&self) -> proc_macro2::Span { +impl ToTokens for VariantMeta { + fn to_tokens(&self, tokens: &mut TokenStream) { match self { - Self::ErrorType { keyword, .. } => keyword.span, - Self::Code { keyword, .. } => keyword.span, - Self::Message { keyword, .. } => keyword.span, - Self::Ignore { keyword, .. } => keyword.span, + Self::ErrorType { keyword, .. } => keyword.to_tokens(tokens), + Self::Code { keyword, .. } => keyword.to_tokens(tokens), + Self::Message { keyword, .. } => keyword.to_tokens(tokens), + Self::Ignore { keyword, .. } => keyword.to_tokens(tokens), } } } diff --git a/crates/router_derive/src/macros/diesel.rs b/crates/router_derive/src/macros/diesel.rs index 07957bef785e..d15eecf41b9c 100644 --- a/crates/router_derive/src/macros/diesel.rs +++ b/crates/router_derive/src/macros/diesel.rs @@ -1,10 +1,8 @@ -#![allow(clippy::use_self)] -use darling::FromMeta; use proc_macro2::{Span, TokenStream}; -use quote::{format_ident, quote}; -use syn::{AttributeArgs, Data, DeriveInput, ItemEnum}; +use quote::{format_ident, quote, ToTokens}; +use syn::{parse::Parse, Data, DeriveInput, ItemEnum}; -use crate::macros::helpers::non_enum_error; +use crate::macros::helpers; pub(crate) fn diesel_enum_text_derive_inner(ast: &DeriveInput) -> syn::Result { let name = &ast.ident; @@ -12,10 +10,11 @@ pub(crate) fn diesel_enum_text_derive_inner(ast: &DeriveInput) -> syn::Result (), - _ => return Err(non_enum_error()), - } + _ => return Err(helpers::non_enum_error()), + }; Ok(quote! { + #[automatically_derived] impl #impl_generics ::diesel::serialize::ToSql<::diesel::sql_types::Text, ::diesel::pg::Pg> for #name #ty_generics #where_clause @@ -42,18 +41,20 @@ pub(crate) fn diesel_enum_text_derive_inner(ast: &DeriveInput) -> syn::Result syn::Result { +pub(crate) fn diesel_enum_db_enum_derive_inner(ast: &DeriveInput) -> syn::Result { let name = &ast.ident; let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); match &ast.data { Data::Enum(_) => (), - _ => return Err(non_enum_error()), - } + _ => return Err(helpers::non_enum_error()), + }; let struct_name = format_ident!("Db{name}"); let type_name = format!("{name}"); + Ok(quote! { + #[derive(::core::clone::Clone, ::core::marker::Copy, ::core::fmt::Debug, ::diesel::QueryId, ::diesel::SqlType)] #[diesel(postgres_type(name = #type_name))] pub struct #struct_name; @@ -84,45 +85,138 @@ pub(crate) fn diesel_enum_derive_inner(ast: &DeriveInput) -> syn::Result syn::Result { - #[derive(FromMeta, Debug)] - enum StorageType { - PgEnum, - Text, +mod diesel_keyword { + use syn::custom_keyword; + + custom_keyword!(storage_type); + custom_keyword!(db_enum); + custom_keyword!(text); +} + +#[derive(Debug, strum::EnumString, strum::EnumIter, strum::Display)] +#[strum(serialize_all = "snake_case")] +pub enum StorageType { + /// Store the Enum as Text value in the database + Text, + /// Store the Enum as Enum in the database. This requires a corresponding enum to be created + /// in the database with the same name + DbEnum, +} + +#[derive(Debug)] +pub enum DieselEnumMeta { + StorageTypeEnum { + keyword: diesel_keyword::storage_type, + value: StorageType, + }, +} + +impl Parse for StorageType { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let text = input.parse::()?; + let value = text.value(); + + value.as_str().parse().map_err(|_| { + syn::Error::new_spanned( + &text, + format!( + "Unexpected value for storage_type: `{value}`. Possible values are `{}`", + helpers::get_possible_values_for_enum::() + ), + ) + }) + } +} + +impl DieselEnumMeta { + pub fn get_storage_type(&self) -> &StorageType { + match self { + Self::StorageTypeEnum { value, .. } => value, + } } +} - #[derive(FromMeta, Debug)] - struct StorageTypeArgs { - storage_type: StorageType, +impl Parse for DieselEnumMeta { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let lookahead = input.lookahead1(); + if lookahead.peek(diesel_keyword::storage_type) { + let keyword = input.parse()?; + input.parse::()?; + let value = input.parse()?; + Ok(Self::StorageTypeEnum { keyword, value }) + } else { + Err(lookahead.error()) + } } +} - let storage_type_args = match StorageTypeArgs::from_list(args) { - Ok(v) => v, - Err(_) => { - return Err(syn::Error::new( - Span::call_site(), - "Expected storage_type of text or pg_enum", - )); +impl ToTokens for DieselEnumMeta { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + Self::StorageTypeEnum { keyword, .. } => keyword.to_tokens(tokens), } - }; + } +} + +trait DieselDeriveInputExt { + /// Get all the error metadata associated with an enum. + fn get_metadata(&self) -> syn::Result>; +} + +impl DieselDeriveInputExt for DeriveInput { + fn get_metadata(&self) -> syn::Result> { + helpers::get_metadata_inner("storage_type", &self.attrs) + } +} + +pub(crate) fn diesel_enum_derive_inner(ast: &DeriveInput) -> syn::Result { + let storage_type = ast.get_metadata()?; + + match storage_type + .first() + .ok_or(syn::Error::new( + Span::call_site(), + "Storage type must be specified", + ))? + .get_storage_type() + { + StorageType::Text => diesel_enum_text_derive_inner(ast), + StorageType::DbEnum => diesel_enum_db_enum_derive_inner(ast), + } +} - match storage_type_args.storage_type { - StorageType::PgEnum => { - let name = &item.ident; - let type_name = format_ident!("Db{name}"); - Ok(quote! { - #[derive(diesel::AsExpression, diesel::FromSqlRow, router_derive::DieselEnum) ] - #[diesel(sql_type = #type_name)] +/// Based on the storage type, derive appropriate diesel traits +/// This will add the appropriate #[diesel(sql_type)] +/// Since the `FromSql` and `ToSql` have to be derived for all the enums, this will add the +/// `DieselEnum` derive trait. +pub(crate) fn diesel_enum_attribute_macro( + diesel_enum_meta: DieselEnumMeta, + item: &ItemEnum, +) -> syn::Result { + let diesel_derives = + quote!(#[derive(diesel::AsExpression, diesel::FromSqlRow, router_derive::DieselEnum) ]); + + match diesel_enum_meta { + DieselEnumMeta::StorageTypeEnum { + value: storage_type, + .. + } => match storage_type { + StorageType::Text => Ok(quote! { + #diesel_derives + #[diesel(sql_type = ::diesel::sql_types::Text)] + #[storage_type(storage_type = "text")] #item - }) - } - StorageType::Text => Ok(quote! { - #[derive(diesel::AsExpression, diesel::FromSqlRow, router_derive::DieselEnumText) ] - #[diesel(sql_type = ::diesel::sql_types::Text)] - #item - }), + }), + StorageType::DbEnum => { + let name = &item.ident; + let type_name = format_ident!("Db{name}"); + Ok(quote! { + #diesel_derives + #[diesel(sql_type = #type_name)] + #[storage_type(storage_type= "db_enum")] + #item + }) + } + }, } } diff --git a/crates/router_derive/src/macros/generate_schema.rs b/crates/router_derive/src/macros/generate_schema.rs index 2669106cecd4..05d5b2919e11 100644 --- a/crates/router_derive/src/macros/generate_schema.rs +++ b/crates/router_derive/src/macros/generate_schema.rs @@ -42,12 +42,14 @@ pub fn polymorphic_macro_derive_inner( let (mandatory_attribute, other_attributes) = field .attrs .iter() - .partition::, _>(|attribute| attribute.path.is_ident("mandatory_in")); + .partition::, _>(|attribute| attribute.path().is_ident("mandatory_in")); // Other attributes ( schema ) are to be printed as is other_attributes .iter() - .filter(|attribute| attribute.path.is_ident("schema") || attribute.path.is_ident("doc")) + .filter(|attribute| { + attribute.path().is_ident("schema") || attribute.path().is_ident("doc") + }) .for_each(|attribute| { // Since attributes will be modified, the field should not contain any attributes // So create a field, with previous attributes removed diff --git a/crates/router_derive/src/macros/helpers.rs b/crates/router_derive/src/macros/helpers.rs index 94005453f8de..b6490c4d6298 100644 --- a/crates/router_derive/src/macros/helpers.rs +++ b/crates/router_derive/src/macros/helpers.rs @@ -23,13 +23,24 @@ pub(super) fn syn_error(span: Span, message: &str) -> syn::Error { syn::Error::new(span, message) } +/// Get all the variants of a enum in the form of a string +pub fn get_possible_values_for_enum() -> String +where + T: strum::IntoEnumIterator + ToString, +{ + T::iter() + .map(|variants| variants.to_string()) + .collect::>() + .join(", ") +} + pub(super) fn get_metadata_inner<'a, T: Parse + Spanned>( ident: &str, attrs: impl IntoIterator, ) -> syn::Result> { attrs .into_iter() - .filter(|attr| attr.path.is_ident(ident)) + .filter(|attr| attr.path().is_ident(ident)) .try_fold(Vec::new(), |mut vec, attr| { vec.extend(attr.parse_args_with(Punctuated::::parse_terminated)?); Ok(vec) diff --git a/crates/router_derive/src/macros/operation.rs b/crates/router_derive/src/macros/operation.rs index fb0ef35ef587..370e03b984ba 100644 --- a/crates/router_derive/src/macros/operation.rs +++ b/crates/router_derive/src/macros/operation.rs @@ -1,25 +1,27 @@ -use std::collections::HashMap; +use std::str::FromStr; use proc_macro2::{Span, TokenStream}; -use quote::quote; -use syn::{self, spanned::Spanned, DeriveInput, Lit, Meta, MetaNameValue, NestedMeta}; +use quote::{quote, ToTokens}; +use strum::IntoEnumIterator; +use syn::{self, parse::Parse, DeriveInput}; -use crate::macros::helpers; +use crate::macros::helpers::{self}; -#[derive(Debug, Clone, Copy)] -enum Derives { +#[derive(Debug, Clone, Copy, strum::EnumString, strum::EnumIter, strum::Display)] +#[strum(serialize_all = "snake_case")] +pub enum Derives { Sync, Cancel, Reject, Capture, - Approvedata, + ApproveData, Authorize, - Authorizedata, - Syncdata, - Canceldata, - Capturedata, + AuthorizeData, + SyncData, + CancelData, + CaptureData, CompleteAuthorizeData, - Rejectdata, + RejectData, SetupMandateData, Start, Verify, @@ -27,31 +29,6 @@ enum Derives { SessionData, } -impl From for Derives { - fn from(s: String) -> Self { - match s.as_str() { - "sync" => Self::Sync, - "cancel" => Self::Cancel, - "reject" => Self::Reject, - "syncdata" => Self::Syncdata, - "authorize" => Self::Authorize, - "approvedata" => Self::Approvedata, - "authorizedata" => Self::Authorizedata, - "canceldata" => Self::Canceldata, - "capture" => Self::Capture, - "capturedata" => Self::Capturedata, - "completeauthorizedata" => Self::CompleteAuthorizeData, - "rejectdata" => Self::Rejectdata, - "start" => Self::Start, - "verify" => Self::Verify, - "setupmandatedata" => Self::SetupMandateData, - "session" => Self::Session, - "sessiondata" => Self::SessionData, - _ => Self::Authorize, - } - } -} - impl Derives { fn to_operation( self, @@ -82,8 +59,9 @@ impl Derives { } } -#[derive(PartialEq, Eq, Hash)] -enum Conversion { +#[derive(Debug, Clone, strum::EnumString, strum::EnumIter, strum::Display)] +#[strum(serialize_all = "snake_case")] +pub enum Conversion { ValidateRequest, GetTracker, Domain, @@ -93,34 +71,20 @@ enum Conversion { Invalid(String), } -impl From for Conversion { - fn from(s: String) -> Self { - match s.as_str() { - "validate_request" => Self::ValidateRequest, - "get_tracker" => Self::GetTracker, - "domain" => Self::Domain, - "update_tracker" => Self::UpdateTracker, - "post_tracker" => Self::PostUpdateTracker, - "all" => Self::All, - s => Self::Invalid(s.to_string()), - } - } -} - impl Conversion { fn get_req_type(ident: Derives) -> syn::Ident { match ident { Derives::Authorize => syn::Ident::new("PaymentsRequest", Span::call_site()), - Derives::Authorizedata => syn::Ident::new("PaymentsAuthorizeData", Span::call_site()), + Derives::AuthorizeData => syn::Ident::new("PaymentsAuthorizeData", Span::call_site()), Derives::Sync => syn::Ident::new("PaymentsRetrieveRequest", Span::call_site()), - Derives::Syncdata => syn::Ident::new("PaymentsSyncData", Span::call_site()), + Derives::SyncData => syn::Ident::new("PaymentsSyncData", Span::call_site()), Derives::Cancel => syn::Ident::new("PaymentsCancelRequest", Span::call_site()), - Derives::Canceldata => syn::Ident::new("PaymentsCancelData", Span::call_site()), - Derives::Approvedata => syn::Ident::new("PaymentsApproveData", Span::call_site()), + Derives::CancelData => syn::Ident::new("PaymentsCancelData", Span::call_site()), + Derives::ApproveData => syn::Ident::new("PaymentsApproveData", Span::call_site()), Derives::Reject => syn::Ident::new("PaymentsRejectRequest", Span::call_site()), - Derives::Rejectdata => syn::Ident::new("PaymentsRejectData", Span::call_site()), + Derives::RejectData => syn::Ident::new("PaymentsRejectData", Span::call_site()), Derives::Capture => syn::Ident::new("PaymentsCaptureRequest", Span::call_site()), - Derives::Capturedata => syn::Ident::new("PaymentsCaptureData", Span::call_site()), + Derives::CaptureData => syn::Ident::new("PaymentsCaptureData", Span::call_site()), Derives::CompleteAuthorizeData => { syn::Ident::new("CompleteAuthorizeData", Span::call_site()) } @@ -231,103 +195,206 @@ impl Conversion { } } -fn find_operation_attr(a: &[syn::Attribute]) -> syn::Result { - a.iter() - .find(|a| { - a.path - .get_ident() - .map(|ident| *ident == "operation") - .unwrap_or(false) +mod operations_keyword { + use syn::custom_keyword; + + custom_keyword!(operations); + custom_keyword!(flow); +} + +#[derive(Debug)] +pub enum OperationsEnumMeta { + Operations { + keyword: operations_keyword::operations, + value: Vec, + }, + Flow { + keyword: operations_keyword::flow, + value: Vec, + }, +} + +#[derive(Clone)] +pub struct OperationProperties { + operations: Vec, + flows: Vec, +} + +fn get_operation_properties( + operation_enums: Vec, +) -> syn::Result { + let mut operations = vec![]; + let mut flows = vec![]; + + for operation in operation_enums { + match operation { + OperationsEnumMeta::Operations { value, .. } => { + operations = value; + } + OperationsEnumMeta::Flow { value, .. } => { + flows = value; + } + } + } + + if operations.is_empty() { + Err(syn::Error::new( + Span::call_site(), + "atleast one operation must be specitied", + ))?; + } + + if flows.is_empty() { + Err(syn::Error::new( + Span::call_site(), + "atleast one flow must be specitied", + ))?; + } + + Ok(OperationProperties { operations, flows }) +} + +impl Parse for Derives { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let text = input.parse::()?; + let value = text.value(); + + value.as_str().parse().map_err(|_| { + syn::Error::new_spanned( + &text, + format!( + "Unexpected value for flow: `{value}`. Possible values are `{}`", + helpers::get_possible_values_for_enum::() + ), + ) }) - .cloned() - .ok_or_else(|| { - helpers::syn_error( - Span::call_site(), - "Cannot find attribute 'operation' in the macro", + } +} + +impl Parse for Conversion { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let text = input.parse::()?; + let value = text.value(); + + value.as_str().parse().map_err(|_| { + syn::Error::new_spanned( + &text, + format!( + "Unexpected value for operation: `{value}`. Possible values are `{}`", + helpers::get_possible_values_for_enum::() + ), ) }) + } +} + +fn parse_list_string(list_string: String, keyword: &str) -> syn::Result> +where + T: FromStr + IntoEnumIterator + ToString, +{ + list_string + .split(',') + .map(str::trim) + .map(T::from_str) + .map(|result| { + result.map_err(|_| { + syn::Error::new( + Span::call_site(), + format!( + "Unexpected {keyword}, possible values are {}", + helpers::get_possible_values_for_enum::() + ), + ) + }) + }) + .collect() +} + +fn get_conversions(input: syn::parse::ParseStream<'_>) -> syn::Result> { + let lit_str_list = input.parse::()?; + parse_list_string(lit_str_list.value(), "operation") +} + +fn get_derives(input: syn::parse::ParseStream<'_>) -> syn::Result> { + let lit_str_list = input.parse::()?; + parse_list_string(lit_str_list.value(), "flow") } -fn find_value(v: &NestedMeta) -> Option<(String, Vec)> { - match v { - NestedMeta::Meta(Meta::NameValue(MetaNameValue { - ref path, - eq_token: _, - lit: Lit::Str(ref litstr), - })) => { - let key = path.get_ident()?.to_string(); - Some(( - key, - litstr.value().split(',').map(ToString::to_string).collect(), - )) +impl Parse for OperationsEnumMeta { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let lookahead = input.lookahead1(); + if lookahead.peek(operations_keyword::operations) { + let keyword = input.parse()?; + input.parse::()?; + let value = get_conversions(input)?; + Ok(Self::Operations { keyword, value }) + } else if lookahead.peek(operations_keyword::flow) { + let keyword = input.parse()?; + input.parse::()?; + let value = get_derives(input)?; + Ok(Self::Flow { keyword, value }) + } else { + Err(lookahead.error()) } - _ => None, } } -fn find_properties(attr: &syn::Attribute) -> syn::Result>> { - let meta = attr.parse_meta(); - match meta { - Ok(syn::Meta::List(syn::MetaList { - ref path, - paren_token: _, - nested, - })) => { - path.get_ident().map(|i| i == "operation").ok_or_else(|| { - helpers::syn_error(path.span(), "Attribute 'operation' was not found") - })?; - Ok(HashMap::from_iter(nested.iter().filter_map(find_value))) +trait OperationsDeriveInputExt { + /// Get all the error metadata associated with an enum. + fn get_metadata(&self) -> syn::Result>; +} + +impl OperationsDeriveInputExt for DeriveInput { + fn get_metadata(&self) -> syn::Result> { + helpers::get_metadata_inner("operation", &self.attrs) + } +} + +impl ToTokens for OperationsEnumMeta { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + Self::Operations { keyword, .. } => keyword.to_tokens(tokens), + Self::Flow { keyword, .. } => keyword.to_tokens(tokens), } - _ => Err(helpers::syn_error( - attr.span(), - "No attributes were found. Expected format is ops=..,flow=..", - )), } } pub fn operation_derive_inner(input: DeriveInput) -> syn::Result { let struct_name = &input.ident; - let op = find_operation_attr(&input.attrs)?; - let prop = find_properties(&op)?; - let ops = prop.get("ops").ok_or_else(|| { - helpers::syn_error( - op.span(), - "Invalid properties. Property 'ops' was not found", - ) - })?; - let flow = prop.get("flow").ok_or_else(|| { - helpers::syn_error( - op.span(), - "Invalid properties. Property 'flow' was not found", - ) - })?; - let current_crate = syn::Ident::new( - &prop - .get("crate") - .map(|v| v.join("")) - .unwrap_or_else(|| String::from("crate")), - Span::call_site(), - ); - - let trait_derive = flow.iter().map(|derive| { - let derive: Derives = derive.to_owned().into(); - let fns = ops.iter().map(|t| { - let con: Conversion = t.to_owned().into(); - con.to_function(derive) - }); - derive.to_operation(fns, struct_name) - }); - let ref_trait_derive = flow.iter().map(|derive| { - let derive: Derives = derive.to_owned().into(); - let fns = ops.iter().map(|t| { - let con: Conversion = t.to_owned().into(); - con.to_ref_function(derive) - }); - derive.to_ref_operation(fns, struct_name) - }); + let operations_meta = input.get_metadata()?; + let operation_properties = get_operation_properties(operations_meta)?; + + let current_crate = syn::Ident::new("crate", Span::call_site()); + + let trait_derive = operation_properties + .clone() + .flows + .into_iter() + .map(|derive| { + let fns = operation_properties + .operations + .iter() + .map(|conversion| conversion.to_function(derive)); + derive.to_operation(fns, struct_name) + }) + .collect::>(); + + let ref_trait_derive = operation_properties + .flows + .into_iter() + .map(|derive| { + let fns = operation_properties + .operations + .iter() + .map(|conversion| conversion.to_ref_function(derive)); + derive.to_ref_operation(fns, struct_name) + }) + .collect::>(); + let trait_derive = quote! { #(#ref_trait_derive)* #(#trait_derive)* }; + let output = quote! { const _: () = { use #current_crate::core::errors::RouterResult; diff --git a/crates/router_derive/src/macros/try_get_enum.rs b/crates/router_derive/src/macros/try_get_enum.rs index 3a534b080df1..f607b7f06c9c 100644 --- a/crates/router_derive/src/macros/try_get_enum.rs +++ b/crates/router_derive/src/macros/try_get_enum.rs @@ -1,21 +1,62 @@ use proc_macro2::Span; -use syn::punctuated::Punctuated; +use quote::ToTokens; +use syn::{parse::Parse, punctuated::Punctuated}; + +mod try_get_keyword { + use syn::custom_keyword; + + custom_keyword!(error_type); +} + +#[derive(Debug)] +pub struct TryGetEnumMeta { + error_type: syn::Ident, + variant: syn::Ident, +} + +impl Parse for TryGetEnumMeta { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let error_type = input.parse()?; + _ = input.parse::()?; + let variant = input.parse()?; + + Ok(Self { + error_type, + variant, + }) + } +} + +trait TryGetDeriveInputExt { + /// Get all the error metadata associated with an enum. + fn get_metadata(&self) -> syn::Result>; +} + +impl TryGetDeriveInputExt for syn::DeriveInput { + fn get_metadata(&self) -> syn::Result> { + super::helpers::get_metadata_inner("error", &self.attrs) + } +} + +impl ToTokens for TryGetEnumMeta { + fn to_tokens(&self, _: &mut proc_macro2::TokenStream) {} +} /// Try and get the variants for an enum pub fn try_get_enum_variant( input: syn::DeriveInput, ) -> Result { let name = &input.ident; + let parsed_error_type = input.get_metadata()?; + + let (error_type, error_variant) = parsed_error_type + .first() + .ok_or(syn::Error::new( + Span::call_site(), + "One error should be specified", + )) + .map(|error_struct| (&error_struct.error_type, &error_struct.variant))?; - let error_attr = input - .attrs - .iter() - .find(|attr| attr.path.is_ident("error")) - .ok_or(super::helpers::syn_error( - proc_macro2::Span::call_site(), - "Unable to find attribute error. Expected #[error(..)]", - ))?; - let (error_type, error_variant) = get_error_type_and_variant(error_attr)?; let (impl_generics, generics, where_clause) = input.generics.split_for_impl(); let variants = get_enum_variants(&input.data)?; @@ -49,52 +90,6 @@ pub fn try_get_enum_variant( Ok(expanded) } -/// Parses the attribute #[error(ErrorType(ErrorVariant))] -fn get_error_type_and_variant(attr: &syn::Attribute) -> syn::Result<(syn::Ident, syn::Path)> { - let meta = attr.parse_meta()?; - let metalist = match meta { - syn::Meta::List(list) => list, - _ => { - return Err(super::helpers::syn_error( - proc_macro2::Span::call_site(), - "Invalid attribute format #[error(ErrorType(ErrorVariant)]", - )) - } - }; - - for meta in metalist.nested.iter() { - if let syn::NestedMeta::Meta(syn::Meta::List(meta)) = meta { - let error_type = meta - .path - .get_ident() - .ok_or(super::helpers::syn_error( - proc_macro2::Span::call_site(), - "Invalid attribute format #[error(ErrorType(ErrorVariant))]", - )) - .cloned()?; - let error_variant = get_error_variant(meta)?; - return Ok((error_type, error_variant)); - }; - } - - Err(super::helpers::syn_error( - proc_macro2::Span::call_site(), - "Invalid attribute format #[error(ErrorType(ErrorVariant))]", - )) -} - -fn get_error_variant(meta: &syn::MetaList) -> syn::Result { - for meta in meta.nested.iter() { - if let syn::NestedMeta::Meta(syn::Meta::Path(meta)) = meta { - return Ok(meta.clone()); - } - } - Err(super::helpers::syn_error( - proc_macro2::Span::call_site(), - "Invalid attribute format expected #[error(ErrorType(ErrorVariant))]", - )) -} - /// Get variants from Enum fn get_enum_variants(data: &syn::Data) -> syn::Result> { if let syn::Data::Enum(syn::DataEnum { variants, .. }) = data { diff --git a/crates/storage_impl/src/redis/kv_store.rs b/crates/storage_impl/src/redis/kv_store.rs index 3eadd8b83ade..c45282da7f5e 100644 --- a/crates/storage_impl/src/redis/kv_store.rs +++ b/crates/storage_impl/src/redis/kv_store.rs @@ -60,7 +60,7 @@ pub enum KvOperation<'a, S: serde::Serialize + Debug> { } #[derive(TryGetEnumVariant)] -#[error(RedisError(UnknownResult))] +#[error(RedisError::UnknownResult)] pub enum KvResult { HGet(T), Get(T), From 6954de77a0fda14d87b79ec7ceee7cc8f1c491db Mon Sep 17 00:00:00 2001 From: Kartikeya Hegde Date: Wed, 22 Nov 2023 16:34:17 +0530 Subject: [PATCH 067/146] fix: kv logs when KeyNotSet is returned (#2928) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- crates/redis_interface/src/errors.rs | 2 ++ crates/storage_impl/src/redis/kv_store.rs | 10 +++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/redis_interface/src/errors.rs b/crates/redis_interface/src/errors.rs index 213fb799892e..5289ec4fec47 100644 --- a/crates/redis_interface/src/errors.rs +++ b/crates/redis_interface/src/errors.rs @@ -8,6 +8,8 @@ pub enum RedisError { InvalidConfiguration(String), #[error("Failed to set key value in Redis")] SetFailed, + #[error("Failed to set key value in Redis. Duplicate value")] + SetNxFailed, #[error("Failed to set key value with expiry in Redis")] SetExFailed, #[error("Failed to set expiry for key value in Redis")] diff --git a/crates/storage_impl/src/redis/kv_store.rs b/crates/storage_impl/src/redis/kv_store.rs index c45282da7f5e..9339b11a9b9c 100644 --- a/crates/storage_impl/src/redis/kv_store.rs +++ b/crates/storage_impl/src/redis/kv_store.rs @@ -1,6 +1,7 @@ use std::{fmt::Debug, sync::Arc}; use common_utils::errors::CustomResult; +use error_stack::IntoReport; use redis_interface::errors::RedisError; use router_derive::TryGetEnumVariant; use router_env::logger; @@ -145,8 +146,10 @@ where store .push_to_drainer_stream::(sql, partition_key) .await?; + Ok(KvResult::HSetNx(result)) + } else { + Err(RedisError::SetNxFailed).into_report() } - Ok(KvResult::HSetNx(result)) } KvOperation::SetNx(value, sql) => { @@ -160,9 +163,10 @@ where store .push_to_drainer_stream::(sql, partition_key) .await?; + Ok(KvResult::SetNx(result)) + } else { + Err(RedisError::SetNxFailed).into_report() } - - Ok(KvResult::SetNx(result)) } KvOperation::Get => { From 341374b8e5eced329587b93cbb6bd58e16dd9932 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Wed, 22 Nov 2023 16:54:40 +0530 Subject: [PATCH 068/146] refactor(mca): Add Serialization for `ConnectorAuthType` (#2945) --- crates/router/src/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 203d4e30bf9a..a03e41650408 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -904,7 +904,7 @@ pub struct ResponseRouterData { } // Different patterns of authentication. -#[derive(Default, Debug, Clone, serde::Deserialize)] +#[derive(Default, Debug, Clone, serde::Deserialize, serde::Serialize)] #[serde(tag = "auth_type")] pub enum ConnectorAuthType { TemporaryAuth, From 4e15d7792e3167de170c3d8310f33419f4dfb0db Mon Sep 17 00:00:00 2001 From: Sarthak Soni <76486416+Sarthak1799@users.noreply.github.com> Date: Wed, 22 Nov 2023 17:00:30 +0530 Subject: [PATCH 069/146] feat(routing): Routing prometheus metrics (#2870) Co-authored-by: Prajjwal Kumar --- crates/router/src/core/metrics.rs | 33 ++++++++++++++++++++++ crates/router/src/core/routing.rs | 47 +++++++++++++++++++++++++++++-- 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/crates/router/src/core/metrics.rs b/crates/router/src/core/metrics.rs index eb8a5be8d4ad..c5a05a169c75 100644 --- a/crates/router/src/core/metrics.rs +++ b/crates/router/src/core/metrics.rs @@ -44,3 +44,36 @@ counter_metric!( WEBHOOK_EVENT_TYPE_IDENTIFICATION_FAILURE_COUNT, GLOBAL_METER ); + +counter_metric!(ROUTING_CREATE_REQUEST_RECEIVED, GLOBAL_METER); +counter_metric!(ROUTING_CREATE_SUCCESS_RESPONSE, GLOBAL_METER); +counter_metric!(ROUTING_MERCHANT_DICTIONARY_RETRIEVE, GLOBAL_METER); +counter_metric!( + ROUTING_MERCHANT_DICTIONARY_RETRIEVE_SUCCESS_RESPONSE, + GLOBAL_METER +); +counter_metric!(ROUTING_LINK_CONFIG, GLOBAL_METER); +counter_metric!(ROUTING_LINK_CONFIG_SUCCESS_RESPONSE, GLOBAL_METER); +counter_metric!(ROUTING_RETRIEVE_CONFIG, GLOBAL_METER); +counter_metric!(ROUTING_RETRIEVE_CONFIG_SUCCESS_RESPONSE, GLOBAL_METER); +counter_metric!(ROUTING_RETRIEVE_DEFAULT_CONFIG, GLOBAL_METER); +counter_metric!( + ROUTING_RETRIEVE_DEFAULT_CONFIG_SUCCESS_RESPONSE, + GLOBAL_METER +); +counter_metric!(ROUTING_RETRIEVE_LINK_CONFIG, GLOBAL_METER); +counter_metric!(ROUTING_RETRIEVE_LINK_CONFIG_SUCCESS_RESPONSE, GLOBAL_METER); +counter_metric!(ROUTING_UNLINK_CONFIG, GLOBAL_METER); +counter_metric!(ROUTING_UNLINK_CONFIG_SUCCESS_RESPONSE, GLOBAL_METER); +counter_metric!(ROUTING_UPDATE_CONFIG, GLOBAL_METER); +counter_metric!(ROUTING_UPDATE_CONFIG_SUCCESS_RESPONSE, GLOBAL_METER); +counter_metric!(ROUTING_UPDATE_CONFIG_FOR_PROFILE, GLOBAL_METER); +counter_metric!( + ROUTING_UPDATE_CONFIG_FOR_PROFILE_SUCCESS_RESPONSE, + GLOBAL_METER +); +counter_metric!(ROUTING_RETRIEVE_CONFIG_FOR_PROFILE, GLOBAL_METER); +counter_metric!( + ROUTING_RETRIEVE_CONFIG_FOR_PROFILE_SUCCESS_RESPONSE, + GLOBAL_METER +); diff --git a/crates/router/src/core/routing.rs b/crates/router/src/core/routing.rs index 4171c3385637..e9ddcb4a5632 100644 --- a/crates/router/src/core/routing.rs +++ b/crates/router/src/core/routing.rs @@ -19,7 +19,7 @@ use crate::{ consts, core::{ errors::{RouterResponse, StorageErrorExt}, - utils as core_utils, + metrics, utils as core_utils, }, routes::AppState, types::domain, @@ -35,6 +35,7 @@ pub async fn retrieve_merchant_routing_dictionary( merchant_account: domain::MerchantAccount, #[cfg(feature = "business_profile_routing")] query_params: RoutingRetrieveQuery, ) -> RouterResponse { + metrics::ROUTING_MERCHANT_DICTIONARY_RETRIEVE.add(&metrics::CONTEXT, 1, &[]); #[cfg(feature = "business_profile_routing")] { let routing_metadata = state @@ -51,11 +52,18 @@ pub async fn retrieve_merchant_routing_dictionary( .map(ForeignInto::foreign_into) .collect::>(); + metrics::ROUTING_MERCHANT_DICTIONARY_RETRIEVE_SUCCESS_RESPONSE.add( + &metrics::CONTEXT, + 1, + &[], + ); Ok(service_api::ApplicationResponse::Json( routing_types::RoutingKind::RoutingAlgorithm(result), )) } #[cfg(not(feature = "business_profile_routing"))] + metrics::ROUTING_MERCHANT_DICTIONARY_RETRIEVE_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); + #[cfg(not(feature = "business_profile_routing"))] Ok(service_api::ApplicationResponse::Json( routing_types::RoutingKind::Config( helpers::get_merchant_routing_dictionary( @@ -73,6 +81,7 @@ pub async fn create_routing_config( key_store: domain::MerchantKeyStore, request: routing_types::RoutingConfigRequest, ) -> RouterResponse { + metrics::ROUTING_CREATE_REQUEST_RECEIVED.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); let name = request @@ -147,6 +156,7 @@ pub async fn create_routing_config( let new_record = record.foreign_into(); + metrics::ROUTING_CREATE_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json(new_record)) } @@ -213,6 +223,7 @@ pub async fn create_routing_config( ) .await?; + metrics::ROUTING_CREATE_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json(new_record)) } } @@ -223,6 +234,7 @@ pub async fn link_routing_config( #[cfg(not(feature = "business_profile_routing"))] key_store: domain::MerchantKeyStore, algorithm_id: String, ) -> RouterResponse { + metrics::ROUTING_LINK_CONFIG.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); #[cfg(feature = "business_profile_routing")] { @@ -268,6 +280,7 @@ pub async fn link_routing_config( helpers::update_business_profile_active_algorithm_ref(db, business_profile, routing_ref) .await?; + metrics::ROUTING_LINK_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json( routing_algorithm.foreign_into(), )) @@ -317,6 +330,7 @@ pub async fn link_routing_config( .await?; helpers::update_merchant_active_algorithm_ref(db, &key_store, routing_ref).await?; + metrics::ROUTING_LINK_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json(response)) } } @@ -326,6 +340,7 @@ pub async fn retrieve_routing_config( merchant_account: domain::MerchantAccount, algorithm_id: RoutingAlgorithmId, ) -> RouterResponse { + metrics::ROUTING_RETRIEVE_CONFIG.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); #[cfg(feature = "business_profile_routing")] { @@ -350,6 +365,8 @@ pub async fn retrieve_routing_config( .foreign_try_into() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("unable to parse routing algorithm")?; + + metrics::ROUTING_RETRIEVE_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json(response)) } @@ -387,6 +404,7 @@ pub async fn retrieve_routing_config( modified_at: record.modified_at, }; + metrics::ROUTING_RETRIEVE_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json(response)) } } @@ -396,6 +414,7 @@ pub async fn unlink_routing_config( #[cfg(not(feature = "business_profile_routing"))] key_store: domain::MerchantKeyStore, #[cfg(feature = "business_profile_routing")] request: routing_types::RoutingConfigRequest, ) -> RouterResponse { + metrics::ROUTING_UNLINK_CONFIG.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); #[cfg(feature = "business_profile_routing")] { @@ -451,6 +470,12 @@ pub async fn unlink_routing_config( routing_algorithm, ) .await?; + + metrics::ROUTING_UNLINK_CONFIG_SUCCESS_RESPONSE.add( + &metrics::CONTEXT, + 1, + &[], + ); Ok(service_api::ApplicationResponse::Json(response)) } None => Err(errors::ApiErrorResponse::PreconditionFailed { @@ -559,6 +584,7 @@ pub async fn unlink_routing_config( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to update routing algorithm ref in merchant account")?; + metrics::ROUTING_UNLINK_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json(response)) } } @@ -568,6 +594,7 @@ pub async fn update_default_routing_config( merchant_account: domain::MerchantAccount, updated_config: Vec, ) -> RouterResponse> { + metrics::ROUTING_UPDATE_CONFIG.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); let default_config = helpers::get_merchant_default_config(db, &merchant_account.merchant_id).await?; @@ -606,6 +633,7 @@ pub async fn update_default_routing_config( ) .await?; + metrics::ROUTING_UPDATE_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json(updated_config)) } @@ -613,11 +641,19 @@ pub async fn retrieve_default_routing_config( state: AppState, merchant_account: domain::MerchantAccount, ) -> RouterResponse> { + metrics::ROUTING_RETRIEVE_DEFAULT_CONFIG.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); helpers::get_merchant_default_config(db, &merchant_account.merchant_id) .await - .map(service_api::ApplicationResponse::Json) + .map(|conn_choice| { + metrics::ROUTING_RETRIEVE_DEFAULT_CONFIG_SUCCESS_RESPONSE.add( + &metrics::CONTEXT, + 1, + &[], + ); + service_api::ApplicationResponse::Json(conn_choice) + }) } pub async fn retrieve_linked_routing_config( @@ -625,6 +661,7 @@ pub async fn retrieve_linked_routing_config( merchant_account: domain::MerchantAccount, #[cfg(feature = "business_profile_routing")] query_params: RoutingRetrieveLinkQuery, ) -> RouterResponse { + metrics::ROUTING_RETRIEVE_LINK_CONFIG.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); #[cfg(feature = "business_profile_routing")] @@ -672,6 +709,7 @@ pub async fn retrieve_linked_routing_config( } } + metrics::ROUTING_RETRIEVE_LINK_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json( routing_types::LinkedRoutingConfigRetrieveResponse::ProfileBased(active_algorithms), )) @@ -718,6 +756,7 @@ pub async fn retrieve_linked_routing_config( routing_types::RoutingRetrieveResponse { algorithm }, ); + metrics::ROUTING_RETRIEVE_LINK_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json(response)) } } @@ -726,6 +765,7 @@ pub async fn retrieve_default_routing_config_for_profiles( state: AppState, merchant_account: domain::MerchantAccount, ) -> RouterResponse> { + metrics::ROUTING_RETRIEVE_CONFIG_FOR_PROFILE.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); let all_profiles = db @@ -755,6 +795,7 @@ pub async fn retrieve_default_routing_config_for_profiles( ) .collect::>(); + metrics::ROUTING_RETRIEVE_CONFIG_FOR_PROFILE_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json(default_configs)) } @@ -764,6 +805,7 @@ pub async fn update_default_routing_config_for_profile( updated_config: Vec, profile_id: String, ) -> RouterResponse { + metrics::ROUTING_UPDATE_CONFIG_FOR_PROFILE.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); let business_profile = core_utils::validate_and_get_business_profile( @@ -829,6 +871,7 @@ pub async fn update_default_routing_config_for_profile( ) .await?; + metrics::ROUTING_UPDATE_CONFIG_FOR_PROFILE_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json( routing_types::ProfileDefaultRoutingConfig { profile_id: business_profile.profile_id, From 998948953ab8a444aca79957f48e7cfb3066c334 Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Wed, 22 Nov 2023 17:11:02 +0530 Subject: [PATCH 070/146] feat(payment_methods): add support for tokenising bank details and fetching masked details while listing (#2585) Co-authored-by: shashank_attarde Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- crates/api_models/src/payment_methods.rs | 10 ++ .../router/src/core/payment_methods/cards.rs | 114 +++++++++++++++++- crates/router/src/openapi.rs | 1 + openapi/openapi_spec.json | 19 +++ 4 files changed, 141 insertions(+), 3 deletions(-) diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index c40dffe4cf31..8710c69aa5c6 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -811,10 +811,20 @@ pub struct CustomerPaymentMethod { #[schema(value_type = Option)] pub bank_transfer: Option, + /// Masked bank details from PM auth services + #[schema(example = json!({"mask": "0000"}))] + pub bank: Option, + /// Whether this payment method requires CVV to be collected #[schema(example = true)] pub requires_cvv: bool, } + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +pub struct MaskedBankDetails { + pub mask: String, +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct PaymentMethodId { pub payment_method_id: String, diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 60fd3f315ea6..85a0ca5f2441 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -7,9 +7,10 @@ use api_models::{ admin::{self, PaymentMethodsEnabled}, enums::{self as api_enums}, payment_methods::{ - CardDetailsPaymentMethod, CardNetworkTypes, PaymentExperienceTypes, PaymentMethodsData, - RequestPaymentMethodTypes, RequiredFieldInfo, ResponsePaymentMethodIntermediate, - ResponsePaymentMethodTypes, ResponsePaymentMethodsEnabled, + BankAccountConnectorDetails, CardDetailsPaymentMethod, CardNetworkTypes, MaskedBankDetails, + PaymentExperienceTypes, PaymentMethodsData, RequestPaymentMethodTypes, RequiredFieldInfo, + ResponsePaymentMethodIntermediate, ResponsePaymentMethodTypes, + ResponsePaymentMethodsEnabled, }, payments::BankCodeResponse, surcharge_decision_configs as api_surcharge_decision_configs, @@ -2210,6 +2211,22 @@ pub async fn list_customer_payment_method( ) } + enums::PaymentMethod::BankDebit => { + // Retrieve the pm_auth connector details so that it can be tokenized + let bank_account_connector_details = get_bank_account_connector_details(&pm, key) + .await + .unwrap_or_else(|err| { + logger::error!(error=?err); + None + }); + if let Some(connector_details) = bank_account_connector_details { + let token_data = PaymentTokenData::AuthBankDebit(connector_details); + (None, None, token_data) + } else { + continue; + } + } + _ => ( None, None, @@ -2217,6 +2234,18 @@ pub async fn list_customer_payment_method( ), }; + // Retrieve the masked bank details to be sent as a response + let bank_details = if pm.payment_method == enums::PaymentMethod::BankDebit { + get_masked_bank_details(&pm, key) + .await + .unwrap_or_else(|err| { + logger::error!(error=?err); + None + }) + } else { + None + }; + //Need validation for enabled payment method ,querying MCA let pma = api::CustomerPaymentMethod { payment_token: parent_payment_method_token.to_owned(), @@ -2232,6 +2261,7 @@ pub async fn list_customer_payment_method( payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), created: Some(pm.created_at), bank_transfer: pmd, + bank: bank_details, requires_cvv, }; customer_pms.push(pma.to_owned()); @@ -2356,6 +2386,84 @@ pub async fn get_lookup_key_from_locker( Ok(resp) } +async fn get_masked_bank_details( + pm: &payment_method::PaymentMethod, + key: &[u8], +) -> errors::RouterResult> { + let payment_method_data = + decrypt::(pm.payment_method_data.clone(), key) + .await + .change_context(errors::StorageError::DecryptionError) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to decrypt bank details")? + .map(|x| x.into_inner().expose()) + .map( + |v| -> Result> { + v.parse_value::("PaymentMethodsData") + .change_context(errors::StorageError::DeserializationFailed) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to deserialize Payment Method Auth config") + }, + ) + .transpose()?; + + match payment_method_data { + Some(pmd) => match pmd { + PaymentMethodsData::Card(_) => Ok(None), + PaymentMethodsData::BankDetails(bank_details) => Ok(Some(MaskedBankDetails { + mask: bank_details.mask, + })), + }, + None => Err(errors::ApiErrorResponse::InternalServerError.into()) + .attach_printable("Unable to fetch payment method data"), + } +} + +async fn get_bank_account_connector_details( + pm: &payment_method::PaymentMethod, + key: &[u8], +) -> errors::RouterResult> { + let payment_method_data = + decrypt::(pm.payment_method_data.clone(), key) + .await + .change_context(errors::StorageError::DecryptionError) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to decrypt bank details")? + .map(|x| x.into_inner().expose()) + .map( + |v| -> Result> { + v.parse_value::("PaymentMethodsData") + .change_context(errors::StorageError::DeserializationFailed) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to deserialize Payment Method Auth config") + }, + ) + .transpose()?; + + match payment_method_data { + Some(pmd) => match pmd { + PaymentMethodsData::Card(_) => Err(errors::ApiErrorResponse::UnprocessableEntity { + message: "Card is not a valid entity".to_string(), + }) + .into_report(), + PaymentMethodsData::BankDetails(bank_details) => { + let connector_details = bank_details + .connector_details + .first() + .ok_or(errors::ApiErrorResponse::InternalServerError)?; + Ok(Some(BankAccountConnectorDetails { + connector: connector_details.connector.clone(), + account_id: connector_details.account_id.clone(), + mca_id: connector_details.mca_id.clone(), + access_token: connector_details.access_token.clone(), + })) + } + }, + None => Err(errors::ApiErrorResponse::InternalServerError.into()) + .attach_printable("Unable to fetch payment method data"), + } +} + #[cfg(feature = "payouts")] pub async fn get_lookup_key_for_payout_method( state: &routes::AppState, diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index 04ef90546cfa..d191890b8cdb 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -315,6 +315,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::PaymentAttemptResponse, api_models::payments::CaptureResponse, api_models::payment_methods::RequiredFieldInfo, + api_models::payment_methods::MaskedBankDetails, api_models::refunds::RefundListRequest, api_models::refunds::RefundListResponse, api_models::payments::TimeRange, diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index df9df43a43ee..056601ac707d 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -4828,6 +4828,14 @@ ], "nullable": true }, + "bank": { + "allOf": [ + { + "$ref": "#/components/schemas/MaskedBankDetails" + } + ], + "nullable": true + }, "requires_cvv": { "type": "boolean", "description": "Whether this payment method requires CVV to be collected", @@ -6434,6 +6442,17 @@ } ] }, + "MaskedBankDetails": { + "type": "object", + "required": [ + "mask" + ], + "properties": { + "mask": { + "type": "string" + } + } + }, "MbWayRedirection": { "type": "object", "required": [ From f4d534c626923d16cc00348366717d5d3095024f Mon Sep 17 00:00:00 2001 From: likhinbopanna <131246334+likhinbopanna@users.noreply.github.com> Date: Wed, 22 Nov 2023 17:50:22 +0530 Subject: [PATCH 071/146] ci(postman): Add delay for checkout refunds test cases (#2947) --- .../Refunds - Create/event.prerequest.js | 3 + .../Refunds - Create/event.prerequest.js | 3 + .../Refunds - Create Copy/event.prerequest.js | 3 + .../Refunds - Create Copy/.event.meta.json | 5 -- .../Refunds - Create Copy/event.test.js | 55 ------------------- .../Refunds - Create Copy/request.json | 42 -------------- .../Refunds - Create Copy/response.json | 1 - .../Refunds - Retrieve Copy/.event.meta.json | 5 -- .../Refunds - Retrieve Copy/event.test.js | 50 ----------------- .../Refunds - Retrieve Copy/request.json | 27 --------- .../Refunds - Retrieve Copy/response.json | 1 - .../Refunds - Create/event.prerequest.js | 3 + .../Refunds - Create-copy/event.prerequest.js | 3 + .../Refunds - Create/event.prerequest.js | 3 + .../Refunds - Create/event.prerequest.js | 3 + .../Refunds - Create/event.prerequest.js | 3 + .../Refunds - Create/event.prerequest.js | 3 + 17 files changed, 27 insertions(+), 186 deletions(-) create mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Refunds - Create Copy/event.prerequest.js delete mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/.event.meta.json delete mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/event.test.js delete mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/request.json delete mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/response.json delete mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/.event.meta.json delete mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/event.test.js delete mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/request.json delete mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/response.json create mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create-copy/event.prerequest.js create mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create/event.prerequest.js create mode 100644 postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/event.prerequest.js create mode 100644 postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Refunds - Create/event.prerequest.js diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Refunds - Create/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Refunds - Create/event.prerequest.js index e69de29bb2d1..97b68c987bdf 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Refunds - Create/event.prerequest.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Refunds - Create/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Create/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Create/event.prerequest.js index e69de29bb2d1..97b68c987bdf 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Create/event.prerequest.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Create/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Refunds - Create Copy/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Refunds - Create Copy/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Refunds - Create Copy/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/.event.meta.json deleted file mode 100644 index 688c85746ef1..000000000000 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/.event.meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "eventOrder": [ - "event.test.js" - ] -} diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/event.test.js deleted file mode 100644 index c549d5d0c097..000000000000 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/event.test.js +++ /dev/null @@ -1,55 +0,0 @@ -// Validate status 2xx -pm.test("[POST]::/refunds - Status code is 2xx", function () { - pm.response.to.be.success; -}); - -// Validate if response header has matching content-type -pm.test("[POST]::/refunds - Content-Type is application/json", function () { - pm.expect(pm.response.headers.get("Content-Type")).to.include( - "application/json", - ); -}); - -// Set response object as internal variable -let jsonData = {}; -try { - jsonData = pm.response.json(); -} catch (e) {} - -// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id -if (jsonData?.refund_id) { - pm.collectionVariables.set("refund_id", jsonData.refund_id); - console.log( - "- use {{refund_id}} as collection variable for value", - jsonData.refund_id, - ); -} else { - console.log( - "INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.", - ); -} - -// Response body should have value "succeeded" for "status" -if (jsonData?.status) { - pm.test( - "[POST]::/refunds - Content check if value for 'status' matches 'succeeded'", - function () { - pm.expect(jsonData.status).to.eql("succeeded"); - }, - ); -} - -// Response body should have value "540" for "amount" -if (jsonData?.status) { - pm.test( - "[POST]::/refunds - Content check if value for 'amount' matches '540'", - function () { - pm.expect(jsonData.amount).to.eql(540); - }, - ); -} - -// Validate the connector -pm.test("[POST]::/payments - connector", function () { - pm.expect(jsonData.connector).to.eql("checkout"); -}); diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/request.json deleted file mode 100644 index d18aaf8befdf..000000000000 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/request.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw_json_formatted": { - "payment_id": "{{payment_id}}", - "amount": 540, - "reason": "Customer returned product", - "refund_type": "instant", - "metadata": { - "udf1": "value1", - "new_customer": "true", - "login_date": "2019-09-10T10:11:12Z" - } - } - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" -} diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/response.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/response.json deleted file mode 100644 index fe51488c7066..000000000000 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/response.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/.event.meta.json deleted file mode 100644 index 688c85746ef1..000000000000 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/.event.meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "eventOrder": [ - "event.test.js" - ] -} diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/event.test.js deleted file mode 100644 index 920a7c47f361..000000000000 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/event.test.js +++ /dev/null @@ -1,50 +0,0 @@ -// Validate status 2xx -pm.test("[GET]::/refunds/:id - Status code is 2xx", function () { - pm.response.to.be.success; -}); - -// Validate if response header has matching content-type -pm.test("[GET]::/refunds/:id - Content-Type is application/json", function () { - pm.expect(pm.response.headers.get("Content-Type")).to.include( - "application/json", - ); -}); - -// Set response object as internal variable -let jsonData = {}; -try { - jsonData = pm.response.json(); -} catch (e) {} - -// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id -if (jsonData?.refund_id) { - pm.collectionVariables.set("refund_id", jsonData.refund_id); - console.log( - "- use {{refund_id}} as collection variable for value", - jsonData.refund_id, - ); -} else { - console.log( - "INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.", - ); -} - -// Response body should have value "succeeded" for "status" -if (jsonData?.status) { - pm.test( - "[POST]::/refunds - Content check if value for 'status' matches 'succeeded'", - function () { - pm.expect(jsonData.status).to.eql("succeeded"); - }, - ); -} - -// Response body should have value "6540" for "amount" -if (jsonData?.status) { - pm.test( - "[POST]::/refunds - Content check if value for 'amount' matches '540'", - function () { - pm.expect(jsonData.amount).to.eql(540); - }, - ); -} diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/request.json deleted file mode 100644 index 6c28619e8566..000000000000 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/request.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" -} diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/response.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/response.json deleted file mode 100644 index fe51488c7066..000000000000 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/response.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Create/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Create/event.prerequest.js index e69de29bb2d1..97b68c987bdf 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Create/event.prerequest.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Create/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create-copy/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create-copy/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create-copy/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Create/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Create/event.prerequest.js index e69de29bb2d1..97b68c987bdf 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Create/event.prerequest.js +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Create/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Refunds - Create/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Refunds - Create/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Refunds - Create/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file From 160acc8d49a08d23989a5653205646ae2e329bea Mon Sep 17 00:00:00 2001 From: Gnanasundari24 <118818938+Gnanasundari24@users.noreply.github.com> Date: Wed, 22 Nov 2023 19:59:27 +0530 Subject: [PATCH 072/146] ci(Postman): Fix for automation test failure (#2949) --- .../Recurring Payments - Create/request.json | 2 +- .../Recurring Payments - Create/request.json | 2 +- .../Payments - Capture/event.test.js | 12 ++++++------ .../Payments - Capture/request.json | 2 +- .../Payments - Retrieve-copy/event.test.js | 10 +++++----- .../Payments - Capture/event.test.js | 4 ++-- .../Payments - Retrieve/event.test.js | 4 ++-- .../stripe/Payments/Payments - Update/request.json | 8 ++++++-- 8 files changed, 24 insertions(+), 20 deletions(-) diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Refund recurring payment/Recurring Payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Refund recurring payment/Recurring Payments - Create/request.json index fb25f7ceebf2..13a48ea7de38 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Refund recurring payment/Recurring Payments - Create/request.json +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Refund recurring payment/Recurring Payments - Create/request.json @@ -23,7 +23,7 @@ "confirm": true, "capture_method": "automatic", "capture_on": "2022-09-10T10:11:12Z", - "amount_to_capture": 6540, + "amount_to_capture": 6570, "customer_id": "StripeCustomer", "email": "guest@example.com", "name": "John Doe", diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario9-Create a recurring payment with greater mandate amount/Recurring Payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario9-Create a recurring payment with greater mandate amount/Recurring Payments - Create/request.json index 8fc4831ccc32..1cc1bce98079 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario9-Create a recurring payment with greater mandate amount/Recurring Payments - Create/request.json +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario9-Create a recurring payment with greater mandate amount/Recurring Payments - Create/request.json @@ -23,7 +23,7 @@ "confirm": true, "capture_method": "automatic", "capture_on": "2022-09-10T10:11:12Z", - "amount_to_capture": 6540, + "amount_to_capture": 8040, "customer_id": "StripeCustomer", "email": "guest@example.com", "name": "John Doe", diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Capture/event.test.js b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Capture/event.test.js index 40445db0fb3f..b06e6c3e1150 100644 --- a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Capture/event.test.js +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Capture/event.test.js @@ -88,22 +88,22 @@ if (jsonData?.amount) { ); } -// Response body should have value "6000" for "amount_received" +// Response body should have value "6540" for "amount_received" if (jsonData?.amount_received) { pm.test( - "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'", + "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'", function () { - pm.expect(jsonData.amount_received).to.eql(6000); + pm.expect(jsonData.amount_received).to.eql(6540); }, ); } -// Response body should have value "6540" for "amount_capturable" +// Response body should have value "0" for "amount_capturable" if (jsonData?.amount_capturable) { pm.test( - "[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'", + "[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'", function () { - pm.expect(jsonData.amount_capturable).to.eql(6540); + pm.expect(jsonData.amount_capturable).to.eql(0); }, ); } diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Capture/request.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Capture/request.json index 8975575ca40e..8efb99d3c905 100644 --- a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Capture/request.json +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Capture/request.json @@ -18,7 +18,7 @@ } }, "raw_json_formatted": { - "amount_to_capture": 6000, + "amount_to_capture": 6540, "statement_descriptor_name": "Joseph", "statement_descriptor_suffix": "JS" } diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Retrieve-copy/event.test.js b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Retrieve-copy/event.test.js index 0bf6890ea3b6..01f51559ed18 100644 --- a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Retrieve-copy/event.test.js +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Retrieve-copy/event.test.js @@ -85,20 +85,20 @@ if (jsonData?.amount) { ); } -// Response body should have value "6000" for "amount_received" +// Response body should have value "6540" for "amount_received" if (jsonData?.amount_received) { pm.test( - "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'", + "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'", function () { - pm.expect(jsonData.amount_received).to.eql(6000); + pm.expect(jsonData.amount_received).to.eql(6540); }, ); } -// Response body should have value "6540" for "amount_capturable" +// Response body should have value "0" for "amount_capturable" if (jsonData?.amount) { pm.test( - "[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'", + "[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'", function () { pm.expect(jsonData.amount_capturable).to.eql(0); }, diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js index 2d7dbc507fb0..f560d84ea730 100644 --- a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js @@ -66,9 +66,9 @@ if (jsonData?.client_secret) { // Response body should have value "succeeded" for "status" if (jsonData?.status) { pm.test( - "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'", + "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("partially_captured"); }, ); } diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/event.test.js b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/event.test.js index 5c7196baa4f7..ca68dd7045be 100644 --- a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/event.test.js +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/event.test.js @@ -63,9 +63,9 @@ if (jsonData?.client_secret) { // Response body should have value "succeeded" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments - Content check if value for 'status' matches 'succeeded'", + "[POST]::/payments - Content check if value for 'status' matches 'partially_captured'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("partially_captured"); }, ); } diff --git a/postman/collection-dir/stripe/Payments/Payments - Update/request.json b/postman/collection-dir/stripe/Payments/Payments - Update/request.json index 09e3dbb307e6..1809770bd35c 100644 --- a/postman/collection-dir/stripe/Payments/Payments - Update/request.json +++ b/postman/collection-dir/stripe/Payments/Payments - Update/request.json @@ -49,7 +49,9 @@ "city": "San Fransico", "state": "California", "zip": "94122", - "country": "US" + "country": "US", + "first_name": "John", + "last_name": "Doe" } }, "shipping": { @@ -60,7 +62,9 @@ "city": "San Fransico", "state": "California", "zip": "94122", - "country": "US" + "country": "US", + "first_name": "John", + "last_name": "Doe" } }, "statement_descriptor_name": "joseph", From b96052f9c64dd6e49d52ba8befd1f60a843b482a Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 22 Nov 2023 14:33:05 +0000 Subject: [PATCH 073/146] test(postman): update postman collection files --- .../adyen_uk.postman_collection.json | 4 +- .../bluesnap.postman_collection.json | 32 +-- .../checkout.postman_collection.json | 265 +++++------------- .../stripe.postman_collection.json | 2 +- 4 files changed, 86 insertions(+), 217 deletions(-) diff --git a/postman/collection-json/adyen_uk.postman_collection.json b/postman/collection-json/adyen_uk.postman_collection.json index ad916657948f..33aadeb6f970 100644 --- a/postman/collection-json/adyen_uk.postman_collection.json +++ b/postman/collection-json/adyen_uk.postman_collection.json @@ -6634,7 +6634,7 @@ "language": "json" } }, - "raw": "{\"amount\":6570,\"currency\":\"USD\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6570,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6570,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -13918,7 +13918,7 @@ "language": "json" } }, - "raw": "{\"amount\":8040,\"currency\":\"USD\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":8040,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":8040,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", diff --git a/postman/collection-json/bluesnap.postman_collection.json b/postman/collection-json/bluesnap.postman_collection.json index 82af7ce7bb6c..34ad07ae67a3 100644 --- a/postman/collection-json/bluesnap.postman_collection.json +++ b/postman/collection-json/bluesnap.postman_collection.json @@ -3166,22 +3166,22 @@ " );", "}", "", - "// Response body should have value \"6000\" for \"amount_received\"", + "// Response body should have value \"6540\" for \"amount_received\"", "if (jsonData?.amount_received) {", " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'\",", " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", + " pm.expect(jsonData.amount_received).to.eql(6540);", " },", " );", "}", "", - "// Response body should have value \"6540\" for \"amount_capturable\"", + "// Response body should have value \"0\" for \"amount_capturable\"", "if (jsonData?.amount_capturable) {", " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(6540);", + " pm.expect(jsonData.amount_capturable).to.eql(0);", " },", " );", "}", @@ -3210,7 +3210,7 @@ "language": "json" } }, - "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + "raw": "{\"amount_to_capture\":6540,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" }, "url": { "raw": "{{baseUrl}}/payments/:id/capture", @@ -3328,20 +3328,20 @@ " );", "}", "", - "// Response body should have value \"6000\" for \"amount_received\"", + "// Response body should have value \"6540\" for \"amount_received\"", "if (jsonData?.amount_received) {", " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'\",", " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", + " pm.expect(jsonData.amount_received).to.eql(6540);", " },", " );", "}", "", - "// Response body should have value \"6540\" for \"amount_capturable\"", + "// Response body should have value \"0\" for \"amount_capturable\"", "if (jsonData?.amount) {", " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", " function () {", " pm.expect(jsonData.amount_capturable).to.eql(0);", " },", @@ -6596,9 +6596,9 @@ "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", @@ -6743,9 +6743,9 @@ "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", diff --git a/postman/collection-json/checkout.postman_collection.json b/postman/collection-json/checkout.postman_collection.json index 2bd0ac0f26e0..54892f116a0e 100644 --- a/postman/collection-json/checkout.postman_collection.json +++ b/postman/collection-json/checkout.postman_collection.json @@ -730,7 +730,9 @@ "listen": "prerequest", "script": { "exec": [ - "" + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" ], "type": "text/javascript" } @@ -1662,6 +1664,17 @@ { "name": "Refunds - Create Copy", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -4045,200 +4058,6 @@ "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, - { - "name": "Refunds - Create Copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(540);", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve Copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] } ] }, @@ -7904,7 +7723,9 @@ "listen": "prerequest", "script": { "exec": [ - "" + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" ], "type": "text/javascript" } @@ -8356,6 +8177,17 @@ { "name": "Refunds - Create", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -8550,6 +8382,17 @@ { "name": "Refunds - Create-copy", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -9610,7 +9453,9 @@ "listen": "prerequest", "script": { "exec": [ - "" + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" ], "type": "text/javascript" } @@ -11071,7 +10916,9 @@ "listen": "prerequest", "script": { "exec": [ - "" + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" ], "type": "text/javascript" } @@ -13453,6 +13300,17 @@ { "name": "Refunds - Create", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -13802,6 +13660,17 @@ { "name": "Refunds - Create", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { diff --git a/postman/collection-json/stripe.postman_collection.json b/postman/collection-json/stripe.postman_collection.json index 9c9a8a5d685c..9bdb5fdb44d9 100644 --- a/postman/collection-json/stripe.postman_collection.json +++ b/postman/collection-json/stripe.postman_collection.json @@ -3540,7 +3540,7 @@ "language": "json" } }, - "raw": "{\"amount\":20000,\"currency\":\"SGD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"email\":\"joseph@example.com\",\"name\":\"joseph Doe\",\"phone\":\"8888888888\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"payment_method\":\"card\",\"return_url\":\"https://duck.com\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":20000,\"currency\":\"SGD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"email\":\"joseph@example.com\",\"name\":\"joseph Doe\",\"phone\":\"8888888888\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"payment_method\":\"card\",\"return_url\":\"https://duck.com\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments/:id", From 6c15fc312345ed9520accb4d02b12688f98ba1a1 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 22 Nov 2023 14:33:06 +0000 Subject: [PATCH 074/146] chore(version): v1.87.0 --- CHANGELOG.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d7b6770d471..f4b86696691e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,44 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.87.0 (2023-11-22) + +### Features + +- **api_event_errors:** Error field in APIEvents ([#2808](https://github.com/juspay/hyperswitch/pull/2808)) ([`ce10579`](https://github.com/juspay/hyperswitch/commit/ce10579a729fe4a7d4ab9f1a4cbd38c3ca00e90b)) +- **payment_methods:** Add support for tokenising bank details and fetching masked details while listing ([#2585](https://github.com/juspay/hyperswitch/pull/2585)) ([`9989489`](https://github.com/juspay/hyperswitch/commit/998948953ab8a444aca79957f48e7cfb3066c334)) +- **router:** + - Migrate `payment_method_data` to rust locker only if `payment_method` is card ([#2929](https://github.com/juspay/hyperswitch/pull/2929)) ([`f8261a9`](https://github.com/juspay/hyperswitch/commit/f8261a96e758498a32c988191bf314aa6c752059)) + - Add list payment link support ([#2805](https://github.com/juspay/hyperswitch/pull/2805)) ([`b441a1f`](https://github.com/juspay/hyperswitch/commit/b441a1f2f9d9d84601cf78a6e39145e8fb847593)) +- **routing:** Routing prometheus metrics ([#2870](https://github.com/juspay/hyperswitch/pull/2870)) ([`4e15d77`](https://github.com/juspay/hyperswitch/commit/4e15d7792e3167de170c3d8310f33419f4dfb0db)) + +### Bug Fixes + +- cybersource mandates and fiserv exp year ([#2920](https://github.com/juspay/hyperswitch/pull/2920)) ([`7f74ae9`](https://github.com/juspay/hyperswitch/commit/7f74ae98a1d48eed98341e4505d3801a61e69fc7)) +- Kv logs when KeyNotSet is returned ([#2928](https://github.com/juspay/hyperswitch/pull/2928)) ([`6954de7`](https://github.com/juspay/hyperswitch/commit/6954de77a0fda14d87b79ec7ceee7cc8f1c491db)) + +### Refactors + +- **macros:** Use syn2.0 ([#2890](https://github.com/juspay/hyperswitch/pull/2890)) ([`46e13d5`](https://github.com/juspay/hyperswitch/commit/46e13d54759168ad7667af08d5481ab510e5706a)) +- **mca:** Add Serialization for `ConnectorAuthType` ([#2945](https://github.com/juspay/hyperswitch/pull/2945)) ([`341374b`](https://github.com/juspay/hyperswitch/commit/341374b8e5eced329587b93cbb6bd58e16dd9932)) + +### Testing + +- **postman:** Update postman collection files ([`b96052f`](https://github.com/juspay/hyperswitch/commit/b96052f9c64dd6e49d52ba8befd1f60a843b482a)) + +### Documentation + +- **README:** Update feature support link ([#2894](https://github.com/juspay/hyperswitch/pull/2894)) ([`7d223ee`](https://github.com/juspay/hyperswitch/commit/7d223ee0d1b53c02421ed6bd1b5584362d7a7456)) + +### Miscellaneous Tasks + +- Address Rust 1.74 clippy lints ([#2942](https://github.com/juspay/hyperswitch/pull/2942)) ([`c6a5a85`](https://github.com/juspay/hyperswitch/commit/c6a5a8574825dc333602f4f1cee7e26969eab030)) + +**Full Changelog:** [`v1.86.0...v1.87.0`](https://github.com/juspay/hyperswitch/compare/v1.86.0...v1.87.0) + +- - - + + ## 1.86.0 (2023-11-21) ### Features From f91d4ae11b02def92c1dde743a0c01b5aac5703f Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Wed, 22 Nov 2023 22:01:07 +0530 Subject: [PATCH 075/146] feat(connector): [BANKOFAMERICA] Implement Google Pay (#2940) --- .../connector/bankofamerica/transformers.rs | 256 ++++++++++++++---- 1 file changed, 210 insertions(+), 46 deletions(-) diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index a6fa8652b27d..f6cda8ac23ce 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -1,4 +1,5 @@ use api_models::payments; +use base64::Engine; use common_utils::pii; use masking::Secret; use serde::{Deserialize, Serialize}; @@ -87,6 +88,7 @@ pub struct BankOfAmericaPaymentsRequest { #[serde(rename_all = "camelCase")] pub struct ProcessingInformation { capture: bool, + payment_solution: Option, } #[derive(Debug, Serialize)] @@ -97,10 +99,24 @@ pub struct CaptureOptions { } #[derive(Debug, Serialize)] -pub struct PaymentInformation { +#[serde(rename_all = "camelCase")] +pub struct CardPaymentInformation { card: Card, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GooglePayPaymentInformation { + fluid_data: FluidData, +} + +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub enum PaymentInformation { + Cards(CardPaymentInformation), + GooglePay(GooglePayPaymentInformation), +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct Card { @@ -112,6 +128,12 @@ pub struct Card { card_type: Option, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FluidData { + value: Secret, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct OrderInformationWithBill { @@ -177,12 +199,165 @@ impl From for String { } } +#[derive(Debug, Serialize)] +pub enum PaymentSolution { + ApplePay, + GooglePay, +} + +impl From for String { + fn from(solution: PaymentSolution) -> Self { + let payment_solution = match solution { + PaymentSolution::ApplePay => "001", + PaymentSolution::GooglePay => "012", + }; + payment_solution.to_string() + } +} + +impl + From<( + &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + BillTo, + )> for OrderInformationWithBill +{ + fn from( + (item, bill_to): ( + &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + BillTo, + ), + ) -> Self { + Self { + amount_details: Amount { + total_amount: item.amount.to_owned(), + currency: item.router_data.request.currency, + }, + bill_to, + } + } +} + +impl + From<( + &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + Option, + )> for ProcessingInformation +{ + fn from( + (item, solution): ( + &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + Option, + ), + ) -> Self { + Self { + capture: matches!( + item.router_data.request.capture_method, + Some(enums::CaptureMethod::Automatic) | None + ), + payment_solution: solution.map(String::from), + } + } +} + +impl From<&BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>> + for ClientReferenceInformation +{ + fn from(item: &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>) -> Self { + Self { + code: Some(item.router_data.connector_request_reference_id.clone()), + } + } +} + #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ClientReferenceInformation { code: Option, } +impl + TryFrom<( + &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + payments::Card, + )> for BankOfAmericaPaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + (item, ccard): ( + &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + payments::Card, + ), + ) -> Result { + let email = item.router_data.request.get_email()?; + let bill_to = build_bill_to(item.router_data.get_billing()?, email)?; + let order_information = OrderInformationWithBill::from((item, bill_to)); + + let card_issuer = ccard.get_card_issuer(); + let card_type = match card_issuer { + Ok(issuer) => Some(String::from(issuer)), + Err(_) => None, + }; + + let payment_information = PaymentInformation::Cards(CardPaymentInformation { + card: Card { + number: ccard.card_number, + expiration_month: ccard.card_exp_month, + expiration_year: ccard.card_exp_year, + security_code: ccard.card_cvc, + card_type, + }, + }); + + let processing_information = ProcessingInformation::from((item, None)); + let client_reference_information = ClientReferenceInformation::from(item); + + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + }) + } +} + +impl + TryFrom<( + &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + payments::GooglePayWalletData, + )> for BankOfAmericaPaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + (item, google_pay_data): ( + &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + payments::GooglePayWalletData, + ), + ) -> Result { + let email = item.router_data.request.get_email()?; + let bill_to = build_bill_to(item.router_data.get_billing()?, email)?; + let order_information = OrderInformationWithBill::from((item, bill_to)); + + let payment_information = PaymentInformation::GooglePay(GooglePayPaymentInformation { + fluid_data: FluidData { + value: Secret::from( + consts::BASE64_ENGINE.encode(google_pay_data.tokenization_data.token), + ), + }, + }); + + let processing_information = + ProcessingInformation::from((item, Some(PaymentSolution::GooglePay))); + let client_reference_information = ClientReferenceInformation::from(item); + + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + }) + } +} + impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>> for BankOfAmericaPaymentsRequest { @@ -191,52 +366,41 @@ impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>> item: &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, ) -> Result { match item.router_data.request.payment_method_data.clone() { - api::PaymentMethodData::Card(ccard) => { - let email = item.router_data.request.get_email()?; - let bill_to = build_bill_to(item.router_data.get_billing()?, email)?; - - let order_information = OrderInformationWithBill { - amount_details: Amount { - total_amount: item.amount.to_owned(), - currency: item.router_data.request.currency, - }, - bill_to, - }; - let card_issuer = ccard.get_card_issuer(); - let card_type = match card_issuer { - Ok(issuer) => Some(String::from(issuer)), - Err(_) => None, - }; - let payment_information = PaymentInformation { - card: Card { - number: ccard.card_number, - expiration_month: ccard.card_exp_month, - expiration_year: ccard.card_exp_year, - security_code: ccard.card_cvc, - card_type, - }, - }; - - let processing_information = ProcessingInformation { - capture: matches!( - item.router_data.request.capture_method, - Some(enums::CaptureMethod::Automatic) | None - ), - }; - - let client_reference_information = ClientReferenceInformation { - code: Some(item.router_data.connector_request_reference_id.clone()), - }; - - Ok(Self { - processing_information, - payment_information, - order_information, - client_reference_information, - }) - } + payments::PaymentMethodData::Card(ccard) => Self::try_from((item, ccard)), + payments::PaymentMethodData::Wallet(wallet_data) => match wallet_data { + payments::WalletData::GooglePay(google_pay_data) => { + Self::try_from((item, google_pay_data)) + } + payments::WalletData::AliPayQr(_) + | payments::WalletData::AliPayRedirect(_) + | payments::WalletData::AliPayHkRedirect(_) + | payments::WalletData::MomoRedirect(_) + | payments::WalletData::KakaoPayRedirect(_) + | payments::WalletData::GoPayRedirect(_) + | payments::WalletData::GcashRedirect(_) + | payments::WalletData::ApplePay(_) + | payments::WalletData::ApplePayRedirect(_) + | payments::WalletData::ApplePayThirdPartySdk(_) + | payments::WalletData::DanaRedirect {} + | payments::WalletData::GooglePayRedirect(_) + | payments::WalletData::GooglePayThirdPartySdk(_) + | payments::WalletData::MbWayRedirect(_) + | payments::WalletData::MobilePayRedirect(_) + | payments::WalletData::PaypalRedirect(_) + | payments::WalletData::PaypalSdk(_) + | payments::WalletData::SamsungPay(_) + | payments::WalletData::TwintRedirect {} + | payments::WalletData::VippsRedirect {} + | payments::WalletData::TouchNGoRedirect(_) + | payments::WalletData::WeChatPayRedirect(_) + | payments::WalletData::WeChatPayQr(_) + | payments::WalletData::CashappQr(_) + | payments::WalletData::SwishQr(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Bank of America"), + ) + .into()), + }, payments::PaymentMethodData::CardRedirect(_) - | payments::PaymentMethodData::Wallet(_) | payments::PaymentMethodData::PayLater(_) | payments::PaymentMethodData::BankRedirect(_) | payments::PaymentMethodData::BankDebit(_) From cb653706066b889eaa9423a6227ce1df954b4759 Mon Sep 17 00:00:00 2001 From: HeetVekariya <91054457+HeetVekariya@users.noreply.github.com> Date: Thu, 23 Nov 2023 00:57:45 +0530 Subject: [PATCH 076/146] refactor(connector): [Payeezy] update error message (#2919) --- .../router/src/connector/payeezy/transformers.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/crates/router/src/connector/payeezy/transformers.rs b/crates/router/src/connector/payeezy/transformers.rs index e2e837929c41..817ab43ac717 100644 --- a/crates/router/src/connector/payeezy/transformers.rs +++ b/crates/router/src/connector/payeezy/transformers.rs @@ -72,11 +72,9 @@ impl TryFrom for PayeezyCardType { utils::CardIssuer::Maestro | utils::CardIssuer::DinersClub | utils::CardIssuer::JCB - | utils::CardIssuer::CarteBlanche => Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Payeezy", - } - .into()), + | utils::CardIssuer::CarteBlanche => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Payeezy"), + ))?, } } } @@ -262,11 +260,9 @@ fn get_payment_method_data( | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Payeezy", - } - .into()), + | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Payeezy"), + ))?, } } From e721b06c7077e00458450a4fb98f4497e8227dc6 Mon Sep 17 00:00:00 2001 From: Kaustubh Sharma <123895549+kaustubh1106@users.noreply.github.com> Date: Thu, 23 Nov 2023 00:59:56 +0530 Subject: [PATCH 077/146] refactor(connector): [Worldline] change error message from NotSupported to NotImplemented (#2893) --- crates/router/src/connector/worldline/transformers.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/router/src/connector/worldline/transformers.rs b/crates/router/src/connector/worldline/transformers.rs index 6cb8862f69b1..049453e325ae 100644 --- a/crates/router/src/connector/worldline/transformers.rs +++ b/crates/router/src/connector/worldline/transformers.rs @@ -306,10 +306,9 @@ impl TryFrom for Gateway { utils::CardIssuer::Master => Ok(Self::MasterCard), utils::CardIssuer::Discover => Ok(Self::Discover), utils::CardIssuer::Visa => Ok(Self::Visa), - _ => Err(errors::ConnectorError::NotSupported { - message: issuer.to_string(), - connector: "worldline", - } + _ => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("worldline"), + ) .into()), } } From 75eea7e81787f2e0697b930b82a8188193f8d51f Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Thu, 23 Nov 2023 13:03:42 +0530 Subject: [PATCH 078/146] fix: amount_captured goes to 0 for 3ds payments (#2954) --- crates/api_models/src/payments.rs | 12 +++++ crates/router/src/connector/utils.rs | 10 +--- crates/router/src/core/payments/helpers.rs | 16 +++---- .../payments/operations/payment_response.rs | 2 +- crates/router/src/core/refunds.rs | 13 +++--- crates/router/src/types.rs | 46 ++++++++++++++----- .../Payments - Create/request.json | 2 +- 7 files changed, 65 insertions(+), 36 deletions(-) diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 508eeb8d7310..a997960edc7e 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -312,6 +312,18 @@ pub struct PaymentsRequest { pub payment_type: Option, } +impl PaymentsRequest { + pub fn get_total_capturable_amount(&self) -> Option { + let surcharge_amount = self + .surcharge_details + .map(|surcharge_details| { + surcharge_details.surcharge_amount + surcharge_details.tax_amount.unwrap_or(0) + }) + .unwrap_or(0); + self.amount + .map(|amount| i64::from(amount) + surcharge_amount) + } +} #[derive( Default, Debug, Clone, serde::Serialize, serde::Deserialize, Copy, ToSchema, PartialEq, )] diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index a098cef5b778..e096f1878a9c 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -111,14 +111,8 @@ where } } enums::AttemptStatus::Charged => { - let captured_amount = if self.request.is_psync() { - payment_data - .payment_attempt - .amount_to_capture - .or(Some(payment_data.payment_attempt.get_total_amount())) - } else { - types::Capturable::get_capture_amount(&self.request) - }; + let captured_amount = + types::Capturable::get_capture_amount(&self.request, payment_data); if Some(payment_data.payment_attempt.get_total_amount()) == captured_amount { enums::AttemptStatus::Charged } else if captured_amount.is_some() { diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 4d8daa1fe69d..d813c96ce94b 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -601,19 +601,19 @@ pub fn validate_request_amount_and_amount_to_capture( } } -/// if confirm = true and capture method = automatic, amount_to_capture(if provided) must be equal to amount +/// if capture method = automatic, amount_to_capture(if provided) must be equal to amount #[instrument(skip_all)] pub fn validate_amount_to_capture_in_create_call_request( request: &api_models::payments::PaymentsRequest, ) -> CustomResult<(), errors::ApiErrorResponse> { - if request.capture_method.unwrap_or_default() == api_enums::CaptureMethod::Automatic - && request.confirm.unwrap_or(false) - { - if let Some((amount_to_capture, amount)) = request.amount_to_capture.zip(request.amount) { - let amount_int: i64 = amount.into(); - utils::when(amount_to_capture != amount_int, || { + if request.capture_method.unwrap_or_default() == api_enums::CaptureMethod::Automatic { + let total_capturable_amount = request.get_total_capturable_amount(); + if let Some((amount_to_capture, total_capturable_amount)) = + request.amount_to_capture.zip(total_capturable_amount) + { + utils::when(amount_to_capture != total_capturable_amount, || { Err(report!(errors::ApiErrorResponse::PreconditionFailed { - message: "amount_to_capture must be equal to amount when confirm = true and capture_method = automatic".into() + message: "amount_to_capture must be equal to total_capturable_amount when capture_method = automatic".into() })) }) } else { diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 3734abfc6ab5..1fff2fce69a0 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -751,7 +751,7 @@ fn get_total_amount_captured( } None => { //Non multiple capture - let amount = request.get_capture_amount(); + let amount = request.get_capture_amount(payment_data); amount_captured.or_else(|| { if router_data_status == enums::AttemptStatus::Charged { amount diff --git a/crates/router/src/core/refunds.rs b/crates/router/src/core/refunds.rs index a42e46ca62d5..b2f73c0b7ce7 100644 --- a/crates/router/src/core/refunds.rs +++ b/crates/router/src/core/refunds.rs @@ -58,13 +58,12 @@ pub async fn refund_create_core( )?; // Amount is not passed in request refer from payment intent. - amount = req.amount.unwrap_or( - payment_intent - .amount_captured - .ok_or(errors::ApiErrorResponse::InternalServerError) - .into_report() - .attach_printable("amount captured is none in a successful payment")?, - ); + amount = req + .amount + .or(payment_intent.amount_captured) + .ok_or(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("amount captured is none in a successful payment")?; //[#299]: Can we change the flow based on some workflow idea utils::when(amount <= 0, || { diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index a03e41650408..9fdb96efe55d 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -30,9 +30,10 @@ use crate::core::utils::IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_DISPUTE_FLO use crate::{ core::{ errors::{self, RouterResult}, - payments::RecurringMandatePaymentData, + payments::{PaymentData, RecurringMandatePaymentData}, }, services, + types::storage::payment_attempt::PaymentAttemptExt, utils::OptionExt, }; @@ -544,7 +545,10 @@ pub struct AccessTokenRequestData { } pub trait Capturable { - fn get_capture_amount(&self) -> Option { + fn get_capture_amount(&self, _payment_data: &PaymentData) -> Option + where + F: Clone, + { None } fn get_surcharge_amount(&self) -> Option { @@ -553,13 +557,13 @@ pub trait Capturable { fn get_tax_on_surcharge_amount(&self) -> Option { None } - fn is_psync(&self) -> bool { - false - } } impl Capturable for PaymentsAuthorizeData { - fn get_capture_amount(&self) -> Option { + fn get_capture_amount(&self, _payment_data: &PaymentData) -> Option + where + F: Clone, + { let final_amount = self .surcharge_details .as_ref() @@ -579,24 +583,44 @@ impl Capturable for PaymentsAuthorizeData { } impl Capturable for PaymentsCaptureData { - fn get_capture_amount(&self) -> Option { + fn get_capture_amount(&self, _payment_data: &PaymentData) -> Option + where + F: Clone, + { Some(self.amount_to_capture) } } impl Capturable for CompleteAuthorizeData { - fn get_capture_amount(&self) -> Option { + fn get_capture_amount(&self, _payment_data: &PaymentData) -> Option + where + F: Clone, + { Some(self.amount) } } impl Capturable for SetupMandateRequestData {} -impl Capturable for PaymentsCancelData {} +impl Capturable for PaymentsCancelData { + fn get_capture_amount(&self, payment_data: &PaymentData) -> Option + where + F: Clone, + { + // return previously captured amount + payment_data.payment_intent.amount_captured + } +} impl Capturable for PaymentsApproveData {} impl Capturable for PaymentsRejectData {} impl Capturable for PaymentsSessionData {} impl Capturable for PaymentsSyncData { - fn is_psync(&self) -> bool { - true + fn get_capture_amount(&self, payment_data: &PaymentData) -> Option + where + F: Clone, + { + payment_data + .payment_attempt + .amount_to_capture + .or(Some(payment_data.payment_attempt.get_total_amount())) } } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario12-BNPL-klarna/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario12-BNPL-klarna/Payments - Create/request.json index b0bc12a6ac89..f621bd52f00d 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario12-BNPL-klarna/Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario12-BNPL-klarna/Payments - Create/request.json @@ -23,7 +23,7 @@ "confirm": false, "capture_method": "automatic", "capture_on": "2022-09-10T10:11:12Z", - "amount_to_capture": 6540, + "amount_to_capture": 8000, "customer_id": "StripeCustomer", "email": "guest@example.com", "name": "John Doe", From 35a44ed2533b748e3fabb8a2f8db4fa7e5d3cf7e Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Thu, 23 Nov 2023 14:50:13 +0530 Subject: [PATCH 079/146] fix(core): Fix Default Values Enum FieldType (#2934) --- crates/api_models/src/enums.rs | 4 ++-- crates/router/src/configs/defaults.rs | 14 +++++++------- openapi/openapi_spec.json | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index c4e4aa90c4b8..ffefaa2ad2c4 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -531,8 +531,8 @@ pub enum FieldType { UserCountry { options: Vec }, //for country inside payment method data ex- bank redirect UserCurrency { options: Vec }, UserBillingName, - UserAddressline1, - UserAddressline2, + UserAddressLine1, + UserAddressLine2, UserAddressCity, UserAddressPincode, UserAddressState, diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index a0da9c88ef35..2320eabacdca 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -582,7 +582,7 @@ impl Default for super::settings::RequiredFields { RequiredFieldInfo { required_field: "billing.address.line1".to_string(), display_name: "line1".to_string(), - field_type: enums::FieldType::UserAddressline1, + field_type: enums::FieldType::UserAddressLine1, value: None, } ), @@ -806,7 +806,7 @@ impl Default for super::settings::RequiredFields { RequiredFieldInfo { required_field: "billing.address.line1".to_string(), display_name: "line1".to_string(), - field_type: enums::FieldType::UserAddressline1, + field_type: enums::FieldType::UserAddressLine1, value: None, } ), @@ -1238,7 +1238,7 @@ impl Default for super::settings::RequiredFields { RequiredFieldInfo { required_field: "billing.address.line1".to_string(), display_name: "line1".to_string(), - field_type: enums::FieldType::UserAddressline1, + field_type: enums::FieldType::UserAddressLine1, value: None, } ), @@ -1247,7 +1247,7 @@ impl Default for super::settings::RequiredFields { RequiredFieldInfo { required_field: "billing.address.line2".to_string(), display_name: "line2".to_string(), - field_type: enums::FieldType::UserAddressline2, + field_type: enums::FieldType::UserAddressLine2, value: None, } ), @@ -2582,7 +2582,7 @@ impl Default for super::settings::RequiredFields { RequiredFieldInfo { required_field: "billing.address.line1".to_string(), display_name: "line1".to_string(), - field_type: enums::FieldType::UserAddressline1, + field_type: enums::FieldType::UserAddressLine1, value: None, } ), @@ -3014,7 +3014,7 @@ impl Default for super::settings::RequiredFields { RequiredFieldInfo { required_field: "billing.address.line1".to_string(), display_name: "line1".to_string(), - field_type: enums::FieldType::UserAddressline1, + field_type: enums::FieldType::UserAddressLine1, value: None, } ), @@ -3023,7 +3023,7 @@ impl Default for super::settings::RequiredFields { RequiredFieldInfo { required_field: "billing.address.line2".to_string(), display_name: "line2".to_string(), - field_type: enums::FieldType::UserAddressline2, + field_type: enums::FieldType::UserAddressLine2, value: None, } ), diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 056601ac707d..88a0d115ff01 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -5372,13 +5372,13 @@ { "type": "string", "enum": [ - "user_addressline1" + "user_address_line1" ] }, { "type": "string", "enum": [ - "user_addressline2" + "user_address_line2" ] }, { From 42eedf3a8c2e62fc22bcead370d129ebaf11a00b Mon Sep 17 00:00:00 2001 From: Kartikeya Hegde Date: Thu, 23 Nov 2023 17:22:20 +0530 Subject: [PATCH 080/146] fix(drainer): increase jobs picked only when stream is not empty (#2958) --- config/docker_compose.toml | 2 +- crates/drainer/src/lib.rs | 24 +++++++++++++++++------- crates/drainer/src/utils.rs | 16 ++++++++++------ 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/config/docker_compose.toml b/config/docker_compose.toml index a5294546de41..986240f0a36b 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -15,7 +15,7 @@ level = "DEBUG" # What you see in your terminal. [log.telemetry] traces_enabled = false # Whether traces are enabled. -metrics_enabled = false # Whether metrics are enabled. +metrics_enabled = true # Whether metrics are enabled. ignore_errors = false # Whether to ignore errors during traces or metrics pipeline setup. otel_exporter_otlp_endpoint = "https://otel-collector:4317" # Endpoint to send metrics and traces to. use_xray_generator = false diff --git a/crates/drainer/src/lib.rs b/crates/drainer/src/lib.rs index 7ccfd600d662..04dff49b7469 100644 --- a/crates/drainer/src/lib.rs +++ b/crates/drainer/src/lib.rs @@ -23,7 +23,7 @@ pub async fn start_drainer( loop_interval: u32, ) -> errors::DrainerResult<()> { let mut stream_index: u8 = 0; - let mut jobs_picked: u8 = 0; + let jobs_picked = Arc::new(atomic::AtomicU8::new(0)); let mut shutdown_interval = tokio::time::interval(std::time::Duration::from_millis(shutdown_interval.into())); @@ -61,11 +61,11 @@ pub async fn start_drainer( stream_index, max_read_count, active_tasks.clone(), + jobs_picked.clone(), )); - jobs_picked += 1; } - (stream_index, jobs_picked) = utils::increment_stream_index( - (stream_index, jobs_picked), + stream_index = utils::increment_stream_index( + (stream_index, jobs_picked.clone()), number_of_streams, &mut loop_interval, ) @@ -119,13 +119,19 @@ async fn drainer_handler( stream_index: u8, max_read_count: u64, active_tasks: Arc, + jobs_picked: Arc, ) -> errors::DrainerResult<()> { active_tasks.fetch_add(1, atomic::Ordering::Release); let stream_name = utils::get_drainer_stream_name(store.clone(), stream_index); - let drainer_result = - Box::pin(drainer(store.clone(), max_read_count, stream_name.as_str())).await; + let drainer_result = Box::pin(drainer( + store.clone(), + max_read_count, + stream_name.as_str(), + jobs_picked, + )) + .await; if let Err(error) = drainer_result { logger::error!(?error) @@ -145,11 +151,15 @@ async fn drainer( store: Arc, max_read_count: u64, stream_name: &str, + jobs_picked: Arc, ) -> errors::DrainerResult<()> { let stream_read = match utils::read_from_stream(stream_name, max_read_count, store.redis_conn.as_ref()).await { - Ok(result) => result, + Ok(result) => { + jobs_picked.fetch_add(1, atomic::Ordering::SeqCst); + result + } Err(error) => { if let errors::DrainerError::RedisError(redis_err) = error.current_context() { if let redis_interface::errors::RedisError::StreamEmptyOrNotAvailable = diff --git a/crates/drainer/src/utils.rs b/crates/drainer/src/utils.rs index 5a995652bb11..5abc7e474c25 100644 --- a/crates/drainer/src/utils.rs +++ b/crates/drainer/src/utils.rs @@ -1,4 +1,7 @@ -use std::{collections::HashMap, sync::Arc}; +use std::{ + collections::HashMap, + sync::{atomic, Arc}, +}; use error_stack::IntoReport; use redis_interface as redis; @@ -127,19 +130,20 @@ pub fn parse_stream_entries<'a>( // Here the output is in the format (stream_index, jobs_picked), // similar to the first argument of the function pub async fn increment_stream_index( - (index, jobs_picked): (u8, u8), + (index, jobs_picked): (u8, Arc), total_streams: u8, interval: &mut tokio::time::Interval, -) -> (u8, u8) { +) -> u8 { if index == total_streams - 1 { interval.tick().await; - match jobs_picked { + match jobs_picked.load(atomic::Ordering::SeqCst) { 0 => metrics::CYCLES_COMPLETED_UNSUCCESSFULLY.add(&metrics::CONTEXT, 1, &[]), _ => metrics::CYCLES_COMPLETED_SUCCESSFULLY.add(&metrics::CONTEXT, 1, &[]), } - (0, 0) + jobs_picked.store(0, atomic::Ordering::SeqCst); + 0 } else { - (index + 1, jobs_picked) + index + 1 } } From 59ef162219db3e4650dde65710850bc9f3280530 Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Thu, 23 Nov 2023 17:22:59 +0530 Subject: [PATCH 081/146] feat(router): allow billing and shipping address update in payments confirm flow (#2963) --- crates/router/src/core/payments/operations/payment_confirm.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 125787e1a30f..33270795b343 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -185,7 +185,7 @@ impl let shipping_address_fut = tokio::spawn( async move { - helpers::create_or_find_address_for_payment_by_request( + helpers::create_or_update_address_for_payment_by_request( store.as_ref(), m_request_shipping.as_ref(), m_payment_intent_shipping_address_id.as_deref(), @@ -213,7 +213,7 @@ impl let billing_address_fut = tokio::spawn( async move { - helpers::create_or_find_address_for_payment_by_request( + helpers::create_or_update_address_for_payment_by_request( store.as_ref(), m_request_billing.as_ref(), m_payment_intent_billing_address_id.as_deref(), From dd3e22a938714f373477e08d1d25e4b84ac796c6 Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Thu, 23 Nov 2023 17:33:55 +0530 Subject: [PATCH 082/146] fix(connector): [Prophetpay] Use refund_id as reference_id for Refund (#2966) --- .../src/connector/prophetpay/transformers.rs | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/crates/router/src/connector/prophetpay/transformers.rs b/crates/router/src/connector/prophetpay/transformers.rs index b8cf3e3a1f5b..43816bc2ee52 100644 --- a/crates/router/src/connector/prophetpay/transformers.rs +++ b/crates/router/src/connector/prophetpay/transformers.rs @@ -8,6 +8,7 @@ use url::Url; use crate::{ connector::utils::{self, to_connector_meta}, + consts as const_val, core::errors, services, types::{self, api, storage::enums}, @@ -432,7 +433,6 @@ pub struct ProphetpaySyncResponse { pub response_text: String, #[serde(rename = "transactionID")] pub transaction_id: String, - pub response_code: String, } impl @@ -462,7 +462,7 @@ impl Ok(Self { status: enums::AttemptStatus::Failure, response: Err(types::ErrorResponse { - code: item.response.response_code, + code: const_val::NO_ERROR_CODE.to_string(), message: item.response.response_text.clone(), reason: Some(item.response.response_text), status_code: item.http_code, @@ -481,7 +481,6 @@ pub struct ProphetpayVoidResponse { pub response_text: String, #[serde(rename = "transactionID")] pub transaction_id: String, - pub response_code: String, } impl @@ -511,7 +510,7 @@ impl Ok(Self { status: enums::AttemptStatus::VoidFailed, response: Err(types::ErrorResponse { - code: item.response.response_code, + code: const_val::NO_ERROR_CODE.to_string(), message: item.response.response_text.clone(), reason: Some(item.response.response_text), status_code: item.http_code, @@ -576,8 +575,8 @@ impl TryFrom<&ProphetpayRouterData<&types::RefundsRouterData>> for Prophet amount: item.amount.to_owned(), card_token: card_token_data.card_token, profile: auth_data.profile_id, - ref_info: item.router_data.connector_request_reference_id.to_owned(), - inquiry_reference: item.router_data.connector_request_reference_id.clone(), + ref_info: item.router_data.request.refund_id.to_owned(), + inquiry_reference: item.router_data.request.refund_id.clone(), action_type: ProphetpayActionType::get_action_type(&ProphetpayActionType::Refund), }) } else { @@ -594,8 +593,7 @@ impl TryFrom<&ProphetpayRouterData<&types::RefundsRouterData>> for Prophet pub struct ProphetpayRefundResponse { pub success: bool, pub response_text: String, - pub tran_seq_number: String, - pub response_code: String, + pub tran_seq_number: Option, } impl TryFrom> @@ -609,7 +607,11 @@ impl TryFrom TryFrom> @@ -658,7 +659,7 @@ impl TryFrom Date: Thu, 23 Nov 2023 18:29:14 +0530 Subject: [PATCH 083/146] fix: make drainer sleep on every loop interval instead of cycle end (#2951) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: preetamrevankar <132073736+preetamrevankar@users.noreply.github.com> --- crates/drainer/src/lib.rs | 3 ++- crates/drainer/src/settings.rs | 2 +- crates/drainer/src/utils.rs | 5 ++--- crates/router/src/configs/defaults.rs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/drainer/src/lib.rs b/crates/drainer/src/lib.rs index 04dff49b7469..94a29e3b0a04 100644 --- a/crates/drainer/src/lib.rs +++ b/crates/drainer/src/lib.rs @@ -67,9 +67,9 @@ pub async fn start_drainer( stream_index = utils::increment_stream_index( (stream_index, jobs_picked.clone()), number_of_streams, - &mut loop_interval, ) .await; + loop_interval.tick().await; } Ok(()) | Err(mpsc::error::TryRecvError::Disconnected) => { logger::info!("Awaiting shutdown!"); @@ -114,6 +114,7 @@ pub async fn redis_error_receiver(rx: oneshot::Receiver<()>, shutdown_channel: m } } +#[router_env::instrument(skip_all)] async fn drainer_handler( store: Arc, stream_index: u8, diff --git a/crates/drainer/src/settings.rs b/crates/drainer/src/settings.rs index cc64a99e463c..8101abf5028e 100644 --- a/crates/drainer/src/settings.rs +++ b/crates/drainer/src/settings.rs @@ -79,7 +79,7 @@ impl Default for DrainerSettings { num_partitions: 64, max_read_count: 100, shutdown_interval: 1000, // in milliseconds - loop_interval: 500, // in milliseconds + loop_interval: 100, // in milliseconds } } } diff --git a/crates/drainer/src/utils.rs b/crates/drainer/src/utils.rs index 5abc7e474c25..2bd9f092f12c 100644 --- a/crates/drainer/src/utils.rs +++ b/crates/drainer/src/utils.rs @@ -8,12 +8,13 @@ use redis_interface as redis; use crate::{ errors::{self, DrainerError}, - logger, metrics, services, + logger, metrics, services, tracing, }; pub type StreamEntries = Vec<(String, HashMap)>; pub type StreamReadResult = HashMap; +#[router_env::instrument(skip_all)] pub async fn is_stream_available(stream_index: u8, store: Arc) -> bool { let stream_key_flag = get_stream_key_flag(store.clone(), stream_index); @@ -132,10 +133,8 @@ pub fn parse_stream_entries<'a>( pub async fn increment_stream_index( (index, jobs_picked): (u8, Arc), total_streams: u8, - interval: &mut tokio::time::Interval, ) -> u8 { if index == total_streams - 1 { - interval.tick().await; match jobs_picked.load(atomic::Ordering::SeqCst) { 0 => metrics::CYCLES_COMPLETED_UNSUCCESSFULLY.add(&metrics::CONTEXT, 1, &[]), _ => metrics::CYCLES_COMPLETED_SUCCESSFULLY.add(&metrics::CONTEXT, 1, &[]), diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index 2320eabacdca..2eddaf3084d7 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -99,7 +99,7 @@ impl Default for super::settings::DrainerSettings { num_partitions: 64, max_read_count: 100, shutdown_interval: 1000, - loop_interval: 500, + loop_interval: 100, } } } From 9a3fa00426d74f6d18b3c712b292d98d80d517ba Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 23 Nov 2023 13:24:23 +0000 Subject: [PATCH 084/146] test(postman): update postman collection files --- postman/collection-json/stripe.postman_collection.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/postman/collection-json/stripe.postman_collection.json b/postman/collection-json/stripe.postman_collection.json index 9bdb5fdb44d9..4d3e548f535f 100644 --- a/postman/collection-json/stripe.postman_collection.json +++ b/postman/collection-json/stripe.postman_collection.json @@ -14445,7 +14445,7 @@ "language": "json" } }, - "raw": "{\"amount\":8000,\"currency\":\"USD\",\"confirm\":false,\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":8000,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":8000,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", From 394ed908207b8e83839e896e2d1190bd3143f074 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 23 Nov 2023 13:24:23 +0000 Subject: [PATCH 085/146] chore(version): v1.88.0 --- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4b86696691e..e427f33e8fbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,36 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.88.0 (2023-11-23) + +### Features + +- **connector:** [BANKOFAMERICA] Implement Google Pay ([#2940](https://github.com/juspay/hyperswitch/pull/2940)) ([`f91d4ae`](https://github.com/juspay/hyperswitch/commit/f91d4ae11b02def92c1dde743a0c01b5aac5703f)) +- **router:** Allow billing and shipping address update in payments confirm flow ([#2963](https://github.com/juspay/hyperswitch/pull/2963)) ([`59ef162`](https://github.com/juspay/hyperswitch/commit/59ef162219db3e4650dde65710850bc9f3280530)) + +### Bug Fixes + +- **connector:** [Prophetpay] Use refund_id as reference_id for Refund ([#2966](https://github.com/juspay/hyperswitch/pull/2966)) ([`dd3e22a`](https://github.com/juspay/hyperswitch/commit/dd3e22a938714f373477e08d1d25e4b84ac796c6)) +- **core:** Fix Default Values Enum FieldType ([#2934](https://github.com/juspay/hyperswitch/pull/2934)) ([`35a44ed`](https://github.com/juspay/hyperswitch/commit/35a44ed2533b748e3fabb8a2f8db4fa7e5d3cf7e)) +- **drainer:** Increase jobs picked only when stream is not empty ([#2958](https://github.com/juspay/hyperswitch/pull/2958)) ([`42eedf3`](https://github.com/juspay/hyperswitch/commit/42eedf3a8c2e62fc22bcead370d129ebaf11a00b)) +- Amount_captured goes to 0 for 3ds payments ([#2954](https://github.com/juspay/hyperswitch/pull/2954)) ([`75eea7e`](https://github.com/juspay/hyperswitch/commit/75eea7e81787f2e0697b930b82a8188193f8d51f)) +- Make drainer sleep on every loop interval instead of cycle end ([#2951](https://github.com/juspay/hyperswitch/pull/2951)) ([`e8df690`](https://github.com/juspay/hyperswitch/commit/e8df69092f4c6acee58109aaff2a9454fceb571a)) + +### Refactors + +- **connector:** + - [Payeezy] update error message ([#2919](https://github.com/juspay/hyperswitch/pull/2919)) ([`cb65370`](https://github.com/juspay/hyperswitch/commit/cb653706066b889eaa9423a6227ce1df954b4759)) + - [Worldline] change error message from NotSupported to NotImplemented ([#2893](https://github.com/juspay/hyperswitch/pull/2893)) ([`e721b06`](https://github.com/juspay/hyperswitch/commit/e721b06c7077e00458450a4fb98f4497e8227dc6)) + +### Testing + +- **postman:** Update postman collection files ([`9a3fa00`](https://github.com/juspay/hyperswitch/commit/9a3fa00426d74f6d18b3c712b292d98d80d517ba)) + +**Full Changelog:** [`v1.87.0...v1.88.0`](https://github.com/juspay/hyperswitch/compare/v1.87.0...v1.88.0) + +- - - + + ## 1.87.0 (2023-11-22) ### Features From 203bbd73751e1513206e81d7cf920ec263f83c58 Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Thu, 23 Nov 2023 18:57:18 +0530 Subject: [PATCH 086/146] =?UTF-8?q?fix(connector):=20[BANKOFAMERICA]=20Add?= =?UTF-8?q?=20status=20VOIDED=20in=20enum=20Bankofameri=E2=80=A6=20(#2969)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/router/src/connector/bankofamerica/transformers.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index f6cda8ac23ce..8af7cfd6c45e 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -851,7 +851,7 @@ impl From for enums::RefundStatus { BankofamericaRefundStatus::Succeeded | BankofamericaRefundStatus::Transmitted => { Self::Success } - BankofamericaRefundStatus::Failed => Self::Failure, + BankofamericaRefundStatus::Failed | BankofamericaRefundStatus::Voided => Self::Failure, BankofamericaRefundStatus::Pending => Self::Pending, } } @@ -888,6 +888,7 @@ pub enum BankofamericaRefundStatus { Transmitted, Failed, Pending, + Voided, } #[derive(Debug, Deserialize)] From 5767cecab5c819ca82d97c8b925d8f94c0aa26f5 Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Thu, 23 Nov 2023 19:44:39 +0530 Subject: [PATCH 087/146] CI: update actions to latest versions and use bot credentials to generate token (#2957) --- .github/workflows/CI-pr.yml | 52 ++++++++++++------- .github/workflows/CI-push.yml | 20 +++---- .github/workflows/auto-release-tag.yml | 12 ++--- .github/workflows/connector-sanity-tests.yml | 4 +- .../workflows/connector-ui-sanity-tests.yml | 6 +-- .../workflows/conventional-commit-check.yml | 2 +- .github/workflows/create-hotfix-branch.yml | 12 ++++- .github/workflows/create-hotfix-tag.yml | 18 +++++-- .github/workflows/hotfix-pr-check.yml | 2 +- .github/workflows/manual-release.yml | 12 ++--- .github/workflows/migration-check.yaml | 4 +- .../workflows/postman-collection-runner.yml | 6 +-- .github/workflows/pr-title-spell-check.yml | 2 +- .github/workflows/release-new-version.yml | 18 +++++-- .github/workflows/validate-openapi-spec.yml | 20 ++++--- 15 files changed, 119 insertions(+), 71 deletions(-) diff --git a/.github/workflows/CI-pr.yml b/.github/workflows/CI-pr.yml index c79ffa63709a..ecb13f3c1a85 100644 --- a/.github/workflows/CI-pr.yml +++ b/.github/workflows/CI-pr.yml @@ -41,17 +41,25 @@ jobs: name: Check formatting runs-on: ubuntu-latest steps: + - name: Generate a token + if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} + id: generate_token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.HYPERSWITCH_BOT_APP_ID }} + private-key: ${{ secrets.HYPERSWITCH_BOT_APP_PRIVATE_KEY }} + - name: Checkout repository with token if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.ref }} - token: ${{ secrets.AUTO_FILE_UPDATE_PAT }} + token: ${{ steps.generate_token.outputs.token }} - name: Checkout repository for fork if: ${{ github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name }} - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@master @@ -71,8 +79,8 @@ jobs: cargo +nightly fmt --all if ! git diff --exit-code --quiet -- crates; then echo "::notice::Formatting check failed" - git config --local user.name 'github-actions[bot]' - git config --local user.email '41898282+github-actions[bot]@users.noreply.github.com' + git config --local user.name 'hyperswitch-bot[bot]' + git config --local user.email '148525504+hyperswitch-bot[bot]@users.noreply.github.com' git add crates git commit --message 'chore: run formatter' git push @@ -91,7 +99,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Fetch base branch" shell: bash @@ -108,12 +116,12 @@ jobs: with: toolchain: 1.65 - - uses: Swatinem/rust-cache@v2.4.0 + - uses: Swatinem/rust-cache@v2.7.0 with: save-if: ${{ github.event_name == 'push' }} - name: Install cargo-hack - uses: baptiste0928/cargo-install@v2.1.0 + uses: baptiste0928/cargo-install@v2.2.0 with: crate: cargo-hack version: 0.6.5 @@ -280,7 +288,7 @@ jobs: # steps: # - name: Checkout repository - # uses: actions/checkout@v3 + # uses: actions/checkout@v4 # - name: Run cargo-deny # uses: EmbarkStudios/cargo-deny-action@v1.3.2 @@ -299,17 +307,25 @@ jobs: # - windows-latest steps: + - name: Generate a token + if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} + id: generate_token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.HYPERSWITCH_BOT_APP_ID }} + private-key: ${{ secrets.HYPERSWITCH_BOT_APP_PRIVATE_KEY }} + - name: Checkout repository for fork if: ${{ (github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) }} - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Checkout repository with token if: ${{ (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name) }} - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.ref }} - token: ${{ secrets.AUTO_FILE_UPDATE_PAT }} + token: ${{ steps.generate_token.outputs.token }} - name: "Fetch base branch" shell: bash @@ -328,16 +344,16 @@ jobs: components: clippy - name: Install cargo-hack - uses: baptiste0928/cargo-install@v2.1.0 + uses: baptiste0928/cargo-install@v2.2.0 with: crate: cargo-hack # - name: Install cargo-nextest - # uses: baptiste0928/cargo-install@v2.1.0 + # uses: baptiste0928/cargo-install@v2.2.0 # with: # crate: cargo-nextest - - uses: Swatinem/rust-cache@v2.4.0 + - uses: Swatinem/rust-cache@v2.7.0 with: save-if: ${{ github.event_name == 'push' }} @@ -360,8 +376,8 @@ jobs: shell: bash run: | if ! git diff --quiet --exit-code -- Cargo.lock ; then - git config --local user.name 'github-actions[bot]' - git config --local user.email '41898282+github-actions[bot]@users.noreply.github.com' + git config --local user.name 'hyperswitch-bot[bot]' + git config --local user.email '148525504+hyperswitch-bot[bot]@users.noreply.github.com' git add Cargo.lock git commit --message 'chore: update Cargo.lock' git push @@ -516,7 +532,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Spell check uses: crate-ci/typos@master diff --git a/.github/workflows/CI-push.yml b/.github/workflows/CI-push.yml index edc9317e526d..a6a4bde5a5d4 100644 --- a/.github/workflows/CI-push.yml +++ b/.github/workflows/CI-push.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@master @@ -50,7 +50,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install mold linker uses: rui314/setup-mold@v1 @@ -63,12 +63,12 @@ jobs: with: toolchain: 1.65 - - uses: Swatinem/rust-cache@v2.4.0 + - uses: Swatinem/rust-cache@v2.7.0 with: save-if: ${{ github.event_name == 'push' }} - name: Install cargo-hack - uses: baptiste0928/cargo-install@v2.1.0 + uses: baptiste0928/cargo-install@v2.2.0 with: crate: cargo-hack version: 0.6.5 @@ -101,7 +101,7 @@ jobs: # steps: # - name: Checkout repository - # uses: actions/checkout@v3 + # uses: actions/checkout@v4 # - name: Run cargo-deny # uses: EmbarkStudios/cargo-deny-action@v1.3.2 @@ -121,7 +121,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install mold linker uses: rui314/setup-mold@v1 @@ -136,16 +136,16 @@ jobs: components: clippy - name: Install cargo-hack - uses: baptiste0928/cargo-install@v2.1.0 + uses: baptiste0928/cargo-install@v2.2.0 with: crate: cargo-hack # - name: Install cargo-nextest - # uses: baptiste0928/cargo-install@v2.1.0 + # uses: baptiste0928/cargo-install@v2.2.0 # with: # crate: cargo-nextest - - uses: Swatinem/rust-cache@v2.4.0 + - uses: Swatinem/rust-cache@v2.7.0 with: save-if: ${{ github.event_name == 'push' }} @@ -178,7 +178,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Spell check uses: crate-ci/typos@master diff --git a/.github/workflows/auto-release-tag.yml b/.github/workflows/auto-release-tag.yml index 5334c914cda5..4555b68764c1 100644 --- a/.github/workflows/auto-release-tag.yml +++ b/.github/workflows/auto-release-tag.yml @@ -10,18 +10,18 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USER }} password: ${{ secrets.DOCKERHUB_PASSWD }} - name: Build and push router Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: build-args: | BINARY=router @@ -30,7 +30,7 @@ jobs: tags: juspaydotin/orca:${{ github.ref_name }}, juspaydotin/hyperswitch-router:${{ github.ref_name }} - name: Build and push consumer Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: build-args: | BINARY=scheduler @@ -40,7 +40,7 @@ jobs: tags: juspaydotin/orca-consumer:${{ github.ref_name }}, juspaydotin/hyperswitch-consumer:${{ github.ref_name }} - name: Build and push producer Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: build-args: | BINARY=scheduler @@ -50,7 +50,7 @@ jobs: tags: juspaydotin/orca-producer:${{ github.ref_name }}, juspaydotin/hyperswitch-producer:${{ github.ref_name }} - name: Build and push drainer Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: build-args: | BINARY=drainer diff --git a/.github/workflows/connector-sanity-tests.yml b/.github/workflows/connector-sanity-tests.yml index 40a3c3612503..48e6a946a450 100644 --- a/.github/workflows/connector-sanity-tests.yml +++ b/.github/workflows/connector-sanity-tests.yml @@ -79,14 +79,14 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@master with: toolchain: stable 2 weeks ago - - uses: Swatinem/rust-cache@v2.4.0 + - uses: Swatinem/rust-cache@v2.7.0 - name: Decrypt connector auth file env: diff --git a/.github/workflows/connector-ui-sanity-tests.yml b/.github/workflows/connector-ui-sanity-tests.yml index 5db45f2962a5..d4317681a113 100644 --- a/.github/workflows/connector-ui-sanity-tests.yml +++ b/.github/workflows/connector-ui-sanity-tests.yml @@ -82,7 +82,7 @@ jobs: - name: Checkout repository if: ${{ (github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name) }} - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Decrypt connector auth file if: ${{ (github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name) }} @@ -113,10 +113,10 @@ jobs: toolchain: stable - name: Build and Cache Rust Dependencies - uses: Swatinem/rust-cache@v2.4.0 + uses: Swatinem/rust-cache@v2.7.0 - name: Install Diesel CLI with Postgres Support - uses: baptiste0928/cargo-install@v2.1.0 + uses: baptiste0928/cargo-install@v2.2.0 if: ${{ (github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name) }} with: crate: diesel_cli diff --git a/.github/workflows/conventional-commit-check.yml b/.github/workflows/conventional-commit-check.yml index 5fd25e9332d1..ad01642068b5 100644 --- a/.github/workflows/conventional-commit-check.yml +++ b/.github/workflows/conventional-commit-check.yml @@ -45,7 +45,7 @@ jobs: with: toolchain: stable 2 weeks ago - - uses: baptiste0928/cargo-install@v2.1.0 + - uses: baptiste0928/cargo-install@v2.2.0 with: crate: cocogitto diff --git a/.github/workflows/create-hotfix-branch.yml b/.github/workflows/create-hotfix-branch.yml index 77a8bad6bc66..6fd2d4947719 100644 --- a/.github/workflows/create-hotfix-branch.yml +++ b/.github/workflows/create-hotfix-branch.yml @@ -8,11 +8,19 @@ jobs: runs-on: ubuntu-latest steps: + - name: Generate a token + if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} + id: generate_token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.HYPERSWITCH_BOT_APP_ID }} + private-key: ${{ secrets.HYPERSWITCH_BOT_APP_PRIVATE_KEY }} + - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - token: ${{ secrets.AUTO_RELEASE_PAT }} + token: ${{ steps.generate_token.outputs.token }} - name: Check if the input is valid tag shell: bash diff --git a/.github/workflows/create-hotfix-tag.yml b/.github/workflows/create-hotfix-tag.yml index 45699bda24dc..e9df004139e0 100644 --- a/.github/workflows/create-hotfix-tag.yml +++ b/.github/workflows/create-hotfix-tag.yml @@ -8,14 +8,22 @@ jobs: runs-on: ubuntu-latest steps: + - name: Generate a token + if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} + id: generate_token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.HYPERSWITCH_BOT_APP_ID }} + private-key: ${{ secrets.HYPERSWITCH_BOT_APP_PRIVATE_KEY }} + - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - token: ${{ secrets.AUTO_RELEASE_PAT }} + token: ${{ steps.generate_token.outputs.token }} - name: Install git-cliff - uses: baptiste0928/cargo-install@v2.1.0 + uses: baptiste0928/cargo-install@v2.2.0 with: crate: git-cliff version: 1.2.0 @@ -86,8 +94,8 @@ jobs: - name: Set Git Configuration shell: bash run: | - git config --local user.name 'github-actions' - git config --local user.email '41898282+github-actions[bot]@users.noreply.github.com' + git config --local user.name 'hyperswitch-bot[bot]' + git config --local user.email '148525504+hyperswitch-bot[bot]@users.noreply.github.com' - name: Push created commit and tag shell: bash diff --git a/.github/workflows/hotfix-pr-check.yml b/.github/workflows/hotfix-pr-check.yml index 7a724b602586..e178ba31c1e8 100644 --- a/.github/workflows/hotfix-pr-check.yml +++ b/.github/workflows/hotfix-pr-check.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Get hotfix pull request body shell: bash diff --git a/.github/workflows/manual-release.yml b/.github/workflows/manual-release.yml index 0b70631e113d..9ae80047a669 100644 --- a/.github/workflows/manual-release.yml +++ b/.github/workflows/manual-release.yml @@ -17,18 +17,18 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USER }} password: ${{ secrets.DOCKERHUB_PASSWD }} - name: Build and push router Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: build-args: | RUN_ENV=${{ inputs.environment }} @@ -39,7 +39,7 @@ jobs: tags: juspaydotin/orca:${{ github.sha }} - name: Build and push consumer Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: build-args: | RUN_ENV=${{ inputs.environment }} @@ -50,7 +50,7 @@ jobs: tags: juspaydotin/orca-consumer:${{ github.sha }} - name: Build and push producer Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: build-args: | RUN_ENV=${{ inputs.environment }} @@ -61,7 +61,7 @@ jobs: tags: juspaydotin/orca-producer:${{ github.sha }} - name: Build and push drainer Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: build-args: | RUN_ENV=${{ inputs.environment }} diff --git a/.github/workflows/migration-check.yaml b/.github/workflows/migration-check.yaml index 0c4baaa96193..b740bd3a5b77 100644 --- a/.github/workflows/migration-check.yaml +++ b/.github/workflows/migration-check.yaml @@ -40,14 +40,14 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@master with: toolchain: stable 2 weeks ago - - uses: baptiste0928/cargo-install@v2.1.0 + - uses: baptiste0928/cargo-install@v2.2.0 with: crate: diesel_cli features: postgres diff --git a/.github/workflows/postman-collection-runner.yml b/.github/workflows/postman-collection-runner.yml index 3291755b56cf..d5434520715f 100644 --- a/.github/workflows/postman-collection-runner.yml +++ b/.github/workflows/postman-collection-runner.yml @@ -50,7 +50,7 @@ jobs: steps: - name: Repository checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Decrypt connector auth file if: ${{ ((github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name)) || (github.event_name == 'merge_group')}} @@ -82,11 +82,11 @@ jobs: - name: Build and Cache Rust Dependencies if: ${{ ((github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name)) || (github.event_name == 'merge_group')}} - uses: Swatinem/rust-cache@v2.4.0 + uses: Swatinem/rust-cache@v2.7.0 - name: Install Diesel CLI with Postgres Support if: ${{ ((github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name)) || (github.event_name == 'merge_group')}} - uses: baptiste0928/cargo-install@v2.1.0 + uses: baptiste0928/cargo-install@v2.2.0 with: crate: diesel_cli features: postgres diff --git a/.github/workflows/pr-title-spell-check.yml b/.github/workflows/pr-title-spell-check.yml index 6ab6f184739d..03b5a8758870 100644 --- a/.github/workflows/pr-title-spell-check.yml +++ b/.github/workflows/pr-title-spell-check.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Store PR title in a file shell: bash diff --git a/.github/workflows/release-new-version.yml b/.github/workflows/release-new-version.yml index eda2df05153b..b54e240d96fc 100644 --- a/.github/workflows/release-new-version.yml +++ b/.github/workflows/release-new-version.yml @@ -23,11 +23,19 @@ jobs: runs-on: ubuntu-latest steps: + - name: Generate a token + if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} + id: generate_token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.HYPERSWITCH_BOT_APP_ID }} + private-key: ${{ secrets.HYPERSWITCH_BOT_APP_PRIVATE_KEY }} + - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - token: ${{ secrets.AUTO_RELEASE_PAT }} + token: ${{ steps.generate_token.outputs.token }} - name: Install Rust uses: dtolnay/rust-toolchain@master @@ -35,7 +43,7 @@ jobs: toolchain: stable 2 weeks ago - name: Install cocogitto - uses: baptiste0928/cargo-install@v2.1.0 + uses: baptiste0928/cargo-install@v2.2.0 with: crate: cocogitto version: 5.4.0 @@ -43,8 +51,8 @@ jobs: - name: Set Git Configuration shell: bash run: | - git config --local user.name 'github-actions' - git config --local user.email '41898282+github-actions[bot]@users.noreply.github.com' + git config --local user.name 'hyperswitch-bot[bot]' + git config --local user.email '148525504+hyperswitch-bot[bot]@users.noreply.github.com' - name: Update Postman collection files from Postman directories shell: bash diff --git a/.github/workflows/validate-openapi-spec.yml b/.github/workflows/validate-openapi-spec.yml index 530c59c9236d..bdb987d625ac 100644 --- a/.github/workflows/validate-openapi-spec.yml +++ b/.github/workflows/validate-openapi-spec.yml @@ -16,24 +16,32 @@ jobs: name: Validate generated OpenAPI spec file runs-on: ubuntu-latest steps: + - name: Generate a token + if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} + id: generate_token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.HYPERSWITCH_BOT_APP_ID }} + private-key: ${{ secrets.HYPERSWITCH_BOT_APP_PRIVATE_KEY }} + - name: Checkout PR from fork if: ${{ (github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) }} - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.ref }} repository: ${{ github.event.pull_request.head.repo.full_name }} - name: Checkout PR with token if: ${{ (github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name) }} - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.ref }} repository: ${{ github.event.pull_request.head.repo.full_name }} - token: ${{ secrets.AUTO_FILE_UPDATE_PAT }} + token: ${{ steps.generate_token.outputs.token }} - name: Checkout merge group HEAD commit if: ${{ github.event_name == 'merge_group' }} - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.event.merge_group.head_sha }} @@ -60,8 +68,8 @@ jobs: shell: bash run: | if ! git diff --quiet --exit-code -- openapi/openapi_spec.json ; then - git config --local user.name 'github-actions[bot]' - git config --local user.email '41898282+github-actions[bot]@users.noreply.github.com' + git config --local user.name 'hyperswitch-bot[bot]' + git config --local user.email '148525504+hyperswitch-bot[bot]@users.noreply.github.com' git add openapi/openapi_spec.json git commit --message 'docs(openapi): re-generate OpenAPI specification' git push From b2f7dd13925a1429e316cd9eaf0e2d31d46b6d4a Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Thu, 23 Nov 2023 19:46:14 +0530 Subject: [PATCH 088/146] docs: add Rust locker information in architecture doc (#2964) --- docs/architecture.md | 7 +------ docs/imgs/hyperswitch-architecture.png | Bin 1233949 -> 1118587 bytes 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 3ab3b6a7eafa..24b0c726205a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -49,12 +49,7 @@ In addition to the database, Hyperswitch incorporates Redis for two main purpose ## Locker -The application utilizes a Locker, which consists of two distinct services: Temporary Locker and Permanent Locker. These services are responsible for securely storing payment-method information and adhere strictly to **Payment Card Industry Data Security Standard (PCI DSS)** compliance standards, ensuring that all payment-related data is handled and stored securely. - -- **Temporary Locker:** The Temporary Locker service handles the temporary storage of payment-method information. This temporary storage facilitates the smooth processing of transactions and reduces the exposure of sensitive information. -- **Permanent Locker:** The Permanent Locker service is responsible for the long-term storage of payment-method related data. It securely stores card details, such as cardholder information or payment method details, for future reference or recurring payments. - -> Currently, Locker service is not part of open-source +The application utilizes a Rust locker built with a GDPR compliant PII (personal identifiable information) storage. It also uses secure encryption algorithms to be fully compliant with **PCI DSS** (Payment Card Industry Data Security Standard) requirements, this ensures that all payment-related data is handled and stored securely. You can find the source code of locker [here](https://github.com/juspay/hyperswitch-card-vault). ## Monitoring diff --git a/docs/imgs/hyperswitch-architecture.png b/docs/imgs/hyperswitch-architecture.png index 18f42f9a55c50f9b16def5829e9b7a5700d2723b..f73f60f3e35e9bcf6bf09d99d04442220cd92331 100644 GIT binary patch literal 1118587 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%0Da#&W13cqkEHGR%f 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

rR!waTzmD?t;hL=0Y9l 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⁡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

$Kx0PAUtC!u+q>q@6)U|z3I{?|xq zU{S2N@@<)iK>YV9h^grssQ!PEHry(PNN)T5Iqe(m^3P9)r5Rwf#Qf#@Ju%O&B zwLan%ym@Q6|FT;rj zZ3Tt%u4z%QWLUgmro~#kIl*jn){-M*LfKN9voY;l22~A?KCfjSNqJI*y}hFpUG^@1rK_4F8#_ zbv@(te07PXfz4fQLWWzz)Dhs589$!Q2rL-{8^>>;zbRW}4#5|dr;#8uf&h;#t;E;ad6?UMKclm7hW8hx@cyj$ZftcI zJjnB{wBoxX%F~~Y!)+Q&0s@*QiNBpBs-_;k1oakUUGhw3BZ>~*oA1B7R&+j-06n~` zZzmHh-Surc^TpFI{`k-T?sSmv&f56?Op@tO`GEGH2Gcmj{WMJy|5B2;PDd2aV6ZUL zHpYYqoK|Dwu$Z=N1Vk}S5(1L=sD*GYrqF~7V+0TyzqTparOfbm`fbC^w(AZ}BJ*&+ z%;!&_Zu|0y=)WD8zc(xBAKx7h`0k9;fBC1yqTS@*pZhoN&ey%!@qZis|Nk1ywj2n# zCN<~Yh5&B$Pk(Ot$F2=)mfV!bBo6flJ+}yYE}?y5*=d`HN76k=XV4eO1@jQo@hJg- z|C#7X52AP>fk234#Kywgf=`*U8=yID@IrvzL0^<`Z>33D)k?u41RH~IN+AT|6&U@Q z^wGrMX)9d-i)kx;91_zc@qd{luDc;Du8>HiZwZYMFk7{F83=jScXV`E33@W=>Td~F zJvm?g)@zV^IzA!*+21A?049Zr>q;hrjp`>QG2)~LqJRJW2Fygp5Ys-L1?y@iCARFu z(ejgCfxla)!}xrl{4*ZwDQQIg2k%11RzgwHw@ik5YcOL~30UlD#P9e(5@5o66z`CM zuTjN&GCe3G0L-*wJr0R~hH|IJ>BfQhFQTT}8UE6(Wb*gj5O|*1ow~ZZ?;9u6Ql5{> ze-S39^(Y_w)6!^Mq9Fb@Lgbkn?)eU_XcX#yVQz5z$OS&%ScQvd8<_T41PBBE+5h|K z>g=xRfDj*${{#@HKNZm8-{~J)T*=81T~scplXsnnbd+5D)>`v2UI_I|H@X-((tl6y z#D;(eT?dY>x0oS!Z^i=86IW-;SGCny?&w&!K)igDYL*nlvi5`T%^r84~uASxE5MVcS#F6TJDb`WMd)$M&|QJGL81T&lk4BC|rvw)^3M%DBfmTSjVV zViDKY?VhnbvLSh`^e>lU+7(8o9LwMF{m|ogcOIGElx>`05dU49vh6oVwEY0)q)xrj z4LCa>1V|bqAZglGIXO9fKUde(dt&ndJH3ahfWD^p&=NvoT3`u5@o%{A-OwzA{AU`? zWrWq@s4wiumpA@&_>7JVV7;8U7Z!_Var_7hU*-`0EA|56*VFf9g%7^E+422_|I_+S z0I_LhE`Y>;mooqE;>Lf&-Rj4FK#125F#ZXvBMwNL=IApKuhl~6)KAgeafhlLg+|XS z-ag3sQ9vlXt5#yAHNDTxR2BOJ!kG+Dw(Nho@lSW}FER=L*d?*bhdHUUUuQR^7=XhS zq`|6Ugh9hbZFemhNGPcX1y*r<%D-&h@NiTslUE))&!yZ zX;w3XvwYJ`L>mhB)spYcE?;=%;+Cn$u@;|@xU8-_GBl*C5_xb2!ot)P{2UopTX$&c z0Mo-KO+<5ZbNW(n6iH9_JOm%dh#MtD-6yDZ-MVy#Wrj7>4__P32%HMdb7%ky1Zjw|$uGs5}#qCI_E2 zQVP2M;u|^By7fRWi10Zhr%y}FcA5HQjJEqVDV-FW0q8&oP)s{khnR{~Cvpo(T|Uw3 zI2&QXTdxy*HFRo}vbTnS^HTVk7HGd)^No$ZKS!RB=v-ifKs3U1a@t~f+0-+iiak`K zW+1}gfq*x~G|#EeYeD$(jsk%dqQ6w+<1-V-(u=eof$-IzY2^+|<5iiP?p;51m2EcJ z6PeCLe8Y@KXLf*da^Ul-ii3kG0DM!W(Yr86eZCzzk=qNX;@);%!@qS>(5R+y6#~%* zAKLP*G;KNxA@e_&j<68*^zu@lw!MT912_Mx9b!d4n7IER;Qie>`u}Mfdb+dYbnzc& zX!67c2D5pB?!UMzb`v^w4SnwagoXCq{U4{fQ~>m8eJ6m#zu$2){!o4X#fX~w8-gCF z9(=@>O!J?WhLDuF59X}zoGF?1dHK*j^k-yD~3yLx{oA;N>?jyX*enEvukWM&P6 zq59=J;XmG1DqCJm9VOu9n%+u90IccdGUJf&|NbER^nS5?IQ^dlli?gh?`Q8ql>xA$ ztbnj5Uz>nmvjsj>KHrxB4)>ipEw=>wtW3|U83)e4eA~*ek@PQ~%ky=Ez>{e(jf3Ui z4>_+iz|L*Nj&E5Cd_#gKG@=g0PYVq`p8x&O{N3p6H2ePBvhTkJuRNi_U=?~1%<$iz zo(rPr^&NDked~5`=KPfDGZtQ(_`N;b{&P2_{u&_Dl0^WMX`wU@iT^Z`NS=vsSBoWm zOK$MCj{ZNky@5ZN!S0Y?!oaNmOqM3y-ltP1(`RJIyq}H#-TvvY381uS(I;TnTjF~U3M8I&utN)p` zU?RhKlCbsjLxn6?|26bMjcv*KnR6yuA;|Lhw!hGPw0KxjQgV8mq~gzweeQw`!fWxi z-zKJz1iiOcu{?2NWp@6OLt!yu!c2mj$9F}Fl`9L8ZmRuY{;6T9xcC%3ix$lrymxg7 z5-rnoJile3pfuri2XKJLQ`+1si;y8qCbQmg;c~UbDSA`P(41i<$X&cCtEsQvaY~RO zP%9=tW^fx6X<%S*wGV)7PiBJ4+}Ok~fmEwz1h4 zL^@YCzO_NC=`W2?f~jkz5#t(%k+v(B9~XfWmtHp{T1qk6IkR zUVq`NDguOvN)m|?l2U7%<^4b8d={nVvMSqXDjeL466uj?M5VQyoZL&jGg(H3q%Wtf z!4P}>^TbZ^wGJ*Wiju*5?X#C?*E;5W-6sickv=(YjFP)_%OM-k*DfKvg-Gy!{^JDr zq)q}#goz+>?X1u&qZDB-;mu_c(zOq#l;O&(8!8xIjoSEc`elN|?ZW{jw5C@ZrRXh3 z`Kk%a65RM#i#DPA<+b0cPj5T98qk>_$HcXC^#q00AFi}kTDo-U$+(Bk8ACA>na1ym zJw?myuTQsx%Pl;*#nAifIm#BkBE-1h&R^v)@AT9-=BTde`kHhfUlT#yv8P5U;jyu} zEL)YxLdI9kK8(#6ZDkaRiz}Ede%s2d-CO*i!tB2>0HCpWi#-r58EP|08CG$2gON^PDAi={wug~MM(oV^D-r_ zs?pMk)~HE3Kgk-7z1h8} zqzJ=0>F6l$-ru}z_!*YN>x6x(V~!M&NOb9VecO}K;z6OJA+P3K7t3DOvCU7bPrrPz z2)%q#D>Bx{c&?Jc3>`Ut3VvA}cEGK?(1$S+q8qQ> z>c{FErQxv6^W>$>*?MIxxugMSpRUHInFUh^K46Rul;f@&ops9 zMAM=l-pe>w!0e1Hyq6)R>Nwvt*7iaCq38f=jE_A3Tj6hF!czXdc1orVX>a_yWqFR& zs%RQ$hY5cMa$3qGN$W(b#}SbQNE_!EhL+gPJ=EZEiE-M#DClEYyaCM(~>-vxCp zd3Ns8+gHjYEqahM&xo{m@JN#S{NkJ|segPp^_3vG_osmAweKIPbU*g46~-v9GwVzf z-5y6!x7?98okVRDDtU{QM)V=M&6^jPx_)kZ&igDOV98Mx?YSFz-18@)`mmUQrypzE zKGoDNG{fyfV)%z=!3_1~zJSl$o#fX6%cZ1>inG@}nq;EQ)BQU{#7wa|x$pnJBRot>Af z6gF=j=XJqnJB|Y_#`_b#HSUMk?tP7Xd;55%E4AuAeF~}&GC!t!n9r=bl?jdU6IzE~ z^aMf#Rf%4AToP0(`c!c5KljNTMDlY$Hj}7JkO5(U3}3(9Bp}GIUx6pOS0{H{G+V-A0WXL?72F?QDWD?36VdCvT>^R* zeJZ&BpTH+#X$n4>xQeZs?(hUO5YreI0mgqONk4?`d@{4j<8l>tC=R*ciw;lxu~3^3 zDhilS2X<3ALgRiS7ymBkF@#1$-XB-eli$MscUXO2#7*<4DAfKbp|pQHzbJo)Pqit= zib_hZ)GYgCwL7OMR)ek=tFEMwuvIHc7_a-KsI3{SK#J9Xxr*5c+lr*zP1;`7e2-jm zkF=S!J%rv$VYBzPC{@*~p_AGNl(ErB1!K+LyQ;eyGE>~K^CP6FK1%ku)J|5U`LO~r z7l{I;j1V|ZP!rTJe9tBW>VtN=jz1W|2C;+K7)rCTJJ!Wll7SZOF1WXUD9}m_ZQH%k z$`9p>mX^& zC7Gw>LFz@xH&nEx?&+NkHk|`GP~168!*(mi0?AC!1DDn&F0w!#$^#uicfsHGLHTGU zEP;c3)I;pwnD6u^9CR5+ZykZ!+?b>+sEz;;M{3o`$0v1hgD^6YU7z;&(~AflT$4mW z^2*WN;F3AY?_{>=f#DzfzO=jysx)%@;>Fi9^2Ri<7aqNZFJ$yZbx%g@RPn&!K4sa@sN<+ zBjuE@AG&|4t|}&oLB{lq(0E&ASv+eoDSE4Uv~WY{U$5P_GfkQa19u)|yE?WNGK;sK znB*KHZcP+-x?^v=xVC#=WoQlw&#QL++d^+tD!Wa=n;N_it3I;a4#;si!~b)mOkTZHtYUffu!b#HhRGSeqQ(UlyMP-6p|bN=1NpIWcp?m1B1S0LVE zCr;tWQYjo_N7lnfO8An&ruYZUYV%+t%T$G1avO`*{WaM|SS*O5Nv*J(U+CLE$7Og( z^<+osaDMGWM26{{m^_Tu<{;Qhu?Tq7csQjsXsqA*CBZvc)tx@(Q+Pe&QI6jmO;UcI zuR52Rg$~NIfsQp3pu?YhmhLA0Vtc=41`3pV!Ne@ROmzMy$s`)9v3DHyEK0hW|ceAq${{%_p6MWMDmadGnv&nynr|9tgz`xAi21Ah zXmp}nl>M8@k;ochkf`>A74uIM=5zC*;72<$Fi^I1{z5GfO#$%~nS2@MVRjD|Li#Aj z7d%38+?osA^A!A(CH98>MHDuUe-nO4*y#ls)}#SHst%02qP!)CvMJ~J{*cuDqWoox zP#ok~siPadtfLNMU!?EaVpyHvHL0{jVRS5cSJ7QMpxnSn;vH5eHAdsDodi$OOPwj3 z$+GM(*CQJ+x|vPNiG;wrckdR_k}uybe9Sy2pD-!VWZ^B)+y06jQkr-<^J{=Ghbsqs zyPnj$wS9tH;9r^6b7N77xlUsY zh0#LL9AUrBFw{2_GrBEO- zmJ^*~yCcc;(agf@JTJLkJ8LYYo?cJrkuvt9=Yv3lF5&gASFm#&A#&$4TC0 z+r{SQI}Havvd93G%=44$=fZe>-<|1^dQ7b$Yspc)pH?3TIDg8@ZW0m_WCWeua{2u$ zb!DeR__`{;94AM>BRn#;e+1a_Q+^Sx-`L}?vbT1qtH~07>P1bGk@viXvP2@|a%GF% zoj9Li8;mPp03=*v+HH5~(Zl57;6SI6l>XtjTQVL1L{4hUy~get2s>wi6Cb|R@HguF zTqef@+o%d6*!}rfUf8Z!FR{ksWk?=}#=TAVC{2yUu69R_*$-$TcZF3Q3zn6VowxZ* z-?LwO!Z_5yj=kd{Z_9$g%7}MffUb&|?|+np+tPmth#T`AhKvzr%D%A|Wf{&HYmj_y z7BSgXhEctym@~lo=jK|SJGw{-tw+Dzh6F@V0RFeCZ{Q^nSV1um0NZgzUYi3>W`(` zF$Q=9mn&}FAK8~*sh0YI!t&x(M2Ck4_hj7HHF{0>-XGp(!c8evrUbtlw# z44IO<%?9!h4JR)-8a68|B{{kIiHjA!VNw~08lfxt=jjTV4~rkl*5+muXJn{PYI5Qp z0j=KJ4?xbVICRY{oYl@#%rU*C`}LB&o}3?tr{GiFPyxeixS?z>qs*hlrh}-5a!yuO z9jWQ(Qu-%FLk@2jX#YPuq?GL5@|(1wHsh#)p~fJcIhpbkow_v=Pl19>#<6(8fHuP ze&?%u|F~QMH�_qft+iSW2f`ePuIg3uW=)>rStuB9v|FcJZ`RegDzTcDh{#>62Tv zV_2vPF8ChnG)j5QhtqGXldKkaS5_TsnFm)ok^H*{vA6j+E?)e^kX})t4ZkdYxPLe~ z`SI;&aXS-PS=mWsIzbi^oA~3l#cTHOJY_YUoN+zlM9-w$;r0S*7`vrxE2F|=+NM2o zr4H;fTw#d1Di=wt)013rbd-qyR-envqdjApm6Pig_!E=Hk>qJX65Dg35G- z8gs*|2ql}~yMYHId32lqr*g{950*f9lt@l$8lpxxOg=XtI3lwrV|Eo>UYcpgi`Rnz z&#f8+ZkwQ%#N895KD*M8(&;;snqkfVC_3oai%s)Z9}}6P!TT&B%zONE%;18EIniJ0 z8vZMLeF3Itzur}DZEejTSGTu$5_7xeX-1E)sVw@=Dc2GgK|{u`>x{?UVT46!;UH6= za+{2GOU^?2^slBpLXSc)1dI;Pw@Oy2Px~6|0^>xjo z(RL(8y$CZntj53%G#3>~;TamBb;ZpG} zC@gGNCT;?{^BJ8bQ+z;>O5vE{rqvFKh_ZtplICx0Kj{&h;U%BDg+$6;u4;}Ux)@LQJtAHf0+z`VP zYrjOSu?~bqs0gXjg7|R96FgK&4k_e<{>aX-nm_cKYA(I(P}m23Kjh7tA0t3n+5s6B8QLmgc-Kei83HO6U_d)tNc#Z` z?jCz#9UM_N(6rv%{FuZW{i3luG))CaZ%1o$>-Kab-1b3dXQzjs)5xBma>~pgh|93i z-ChIa(cT2*-X^4w3)AI#kCeYgUV_{-YsG7ocqSpNKe+?;uPe`oI8 zB^Ii1bH=;7bCeL*+~;RJ@_#n-$jNrZWA5*%(mwnDtVOoA7*eCM7)~jfq-fXv=DT^+ zX41Z8(OA2I!rDf=mK9C3fpHa)FO(A7PNsJz7wA7^zSg33tbcdnqeUE@go>?d_3^l{ z0e`JPrPi&}A!mImt^wue$nkNLP=6zL4?!2N=8bH$C=O*~y;_{aVOm^k-(R z?&-RdKSFcv!s=efws_yAkQ$B4nwo zSt{*FNXRmlO7>;!%uFjqQCUMK6|zo}otd&t))6rnOo)jwCdSN|88hd9MmOE-{$IcI zx-ZU+Ip=de&+~bn_xt@kpQD4N0a(6g{m5LLPngQ~g~{)}EUZR-o)mK9ig`Tnh|^)1Ei zRk@kxtQDg(j+4hR?9qiOGkBo5w#LMaeh99<#ixco**JR?*7InJiC{uYf0XoS4Ii1~ zn3irY9coVF-^Z4t?5S92a&y%beegM&^z+s?&mZWly&&hNLghcj^N8uG!0doe{8=%( z-DgA%_hV3(pb%3D2z;MI?c#Y|7`r);ky&&7mQ{vceB_UHrHC-o+Jz2kJGzEaq?LO( zWW>i5yz)_1143FjqH6qK4=5nezwH;$dGUE;LK2R`EF>+J;&Na4@v2B}B;5$=uu#so zV~6fv_Z-Vy4{q{)nW*+|Ue%6%Wt05BV~Ie&euU7^tG_W3yAikH`9QlXBvNm864XpT zf&T*=Ghrnq)C6n^^drow4?ESYWb5Wjx2_A5>bIzN@^&Z5qvx;k;lo!aOI#^~!HA;P z6x*?3)2o9*dCD(1c;lN(yZ7ZGz;?mz>MX^c}Ds?g0P^r7NiiRta*w6UD^+lF-tht&(`|gI2hfR&t^qA`a zGuC*I5Q3fWg^J;D+P67UW;ojdz@#~{`zi|5Gtg7bfs>s_uVW|F4SvLNy3PBW4Ita9 zh!SYV*dPTr5@D@-K1>E|h>rij9P_)u2L3mT$tKB;JpyGNT6pn=T7iht0&sL(08{?OH$T=zJrcpWxLo zWx3;JgK+jvSyPcZ&c{)x@NC#ZDbmpIVHI-UYsv$Zo{rQw->ri)8=@|GyCPA^wao_e zb7n=onoRC9d4!fjo}OVmY;uApDYy2bPQNIftSUcHw=pAhfLzG#aHfxJjF%{p1ZXZP+n=(-s& ztJio4K35XVn3#i);SHguNQ2jO+LdtTnY5t#mdh=)=TzHg)K~Trf)AJ)7#!YU#wGws zBc?`z*!3%0anw~kweUW!YW?bNkr~ZjIaSSB@v+IrNqUmstDtXw@4x_-+BpJc&IIjF zzl+?Rq?fZqMey!LqZIDH$xsqxefv;Q;4a09lupvA?RI*187*J>q4dwx9%-K-Q6}pJ z6jIPt==~V~@r(f@&o&irjx)}rN^c6~4ZOmn6urwNjRw=4YLt?2Pe@?wt%P6J?`cI* zXhr1*e@*P)-+!kollx82a_d{Q`}cp;6{#IZk8=6$Q{)=3si$lc^?^$5k1^#BB!!0i z0bhH2w=NBW6RQ;_4Ar)V*Ig+Nya1K_A`j;tKC>&cPf^ZW!g2Am?o_LNp;Ucnzh^Dz zt*shc)Y}pAaRHA=RV|)7z(Ju~X@yI>?zHHUR`D3^KUM^y^{3KN{Ad?RDHj<67;~^q zN;MfY98AN(SSNK#;SM@R`U1UQq7R8O^>RhVC+rvs)VPC*X^2Xk@OxMAN21t7SSVGM z+iGQNNqAZKz*9re`snr_V{NLCvbk?S#Mdi==fIoyj6sX%tCm)W&XC{BQaus0UB|Z< z>04B5D=!mDnM2Pn5Cg|sae@PXt*2DB$($K zbXFO`U|}{y1;H#9?9mOl*v)c7n$_%ypZHUjb7^otp`T>>t;8W-pd7@fZhf0iwaMK~ zpDBA4IC{_5zQPk?7S@heN~G=t<`3Jkn6pbz9BQ`>KP)|$JYSB%Kwucf`^pt!t0XLe zw{{oy{qz;5b1I+gHy_@jKrj)0ZHpg>m~~{&-U=B?k>QMuyYG&NGeWvkX6ueRnC1Fb zPjzFYdc1w>biicVs$4szHeWj-@Uj%|qz8`EbLxSrVby0{mHFC{U~zKspqQH z&u0u4ZnY3E7e$+{E$0IewfLf1A5>XGySYCY*0;jYPq~aI@d!mOk7}SvsIj)=XzfKM znBoKcXGM_cKto?Mkh+&i+-X%|%KUqI+#FufTK2!sx_Y9u56Y`Tb>8;sd(zMnaMr?8 z+$6B)QR$1EwwB|DRl{YQW*!8D;y_GsLo*8W5i4#o2aH#_J)rfDrc90z+;>_Z_VWgp zb)6o20O$L~K=2}rogn-Co?4lwI`skgtd>$8p2GF5qmY6=`aKE93@+g{0>U2hRiRYu zxRi`HLXYs{T;Hsl<6+CuKvpD#t%a%jVgNW)Qa?h@lG&6q2=94n1O44p+Nb#pgN0(j zlxF{aZZhddiE5#6)X~jV7=FIU#{AFLu`OE2qm3`{;Lix{i3K))T$XPBF#|Qezk_eq z^$qFxlm^c}dRGlmhXoV97XFo2ksI{w1=6%QOf;ashYHM+y8o12`f&BW*(^es)pF~i zj+ggN5;6i_Y(Y0DI{YMlP!oE^Ea0=andJ)F3M`Di(Tkz z#UR)Tz!C4UAxqrr1g9Pib)s*42ZQjXHiesv?^s8*q53+^^-IWN6gdY4CE-(0AYA;t z7F}y^8<9lR+IF@L@h7;vJe33!#OA6l^UzD;#0bVJCF*VZN|4!H;&eDgYs(f+EBoU) zH)>VHgl|E8TaJC(R2vZr{Z<&n!F=fW**uss1pNUA!1 zo(X%h%@AX++;_?X5X0Kf)t%0lEFIaC6+P8`Lai|}l>oYofL3V(up=T!~>`>94(VM(?y@g8Z*ih848FkFrK;8fe z?}5A%8KBm?|JJFL8lH1pRVEDWs3|niCKv8TtyA*eD`-7YrO*5lj&J#BV@YQBAeY?S z9nxoG2|l)l_MTQZ2UQ0C+B#Ei=X)+b^7}e>%P*;zt#J+}NM7@H5zZ9Is_)^LXCKo`0 zQ^fV{>8k9>^&NZz8{yO_N8c(^!ZDrom(WbFFYpAlx&k>=f$7@r3nx;+I-IopJ}+fV zGOl3Iv~E@2d@Hr$(s%Rf+hdNP7+`7?+0-_AmAsyi!FQ8}q=3ba{cGIugX^fFoYNFl z&|fb0^}c%qLt9AQn;DK^J+3=YQAba14F2R9o*+;jJwkWVEnKSbNzf?s<}4SI>?7uM zE^k#ze~Q2Xt6a4IU@n;1`G?w6KvVi3e46z$YZdpZ2r_;dJD`XW51V^n6n)ykJ4{(8 z(R=vqE7KECij%!(Xi3_@khPWvVzQF}Qs!|3?gK`I&dUZ|SiMb*|p0>--5r_pB#N(-w%L! z3MNi`$c$CR9NN;S8^=!}efMBsyb>XgHKpJ|ut^YBLg9jp*l{P&s(gx(jrYZkKq4oS zj9p>Z%B^z>*}T81f7Hi%Rt$fm4l1VK9r5PCK{!!dBzd_U{9ECoj+aCK6M*bitaUB% z#k=|Qln1IPi9N{^+mWXA&q0-dsodrMyzBMQe&)m|6C=O-O!`Ak<=vCZ{?3OCSaciEw1qBnQiX?D>?joAi zl~ex~4SrhcpIr6pp@3VH(id_r(%wl&1%jCi?*oA0{+Z?zBSgRr90)id1zeD->y)p# zmcCwccgn>)&}7aIwUxtbJi}BC827L}e6&Vx7>kSTI6*GfQ}F6}e3%Kca_xs!xXO9v z|A!j7{z@{&=glX<5n3K)Mw|JHK81p8tS3i?9Zil-1A{ATp}V3_M65bFZtsWhSHNf{ z+qa(_+OsFG;9&IAVdhuW*P!+hmSu%+F)w^z$|RIWgB3yn{Dnxg)!I+%pR9W38MLL4 zfbgh1VqShHe;z&*#~i;Jqai!o&qIWkN40?YInP1p^I{(G0^{{-}&BYe2}o50bv^lW@G!L4P9dRySh?)SF%tMnIaK3Sm!%+|2~%%ByN#xj+x!ii(JcXb2H3Ta`VXLVm9) z%*9q&Y0S-cvE=A0;JtDJr9o&e;+ej-732^u;R_W8n9u4u zi8EOtn8W%YKoOB9vqU*R7hTl z`Yd9s%`9hJIeNqHie)WfS#%r!pB_jB3<|*lHJ6!zAv=mHrT3!=T!6j{LdkMcxq8ub@4^mLSqUtW5bc%Avz}y zGHKfD-Mn|*;ES2LWInjvH?OM$YIUCRI?)x+!9@N(PY)F)QZx>7g$mJ=LA(Le6NNrR z_SZ_Xdk-g~U&GvM$5JX4WTqb!Syh+A4$57dfO4c(O+w8|q0#>IfuULq26=Xhj}N_{ z+<76x>JsdxgG-5iC;FXc;&eD)4az(0TjNUcM}n{{T38{157`R5g(W;m^f4u=yUju+ z)q8c`su1r4t^0#UY;M>HDzEmSQdCAj)-j9uik!~A4CScACgNu2YpciNz7n!rMkg&^ zR2}>lxnpAcb5M|bUf#4U&Iyt?T3iFAt2SlPsc=5@%KMq}qlX1SZrnWRO{*>ciMj$l z^p#gH&a;nuS@(cqEsRob7CF&k#N>P?UO`~4xlwiOx9q&^KjCRBUx;Erk)dPqQkziAB;RjFShCC{sWU z^(crtTbmK0GND*7f$pBfXzASW>ft6+Ermgs7M3U6J9Z2dN2VX(-nDWVH9Jf$8(_mxJ96y!5UTETL)+5r4!O5X;f2)Tgmm5M!yA!e;)7Qhi1Bj(G96U(;ZIFhS^hHKTM?-IMl=Q~( zmbCsr%*4e~zA|(nJ@or9)dak;V2iVBLF0>e<=o@TweIE7{QUA3wvwUm+862oZTFtF zVJtqdUh`AE)IIg#VDiDK_U9G3ugcveFAHyJl{pTt|WjT#u{qTpd`MPR_)&3mrqZ_A<<9!lBp4lt{Pbr zzHZx1B_X-fg{HHl-eeSJRxj~KF+cJuCLY9*n^GW|k4RSr0!^<{;OWB^iBVjB+7c1V ztn}n;0|A5MpqukQtieC^TIteLlZ$$i#FW>6;ngGXs^E`3i48bSp)I=1FQ5W-F?m8D zJ3eyBa_Y?xXrj_Rua~7XeZBdQ5ECjaxRjpRkTZ{Y_4rO_PJLsmFLIHNdu{sdZQ+tC zu|V1+g)donnfdN|9JDXsB7XmI?<$HsZ(`ntHe}y+^WBV^=hz#jozO=R-&JO4UM*Z7 z--NHLs~fGD|BrewhDiUr73T8&rVJXVypnb<02CcY6l@d(t-%Gv2rTp3)eSAWlPWy^ zM1(umdNF_8LyA)yC;bg9uEc3}`8b48^+4gZaEZHMs+Cj9lu;2?q2A#O%ritV(-}O~ zpX=7b12ukpPb;s^^xRuBR1Hqm{ebT_eFklcxQd*$?X8BZCNemBC8Z>u=qO30om1^z zYP9A#fpKF90jW+C-q{T@85uLIm9XL9kw6MRhWYM|KF%kbfkj-#D@^b}R|TI)mg@US z*De(KqK1PB_%gGR{vaZR0 zHYTz!fF|sb+D!wC0%q2P9Jw1HQ3&0bxV+Rra(CmW&NU(MHca8m!Ko`P&~T6_s1j5J zttJG@#=nBqjpAWV+?4s<#0ktfe48&&0I8Y$7(_1SkEZQeI9d#OOGpVc-9L~1mBxv$ zhjP}oU3dzXH8AEJ>Fp-Y-5nuR=v&bw^h0SWY-LkZ91Lc6qPp<8* zb%VDqfwGsocoZ0C;EOP7m7?6Wu)_59TYYw1Oy+A@dRr6;m#e54paz-V0rX<}OjC;K z{ZH;zgJ?_Mm?Q0J^~1(T0&%njhxzB&!1LfIzl>#*C3>S1Rj^qHxgMOCxkrP@I?{A> z!ZjU*?=yAj_k2OV^(=6o0kVrd+&XzuL z`F6_~tvF$62As@)9!}OPcuXvyWODur@A(ma{bUL>hfiQTs3WU6D1m-OJWW{^?d}~g zZOJ;RBRYM4ZE}E<>L@Au8;4JzNJt5M)i@7sc4-__emR0QF`9fjZzY7RfHY* z=$wAnWrEAc9wzCZvb<<7Y6^DF1{HnNe}eT-M9}6lmiTTtc3WVLz|B{91H9sSwSjZ6 zzE#lG@9A&odf&$*5zEEnDg!88zL}_y^hC?ULb#>fXWDE z${5MPm#V!{654X~A&8~@6V|;#FZjZtnzCUsB0Jym3rKEIR(k>vEbbR{iQ7uzEXW&H zo8P>G#_S7KVeDi2C(trLMB;9_@3pcZ$y+y`Tuk(o!@)9{(mPV9o-+8H`Ox_cQcx1r zbp#5|@tJGBzDt0Lf!Jkn#44|KstBwDyaNPo{z<GCqo|{!!-pdJ6>MFMj*-xAI)y zT7Pi{zy2Bb*H=$49<}kIyDl(918cx=`ML!P^%C^i1gn;koZj=SV%Wc9@-XU&EhZBN z&og7Os|>Le-sI!bEvk{Oebgb~O<{{PkB<)_;dT1E-LCOyyjB`9E_dh-pLIl=6j!5pzi@r>1YXjyZ{xF~&6=wn<><=t)Hv|7*HQDn+_kxyJSxF;)dT#l zTRUy5Z{h#2Uz}xoSx7qeqwB;U_%F*{wg33c%u3UU-ybC}mYIuJ!Tdv|sHb78n=XGV zuE+iqzhUrwxzyY*4pyZkO(q9c^-zA8)%jJpPjbncTwG4Aaj)*wH@Es`LjSh%uW`(* zbo_PQ-;f6VJh{YrQNP@n+QBNXyH71{nH8D z1VmlTY1IclFjl%E9@!+?mi+VtTcN+~=w1ce)K|~AR>pY&zfFia355Sws&81E-SXW& zl*eRUB}Xp5x(912EUlE~%F>8RbeR1&*14KUX`_pb3u_S;oi1Z@Icd6O;FaGo1 z^tRr7xMEqtagu5z*;-~@os0N25zF7dvdvd%0-H5S)~C`!dccrs0u~c6jx}8Gijj=s z3hx>Io&6|DB-?%Wy?T8G%T_-~#ER>Px!jsGIXf`h;tT;bawYGCzdsCnpXCa!9XmK< z-|DwMIEBpY`#H217Ms;mj=h_=z4iCRg^gH^U~!qS)t3CHJMr68K+Jc?>XvK4i59(e ztH$;_Yde|#ZNr0?_1mt9*_TiMdqxBV%2!&1Z*ooa( zZSde3f9*VT!J}(gNC6TVou`Y)p5r6OZ|M`gguWuuMF@|G*O1O!i=3nze zxd+laN#LF2X>p?#y)COBLjRXydhtQiGd%03pT}x?%>*D14Jqy_*lI|Fr6NFKql(dhYskD$R9|Ezh3EpY0F+mouHqP!n9R+-_mF>O;XR zZxyl0y8ATg#UcLBwP#yP3mIj#s<@g~OgRY6|8>-?h)+ibUv^k7d*)Z|)-_k>G~lv` zSWHUS@oB@Ha<#nv!gIqHSvMqXQdyxa%&U6=S-mgv`&Vy0U?HJ z>^G2)`^Rc=C_AFF3SR5=}RU`8y`2h*-u3L*Ip~rc_Z<5dr2r%d2CtA!uEcF z7Fw;jYWd%suwpg8n>#GZ^Y8z3yb$%sLOs%sv#Xr+E#a5HeOPmzU!hx`pN%>7y)EzIg`blP2c2@0l*<)+>zQ@f z$VAm@WBpDE>yp-Xa5$U?uTsW|^ILJyuaoymIP(oln0Zs1_fyg1*D35dCaZoIb60wbZ-i)j-tIiimAZ^1_>a#6i3-cwhd%tpH;Mvz}=e*SQe6`QS49{3i6qEibvsU{JV}W~e5`Sk{HZcI=~AU?Ss_Rj$O@O`GK7N<$Cg z$%96OJX3d8!US8BiCH>G5zp zWITvHH{?EJY(+&E#C~DIIOuk~-?8s@Z{9#UXfI)s5UWpU6zL_Q0B6n~tl--1qG`*+ zGROfYL_AYfgZ2li~&M;{y)l_-}hsdh{u&xvY$FP?q;ynW-f;&fvJQPX-LIF;t|2Y5z zAzCx&ZI(?|ml&rxgVPN}q6hBZQog|3oxg;(7Zx#xDv)CCBleg@l#jV~9WWPf0Sv}> z#{=w%EK}t=%n@duN~L4xz&1=^_7b;SJ?I?Z(|4%c#>W!ry6L#h=jd+Yp-%=)SLR(Y zgX6g)@%n}Ff81GDy?-c6X8a5J<(<6EUN>TT@m%}Nw!UswLq)-4spsk4sFeN1+UvI` zHYE&(rF}QTYe+ci#~ElaE7AznX2Y#w@4{s)uAos9_pR!#IBucW7|^{hd|cpOC}6bB z_vRGiWxFcg86=Z#u&OD(y%mBKe-1|G4i{+w?q}vthv5qm27(ULi{>JuMzG=x1i=ja zSIBUM(f}}=htIY2WuqqDo*is~YkO$E+rG#8L`QX_c`R8!Dq#W#)(H|QtFn2&{_iG8 z@oSe~haEO@VA+Q(VGT`)z%0VVkT(L!mgV&#E}*zyd-+P>OgVUymW7?ZkBH;()e!`m%&}vt6Q059!S=Z>&u~`c>)vd?(n^4eyIBD|$+u#7PxN>~fM%P@ zGDc*KEFA{8%7w}slP1onjAM;)P8KBFcCpH#TGI_FVXOC~{Du-A)_`r5I|bV|G%^v3 z9_#|&45XM8CZi6IIS>?)YY{RXWe=^j5c^A&{#Iht&6_};zR62e|I*Z(Yv{&ZGwu;T zu-VxshmGjPzMs5e+Z+FCmUxLtn*tITh^1F8!CFHLmQ!CI zAr24wjc4PvGA|694XGKTQJwF>=7(GPw--HS4u{p*T!jti&$o$9SWBR(QSc_F(go&5}T8WR9d&YTh=6TZZaof1UM*f|`2mcE)M36!tw8@REc6q6XD=q%FOUb}?kQ5WibF=_*o+YIT4r-FQ0&urBO z)Drf6_kNUTU!xX`xbM0j3}{SMy1(z7bN)$oV^ca!dVy>6DueZo-&qrpUI-_lvkz_hAaxId(3vb-Rl~SAxnTwv= zl~-_dJ4xdp1Zp1}i#g?9s$*O*sM>c7R%OM^dlnCG+EsJ@*Cb)=8;qnu>nwTzIGis4 ziQ^)I`hgan83RzE0G2whm%1@pOGTJ;24a!Bz@>irg?)~Rn~gL0A$efWznx$spUFZYr}g5eFG zIF)?q?)xFk!2cxBpd)33up&5!UX7Gp z_n@!KI=uli2UYK`{oY+Fk-h**n>x>A0QsIO8kSM7IsB7(J|ZqYTCUE#Ef`$e;axJBkS_vgfc;tA z#%)s_72J?1I_QG~BrdnSD}3o<48WvU)s#5n>F&Bai)CY;pf*3UGYAg)ym;ynt;s)S zmRUFyw(EBPB-ScaBzu;X#D3 zx3TPCIla>2ihg0aER1HwjgW@Y34Zn%_D~APXDm~>WAsa;>M-F*So;!fvBJf`myE*h z+sQ19l*Lbd99YY>kWHDj>yI#YsBEv>jX+kqWev}B;B;SO&)4XYndi%I^l0Jvay-GH zr-UvAqN&Yjd5+-#0Iq5xPc=n1hD%HestV!O!(kMm#o-ca@5BKBfA4|ygT48i)Bl|6U9{U{v=f~rFPIatt<>@` zF3q5h-7TwnFJWTuIM9pZa=#4!$jP}F_5~j_O4v&`y|pl3E(@QHI=sMLqNQ$rt6*+` zf@d;}|2Lb3FKB*WiHj}$Hs1O_?hj5svpC-Rp%J-x`dkL=Jz+zZ1+Ap3XIlCSE1#1vT>r;gn z98we)1B?fjQhV{8Sieq>PRpK(2`fEFtTTmaggd|a7|bMydLr?d?W+!IiG=^Q3zK^m z+$DV@c>e9<2I-OB&Bz&GfNum}0&b8WJSu?kO%%ZSCIhhyR}dM6WYAvFZ8V}rbcfxf z6HUrzDKBuY2{=pvesGM^r7ilNSIZ595x^QIBkxkOL!#k&*}RwMbj>`BkTbL4znFPt z=Z^j9t;$F9&d*V)zyR{KHE$ulfC%oOAy$r4a|0XFuOlVOh~uBF^w~=s-pd!^4#)Hl zS{qQiFGkj?_VOg;;{gX4MAvTf%a9;*Rte1EL!DW+8t}3!j2=;SMGHPXc8!y-7?C$I zoKnx`a3hDfseTvhH?W_EtBH<^aD}MVe(g(pVIH}v+vEhUbxuPH;8q}?L2nbIj}IlI zDI)TJFpXdx0Vak+x$K-|=%rh08$d$bK6!1f{8ATD)tX>tG{Esd*OOg**ouM9UPs7A ztd|3X;mWVgw>b_ySmy`yhOvD1=Z-cy_U&!wx=Tzyy__vtUa3`@+g?aP#;T=IhdUt= zPvGtvuiuKaW;}vD$C;{|qSD#uVHIjOP$YX+U7yIBaZ>hqf9lWgDv?vbom>UMXZ~9AH`DR^Bl|3^MWt^j7tdq&E=v1D%IUPe-oa^Qj&mGmsAqr!}Z3i}5%`|2c8$w`q)|jEvrX-0q>tUPHzb7kl z@p%IN7h0Op3#A(jF0Y?ukY545J0}q&K=obI{`bfr%>?`jz<6r)z_zJ0owTyr z(QQ*X1i&%8O6WiY%aCUtKK#fK$#4WFFI8L4`r->e!|hGib6qlX3@R{1?9~-d%8Ek0 zf5B8^aj6QDza7JAuK#wRZoXSK7Z3m8L5~oYC;No3dOJ3NHQQa^H#u;aqX=m#->&+n==bbatpci&SHrI+gCl^%hCKb>2w1zmJ zU>}-gi6OefOgLi)he zAnhK#hpF!BrUKdY|KXC-{9-1J z@G~Ff-aGrx7O(%aALx}#;2I84E8zvW10an@z=Sy%om!=Os@d00dCUN(?n6R% zuiYSV=OApLX7tfi4kp5^dU1^P1;cp7(om^$Tr$JuV&?hpdy5vGkuLdc5EJIDgTRlKRyHH#hg;Y00X){+Ug%V63iiQP08eW%@-^+`NX<0j=HR~`3L>Nx~w-5h2J2X??;d4*Uww60sM+{;%U&phB`@H zX=(|&Q~%ARa=TkfwTFq(Em{*R>bUUp%^Q|Da~b0c3}aVCsN}lYNpf*PTwb3^DzgOT z+!^(!t3-|5*O%@79&yiMJ@NSb$x4<1y(9lSwoHuluKkq64hi=4j`#UGW+PTb|>oq7UxhA;y%kPATy0QFVHOLcL}9hYF=`G%$R;Bjhy|MyvOWljWuO=E&A9xlIM-BEoA0 z8+4V7rkiuwc=K~iC-7nsl10jO*}!b>Si_pKhE^}MxY{G z!y7t6aO zvYQ!HraJSxwL#VO5g9dL zSYkHLHdSIGG6;uw1`n|yl|W>~UjPTxvqteY*xlLP-R-T$qA z-XFaxUvdn3U?{VWamTtx1emVrrE2kJhO;(>>b(52PW4_tr#d)X;Rr+wDZ3ddl0Az( zK`S|ID#vJf`6Js~tK54_Gj6(x$_#q(F&mg+a%jXT-b_5nWRyK@6R2PDd^aA4H-Sw4 z)w_X%H4f?h#xO;2@^|n;hb@+1n7c{~G+wj6$flq;>xNq+K z&m!pgoz36kPkry{B4u;_fE0|)=b`aCnQG!#s`dM4W>CnZ+ziY@?!ySfIQ_!(MF(k| z8pPk0o+oro0m2-QY~<|hwCyvSA^Jfqc6+FGPVquJg6mA%gKW3t6=M9nY7FyB$~Z++ z5n&j}?Tz*76x|yn&+k~5xG1_Vr`8vUCJzD~buzQ&e+x?dT;lo{_+$!$YSaLsk^Y>ZdmQ?LE&$X7)0< z7H4fU+D3iBHn@KsM^|>0CU0#TGIP2$nYEwgGK4}eV0)>#d^p&Ujc}EtS)7CwzBEA5 zMyxiBMGsO-)VO0YRbRPLZ z?Jhm8*7hRbVA~lDqP3^IX(7qyGY%ex`a-(U5I$h0G)s7i666iXrp)eVI8{Qti8&_4 zu^tB(mW5)Fw8UXS2u1;=Vgk@02?{rW4HbZop_At5?v+G()pffc-L$BR_Dr3n#Uw*n zIPW7QQmJls!3-y2YXZe5(b3jL;Ip|+UX5i|M$V?~sn9Ge+_|v2;$Zo!aH^IkddfcT zAyOQF5Zj3FZs`yHzUY^E@im4@9wWng3?jW^$3fJmgsjxgQxLV12@ zE;=pUqqCkfK;X{pvJxVoP?nZLHN9K(@?1PBj`qCs?_O`PGV*&Sz5)a1I zrBJIcA&+KfkIfIjh4#If9>oT?o#OWFRoLT*_-^**jG@&c+Kg`Fa`U)dAx1yg(DBvM zC66*K@3s(_&TMxiRpj2c^zr(#>!qKu5uYE?!0JWbWH|+@<*)~RPyt!5ojl8JWKymr zv%6J$zoUw40#TNmcAbUwqMU&c_q@jlUL_IlsXao3Z!)n#qk8+@#>vT~m$q0Vj0Dr& zMCc83HnelmK?^W!2(A7>mrU-$1`P!D$nA8nk@C+>lS_mr2N5Vi4WZ{hoRKBD+CWG3 z7>Hhcf40HHGT*_UuonxB_B;uKt%H0ZF0_Az4+2-GjLo-D(fs9J#lH#2xFmNrim3*9x)%wOx1> zi%8BtT9+CvnbRvGUO7Z36t3bhKEoweG&x`j8 zvs60Gv@|_tTMo&8t-5)_dIK%mg>2fpc*=j?Vxdm-5Vg9R@=`B2|9q3ksb0?0-Da^|liN49g}B~gsXW>+67B0U$NXZoVS+^3l7y?7ODio=W{aYv;&-2*ir@Jz z4!z9oYBE0cQO#-L_Tr3`WoofNr*EP&Fp zCE20&!p4%DrXnbQ#eri%XqE*mqUii*}gLWlZd9%t2%nXv>2+N0!6SG|R~+mwg2b1DMY6 zRvBYDPnDz{ z?#e$gUXv#?&f|#`;BrR-tKIR@W1IGW2;N5$MEf z#j|pZpy1~Qo#zxJWm=2k@vZj;utEjwu9RmnO*2L{Xj%41l=6aqVF$JDegQq%}*2RB5y zM0FN7G*e~$+Q1-48q*b${|98+m5xTrP`?PJ7>N&-4X9G+d38_o>Dn;9q6_un&ngIL z6Mm8q#0>w$cbun63|3-ai5rL*mMWkD-y^^mzPf65(2G-dm*bP}9X2$^kJKgP-hI>* zF&LGw4Ix!i9KPIW^n0#9`1Bk9{J&$_2mAEsTouI)wSp-l7zQ47*_+`L>Z;g;` z52J8SIrG9|j#(Af-d)p(qg{@zpUnzod7&jt-8KxjojUQq#nH@#iFYNnEVZZa^F_mm zqURurXAMQ)Q{9@zNz|IG$AF*7lr>Rg+(yg=9~9ABg+Gl;IW$HA+cu0 zYlPZH*E=lYBtN7iZyv>MNysgsblYd=eO5`r`u4)ob&a&*D$N=mHo>=k+-yvO2;CcZ3z=EToMB~?+HgD<_UpQ{Aj~)cFWsTHMOtp>;8JSZJ z?&(P3c5dN`z@E{)4Kq-vP9$4>tJ5Zf`>?Y?vTZgTzo8}R*gq&~YdPG0oJ|)-@w{xc zy;vEw8Qu$1;bftjA;ng7oskqZGk=4zUFVGqv2jfw?uHM!FV*cf5_41oVCZu8v_S<^ zm9cN5Tp~I6z{1g?glwmq=x1$k?eGjdT$zWo*tw$%Tg-Is#On7%o!(0J{dDwuO0%ut z)%UOhs&SoGPo+rr-oh7EK@C^D`Y2u_ZDG~n@jq#t<@x1^LI6s@$e#cPnHvDsKX;B9 zyp;jW5r6|lJOq#%_Qqz&(8`fb9=RA426UG`I8@BCk1}}8(CY{_d!sGiOUz3I-$?6H z^PksU{G)O^=+X|p&J5EwS@w;iG5BZ$sYV4Zkl zjARuZ2sbFWjMm0>UC&F6gosVV!48%US|&a05+%o-nxxHj9LN`;a2`Lo=unDO**@}U z=ksafBO55uFK(Bdh1JTkK$6y`7DYaITVyPV{lj-8RML}GSM64!>Tf>>Q`RM}H%N?4 zfhV)xh~&r14uY6w4hA+%z+7xY8uZBp+v_V;qHhDSDqH15yf>Y!s2yq3VV}B_jgf8G zF&p)gr>^HTy{iGc!{SNvcq?;mL|G(!2ESdV3;WmI2@I@Pr(tfmHo-XJYAT)^e26`o ztwdG;iyRt1>B8Sr{xg=pKg_|X+Ck|6_zf=vBXjaTzMiajr&UwgTcBBh3pmsmG!g@a zbw#e~WP8i~)r)lO)xp2emuw=Vd+*KVUiosbBl<*NiDrasOq{Amv#ROn4_fVfgu9_5 zMhX$%G{>1HmH0&n2`Sv0u4($bZhekY0^eJ3)eB?QT48ypcS|qIf;qKy~(z&}qYH)1H>c#P`x85ih4~zDy%5BW< zTJp1Wg&z~zx@nE5`X;%u4dW7?Z#<1eEI7lJeaZC)&L?o;MUU@Fc3RhI1{O%J&Z!S;u{6*V3faBlM zNVG^xB^{aV5N$4%1Y!tKmnLpN7bywh`a6y+pY;p4e&6lv-;aMG)Ei9$)Af|ODmCt>4XSL^-~L8{-D** zxSK22$;R|xU)<_uW_^#4w%mooH{}U!AVlL1h|=TM2*kQjV9K+y_1njSqTk#x2>eTw zrMiLZy6J=tWOu0A$eXOMh(#Znp^Gse;_)^^siH<}qE4{6^W6JfsV#lDc;vlPuP@#} z7d|SUI`iF3hiH*{A|-jt_uO#w_%7)A`#J=s$>Z;xXAG_jzSlUYU);VMpynn?$^7lup@&+A&U5p&BqH4Gm#6mDk0Ouj4kJt9iSz zM*Dxm+V%ds`GMs7nPo+2kb&U0Gq@uD_H)2z29R9nGYERqcYGKA%&_7?9B#}6hQajf za@7u&&Y5&_TDmt1U-r~p8nH$odtx+J;rkEezq>*QC%(Q@5v{^aYAmG0THv;%W7kdu z{ckBtAZ!=T(xW-L;L(OYS2QC((gaj(^61?CL#$c36_!*}eDn&ie;U*WR7rmK{M@a)p?MiiyZ5jYI90 zN_S5*cHK2FVJr8`iS|JTFklfvQf@&>gN%OYTI;lHo$mDG+o5s(n;bOaOZ9i&*(VPB zW6&x~WW4rL)W#Cx`6)p|^12N+?uyHu_RJZT|F5si|WbEPi1?VlsLhHifq1tt!r;d%!$P8 z{t%(P{RF#NYgt0J|3f1L;lpjUriB8lj#tw6uPtBOZyG z^fM1ci_XYy;oxaj8~(TpZCuNsp#$lWjqJrydziKv9i1UB+J?fFZGxU1gwAk7xWGHKv;X&4!v6QK-*~U7PrVIvyF~-c8ng8f{`aQk<-`91GdvRSZKKHpl z=bXW8J-6E1ay!@v+>fvvUC0FJFSfg?;Le|4 z>5nm@4a9)}2S?V<>Mu|$-J*aMdw)TC>-5&+pUaH=!luFmNiUC$D?i9_0@fxreSP?f zO_^V^C2G9?%#2vd4@A4W1M1!nHmjH9i5*0Uj$nc`MVITKQsS*vQ#nK(8&>2slr&qI(=_2l5HbJ8Jd(fzSL(=_1-+7P6`z z!qTB4n56Lj#QjfW<8*BZ;|kb-oZ?$D8ODeigS$F~-y5^~Xzv2aq;TiOXFiB<>uSz1 z0p%g0=y8WS|BeG!ff!IbPV^wx*_l(Pb7>u}O8i@!OW@gQ(5fx@pIgTKhr}O(>?gUI z=t!sn$TgR8^Zon#iAWbAy-H54UeS{`Nc4BID>L~2?~DDv+Opo&M>Bp-Vg?w#Ich_* zzNF~2F8TK#l6YcI^|13>nGqi@CO|z~T7W}`;PNT(zEHeuznT~+^VOU+t*P2CTHcy3 ztFrSI*}9tTtbpF8c1UnCY0vW1$yiGOm-TD|_yj)bFzp^uFN#Eq9KFXY)?vHjQ`Moj zKta*9a2b~Ns&~*iro(fz=&{+Rh%+jF(!_&y3-RWf^7?7Cqq41tN|{chtoA9azA92NdkkYjgHsSyp<@?h& z{=K^j_X;+!&iSgyT84*kX#S0y@3j;ra@HmQp~QGT@TMk51nMld68ydD3UhAEWt{Sx z4PV#)KgO8eC--M=TBFj#7GTlS+ZwxZ`j-ct#Xt%V=C$~Cd}{o3n__-j-TCU#AiR)# z*mTsQy9BM?{;~|`W5|_{d>;Bs`KJIfhqZHx%g|tsWY`XH1-N(05Ifu9N@_R1*TzHj z&k$Y61U2`Ie43i(G`ge;2 zy5+zRL4|9-XqDRy3m>^uE%jczcDv`>bNcerS;IzI`q7WS)5r06`^zF8PMjVU_JM-? z=(7I_YJbUtdRq7+#!LO=8ic5$eVx*ShCcJ+!$H-&mw?>$uTL3e%7^2Fj)*L~lu{C@^&a<^_sjAYfCEk(#ML2-!X5iOJECbrqDVR)2MqM^~@qZM#24!SciRb z<67O}O}stFptnQ8S$nd$L7urXT5|Y`8ccC;=bNjv%Ow^{IBBkvG7a~Rs}rOTK0wPu z-_V>I@!mZ&jbTfLX_ORY2pcNhA?a|)0RR5Wy#UtH&K9+qnn~}4{Wda%GS@ytP);8H z-h467Oim`a?+?)lEVfcC149yT6Ig5rY9JkE}$HwDWfD=xxQ$+1=dO3UF^M5M8i#%~Gb!YL3KfdC#<6{6Nzpq!B_`fk) z+$9P=II`bm5=!=28r;$GA(9d3X@Qh|p>?XiEYel>uQb6`P#oj?*X5~Iee1Z@`8ruo zHB=VRH>TG?FXlSn$W^#pzVHeoe}nFPof~B@5Kqlxn4YPn4@haln}Y)pP1?Q3G*i&B z7Z3qG_T8jcPyEke6E&V%ZGN{Z%ea1d1BtMfl9OPtw3qs62Bqq~lb;K}TdjO=ZjgJk{7~~DY}ABQyBE3< zYMU!3%Nmx(@#C~#31gGPoypw2=mf>n$>U!SudkdcRNyq;FWzn1J{)_=y727j9eeHW z56Kp&>&n~Roh!NgQhLb!yqC69x=!~gpF0Ot!%g0md~4Sz72w}~zVNx&P5w$DC!ND0 ziZ?-^?o<0y?T^vs$C&jfJe6LF4+ao7Owq)R93)-~84AIf}wEM(<|^FYfY4e z6@S_4hhKuR4yTf;?~6kve6^BZw-rmTsG8!ev9?lbnb)b#XuC(dSJ%@Fg_keVO%h%P zgGCL~v>__?0)=i;uV}3iB=xz$5dD|0F^fQKD>Ey8qnPwmJr*T6gvH6^zkSN6y|Z{J z@PM?0R6y`PY0$8cIBuc!fj}Qo|DLZ?D8HbTZzeya0WI*wX2%`5PT+tnB zoniXtH^!QioHNVbhG$o*?_$lle%`a2D*d5nw?hZ78Elf=!QFj^>icq?J04?Otrp?W z@DxT}5z^U*lE*HE&BGbR?4)W3&Ja4smes{7g}r8%a$-I_8iq|AY{m{YDQoLrpwJ~G z1T_(7D~2z;Fej~yrC}Nzfxq{||MU2bPV4=s9u^uVO`WY;cA#Y05xp&N9} zO$$guk?qCkKTBwN8zKZXt3k&vNJG&xZOp}UwwAQ;7>^UO?H>C!JcFib_P%vhv^N-M zDVsFtrE!+t?idk-J$9(NzPhl8Z`emdAmBnd){wbr#)C_Hq*Ktz`?dvdCW0f&ODKJV zqOp2D9%Q*7AoCM$nF{9n>#9w;Z*7Bk9x0xPa7x)k~9~ z=$%CPcUQOud`ah<_I>iHPtTY{v+E&H!gBj#M6`Ig^A0t~@f~74dYhV4X|Awq z28H>hyi|LZBWDOeg(J?g`j+fiBuHfR5c&@;u=qqq{X`2pumpQW>;0#%u1}+%(CeGS z$G$ZIK;dawfcO>CKuBHg{LgjEzrIM^^VXHumj8D2XgiCZ!5cGLXB_qnGruT47rtxr zn8hZ(@t##I3ce;cpKC>7y;?MtA|MQ;J|FXWeA+yr39Iat8&B4p@(tj-2=Qt@7WYi{ zFO(AGQDcBev=jvUGz{LHY^}(Dvb9;@D`W$r82qBonyiu{wWivnY-{=LT0?J~zAdxm z+X4PXOM7CZ3DQLoCcwf@YFlBvN!@cB$)>D1Qbo@Y)O*ycFW+!j%hqhKV}a@l>XN=-pZ%Sdxtu5@NWHt?k~#Jlw*Hxst@>3>g10>Boy{5aGu7p##wACqD6*q;{`_L6;!{sNy!n7M9UA&Y+eZw<$ zJIaOM6ZO-LJIxZA{(E`M&X2q~a~Qg?1Au2l6*Dh&feR@^YDqIZGLv||HD3wI&Bp76 zbMNwn1pdlEO?O(b{QF$TF*@jgF0^%L^5y^x?-wtor07?`$~|E^+7g$L*dqlQ;_QfH z?aw+s3uQ(mfl~^IcPecIj4W9glZ%|wtm3m5IfH>*%f9c*xcD)UxaCoNrGNAGx=ZZ=_u1~JF>;SD+b|BYkX|EQ_(DqE+W>Jc2IigJOkLBGA zqti1p2-=KjhjVBUOzef)x^0gorL--A6Je zGUunPDQ27I*FGF{G>1G&r=D?6W6DaIV4tqsx+!X&w!E6vzD5|9%K!-8a7$<>g*$pR zQFlk6apnGT!qrl|%P1jLPqolx>|2)kwVF9%Kn;J*P>z>)9YW&;Fn-4NoQr+j<_LzA zPbutH5-HeEtqUS#>%*$NkJfSj?L%LtSFb5#Yfc&p&e+J`HI)Ng;NQqxe{N8HXthtF zoJ@J(BN25Z@SHjsLrLCitLms$Lg!EER0z2;m!j}SPq_bpfmPJul$;{92aY zxkE_RLC?Q7HyMovZc_01AdcKiLX~_=st*;Y3E*5|a(Us0KZBvosloqToaW;DkP1LU zzuMwq>ETm&?AN+ARi6QJ!}R|kh1QV#UtHIXScAh4Z&3C+^={0W*w0&%w&&)K47i?Y z7GA+NMOS3;zh|}VQQsW&R{W}}u^}S>U^&b4Z-lBw&BZO;3{1Ig&%2z#-$If`aoaX0 z0uxX+_h9F>4TThxRQ#;t&6iLHP*pQMT;hy`Kl_^KXPU*v_zA=A0^%!5VBdAMFg)L^ zV?&E7DMgL3r+DS07n-UJgFg{kW@dk$Y^5bq2~=OCckp2yQ>9%t!@k}I;4_aDcbyqU zB3_fTqjtvvY2IqCXq$lCFVF)=$sx9&^tut~)kS)MW9(bWmBR5C=&nxM)m}w+*sS5H zkK~<70ndMMZN2x=Ax?xkH>%o-tp4T!yw(j9sv6OCLZ~B~RTA;mU+irtC@|doVEG5~ zh5A~xwZ`ApQ2u0qR56Kv+ks-i%Xu@7B5y0_m`$6geIGOL?$94~jsHj`hFzFzHnR_0#cuOUCSsGlFA}&{OK(SDG zxj1y3^7x4ZcS!pxpxhV78G%EpYwcNol`gs3Jlum#Iq_cgv(0Dh{w(t|phTDW_&t!Q zh+9A6DVUlTRg02I`4IzhE|+BT)SPj(&x8b^v6o5a*$u_-O*$2a zl|Q0~!t%xyaKNZMSVgt311Qd0B|PQLN7szIb#J$)Y`eNhGLlp3-6d-~%_+^d(pOS) zU&wM?P&~Xq`&^{&bdPvDygiA34(q4JHxQxpdw}C@9$Iz(|>7SPS>#?Q!14o$0Jt5^^D>pi0 z{jRGz(N(_h6)g|;P@5Gm+}Dmi$oz7Pk{p&csAYTt{Zxa;<4)C}{;m9_#T?lHVBorV z5o8VgzVW^F0>ld%D8Xa8VO}{AY$_NQyvEWMJ8lnBJWb#ws}890E);%Cr{PZIr(MPj zR|3n9-IA;&OMD-vhIQ-Pb?IG1)mC>~jl0cq7CvU4sUrg?T$vkh48_x>V{A#66N|PD zhLn~jAY7Cj4{HF7?=rLo`O>JtHH9CjfJF`bD?(kkc`+EfwtSSJ_>vLz zFP^``-Kz8IR}xMXIS;tCJ$VT~!k?5Ocadzop?bQ~du1>#=B0_)?ldo`n}`uYZ2GI_ z4y2*Nq0bS!+dm-HV};t~(LPe(G_d%S2M2^FR9ij&v^@7BiGU%gDr!A+zLq+&w2M}G z6Xha`07j#c#1yISn0I+rbpjou<_HX%@aXU+R^1__jR~Qbz}2F z&vw{buE#9yX`}n=W{mtL-lO`8Hz8rgNuu6x%q9?CL`_YUFaS%Z_YSL{tx3unq(MNj z-sE{|qRU&~{^0a= zeb8IMfg^`t_D?GDgF>($FLDlE=GKc~{_Hx}Xfk%dSksK)sM`yh#Q%!rzvr<8 zn_uJI8;7ds&hZsy_hM{(8)`)zmz#as!0An=yrMq0Mu6>jO`T&JZVKFkXg706@#)%Y zs!L1xgu0~ldeV7bj`fZ^U-KCw?M%(P-Izul?zlF!L4|ZR!dQ{u93caSq+32B6(DjC z0rLJEGc$xCmkmX=(RLYKL|-=9G6@;dY(jv*8{Nngs{=*_4Lc*Tny@T#26lN`8!M-A#^j zhR<4FB$$BqCg~LoMOfwDpNm0cC?4Nv%pd^{3@A$ashpyuk7ha$hixtrUihc%`rB$_ zhG4EzKRDwDuElKbn*E9oS#J|Tlxqu)_sksJkQcP(a|V=cjl&wB=(8`}v}OJ_-s2~} zH<*pB{W; zj?S*~-pVEzjC#*LIy2U&DNmVV1GYvaXQ3JWTdhKt$2Z4eAX*kl$1$(fIF!u_^$32ZF0mMAu>m{r25zz zZ&ql;CF2=;Xw`le90hUUIrrC+w?E`Bj7#U8z88}-&_2&%%lHB1E5~rOYFUrkl}P8l zaeIkv4(Nopi35|Z#BsJgI_d0_J9|=gI2f_pg(`JF>kfrI)32nkT){nkMIVd|-q%B0 zDpGc=F-xm0OsWvyH_p>IuH!bJ^}PMOJ}Vs>ZOJHKqfJo5a$_9qz~3B$d&CAh{u)}a zAUdqE=J+0x&a&LfwSL&~5IPMIbYUy1faO?o4<P~lMW`XwX#(efXvbJSyBbmWx~;( zV9^U|eBY39ca+I?G7etNFSvny$9SN_YhG{)U&eYd-bqQzuOS8ne6$ReXwjAotIUR(NkY+U|3g~* zPl@46tZqWAn&bx*@|j2;tvpQ14QT3W^$E;y;UD9+?TMb%GM2?fJ&zJ-)4JQ{)RoWB z_|`Jh8g1s(zB19ufSR53QBo4vzzOz3|@NfL`=Z$CW1AFh5LySKl>-ve~aKx!0A zy(O)KE$|d&@X`!t*G=OE-4~NVutGVtq8OU11a-TU2t}Q28Iy zn9<>1m0L+J=ceh`HlC|$#5}k6#wA^tB^F-fR+pv%18%;Rasi5b$6B!VSTea&h4fW2 z@2pY7pFnJbHqFCuH-cSuV!K(k}LaZyZ=7FUQaVMBQAwKG$L(6Nl z83XZa;juc*K$VPxh6=MJCn7JS37*lup_ztAD=j!6JJUCHv*vDu!GW$nuCW4CM#7CS z`;MY+M}D9Mhs#L_C7PKUES-V88HtuEg?e-h`GH1Lp|Bx3w2%$uq)gMQ4Sr<1O8-y+ zE|Y7u;rF=rU=CFH{D(@nx%e@-Jn>WX8&jrZ=oQNE?JEDg`K@z#zXV-e(H3kz&qEXm zfm>nCa-t(YnTf-0af2mWc!kR@l$KcmE}pFK4pptiS=X6~Pj##hvi3)t3Bv2)Akl0) zNtd+Pm~EHTvU$z}zJ->b({55D0B;&W)NEh&X^y<^#(cvLb@GDFvJ$Vi~y$=atuKl4`f-{Q8ZSN+zE6R&Vc_9;t#c3-_{! zP!SSysvxpeyAIcHmFqDC8#82l0B4adWp5wJr)n+zy{+F)s`9ILTSGC$!Eu>%Uve;3^ zO1Zh=CnblttV`b?i<_|=RlP;V*M~7xZvg~|X>DwVyIzi27LTl(I4(8B{{o90HSwNj z$^3Lzio}D$-5Ux?0+Tk&$YTlAyx_pf{wG*lcVAHmBC_&|eCGkILY7G~Z47wx1TfS5 ztkheCe^9A^te)&W%I3JfFpAlyp$wbOWrT`;qGcK?RssjAhn3_;rN@KyD_{xpn7RE~ zdJ&Fy^*HLWK&*L|&_+{1Bt1rN(6RQcMZnQ%;V$#E5W0JT>YIga$lGMiZiL7^0S0zxC~Xlg(G(|;bnMEt6MIcp2)s)btATnH2`uA~@l zI!ka;xjFz2Znf$rLJu33%~-JQcojkg`&ysJQr0s%63$EC_e|wLKlk0CZ^x|( z!o&Lt%zK*Gd;|>iqDojUh2y*0nR|DmXWc2zW_o}N&x*uP!H#v+E z0j$2ErcK8E66|wW#qh5%di{HGW9`>sgL{%oKSSnz%e;sKK-tUeZXpq?=WJ%hDNDdX z`Wq9MCZx-8K}wb@i?RMwR3AC)1LlxktDkIM-w{FNMEi#RN%J6miSzsew8+`)$DNtP zp^g^p z>->rCW~%s&Mwffo5!UNo_L4#OZtwqwPJ@Ze8rtTs4|)y__h5=n3#vZX!e-}=Ri*9U z1QOMlg&$6}Xe1=AView#-)%VvX*fqq98XO<9}~Pizio5TDER1fj}{}O(ctQ{&I8+K z%)|pGZ%@lx1c~ker4$K0`yqw>fD(YBkuGkP9qd^c-7S-C!tt(dh8cIu&7)IT$z?Bd zR&L`qm?3kt{r>{_>)Ds}UlXq{CYQENo1fcmYX0q>RN~1S+?mV$Xz6$6INczmbX1V_ zDE&so8;uzg2Y;`01jzUU@D2;36V(dob0N1ast$QBT14CybEQGv)s2G{-yq^P^eezA z^g@t)?wRsT%$GOh5p?q@OIe03)gpR$Gzhtg(*kh|VT!)MG)#U5mK ziqE9Iy7D5y;8+RNMx%Qe7923S@&Bkkj3ix&PhWi4j= zl7AE%@tBaqRN%MpNIBt;P02}DT)HZjUhW9L!b-DS--NLKEgSxdAkPiu|FFyWM18fr zDV=yubV}qE4*6)jdq)pgU^(;np0>ROoQpNF877taOjg!@!46_FO{3h*1IRb$+q2Eo z0vNOYg{8}qrW_iHUuT&g@TPvg=k>g-5*%QZr@k<7((=Gys%9iKdTH?0$yUxG#r*|3 zYeJ$L(Jhj|iAlRG<#*wo)S*yt&>rbBCrXsE_*dHU=3_)7z^6vr^JS89b8zJsLKQv2 zw~b`IBi>|zGDVHHf3LU^{cs`e738m(An}ffugav*U09z4r<@3D>!QR!EeUT%u%>lP=2U|_{%MaevWH^h|Ge&LsL!!oRO*H$T2@Vk54UN zQ`zq`Y5juZ$n)zZ?#+NQ^OjmC`|}>ouM26u=(73y$9~%@`6sl>8ozcKVEyHMV)pIf zj}h@jkF1#8LnSq4%0=S`=QIV!h?6XxOXv@E-=K|7l6QhRf!-YGs1)W?xsl2gQJ+0EsF{#wcMtGgZeN&)C`%pNm+X0GIdlE8=qWAe zZsY5?Z=TR8{TxrBOd^q%<~y7$zNHi zTUUIMOXxSU>|pPo1*Q_=V$uKd*GL+)U? zA2je!>%or$$A5(5oc&f?5M2+bXO%AQax0s14Zp6#bK=^yjh9U|4TcE9@1tG+!MJtt z{;yKNoxWe1EZWF9+HJ=gzF;TD~)-~Re(fHfM3ib>|jfI5(OMDAVH$FGGk<3+N#0E z;h=sfSu!1?;xyDLHPs;8B4|1GROBT&h41E<#>829kK3?Llzg;{UKq^3?_;D1J}j5I z(rvY5IMAB!p_LL6AsIDcWT5C<`#PYY^h4A&!y34}br+-|zdoJ%(FfbS&qX z3^n(_?ak|wcRbV@WJQZKvh828kGV#1x8nP6UpI1okxG!DH=aRiQ z-f?zwm?Lbn?GJlzrh(K>t($P2Nn~MPpY@V{!(_)Y=|<3E4S*K=^ke88D8L0JUT75@&Vejkw(=TzFzIv~!&awK6u5*38FcnYKgAVo`|AF*G zTV%>lx5jY}MfV1}abgf9`?81AbIr}PjQipKp68xxa#D>63W(n(82`k*b>%-&0fmki zX&x1ni(%ll)rXkuQyaG{B2%IwkV3`w{@-O7suKLGyEE=ondRX4yL|gjTjT87@wECy zJV`*(S{T2j<7aGvbFZ>Yu+P2{2ubm-yI|pBTA*YN%csDC6D{HlXp!7xEr!i zJ@vEM?3VL)uu1xW+S7_Cc$UFNE`B{xVm#s%zG*#i8)~T#mNLG4ZG0_J@RYDW?o8`I z7Ri}nR>9SBRA%>s5ePpa^%S96y0u=IAGI8!+0%oVP~6K6snEIbBHJz2{kwE~hfzNk z&CL%um*(*pmp~(P^wit3qq&hP8#L&f!I~3xbt820iO4=pR2 zQWFg`KVWOg5(br;*?90#%-%&uR``_8)m+?w8bvCmpNiOYxN$2u#>bHnQ8z|?NS$C* z&$(*WLgiX3yytTHf+=}RukuN{Bf2}pK@)-IJ&~H1MF@`mON7W9|d45M$K|;v1b+Q{5XBG^__Whfg0=s zqSSP3x@#7!Pc6K{wv2u}M>TBuZB)ijM2(L9Dy|5D(jjJX7fBkh7}c-neSZz_-)B}y z$X^`c@1_xx+h#$BHp6a;4&l*6^~GU_Lox1y@svQ3k#%W~uFzkZvR~i_^b;|ek?GL_ zMOf7^wYaq=p~oMWz*ocbLt0;%h;6gS(kvpBlnjpwbHF0e-QkMPCU_;8^LBD_cRtwy z4ps#Fg95Os@F#OI)pTjr-7)ZfHB<1h>K+Ydwc4K*#i8@2>F8r*%7CKw(J8|Vc6Ntq zdS3{# z>)dZ1?n*pSdO<#>{1GW3=oiR!o-au|%ZB4g zNqG>YyO<85W|tyyHKNaYbbkagVxy&pzd3w!c>ZQX zG{A2int;+$AqDSjL(ma?4*;jY{n@dnw z)kw$U{^#a*C#1aB%q!Qf>-FQ6BsZQ8nX0^>sHIyP3PuR zCne!o6E3P$ZH}yBCGSd|;nC;YvBhFG&ZyM0O4`@Y!BB;OXSW6;YZ5~i4uqzG7u0Tc zADQW2Klu#2c3SG~sBG(+%ZClZ`Y9#hQPkWWLyLRRbU9=cT%ghB z8v2>2s5Of_y4Vni$2Vb_+?_OBIh++6z}v1&L@k8GsDHwm?#!kjpJp71S+14@DPqQ! z2uQtSQ!8S(urF_q7Orb?duF)i9nI!GhAmxYi;;onxsvUhw6L)l34=k;QC6^Iwyx?6 z*!_*;CAY@I$g^9+ssGDYtN*Y5?S{m|=|!7;?W9v0wo|<0D{Q<*_|*D~wL;0Z`9Z_F z{qOCavXhoFbFT(7BW^dPeyn-w>rf%w zlgVBNO^%=3Gvbu{WJxzr_Kzg#&05LrO>p9YIR>a7M%Lc#y?Jd!WOrJ10cm!i5P2=~ zsPEz_398xwJVuo^56XNI^T_F6@%(0%zJfN#n&0otKtmUvOX<25gh0P&4re+5{MV>hFN^F+t#$qR8Y089M^jP zik4DK(Wh#d=9Af?L-ed(Mg0%PH6*`yUJ{n-BYc@?qNmc2J>NUxG(T$5>RJL@s z-XKtAlgh60l#vbI{uJ*x7x+vpNw2Mf;7SxW?xBaOaGe67cBS{$<7bjuKsxd1JRn#4+OO;nTEDnCT zKD!)lh%Ftnoa@U%{_BkEJOf&fhY#KcQ1N12-2s8p1{#? zz?YJF>=RrMOQZmn{^^F=$^>f|nJZD&?Yia{jqPC{^z#j?pWC>?cf9TksTu3`RC~6= z^T>bU8}2QmD@y;CGjDjL8k9t;6?i6Pz8U)(mV|XHX%OyZVl{lx-ypMf6NR0&*jqhe zIaF?lRKIifb^H|cb{t8@6|f|ey;$Ljv1`x{CV?JS2SaY447cAS zBPIU~B%CW0o^kR&BNIBHQ_oye|H!5$@>rKdH#spoF~<%3{Iu3-4#5fLxe>jmXm#(E zyNrUTY^vEq9DNc;#FSPMJaI60Gl#{p;k*#t2{AtbRw2dV*3T4fr3lhS{VrPs7Ze|3}k9>}!i(Jyh|C*cpqw+102pkGL5Pe5d9y=q_~);L$B-LP32%!n zV5GRk#_8A|D>`cunhpV zeSqixn8XvkEjMj9Ya~5y)bz_F0zR`x$&V(!XA?DZTKaRQB_A0)@8=a67!dqd72bCu zE1pHG$GJ{=m2QiRRJ~|gOpwSBnRKXaIcnvVTi&Qo&3!DP zYToy3s8FNr_z%Ffxh14@21u_ko2J%vGqw(;MjSJ}heRq^ejhZ}yoQ9136}=% zo5k(Bsk`>)F*yiNSwB6oK-<1=Smfb?xtPwbrlrs-b?l{Ve}bR(A6Xh;k_ zC#j5Qkd`RR7Ylg3 z3tdb{U_kNC;GllP)WRL#a&Z>B$ue%@Il%I>Y8Fa=P7cx<6R^dIYtK7kSl{o|%`~ml zY0tyTIRThJY{>#FVhuh9LHQA>%O`L#RD4pU3kUk? zN6-j6p#kr}Zv~F<&Kt>8e7!G#C>=OUwCl;ZtW*qn&>2M~QyWtuesiY(0{!b*>tf2U zQl5UYcMs;!OvKc|T7;sJryE9B0;gr16Aj;v(rAv}(0Jl*O#-7MqC5t=SXrzO%ASb0 zO~rTkj#*N)WF5ltb2?Mk9cEA|XBuq0V4k|N99$z?PR10d``y^=zixpko?frH$gJkI zSj6;Nkuc$_%9|@LekIKb&xqHCoEeXo3{gS4xG+v`H@tI~gjDANiHeW068V(6^_2~i z)HW6&Yh%M9RHV`C86DgaD>6%4>9d+*wP+**^m7mWbtQxN0$k*AMp5(mQLJ;-aMIOC zIH`&Ho!n=#p0MOXKVS&#Uv#-?{a_FJU{{rV2`8QkJ8aGzA}+3iofcN4%*KIRO8>6j zii;c$`>y=roNsO&C2F%;YP=aozrC?Zq-;6QAGJOXcnA$ZjBRL=HOxvkBHylwwIc?$_DjJB^*j_0 z=jNmrkP^-r{&6!?^S{Q-T2QAyMFxxHS&{;k6@ySrR4r{C`0zbC%3a?!MP$>~Gdqh| zGQAMaus@5?a`(M1r|l-PtC&g$r*qh9NQ7yss$6h&rExg`e+1RpbW|@Po#hbrQvSdz z&>|SyYa4!L?<^pDxNIZ>%j%=Qf#%lr>H|Pvy*Sa{8nDdBQKS&3PfzxQB^|P})@bEYsLSN7*ELUw%N#PsfT0R?@ z9B^#=aTlVA!KgE1D!(s@qxIe@iQ_pf(lA@22Y)-0~&ZZXF#@CX3L zQKG`r^4Y--tcmE+BwUy9_;Ob(Lt)}rPQ}OjXVE*p>9gK+;&=Xr>OYsiImL9oHhYbJ zSH5mq98D~#pr8GUcaHrY80gtdy#P%oSmw(ttS{n5$7C+>=PV=>(EATcN|~n*%xN%7J9E@L89@3)Hs55M;~8i zSmYcemq!@#;gOTr`KRVNH8>!1&)g<7`c!ItOv?gQS9u2s+YG2Z@+>5M4&x$Y`zBTgqPkKjzQ9LzD8Q^9O!jnY<&*^hrAbRIKXU{TS%}{wJ z3ww_nfPhSjH|Re~E$CAcqZhTrh|DMJm+v1*P78stj#q|%i%yXm=C~05S2ij9Yi4si z*}K<9LonICH;X3w6zCwyj<|k5FC5n9@MV+TRa8*L)aakQ-tDe62utkeYzMO7BBwLc zBc!M_NP$kKkV-k-Y~9N5zzMYE1^ zN1yw_*K~Q$0ygwI*E{~fk&o+x5xAs`)|`i_W}=l)0KpuPb0Ns=zKZHUH=8lQh;GA(kR@7&4@KenzI%Vv{<%> zUqs!|k-Vp4c@J>*9V}XAiRRGcjODC>07DCDetsO2ac6jf$1%P6J9DQJM!%bOs}l^~{qV`iaRhgX z=iGQC=)O0APP&{ec4jwBxu#BeUdx+#ncUcbV2<07rhuu{wRsRg5!_{MyUqc?2p!#m?qDW;yzqcxxGAEbNWA(qLm9*E1cGsQe!l%( zBl7FV;)OnQj_@sP;fbxrmI<-oXcnSynchbPP`5r?#*B63d|vKz-6hw>Jc>Af;s7Aq z9}yL~QL}3H=RyC;gXdrXI=YOb`C~7j-_iCoT;=fc$j5P6IaX=4o;`y9sUaHeRGn>^ z3R+Yc7D>W$YGI2Pjlv~YGI^q&q|;bfc1BWx6Zz0?%&^=%5W2f^NZlUw(lP4Hrs*N1 zq44bVCbuD_(b%#D#dN({)r5Aked#+v_FTP4a|rEkw(p1^u=VWllm35neP>uxiPp6h zD_|Q%K(LH$EQk;gghWL}MFe3~dPI~Kiip&ZL`6Y|5*?)oLO^Ls2}lVw5fBg|gqqL; zQUVDOAS5B>Ve3b;_FQOtCHNhK zM9Y6;1bv0Y(Qj`YCv2IQ6rzYRbHO^^N<;E{Vh52M2aw%_-cCo*$xS0tc(N(V&?9v& z^ym%!iwE-1$HoWfC^t&{ROrIdE4vb|nS&;L-1jB4O7eq%p{jwcA3DK@mVdp(F>m}K zbm|BJ+yId{>Y?wS*{}pu(K%OipX-FHY(FZ%HdhVQs+%0H#R^m3DPd&gYU1AZg8%*6 zmwq0KGLj*d4ekAzxK$cvZ6)2@zDckcboRxv#qGxj&wUY|Wo-G(j2~09?|5sb!Blb$S`ejr^i`bN#(Ud^dwMux!NK$Trikp z{g`g-o{(xpY@!t!7Ixa5Wli!^O?TE~j=)mU@Z`t3*IG5xtJ+jHHLL87q@}^Owq0C% z5fJhx`IK0bfI&x!NQL6X@)H#w5zHSU&LoCopv!e75rR4mj2~jgBtZ5A!el|H8bn=^ zJfVT2^rfC>nw|r)8#1cgcFg}Py5nrl@-E(y9j%Aa4_}Qsb4+>p_K44K(mzf1y!%wd zJ<4b(MgARB{>L&6|2;<6tn5dgzNYru46h$!tKb}H7H)cdL`~ZZ*xxvWT)QpqC7{2m zN4QL)`i^V5E~66TeeSkMzinH}E~|PwK6bk9_TtH^#B=QZIcW8?_YDU4~i4yP;jAN;4mbqH<4CUr~KTl**4pxc30B z_9?nBv``RjjU`J`fP{oKZ%lX2>~VT2O6#4g@VMf=+tI?(4?g=F7mwCy#wvs z+j+->eHQ3@EJiliFPVhjy(ZlG0K7g`W~$go)xKkXi-HklA_}=7LXGJo%*6~taA3#u z-YMnxKw}))8IKSgg5r`;uktz{x2NS34!P&-6pf)A9TUl6VUBtH&VVM3e}DU&pE z;SqW1c{dvo+fJLNzhMBfsepd~7Kchmn*TAEmeX$DoX)|*K9ayD0PbVb88_^omdL9pa*B}!u=BXWv2Dh2> zBW**#DGHm{T+R0zMk^L@g`bOZ>cGQ>2DtUzG|2(Ob=EtOTI$~Av6H^a?g6ekLn8ya z2bH*(3FQM(+e%=$Z`BxycQ#Tty>B{a>a>&SU5+C)6)SWfmrMqXeP-@p7jI7p3 z1+@BmbF9OV4G#&O{gJm%9p7F(H7oJ|!S_C5TOi|Buy#xXT+CPUY&f9=C(H!f)j~Kc z{Wr__aWW(unP&#{K{=V_Z@82&2w*M$Bkpt(Z3!k&&%?Fzz3+uM1ts+=zwx(=iO3Wa zMI|P+daKJ1oE{-N1Y->TVc7KFDxdrEY9ZL7vsB3}R}D^4tblf9Ja$5LnTy9@lhwoR zS9cHEUaDh)b}qxb!-o(n*eLH`&CzFokmDVDDnt_=!OwBlYmvg+i)y3r)f0?w-gxbY z>MluG!Riq<5rR{U!Smdy4bXuDp}ZK$x8a(?TW;F7C&>p)74E>@u5D*r z9R@WkQ+>`Ip7g`hxbmvv;6*#_WlR;_w2NE91-B|Pbv&{%OqU0bETcHApHo-ayv;uG zPnE52RNEb{DqFw7daH*cESccQMPYfHBqL=JC|*YaO+hy=}n@#z>ng zbYVo+#}d(PI-e;tx2ygrck*2W;k~v~w_iU}@q5L`F%1LyD1a37g8bpEZQmFp zubamNOqR;h)ok1a#?p+5C(>B?UM7+(AvWM$4uyCoIIraX_XUSM(ZUFJV^gic1-`zF zd|84S-uVws;pP4FEH$sAAWYkAxs3iq6yPYdJ>8jByK=0d)m!hje+bo0IL&P5R!vq5 zlzY{5T^+g8^?*J-QSQ7|@P-|b*+Ae*0V7bD+!u3b%xv39_ z&&N3VMeAR1yE2TvP47=V+V2~WdnJ5mFO-?122X|VXiu9eqbw>w@rG4K-pr?DaI&C46t(o;Q^2wErPx& zm>qsCfBPpdm#{Y)7aubwu`+>daGJHvIWIH#OF5395Wk|^N!965@T*7D&u%ZO+9A;? zl}{NBddD~e3rNmmt|mqEITFdMU*-pAK_mP?b4;YuMzVVH>$eco*l0N+{AyQ&&FEs0 z8uqex%k9uThp-_sH@h2*qigC>9pRe!gNdeTL9y@vd&MN~scy^=MaRjNLjubW7iK7$ zFxm`ZxrynVbS{c{=|;kaw1UO8+aW(uerT3)M$~Thyx3ge@3yl9Td!7g)<_8%TX%Qb;1SYjzJbFo)JzQTYvQzZS2tWAXB>rQ zhO~1m<%2On`_`){uPy?LwtY)ZPzv0-&VC~f?&(n%yzhmY7!DY#s9s#TJUdrdJG;?j z@N}1Z?-93aZNHxtmKh*0$7>zTOm~b823K{uexN#eJl*|Fx4CN6Cadc3N#BMV5dwyO6Hqa*XdnJ%ZM zDDkiPBcCEk9^Z@n9>VWAdVIc^;Ttg>#7~U6P$(C1LZlE1Y!hbtE@tbWJQ!E~N2cJ>D*M+-`ds z^VG=T;mpe0KStES(*G`FqnWSG;BJ50QuXxDtme>d`JRRkC00fWmU$AdyuN8|hCaTD zG0UIqQ?+AKxQra-AGqD;;pc{7QutU0Yqt+sxz>${@eHx==+7a>5E4U$D6i)yHgIT;e4n~@ zV&-y)pl=~)84@l^50bI0GgzS-ofqI@Qj zda#{aJ|OB#sOQr1zH7Q2b#}v`(q+U<*)6}V91uehWD6FwXuULK#T``Om=rOebt|vC2@%8}D)Kk|J}98C5y+Pf@}t8AOvEAy z=LY&V_eJw~Ad6V#j1<-DPM_AOEJYI2{eoxN!TkaJfNC^$Y(T<%h4o}J=lP?7G8suX zO*Zm}E(Bb8oEZ%YIrR+$~spH}1szw&NP1m-Ybj4y&q+2XUqcTWSCk za^5eE7lkJz+bSI_zhFX+=_j|N<>SZlg{~e$;X69kih8;YQ*d^8_lU`VB7iI(Q9v*5 zQtTmsbtF%D4az)Q<>u0S#}GZkHXzUQSzcMgy?NK$W|%Bwpr%6~?18v~8}efz{7 z%MF`LCP)?<1k}`e-+;_%5G*G5b z0G*`(B692gf8GWuoa{>vy45Ns_moNEsQ?r`dSXZ4^6}iB9 zk@U8_7$3HBGktI+ze8I82xA`_i}t=zGqxXPxMDo~40r7t^<&QlIdAR~F65I}4I3M+ z@|4>Yv?!`9BG+pE8H$JQ6c4?y?W1M@9iqn~H#wKdz z+@qiPai9}~;)ldt{`+yR^8SIZV&(wXOg7CB$VYpn9quWf$}2++6-k7JW&@6R>Gz81 zxdynaaDW$~TPzERVzzM72Vm?F)%hBE21wID_UHzj)?gyS*|;dQ9dV!#XOF&u#oKkV zz`USAU#2)3;ylpozhRj$?!25sMZH}CBjETf52qW)gZJ5M6{OR^CMZ^^8}f26b0)}Q z6;0FdzwlcMsF5Xenh}WMpfG%12IhJcfcpYP*va+V#R-|IdvJ`Hm! zESO^%5&!88_^=DYyLa2khEeMr+8r@4`pYWFBSqXmeW7Idzm8j=D!ut8c6qGx*K1 zGE-eQ?{W_B)WOr}q)+|(VDT^aU*W&e=&e7c{V6|zfY;_gk>(S~uiVTBOGpN{QF9rW zci4j!7jG^NDS8MYlXsL~AL|)lH6d|cAeS7}5w2D&nJJUhaG30b)6K^-WD&U^zAjUs zgSng^aF77b2&bU==YZ~^3hV8{V1m0!TCz_x4J_VB0HL_0g`NsXj!)nvvEDCrmJr-h z+uIw=MoEaEKn|8rEC$`CMGnS#`P~M^>Nn`hbK!+UhB)3 zE1WI}4`VEJr#tHb{wl5l(&Fnrf<;Tqg;{Dco^%a6BI zvroqAE2iM{gFXMqS2wmV^;UOsuWTg?E^eGBA6r#)%Ua`YVHj0obu&Kd7mNFqay{xV z&y53bzm*oNN9S2GU7ROVCf@@pZXPe`)Er$*eu9cdq!T@3-3^tsqx_vj3T_;umF6=6 zkwxR_c>zZ+HJrEKwr)Tjm!xXNE6=~C@5S6TqMB(ETbYSUg6YM>RI-8;xA7pF*;bv` zu#ca)y>(8Zl6PP$S(0 zi0r*=T&`oiFLaCjH-D5ZxKt$K3YnaxH=$RhPADG1WA0NJ8lybiYRQ4S^CB=5gbI+U z0C><0bL)~h3DGJDP$Z$?AZ|fWm07+_lGD=vN5{x)@dWdte7(rQEH^e2%>AJ~S*Rfi zo<@Bj{sY4!no<9^^_DGr=XUsuD85s_CD+b3Z-2mwZc52v6`b#PwwdnEnd{QufT0B} zRoFfxbvOJ*(OsDZ)TA$LTC}iPR&gN(r^qK3qD6YjKXR$-zSoQfe%O8WR z5gy(g^k?o3h^k+!|MJh{cY6b?xxdJ7yGpOV+guwg>AmT>WuLrUds!?i7iD8Gfjzyd zz0V+CHZ<5eD|M7%5kY5@c?;?o_u!D2FgfhCRZ|?*4*s!SjKoa~J`JRHt6Hv#FivjM zWRJ*Z(SRM|W8o^z;7;}z-R@PI)QcxV2h=Vm>aI~z)>y>S`4Qu*;9;fLydQ0%E~{7D z*a`OVDdR3pD(0ZDO0>h%Z-@Dk|9mnOw3+p~B#Wggz3;A0E9*|zFGvF@E<0|5sES8o zq^}1QVjZSPpZp+S#0{ebk8$%FgRSFt-5H0bSAref8P16ufe43<(-1P(LI1GuM#Agq zrBMy|mHHqYCm+F7bqVnx8O9yAZTD=4U!okjb_-1E;@m=|Dxe{-Gj~>l@8D2QZOuKL70kzLM)XaP90AeWeK2Y8d{>Y{zqRX#(DKdGDk$ z&|lX&)yrkeZ~R^ZjR_(kEbR(!rTiRY2-sGKeDL9s6?MC826cCfJ2H1oChC4^(3g-% zYS=QtpNY$h{4O)H^xq1Kzw=(sF3xeL$ja`6H6whzI#I55P36Z&{CAQ&o1KZ9dBD3~ z;d4l7{LX#7Ov5c~!-&vCjHvac%PGQJwwV5U_gHxSCNWI#yeDUK7#@b$YC@xh2}7bV zQ-iB&?gEvXUu2w%c?f!9o@t`7y0E6;0k)vuF7>Mfu&Jou=HNT2+^c$q3+?B;lOyVx zzt{!3kB}lhg7$?OA2)YL_-6;XF;7jeLyPf*s(1(rS`GaH8^#8=ljtmP2W8>LQh@B{ z=Grz2af>5Bse^1XI~yk>Fvje)KD+0>8s3_%eX!!;WHfLK4vM5KE?BZnW{3kVK_>{Y z#=8U>;u<;w?=}gI9%L==u>j&MtW5snNM!G``)P>DxneBBzIwGR+@y zh8b!%ke0!vcJFgJ54n5d4bOEqZ7MoWU2oHhd&gv6(oLkMtF0Y6uJ;mt)2LQFCehcv zoK^6sc+IRte*bRf%1I}gQ5I%BpNNT^!>WYqu4@$u%g)!x7Ro1;yFyG~%dLqunndl) z<1m}NUIgJ8FHV5%vG-J+nEQZtouAGquT&kx7v}R#k&JYgY)-D^^YK;U%#0NsL)3#e zD+|swqEok9bM=v|N^a(D=#=A+G7XqPs}GlMl3c*l=!)T&P276TiWN45m^S#-1yY@( z&VQ00(N63lx5SAxN-}gm%NX~e&Uup%nJuHtM|t-yF)#<%$+DV4f6@0}Bvnk=tc(TY z;5@S#U;=w5A9v(+##u3+kCh>SXNclVLJ-~SMn#4jI1-LykY~%x`a->fZiBr+2#^u@ zeek6R35$aHS8!TR_i~YN@D~E)T2OxL1n&S6wc_`+b}2Bcy9Z;sZHqly3yHqL?c8Fm zNDAv(&(9t5Up9L*GCPOI_5W?ZQ}=!%1l>QN=n5ZvIsW;LI$3Fade4MqY)F)O5S7gM zGOwnEy$$SrB>NHtyF@NRky<72Fn^}N>}g(R{R$oJtyzcd25T9x^GsGT;-<`#l-N?- z+i?2?xf6| zRyQpt(jvr2ZM4IY(jGoIZ$@_;FDyh&d>A?H4RDoV0}9l}{=fVP8cWlOV6olrzz*9k zYd)^YW|#+VB?Pf!y{?TPr})>w7fzc&{mwAnGnWR!>KDemLpsP(KFi~n-*5$Bwv^Dk zML}~y92ug#ki5-`WR{*sJ)Y=1&5H6b6dZZ4nrvJAnl#|`zsvvsZKV4NKdUsM?F_C` zO*d$}^LMW;H3-X0qntIOTk`oDPD8SXQ%ZN>>Y=OhZ>`&^;j@`{Bg580;jJ>zKd?GNsUFCX)BA3i?b$|JUb1b}zVNTNB8gm zi4jM6ME%KtpO;URph`!hRb;#y^OpJHyI=8(S{V@alyys;bMBF>rb&%XSTW^7UH?h@ z#RMlg0T%cTTM)EKR!aH=wIQMzLW~?&5#Um4Yg6b%_Za$>V(J}tWUyLy+9))HZ#v2JW}y?3B=&Q z4pnfID@J-*xgaqF&_jzi;M_@x8n_WX^bZ`A13E!H``-a#rd@^1`vyxY#X-&Aj8Hm# zhp}nu10nE%`~AQcsGyX3n_&j&(0rQ!GNe*7C4 z!^F02cES?pUXL^0eS^Gyp(mBwSb!Z^D-`YyQA}dYJ~W$$$d8C*kg1;X{|UGj2f2q-$3bI*JIsp^vOcHkKcr#~bCmj_ym;SjZPCLu%vaP#9p) z-E}CF=dIUr7~VobF)u^GN0GQ6xOWDi)$!?mg&c4VLX5I)>KmLl$?gQ35Wri^66wRg z7-becyNz5 z{`9_)y=t(j3^RO;`VoOP;Y>@aJ-T%v+EU7zNt!SqrhxDAx}GJB))R>8lKZ(a*edv7 ziL-wFpTsfh>dMJSEd|_$!QH#Dot#U#k$y=67iC-T{2@k3iT8Ww;;`N*RE0|(*sh+M zj$PeJp`7Xj69%7)K^|qXBzG*&B%n8l140U$;s&_*a>c-O!nJ`Vi49pOW0!GouSf^@3HDeHk>R9EMliWxj^?~h# ze^E__&H-IWAO^_B@rep#@XJcYZUm4CcK)5@{{JV5{L?Dec)j#9<8=oP+(Pypr6TAT zo-|W~v TO2;oU!uDvw@nQspkexbrqh`|bez}|0ay^G9GEYbNJ&$X<_e@%T38vht zoC%GT#K9--80J(7{3apZcfR2P4$6Fc;@USQ$uuT$Q|B8<2JXI-imZd2g)$N&*-MLCJW`!n!5v&tX7g1bL1I_AP^E z?VA>*_&J^shvQZ*xgkykBIT=g-NAT5%QzmGs5e;v93iP~P5tV5KPwyjLPcr&|DK=y z+fvcg#Q%)OzU);L!H;|s9g3u$?xpoj8%&_rm#VLz1KO`2$$mqSoLJRUF#AA*fZ(9D z2h>+A!oE}~Ql5d{dDwyPnkUz&+ba5SqgLnqzK)Dv6#@G7T zsonApGw_+sKbS8yP1*kLp7Y7NSUEsoxz6?UJ0hM{a4qO%p0Wv7#j!15y8Bk!z3n!G%L6ds> za$lho>IHbv)Ocx5Pni*O(-|f{mp$ux>PI=`!zzW=0|XtMTq4yOydg{^XCQ&fbFcp< z(fw_`e}A4*{%_p9(;>J!WW54H$4|yf6&;6#ULtmlnYf#m$&pa^LsrF^WJzUXnkf#U zJfQcUe%Y1xqRw|I3$=ed6-a1s^GC{ia)&{qeChpn z?FW{>8is3cjt|p&#$@S@ev!ag1_wQ@T3IeeP6-C&hvecfflcxUoE^pD%7)?sgIMeC zD!7Y1!nT`qObS;%XqD$8&%tLbFQ!1+G&d%yaJf1G*U`d|Oo-Y}?SYlMLfVElh9)R` zk)d)j+6Rnpj6Hg9(8GxCUf}a(rq7=+rLcTs;VX8Sn35+(Hc>x8&JyI;C3g=m zov-Li_s)@NPz1UHW1KX{{;55$$QUcK7bIspRX&`&R`HOf@%0gQwwBVX`yzNogY3&^ zdiT*Z2$Zsz;3Y9YnKuS15|}`hZzou1RV=%;$&X|XdZRfj1>9sH?g3?3Xsyz*1Z)}t z%_3nNLL7x8HK?PDouiF8JQLg*{ZpkmSoJ$D31d} z#yC%=XeL4sLqw3=eTxR*IN39`^JqAkjD$_Dl8K?- z(J2eOiR0WGShd8;pbn#C zsqkL6<0z=ag8;Va5!+yl6b^fQBpk2x)@W6G+_Qq-O?0B<=J+Sml?w*)Y|=A1KjqKr z!_u~2cQY-CBaZ=0v(E)B73FhjE`^n95vKEZ^JG}+p zJacr_XD_ZW`mnrkxF@>7b?b)9GYpnR#_^33{t*jXzmIR9UDkRwDRXuq%%NQ2D2&mvvhLCV@GA40uz;9APLcmFC1u`TW-}6|^gdLGo za04YWW&>g&3o`~L3dAM|>&zK{M)3G9!gmb^7l8R`Nr8fsCjToqkV3Ah;64>Rvr!_Z z{-O&Der-A#*Z-#ObKQc6ZlbC5s_#FA1+Sl5&J0XSkl~;e$A(((yiKxfKHVt*{C(Xm z4MPB<_MCa?C9oL0io{(>JqA12Y92r_Ta_xIDPJ6{H;5{zH;|Urv6P@oGnuoC?FVnP zV>~Zi^d1pN&`$9uDYP8D>-K@321Ki;w9Er*)%H?OM8m0$$+xCgH1gl3*RM7@h0{Ko zg{f>Sb>s{SM;6g5pS(AH@W~+!ua(CfPomqTjWk0mO$rH7T?AZPpN48SDfCB%!E4Ah!Z$e;OA2vg|6q*GN>gv-ZRUHcIf|MQ_3Hi6*{U zANLUPu1{~nrVEN7PEfHl3s^r$=M5L?_c7k-BELbamsoU$KCEfhsZYOw z?{~#nz=4`@LS5bra8ob z$eUcj0q4h#2zo#sKGWIyh?uGBgN7-wP>Elj6wE2JPfq(ya|KZPY#=Emd4S1c-;MfbhUd_1>N)#^;eXb~m zdRGq>>n?xZ?$dJ3*>Wv+?z7Hb|>BHMSYxh{3s;s94%K%D6KtU9m8 zIL5QgjJrbdj;y4?R*#WJ{nSSu4(uJtt~&cjfpk>+dg~CsN8()gRT6J9Nqbrr-Nx za6|hhCukpmj-YA&UHhU5*;vixt%bxi;Yh zO9W-FQ8Pm0NyZCjMa=bHxGuEp=ysWm@g^aqyBs2ztq^>LR4c`syITNZzb6;X-N*U_ zF5!aod9=FdzWcQdA;u8mStfEzpkmKW@+BO621tvK_&ZeiL0Njo^l*T|LOC1fx8pjP z9b3JgPx)a*mq~G=Oky5=_pA)fEuld#f`HoCA`yXT3NQ@FLM9KgB)}|-DHe30s&rsr z`5+D4u&}f*VTn~J2mljFnG}*>I~ydUu|CX{6H#$Ah*yZ&=uZ|CaAcC-Fc)+)q&o9V zI6sIgIFR`#-y6ur^IE+{A0H1~bkF)`x4mBNR)d$lGwp75i2Ogq2vXrsZP_L5BJ0{# z+WPzCTjYyhTWle=l2DgI=Yko8NHoYA-i?5 z>!AcYCMLm}ZcmFTXA44a$)w;9h^meXk{YQOK{Ho$3=On5Dlo%M?elk7YUh~J2-Y+| zM237?k=NqF$eG=MTb#uBex59W`t#C7DU$cEjsW+rZ<0>Y#q(^B%A$$2w?unxnDqr{ z_k0f-u>1$`a#{4?Pp#aU2U)2N*TZ#5Uqr5Q=A52zGvmuvSaelOD3kxIVhiy>p1L}* zfq(bt$G$TPW9mi|mLrceM={&Qd9_ClXPq7(jV|NrM=~h*{=ju8(zhP26Me{^a;pGa zmCnCB@`vq4r{l0E0}yqh7;pW}KrGM0jJ_jZ1k|e9YGyY|O>)j&PF*ZV1n1#i_=ZU9 z(#~Fpi9pQ)txbCj52};KT(W(y%5QR481x zO))I%o>D-cix1O+#nCH;_Zu~CdFzK3x3FTrI=*zTmp&+C!;R1OPT`FbT z^PqrG#LQ&PCGC*4%Z7=7kO=Z{`-Mz2X{p~E)KwbgQSkRv88%Ckat0E_luH3`Cjbv~ z1$^xoFDan+uvj-}h8z{%B!Xp9Kfs1ZR1`l>7F!lA1Pm2B*z!Q^KhTCtVLz*UAo}WX z!|@+g4M7^m&;Ba49Z+b94T_h^a(kgG7x^6hcedd}qJggOHMgl2FUxUVUxe5Qf$xR} z!ZS~aXp6bJQ%Pz;^(F^IdU70w_hcVb$T~h~q__mDNb@ z=lj@0LAQ0TC) z%E@SYKx5ueT6%H{*3s+AWmJZvc`xaaU$_JHo8=FQw_3ZNKMQaSLLyN;ui5^Al{HmU!bL_ zyE)IdPyPA)k#0}xS4Tx58&9w?9O z5{~i+fAwjcPq^HzGFcpt4;?=N_30Ga`&fhG{Su$6oX@B4cFeTJr5}}fow{Ixx}WRU z#(egFr|w51`9En=8ZAyp+c^#A9xz@%n5c8#B48Rdc{a}wjY|DCAPKAQpyh&4znb6G zR$G88d@~yI)mni{ojqnSkT!QF=1h=^5vj>QTDehP@k-^XKE?eOk`DP`C34KEW)5?_?dac9p16&iWr`@_IX9BUZbroh7^Fa~4vr#GK1L(h2^t zl#pjM=xz{>O@?+UsF%N_fut&aTuT)rfVv%t9qZ!Aq{xn?5`U&bI8(Y~2_Qv|F5#oL zp9&scmdOx3VuZPD-k(_I9@~=p+4%;W(i_4_`s4SeX}vcVEmD=aPLO-(|F+)!$e#Y~7RXXv1(+Ijc}lK1inC*C8?OIXn2)l2m2W&Fc0 z3}yD~&v@@3ASutX@J?6uh>MX(8lC{j#3v(GwVB+IY}ArW`GuWha`rZ#rjFTx*}FZ(3;Q?s8J1r(Ri$edm-; z%+}fVlC$7q8)h3@gq>dA++r5BqB-p1I^M-!+TtBuTg*Gihb<1?W0qFevp#UL8A|nz zpIbUoMb?~z%mOw>+&EnYv3!}_I5rgW^H-vTA`gpGZ0t?Ma)dc zjy&s*yu)o5=-^*)3CsWzcm3OHlnm&PmP`i96mcG?Zf@NMv6S+#VHbp%sOR$_OonR@ zvcM}d6!GbyxdyeWm(Xqo=Ask<$pVDnW3h%P7{Wjk85DWgsnb4-fcd@Y@Z!t7OALc! zBk*|wH1qaL`Ugmc9F*5jV}*H zVY2~cx2ByAl~fA%dXSUiquZRR^!XI_ zBQd6hC!(NdAX4l5+~MH>GNycNHAY66KBm|m1i{t0B~5-IGU6x(+Hz9EhN2@Jq(QfH zwnM>@2-(XHI@vdwjv%hi#Z?h>k0-|9a~co-wt?`vqPW+K^w|seN4?&u|I%!vJ>qX& z>lS@DcotAvrjgOgn@kye)NG@%+*FJEc)sgHo$VpCW|L!ghOlY*3^o@|-{U%vxP0Rt z;IK4g;~j5Dw({?Al-JN%nKC^uG#6X#?$lc*mzTMd4fIugKUes4aCaVIha<2h2l+#` zghRlxSN*xenpo7i!HHyTMXoDqIIv~ABQMyr>ROM77}fhcHR`wVt;2IbhU{p2|2@^J zg7ed77u5!d=+6pHi8{k4%KWE2lLnX#pCnq!tKU3h?)1D`m;%zD-?n49{=P}k*dI@S ztaHBmO=`4wE*<7>B?f-sDx&pgaz_Rb6zMyl_v4+(yA=rgDnI2aV$^i6nCcIRC+A|hg12XFfR2lpW0X+?SI89wZCJd52QD=>C{Za!btvDoa4+U)c}W(g3UK>P9> z39@eEDMQM(G;1m0?fGjK!Rv*2U-9$52(bj;vza$l-1Tq( z(Y=ZWs!Lsy09gq=lDijTv$%dG5xH7yT=`r3wg2WFsSuZtWO8yRPOYz)jZnLU9t?jo z`G>gqXzoXO3bcL&ern`%=HBT#fvQeoRMLa3y8l;x^MZ|nE0-Pri+im*ihv@s$KQa% zdhK_R5+qS58aVt&8X4Mf!l&$c*i-L@2-nbM@&fP2qsn1~8g9cy2oe+qMRL_}UZJiWj+$r}aVTmVMYX-nYxAWg$hjcOZ!MYW_jE-`6(I0i`C-f-eD6v~#xhZwo zL-IZKX7Vn&M)+c;^UlGky)}!u%&T&Fj6{dr0)HLte=T*IDZhw?vke+d<7h=+w81X=n0$ByXW9~AGT&YVVK0=T3 zl1^4PSEe4)q|u}7FaNy;swtH}F-KOgah?#?;Ai}~mj${@fMbgNl{C<|)eyVHW|HZ*~rmvM0&HczgfBaY;$GWIJFH!wUK zP_w}X(I?+AEdkarug+MQ`Xd*@E!re zzR8n`_8m{36@2Bj!8(F$lF_?%9H%zqn3>d%@UA2l3&z@6ZXIe1Vgk%6MJD0zV1rhF zq3|sA;7%-WB?Y^O4+82yH$5NK{jhKeaT)YLEmkY`^)HEey!3tcK-p~*%^IR-LOKp; zmSZEq1SBDX$E_a;x9w=hAU^+9qzDFmv3gZ{Swr zpUlk4LM#JWTqtA*O?q$B@t0`za z=54vJeq?AEd^PEy5FXt02wfUHJR z!=qRaS;}%=)w*-k5ut6@u=0rSd?JaTAN7=znGs0NJ7Btb9F_b;5OFMwj_2GgK6~zH z+x@{sRrdXUoFjMe$;m!}N=Fm$&ZIw|V$t z*_yzb{p*HJDlGOd-{N9AQ^gn)BGH8AobO5L-(NHFb}h)v)yn7gq5~s2+W5py&m08=S-1$s3 zVN<+FZnI+u!fxLRH(9~G5hXURavb$zV%SIk&DlSCU-cjT=Gd1FDOBbosl%U5qN5U> zy9YbLm|K<2$Xg^zMB+rhG^Vh=22*Fe95^FKIU>xXzSUWrg2VJU{ z#(OLt4;RkojyG~asf-fKN3ve2)bRP*b7fP>Y*7CF%-n9i{fr4YdDqq6N;3k7o{0)$ zyg-Zo?>oBC_Kzp|C;k((TOMz8ZmI4%z{6XHca4NiaJSVbudMf748C65;p<9~6A1TA ze8_+3x!XMX;~uUEv)yy==SuAjO3*%m!CT?Fkq1Xvmgdq;qsZ`?8kt|R-bQ=ozLDpL zzrocl$5UC_zD^s1Hu~vIo;}2jTQ(<%cb!ST5b?pMPRcz0dU-^_PW(I4DjH)0as6ox z^dai?rjI%YXSsOi4vFytfu!tcO&~43ZD!xYLMfB7yk z1{gsTdQ^`)TO_?4EFUJ|fZ)Ou7N^BnZgi zTN>y@S*QrjYB{}!P$GMtwL_5~_oN5G6uvePeiY-^gn0hP9zQ<~?f&@^dTabfZ=}=l z6pF!4kG%i_CU@VgGv%QuVK33-+v4vXx;s25v#C5lc>PJ|{vpcKcX4)raD)rt>1lW3oA`|Ls`e+Zj4 zn|9i^-`?-J2FpWq=l+zw=evjwr9@m_9@3e@IDi|Q!tcbmv)-@K=plc zp_KP-!BCWpSKhkHZS5vc?ZM9qqU1WC6dcL8Nz})0ZfJxp$R^6!dVEpq_S+pVQ?&J7 z>bLS$93uQQMvL7NjEXpaIw&P+;na&8jdMZ|Q;)otUS_4qNO|CG&d4io93z(XQD#Qa zbMig)V6inQ7yOXONNi|fGiyRldzf)>=W zMsl36*5_pLt)Nl*yhk>B<4Kl5jHjqGqcV1`n}Zk`M$5;_xHJOx8GmA z7PtN3`Ffo0_xt1iywc*qJmr>D>{F5)EgF^2T@3I>^h;8x8ryUak9j@{mrADc?in`^ zkxiha*g0PEj>&gn((hEP4%_QbU5Mapp6Q-C_+^KEXHD!1D=n}2xGKLv|DiiMr+s_C zq1Yfw%{!FGs`ZWOY2sDS^5x1UOX5YP3*O9ksw>mV&0Td@s*!tNu*oXO$Owh^n$ml| zgavROSKW(_6)={r&BX;!&Kp||lG^U_o)CS8O5P2+l{!F*6APpc6rjyZ{7WKCQe)f5 zH56u1k>#Of2gSbZ)`*tBexr8Lw|lpsihyVBdp`nidx6hi=56zoQH|mm4%8UJg-{#c zG6WDKo9MD01*?U8@yQNc(<*oj+U6q4y+k-t3=s_M4LbN_<2;b}On^4T+B$8Rf)Z&P zDN=zc*(1oeG$0`GD4mFaPRz2=VXr=5x%PTJ{4pTG1L>6-T{jN(q6$ri!Cc= zVv{wl{{*1-Gtsxw^qj{g>;AmhK}KG!Yj{ATVX6_Sdxz>P*G>I)hgt?;k;K?0*{}kv zS-$TV7R>1;tWqYr&~}Aq_=RilbV1q>OtvZbjvZ~K53tA%1lMVkQA_W}b*_9cKdhHwxAARIyI z=0>h!_*(;vEHS$BGGwz?&N$;x{|tx#c}#`}1qUqyXcKP4gU&^zuAvKw$OGSd9EriPhAMRmEK@YQ*1~yQsbH?ZHw zqn5K@H*e!tYcE)~P2boucI4=oeZl%Iy4HcGDy&H@yL^XQ>E=|Mn&wUI#w~1w<>Gt3 zb-2`fO-qH8%9rQ8w;ZIpN6(x2vF>){@hUeV@tC0I2jT=uq_CxtC||$!ofTPT!$#2pOf@J4NuQN>8|Cne z7my;#)XYmW#3k?q7$uWn%~0+KGPaJ~5>ebcC_wZjh@eWej@i3`EPns~8BY8cs6X?x+<)(Gi#++-nuAwB+8vDj z%AX0~s`u1$deEqSXjqwD|Dss_2BxXUl4zway&U?JmFD9?%cCfz=ZdM4>Z%KP)%2Zf zR_vhDmrR*i4j=ACuGpR4rvz8{>d>st*y(ZAf7Yyibm&9Ly1bf-Xr$*%<pqH<|$BdNwHtOn_0l;$TU6k@yf}9-p2)qwAaQXg0@MmsLT&``(@$t)Ogx@ zba`=&M@)>3*0#gmV!AnK<02KyL|rV85UX4&qj+%{K&IUr3JT703Kz3m8DP{T&X0p&qX>191udo$Dw0IQ z3F>!~YJ&ED?D@&5;BC;mYPx^B^C;W1kMrMn`R7xl=%40a=ggwhQ`>Ti2-o3pMepli zk)o1ox)1Hq<7=JE=}k1rkFd2GiFFIVn%HTUzwTjM2U$HC3rhR0LWklyeZoGM!fAQ= zRp0sjoRCHvL{oCfkrC#I5z;gLO_f~l&G2OTUEOte$nnU z*?KyN!XSC-(*n(q0jmPcApa8z4X>-p2IC66K(0OeYPcNu6=3Oi158U3=fI6dF$*j; zJVw#R)XW0{u0;T#zxUUYc4&p=Ytto^$>2hef%mB^b;R%(xHwF9}dEE;ofgL03SsvG{8QL#C!pf40b&aXk3l@0utX*UAc{=_{en4oW zp`ku@VEw8)J)40Y&X!uF07U})o`=`yyzJu2z=)5@0pa2R)I#G^q%7uJ?0 zlgZ*0E1*KjAmH=&-y0~t{Y1aGAg!svrdkxVd8{-hS9Wv1{oQc6P!%1=k+Xvn%51ieQ~J0?SZ$>oyLBft&w8U(1nB0-*m`T4IB}?^g|t^X{cT_ z;&m`q@h+6S*P<-^ZM242d4NmdywMkUK*Ua685(;uqgj)YDx8fHG_85hSKp?=;hdlL z*-geR+ZoYoC8QfhC{f{8y_lC->NE?S`W7w; zEKBZkT-4N}sxY2Kdl)<_x`%FRPIjB4g)_^8hKopea55ck>~lT9$QIWk-ZU*)IFx}= z#^I%c*5e^Dq2J(G;9mP0A7wp1!$B>uzRx?SL}s&+Er>~}?a?V_NtuIjuNjPVo*r9; zfHPLxUX$twIBFyjMoxzmZA;d7nm@7)@;PYnIGq*)#HGhT&3K$ZSaMW&>&UK-4^rnV z&J4MkCMf7$YN2*t{E1(p7Gvjdcd+gt?FPZsPyKb|dN{VzA^Y+)Ce3Yl>Kyu)t9nOv zOoj%sY$PxkFNNv90_=!p{0wjqo6DL_FP7F!hv~zApY$*jw1Xy#7q>K|3&+Gyr9K*% z4%at^uSNJcjx%hxiYpB+D7RayV}VFt`a8}w%3r%h4o?v$LS$vhgVj@+YQX~`!6Id915x$?Dolwr~fzV{`pjI`aj1QI{sMFOC!$; zvjBYm1;4D}=gkM?3V(gmThL!W{WZ^VQE=bVK-S5rt@OPEcP-7oGm?qVcNfb zV@L7P{)V&4S9R4ssd7|DlPALc-`_h|o9PYTffCR3Q=hEmZCY6pAF$VX!nG=Vt60tq zvuA`8uA4pmYxgn@iFhnXS-6}pz18c1Y(o0pm?HA&7l)qk%~d+tP2=c31XVgG#LUiL z^NUdwguM2Z8y;dLDsMYuAV(W{_a<0)7wu3TvWqJh5WO7)*lPutE*}=q^71=A)Vfe3 zEZ0k2uu~D&hk08uQ4$LFO(mS0Ae;~0OnPAr&6m?WTgweZ_?9}r70jNZE=f(qREt!y;+O$BrR z+dMwEKawv9&u5=5zy2j+uLc|X=Oph59TsAzaQPK|YnF%N?#hYt<(Op?aB=R6R)n7^ zzbW%nFk~6er4?c+X{Sgj?TCf*r3>f~;>+dhqSqOADPP9K?4 zkKHd5VdNzA=BbdW;8a%34SUy}9v6}lcHU4oyu*Y*2Xa}V#ThQ5VCfgc^-4&oO=5e` zRh9*`mNK^73@wK_AQAG3c2H6Z`D_7jy!g?Z_AFodAP}4Dy(;}B0otusID{25%Hp_? zwnf)`;7vQ1j~b*KW3n4YD~pYtsC7(c7v-H2QfxI+?ldQg9G#r_!yu@SL8>HY3^M@5 zk%;l{F}cJiH3^C*wITqpnc)_q-d^`y+ZQ!>Gr7vp*MenSmq$>aUJ*Hf7=z)CA;bG8 z_AP|;_5TZ!;fOr(J%v`4mX*%p@ ze@K^&Y4_fT%HQ})$XhiX-18ed;lMQ}7#MqU7Oc|9zjlWWzXKw?8q(<}8!BE}>{v#6 z9DcW%ZJ8hZl_P8TAEy}k7R=W1BZrZ{TN~?3<(n=a-}2c^+Fk$6u-4}ZL5N*QovN`0 z%-El}N6Xjp5pjA`*c-PO9#3Z#eeByJi`H@OH-re}m!k9?rPQUn(*4wsSG!T(j!Dr% zT|eV1GsqVvm!Q~RzcxFqb|oGuhGE}!KW+RalGeQArBY?xMJ70*v6)IgwC+ryF5qOBjYb&6nd|Y!hmc}Q zJOv0h1&wS39LS;&mB}%nqVgG&Jk&6a3-!5RgPQl~b_h;=s`n`;Xy<$bd7Zd>%I))Q z=~lxw7jG?yVDMi^(*f!aGj>FRM+p`sq zPeQCIRin=6$YXP9qtw%kML)Ttm?*aJTsV4%Gqsa5(HekVKhh%9*#^;jys}U?8*fV1 zP>(mLH16lQSBGcTDzLS$jMc@Av$&&rKM_xzQ??25mHAbcjy^Q=NGHbOeDjfWBgXmznnE zc2P(vt%5@l27GE52?|sEM{51YV+rs>pI4u94P)XRHqr9((D(VG{>i79G^?01#mA*< zWRG}-FSo29+;8^B7Li+nd*QD;gUef^Yjd~A?}x+`v>myzShw>6r9l|_LK$XaM7kCb zKBk!Auw}yjAW?Q{)l!C;rfUcJ;P=Ra-NvBOw{+?5Uxb%*2ZDz@sqtKSWU1-2GjgXyVAol*@y@YsA0c+Ct)E9u=gYNQHw%~QXY6mCXsKg z+I9fN(g)Tv(($c|%kumqNur-~-kj4RB~BNJPh^R4VA*TG4zTgF8<7``3R<7iU;@8_ z>`8*M$-~;prKtm9E2Ziql_1Qh!tV(7mdUx-cMWW3jH}GSEZYfYS^U(WXchE;MaDM}5c? zzODNg7vd7&hr*;jMNd!fM>86I(QKw1AzOiMSQZP5s&}u7pZyZ)uso()Xd+1B|H@#W zpRlM#{zL}<{<@1KZ^PXqr)C?olh(Tx>%sdib(Eg+D-ExqG~Of*%UYhsy@P)wr!EOJ zb<-jq6q=5uHjQ!-cqeWn-v8WBG|l#XS8rdYUcA8`j)hM@{WX4jtRJA5pz?4>*0vrV z&ufBWYzSR6T~&)$jGcYGuo*#2H^d~&11-9qoX5{#=F)Sr#MbkdSBu%L69E2Y6f9yl zYwU#w#nWcfC@%y>u80^#*@RUUI{8TaZ0Gh9$*Ox`c}yd2nw!M^sGdK$YI#(xpMZC` zXO6>pdMBhGw+3vfK!$4}z#8)_QrejLrW<^Xykw1577Mw+pJ2Qhgo`N3`P89o0cxd` zP3eC(_bX|oOs5*eBo%8e`h?vB;C_}t9i<>@%wqs{VT9U5S(BCa(5w{%1SO_;CW7i9 z(2NR%@V(mv!DKw|Lh?8CJcp#%zy_T+_=PB?>6NS}!T`0@8J+W^##^i+&M1wURdj3** zr0Y7OZZ^HGy6jmbv|wfn8{s`9R7m$*uhHnz4@ye@9MsM+Il;@zUBt2BLYF{D>pe*!wTHtDH<|6jUCIN@%~Bvzr;AbnUkMt|`%VUU@w4uD}6Cw6h_nQFKS% zXQ4GS>VCydeqsd*-+VInAhxy@UKCz=(7IRLT|lW4uTLvgZ$ukY9qCk2p* zRts+#1qHV+lg0(W=udjKBFcki3IH}jl}e*^JopULuLwk8{@P$KDGdxj4`eU*f&l-p zbCX{}JsVgE1oxoYP}S%Mjs;JE)1n6337wbmELHXSLj=@cge|S^Y%8|g00@8EtIkqlc|^G|EP|Y zcCoja-1IzRS2lJlH|-s}oqCy#K)hVFKsq>_{s`y<;esE^5NFRF4ZB~Qte|BL%r1d= zSsr+7HTvu6_MA@z-Wp)Qlpy(uB%g>nh64b)i^Q}~ykv({^)ijh^SD3`gh-)x>V7gD z7GRoYeUM=8bnAxtbO`okb*c0F`5d@V5hMh<84^dvQEf8u1vIA`# zA?d8E5|j3xwzqv=2dAAi61~uNgKki0^DDkS}yk5hPgV`XA*#Xk-aP3hm1uYqPHhgdUwt{P^-5Fy>JtNke+9gG5sH1zs4;I((#DyrNR>OZDO&%ZLhz8dX&ah%Aso#v0cItVQo zp$@zlWsZOj8+*!%oh_Y@S#qb+b6!4vL9#~=`8Dd_@8rwCls(NJ*Nu5hcuKeKcZpaq zxpWe*w*BLq^B(!{iu35Zv&Z?42=6bvJ&wK+3og5E^Sp;&z3*L(63{M-D6Xhw72<)vZrgy_Nxoj+P66ehkXjeTQx|@!0S^@E$BPxnac`-Jvs_n>90w zE>(g=z2|~MfgtFa_XYw$c?BGuDb@)1ZQZ+@LPYDR6(MAx@QH7SfDcjMFdzpw>wHoV zm_1WNzEo$oi}{e?4ThRaauKmQbK7zph98_J8qFLfx9mLSQyWjcwIhOEWT<94jN<(7 zj6>wsgdcZmRUy{b{L#Z7ZSsDU!kw>UwLxZ-E9>DC@F|Yp{Mun=cyEL;>xf=Yb?|eVOKI#j6A)$J>W7#}B49E|skCC$gT42mQPfo3 zKu8Md1nF{z9Bco1X-~{g)rq?IXu0V}Ui51{A+Zd;Ad0Xg-S`6mN9*)tI`J1^TPAZZ zM=Id#tk`JfY-wd?Jy&*VqhI)th zk^rhy!_Mb<*UtN^OK09YXpaq8l zR|wOSQ}|o);3Z}L;0U0}1k&%=dsFKKp6IrZWK2nz3GekiCeSir7BpW>Y^Ha{&#ol9 zE&t_O1T2ie4N5~G9l7E}MFTH*$aofD6ea@2Ai~uddS+VF2`(hr=NcR(CQVCIJGzsK zIpb!s!^RkZ`TdMb)#q|~5iMh&fAFfM@QXip8v{I8@Vs4)eFwcmn5b&5zs_xg20Rt@ zs`4_G4o)6IO-vl$ydbKdPIJ)NIKN9ah9{qOz zPZVFvIO7B#9vJwcCRvSEd_e3F>}OxsTKn`BA^jewx?;O!UH4I)^aDz~#FfSHB*-&8 zxuewNq#$XZ;KyRM%_||VN~OjV5Owk$zDEYa1rDjZgA^n`CMHlGiw&_l>e8#c&GQZ0 z8C2d~sc)2qW%P#e>@cIT>B-notp1Be()9Q0qxg=Zg)Ptq@bgz8_^E`7R zhv3aZ87-6p>ku$Ugwfcmyqbc_U;@t!xff>u7F>O25zh+r-(-*qzvBBz_?&0WYyfeB zyAAwDh!+=WIOA8Q4swRvPfo#GxCrqnfe=x@?IZenH1Q$c?te75|2XT7M@Qt}oo*Gj zZuts&7%HBI{`ghlA9frqR`s_xvmWFadFK*8Y=k z^>cL|d7rm#Tti2S4>mA0A|V*at}e!rv|4FRO6!c%H@r^$o)L}5bE;1k86a)C=QF$i z$o7w^Jm#ZpJ4(PA5|P3@bv=rVF#f8qKSR=(tage{I6jbn*|gW7W_T~M9GWO>P&0bcD5Y4#X56&d-*agnTmNJEz*;3$-#eD`TRE}ON zt7Z{SHMUtmo^1}yJm{>!YipcwsGNky%XJ5GLSG4)^q)tOCxvI$L^}2W@o7FV53+$_ zH-Qb@Moh7*k@&;j`BP9K;|4HVy^+A-)nN866^e@`T?+u4L?kT&m_#|kXVXjHT7Zp} zMe4Ak@_KXA>I@I1Wh(-@7-D-89WI4k?I;p6hAhA;z9_?jR8!v0n*hFb;MtPG6@ zhIf49>M{qgrHL~~SBSFsi-Dt+wXrUVCaV_=06eIzc(&o43)Dr3G4^0y2dc$<5emtK zD&U@2(+1N8d?>7Dp?|{dX4JFkDZ5-A}UGD)x#yL|EDQhRC~N1~4SNj*u`5-H|G&8FJ*w>{k}r_4xBY_@CcmNVFI z-|=R-JI>(J8Qv%1&^COcGUF1UxC;|CW-)+Did?mB;_L1<%00+k?(=Ng1WCCo2hmYo z>3)?}2qy#@>#f09>C=|B?HvzJyu`8TE zZVoZA5TW=m)Pvv=ew<#O0TWmRQNkMGm@X|{$N}z%Cyu*$?;D5*mpp~Ic@`=Z$P5>Q zFA3=Zu&xjEJyZ&$a4o+m0}Tscz<*|tmiSP)KQyMWU)TVM?suIjGyxMllT-vOiE$@q zk1;d*3^_Tq?X&&2$IfEK)0KtmSCa+*GSn8z{!j%E^mk>O&wqZlS1-?8IphabR&9-c zxAgF>S-SJeMDHm*reGN{SPW8Qt`LjQ!fngmeMWQ${kd9US`gQ+HHE&=ycL&O7 zED^%lQC)39XQ_)S*x_xfZA)CDBtA%{#<#II8!xf7Y4VIrv3`j0c52)&b&#K4%F-K$ zqa{|!h8N$G^vv?99FBam_mL8kah{&x(;>%m)R0GI*jqf>dkJ)6^M?mYHead9TB16A zuV0dsMd?$>U#9UhypP!HT7wFER-oxOrzCc?kD!9`m2JA%zsHY)b9#rI_CWpto%-uXs^u@EhN&MnDnomLzT!RbDIMZ-Af)Lu?pHaxk zMEmTA`c}J#dLl%mZ-2J^-K@_X3I_FR>}rl|>_s;?Itz?{^S9W|!w%tMT49)z#Aqgp zEeY`i8M;9y$~DkE7F;`A>(tkWjpK0M=c)E=f{MT$5(_T^?k#Pk{kI@ERbGsGj(D*y z_Vqz63rX{U02eOnW$WYKPI)nq;W!{Ed8fA69OA6tul~PE_rZ)`Qv7#?PZ$Yh73t-H z(LdDO9Qy-YR2}|(DsZ@XwkcyBdJmqwmh zurauEbmv6+<(8vgK0ff=?^wDJMvYV@L0XD-MilytU>`r$G3#`d(N8gLlh1nq`&B8hI>-~OQFIAqO(A~Ng(-c5VMrct`^s-S=cA%;XuQC%237}s3n~% z@(mv~paQzZ5jpL%8{C0-P#S1do=4xCFBI1b*L@*8_8dl(2)rk;!+smWvVgTCnx*&= zo*QqNjrt}#-^I=&X!S6_Z>S0FcYQnv0@5$O3t3W0tLm#W&Q=**28tHA$bpuk$9T19 zl*z-ya7OLfnuS9&F?MuC6Gh#~R}ub_T*&c_Z4U?r)D~Lp+~v}!B#ABJ8#mlhkHVJ(je+96+tf4qXr^5ZBCx7;Ai3 zI&tJFXTe5A$4_yeb-#UA zrw%>-r7m~Nw9Dw>_K32O=vMsj?nkF_M#OuM|B$<)T$C^fffU-~ArmB(37f(rWwZdjQuP>NBlJdEkWhUJnv%X2q<%`EfGF?*B0=9FRt{6)urZ$ zy@SH+Kv$6uScC3p+M@w_r@oG0f?1jtp(mZ{J4vsgnvTEN4xM&*pC-S=v zpmPz@1L8OZ@gE*3i0NN7SqguN)^jxH$)U_8m6dpW4VN##jQWBu6T!TP-Wi?Y#&H6Y z7knMgTn)x)M&v-V+ePr%())ZgHN3($uibdKsGaOfI!UsomohoPDv##r8^-gc5OYV7 zphp#C46?8=Ur7pmufluT6elcIF$naB;zlt&RwjM=s;t!r>O(Jmh+GX15R<_OS$}Lx zsS$(75U_R%w51bcuprGGVGNRDEShMz;R2tUbH#kR);l{JVP*S5XSmGOQobPlb*az) z)s+7IvBczW4uov7_*ixTLA7^-<3YuMhxPaOv3i`_u#85fv`wE;47dEO=&0E`ueyCO zaav-N|9ZfcJ?zHD)FTJ&J8Yts$xedlFubRfmaMpY`eB#saXrd(XMk;Bd`}8DW47ix z<#gbl{gU6TUKETw4|ru7QH9 zy-&2M*3n%1p1K*d@~fr1$z|^Ce$vA0s-<%#)C+#3TB=X`H}|l_{?A`Av=uuvb(UB6 zwcP&BpvC+d0ldc8JKI&-XV-LQOPIOzo)-e#Nha!=%Ud2+>`R=T59$$u?dh!i8GBwc zl!bPmhGkfYg4;_?!iS5{&O&?iaI~!P2oR!S$7_bogTNA)hDf%Pp#G|`y$E(i_fAud zeKKZ(0QxClH&aL2M{3j_Y+!^U%&mlAOLoUM4yhoOHxd{v62C>chfRmmhBcA%L}G$( z7~3}!R6T+nstIr>{y7a`!NCgEi^8ifkG)~i)>RKR<#6l*oN*P@Zsna$r6CJ0sQD+o zJtL$bw)s%LSB38#qkrjD2Y?@fth0fAx9)(g5q%l-oLoVDe?zLRrz7buKjEOzQ``Hf zEem~*dkwTqp4_og>U!c}`z%}2+{2ob^De^JEQC<|`L`Ea==L8TnRut|uu1h_fz}G| zTvHm=IZU8!_h&Q*9`gCgq<1XvN)|R+!NO-#C!^E+P21;@DI}5;vjP=6kbmwmE|mWH zur-alDSxxLVLhFd&Pxu?aZOF5n9fQvF%K7tdz)$4aLp zP{Ia_&5tR+xqTEdi%o9Mr|$2&cmfrVzB0pS>Gz_%$l8gV>)(48GzwdV_c98})$15? z5nHHSAgAF4EJa&PIASh4q*sE)9ptYPnspTR3`yYcw|SUpFvA>7bWY8Ys8EyQ7j zU+#e*|Ju5~31AzxvY`(F$4c$otxzDV2yjoXRim6n?#*RFG}`d8q1i>ZEL(=y9@oAH z;{P{#S@A!RqmD0UY5Z$v{l9_2hmN!CAJ;o*=y<0g6o%>|VjTmXh2YA6zA=Q9x}x$Q zHj5{3H(oMJt&7{s|3qD(uD1IkvTGDoH8z` zbGS~mr`gEm>ioXAdF8hJ&R3^WLOvVjYRTmDvSv)QP``9ykPMUyAvcRiHl>w00ckG!U-hn=P1?W}ko3drl? z8D8F;Gj;tO-A-QN5@(Bd%mh!G3%LbMD%DwlDr_)hLHcV0EeKm#Qg{6b$kKuelt8qg zC>l)@t+wOq0#gKR`FlmSrweWc*#dVQ{PD)>9iSohqzcj7do`?ImwuKiz<@rX%?wh8 z1vR_FEt3N3dGt3#A(p`WA8gw(=c29@(35gVfBV_AAxu_HuKyoO)6V}==vmQ!pW2W* zgn#*L#Cmo6#A8Y25-XIT=lem|Klvc-sr)3n7!*Wdoq7#+qqq}$=)RWu_`ZX4du}VI z*ZWC1>Pvm;P-QMCdb`(o%m^Qy@SZ`+xKkzo`I_SRonP*F2kl1I6V3)zSHsi}zTNzV zJF58LEmSH-)^cmi%+}SmNkwH0(eo}z&#v>Lzad4wn@zowqOW_*-Jyps39hv}^CN!$ z?4Y1?_q6(`o_sx&euU>V(&DIi0ovhQs&}b{W)A1sq7Bz_H-*^!rx8-hxY~RlZM2Il zWM}KV)e2^t9SDObPId3ixrGUi|E=)r$(B>|oRE_iC+U^(mhD%XHXrZVQFeHb7lxO6 z@7`HxZ7&c6BCwoWe**#;CXdK(%R#jEP@Evw(p*-hOf*MFkNNn9G*yaQL2CIVnqlV# zng=0#>-sxzHi;1bYZ$;hQL(BP;Iik1qD+KPJVec$QNdV>C{HLrJp%|?2lcDq!R3kb zSde1ONB}-a>G?1&79079yYkZYIP{~c(BMQ}Y@BeBThpw&$>hW0qH1-WL~Ck&@Bt7c zxs-;K$!AA*r-D1LJFdz;AJ5hL^SG}WA9rI*XErotPqi{`$A5~2UfP?C^w?d^64Tm4 zA55cYMGkq}ni+W9#ImbT{HOukk1#SE9&K)UApMkVn@!V#g(EUFqv;#6Ndi`NSFwn# zg`J^)V!d1l7}{E<*%OXnyO+lr#g=J`^gcTAT{Y@yx~cikX@!^lG*i`n8CtJLY&#l# zU<=npY^Xwr806j8>|(RsmkD*6lzZgt_-z%BFWkKjX`gybvP$MJ*gj3vdcPnx5PygSPx$!^a24+8mk&F+pYyG;%YMGx3yl@-J z0rn&ES@#=1+8)P>h6%xX&7T6^K2D(%@3y>D%=P@5h&$2E!{@{%`;7@lFt*XN;EtbE zd1`qLeKU42@^XF1qV%?IUEN2hG?t&8^gCIT5?XjqjCT7k`3~uWgBRS9hI*K69raF$ z9fq$DI-gS5PR5bX%CGq7J|f0&Q|YqY`-Yj?lCWmy0AIDxJr!&Zg)38z!X@ILAfnpK zDl){;dA?na=ULP%izEnf1YD}jyiDyN$GMK0$7>HcN4+?e610EX(4~bxG;1Q6EVAFi z<+Q$^1HCYBv&{xEd&F$syScm--eTrV|K}AGU6viS2ND$4@^yYxdtbIV_&xwXgW=M2 z{nBfnZDl(x z($_p_iI1B8HFbDYMOd;yV$Yj{wp+n{x{y=>t>`F~&VkZ>z(i`743f~G-w@cpkXrA~ z&9R_7vrZ)LijIBKgtYc*jncO%7Q;A!$1B=5zn6Wd;-JzY_u!K1l$ln@ zD=lDo?4Z+xr54h__v{|&7)_5wRpUQrcjJBK2QXweE;paD0z2g&LBR4I186dGBWYvp*>yM%uiaC|p&2@|i{!SIBacvLZtWkHlg3cl zZI8?p_3!Txm~dEKa|fhTf`!&`s?LFAW+f-{{(W!dgVL&Y=SY30nq7Jd>koe=6XeBoCIvy5h$!D1NwU3RZ!>$AJVnjdowwTK4#Iq< z4Vbi`o8VE>z`Gqlm@SyVHq%R$BrTO}geHi+ph8kWE97|UDlL35J9sFf6J*OoBHV(< zhUhg1$tgO!ux?gsy>3O~`UpEu@Q!}H4U8(+EeZ5N&P4d{ z%J@y^HH(-Uf>+yXXgzYC9-OfSarQW%?;d$I>r~?ydW}yhaM1<;$d>MfoUtAv2;>O$s zqPMj^C6lQcB;;u_18_l4f*2i;xxt{%pQjeh=b5ZJ)h>Daj)_1S%7$PF2#*k_=8W@D z_r468035}(z2NKML?tB@Tzhz!+5Bf^Y%C!;;&&ZkIWb795`h&udHQyxedj^{d+gtz zA{qPmz@|uo-^-1PTffTz5KxC*Qte$;L^I~|pvbmL0lNN8P6(;>eiW2d%O(ncXdEVN zf7K%0zA^x4(WV0iAqfRCCmW{sXkjvsPt5J9$fP7UVVzqhR`(33Oc@1~zG_`4?t4*- z^yQCjUmM>2K&2?Ai0WZ3=Zb0iW|5CXVb6G`SSOasFJ_W&$#9((Lo%?>Ow;e|(DpcB zYxvl=F{%01c{+L%GMJ!U7z&Cn%g8-QIO`biCbbudp&BeoM${-(3Sg2qp@ZfClOjhdGX=#H;$LZ1t4GD_vko~ zuC}?d_1u2uF@ZlTw1VfUqpm=O8(J@9L4lE^b39uGA1jeB6o8+lfeGDT(5*t@`Mw2} z2~J_-ppY;94k4gQ@PC3B$tgD=8^&S%hvIx|`>xdqQtg%I5h9?&moR+IH=|S&HIKiS zm<`HTz-sPOyAWT@BKPR_hk`w|m%g2uQh_X`Qk>|Ou;h%Rxw0c@?4ga-Fo&MfIXVX@ zYd7wj=Y5}mzH9C01MGQ?&+b6IRU&|}`7$tjKY9=gZn1p7Z0mcQ2DnGmfNw}Ai=^Z6 zJY;9g?Dlq^Z}n_>;J(v%#|OFZ?4_g zgIJmE0rk3FC}RZkCU9SDh$9&OSjJ>DZjUZ5D)}pTJ_}u}0s9@L@H8`842#>WYPtPN z&!z&)tpc+fPeXR;RujXFetiX%K+DFPtkor4?(X2YPp*yCsBH?BdA?`JCnD$kiHQY+ z7W=Jzu>IkFgKU@4DDPU*`&w+B`U`3${*uSnaWuxgp76S|tDe6ep%S3k)WX9_bx>os zPe;ug>w4@{<+bV_+A*z3O4QLp_9`}{t8JyrADqH_G{pwV*7#qbC1vS7Nvj60ZmBQS zlP$T+c<5iTT0_sy-BH$m9DQ(G2<^}2od)k{s6Iqh7k4G2ve}FY-z_q^n@%G)&{XUX zF(D+Ig@&t{&jqA8u=NDn=%D^)C<%B5DWL!jfET=f0}XSQI>h32g(PogP-VYDT9VP3)+Z#9rb9Nm)Buja#sn|`^)!o-sF@+v` z94A?cTZP_bPSHDAshwhN)M+{Jjvx!~Ywuv%#31j03F<|1kDjC-MUrIImKMd2-Pv?m z2wQ=rE5pvAycDda!hGq)&fX#+YbsauHr6?t>(9}l6&||c9J~BlpaMn7l!k;WGfA*A zUAL_tK(cLA9Bw~Zj%Sh1GB%X-yl`>B>U8laFGJ_|T6*6d9;_cZpSP?C&Rn3j4O{nZ znRsl3fA=}o<92X6sAt0qAT@kZc9liLNCc)p3|+EFrm9Af7s{2L1=`A=`2{LPbDA9AczQi0z$3o<2M z;x3P4Pt_k?8ErP@AZJ41FMp|DNNA^ad2#;(h5S6FF~PGxy-6-DvwX>ASMoD@sZ<%J zx++#6%esM~IKx>kdY)QZ+v*aV#)oyW@zS2>2oj5oi?S|+uNcbq0mfvY;>S19vSYqm z01m86a>X1O{YG|8)*rDJLZmL>YsA^)8d%8lBql%5O z58uPo8Zk_gE zk{rmnER+W24^gmXQcW`yt_Kn5gPLD=>)R4u2s?1qDcf9zXIWOm0l6>KMmP@|`*!r{ zx`(zY#M0e2X!xC<#I>gg3wNN-m^$I<)#>)IYB|eluoQTPQEGh#)&*@%jI_BfpG)R? zRJVN~cb{q>@KsEqh>Ou-F7SBbs(Kg>8n}KMG&x)CPTdv%MUQQYqp5%_JtIfeo*Gj; z45bp55J6Lme>J6f(?KoxYr|(7XYR|G->{YN4kWF^5h9RpC$LDxiTl6!;eH918tVOQ z9`iCgjr~Gz#x{1`#;f5)KVea~RcO0=cDcB>yx;X2xsJr)E^F4)Q zyPRhf4yHl(5<0$AvM|ZIt(vbF1qXSp%y6@n_}%F+cSoY$g`=ixru~|iqEIiUOV8#> zP%TQtQ*~1UY-{9|_N_N$qn(Tq8XhAH#O+YY>)6^)Z({l_jG;XT4;GbU=Wi<2a4$`^ zGFqL{L%dLN_k}|*P_8$C9xke{O%pensth-SU92ryFt*>CV?+!u;@ku5C$sSiw%%SD zv*>5@x4;(#U6boH3i4MFWDilC$K%DLmHDfn4txV|fuXp%X34pL__J2(qltc0u&i(R zFnW%K3PRChSPgN?RihnXb4VL0Za5w>>tfrFqE>k0YFJNhLJT_q0QrVcDvoG!ec z2V3OTP>H!Dt1uCtW?uli*-uym;Q@RIhFuN{az+PN!}Eqg$64lUk@37Tj=qsF{*!vu zf#HP!J+5e99<8n8jDd~i<54QgWIG8cHWi`AXq~>>cg}C2hHMWmTMzVuw#l@yN|@o2 zkwSz1!er%a^1r}u{gVH@^EOr@mIVjp%6gGC^SiV{$-9*@lc^lr{|s!afjwGZ3|(>i zQ#X(#Ynm9bVxSnj#`C4xoty$=Tg)qBQI3e8pKGCS^_Umt#lJV4UZbC0I? zY%kO>TMU_LVD9gJGz7m^Y`VWWgZxWWq3K&Q4?kgtyeb(QK#*8SD^E1ifUFIrQzTeI(01)Y&^{*<6~w&Cq&}Jqp|! zUoB#XQ5@vkjeP zx5h<1gey}C6ZgT65xgM#TMhk|{hG5AlZD)_%3y zdBM;YwDZ>=E#Y%x`@%pO#yif(c*2P8YQDI(ImkY_3|l&yjfO23L1-h#L9(T|@8W%o z&)fSUm_Xrg>}2fAJ2?8jN!R0Tw$ z*$T`yk;4J7pPE5RAE#9H=gKF$@bO(^{{mSQD*dw>#rxMI_v;ews+)C$4@|75PX_cP z9i5op8-FP_n7$jNCsMTFuyo0^ZO`H+X1dP6?}mX(##r`+y#kftBqZV5-u~iZ%lXZ6 z=QNKoRf{ZjR3xy@QcaE*H}eA;1GH)HRCOO5yDYk`uytz5qO)2yGF2ix6!xl`2^at54lRaU@BR*9X&dBotuz@P1)~6 z+CT*lT`8aP+Zv+x6l{&C*VtM2NGX{J(T{eA-Rf`cOVT-=4|@}oSTISisrn{#9j!Dq z55sov&(hidP7KPfUm>jl`{*3#Y`Q){z?`Kt7cognxOtj@-LIv8kD59 z^xmna8@!slQ|AU-8dxp8eV>9@A#7Dv%<{Fp76o8Sx%t_X0jC^L^hpo)* z&Hj^T5!~GAp|zC_ILJ7X7-V|;){lZHDf%R#vjM5VWw}{NDiC5Kv3(f47XF&`w(0QzcnJ5K1*|4hYkk?a7xnt zfcuE66ehY|zxMXluo&R=>79b=fOl|g@M_4Eexdveb7{CsDdw}Mz%#6+tit4wVGec> z&~4Reb~boCws!@mXT?^qwFqOeOx_l9I~>+KSXLOZ*5;r3j7~=#iayQl!7_}LP0fY= zybK)8rJ2&ugV3@#JZRl-8vwW%y2K^G;Y_EkebmMIgkd-<4(i{k*%4f!ql!Ksy zPvREh9}K+AGN17M>b5>=d;T{KZFUDy#0Xv5K5O2hbmZsIHc#TAXO~f$if=(C6c<1r zDEqB&p~BD$jz;f=HNiIbzsdIJH3)y_A9H~$Uh=E%i zYV_D;1MW)b#LMw`EBzpEBUgc>XZ$gfc`iAsYY+~iuiE+G>Bjb^TZLyXHI8;Q&pLtN zDMuVQP<)shGDwEq`1)O5L16V-arGp(>M9@~iG(LBGSNraiuw1}fjwiIvE64brXk;6 zdE#`t_qYs+Y4l`T=)5{}GzP+Qseu+4RfBA*`phs-w(6+)=p;-Qkvse? z(^K3>%%20w9|_bEMyRSg>+d+8@j{-)|6D!CcciGVM*x^Ju8$w+qV&^UC=<*(hurGM7a*i+(gxE zWNpPcLhS`56})P-^`i@%j-3GOAM!zr!%l-|gQP$D!bf38R{6)J8pZ{$k(JpnSI);M zE01FBfz=D_s_2VP>&-6r)(6}KFa+3dj$T*-*87Q8s-26&DtN(_8ds?bV5Hs=9yExkz`4)1Sg1e`#>Tei8Adn$`Vedf2i$ zU-)D^1l;_nnZKo{HtPtHY@=zFgX5>iTNr*|lb;1oo5$mh7=tl&X?4blmoxohaTi0U zc@^^il5%<^>C8L5p4+XQ6+=~;@^?gb&w0<2cc?WreYTADI&|^@@l$nlp6sDhabJVR zyZ@AX&?Izv88!IG{L++ZcURwM=ZjvZAKaI!TI>`~yE)Yad3N!7aR<)>r>lyVk9I9o z{86TV`G6dGS+<3LMCcwG9H3*m>&P&+zSxuwI$7ufiL%!e@11OZ@#CUWojs#&3?@XU!ZMu|fTSbEN8csU!B6((znolXWZwJ?^Zl!(%=s zW-ZU8U1EzfFC81b_gUsaub6b^CPp%_V~SkNuEo;%k`zyQrw&y$|ln5ySYmc=1KQ`Qjz;F`9YkHxZ@48#+1Zds=CJY$(hR3g-uL zI4k!3I|eB9Gw~*spXzKYp06S_77f-@!)V=F0n{l%Bhd^!;H|^VUC~_$>JgW517<+& zOPSgARE~$v0mqy$2S}cEyjKOYx}P~aH&s$^Vsz_eT0mD$RY(GVkpC@tM#$5b4u!@nh{N2lOg5(POb)b6XLZ4Ww{q@(<6In0R zK9K&}D(Zj!xzI4daXXEyAT(7N2Tm4dWgb5rjPi7@N+VawG8F5bXTNM_S8OzRiIIEU z=dRT+RiKhZr>^{~{d1pokHUus0?^+b2Wbizs9By!NV%(4oYmea9%4;$KS#?<(W0p_|M zt-}T{*RV0}tdbI}2S3Yj^)@r|wu5eAl?2PP?Cu zl&2YX!=m49#7+A%I|>EwB~gY0d78l~m=al|XNP77Iw30MSL(gZ{PjZ>7JMawS?Jv$ ze4necIIqM7J{wMZu_t>X2o|q22=Gw6evc)@HYHfrQxYTE0Puf4i)$|bApeQpP<>l( z2m2R{y-!ZA7qA)9yFs!&vNoAzl1pd+WId}9nhQI9-j}wW;C2Q`ldkBq52Mk`<#KG> ztRgEIy^i4pK zW9Dn%YGUUdgj{4U_CKWjb3x|$ryqNl`4o?o;D>u)p{GEf+s8~Kz7yt&)3@evA(8=a zWzV(BljOfDI}H2ev^=Y?4gDBzl@Vt_AOVB~lN`z8W%XGh;{~Z>1$P}Adw6U(?D>%| zZZS_s1Afq*?<9zL193B1_I4{POZ&w}5sxVV+pDew8)Z@$vj+1=mzMNMwoK!YwwhKs zs&9!b^4*L31L5P3&J9+#CW|2oS&!GiwQRIyEnDFqaQ0R#1Bh(9_n5{X%}QDQW%ZqM zUwiK!S*-z=G9qr3^#ur4+}iG98u#A3(w`9Ac^{*c5Sb*IP>XHa(o#EW7@J<`j*KEU zGy70|qewN8J8&(!ZrN7^aGjq@7ZjaAwNWCT{HcF&H*=v|xC%BrY~4>#%}5WXn$1tH ztl3iNlQe5Gi2eD6}jlJd{TdOMe?uzvIJ^ zcv)kyHq;V(bDRI(+M#au)>?)U9OZe#;?k~kg-o~J;lrhz-XK8})*ljD%Z)c=pfVU6 z{&jZdH<$xtPDdBKLPou(y}~s5YJy9BuX#^c*nz^=A*qTld$9FHkrzaTD3Nt=lEQJ^ zsk?;MmyNaN#{wg@7W8>pT576bqQ9jvpN_L zE$&UzeuPLDUuPnDX1!o->@1-XCAKTIJelH+oFK0P+HV*>cuh zg3#~`#J_}Gj9C7O{&DG_@dUp^ldip1J7_kr@l)-cTnY^V*>gsQ0=vJIOvS`}nX1bk>7ZoT7 zde*&PT0n=Ajrr|X!u zrw^(B&l9>oa2&bzz3bwjBHM{+amT>r;Z38@-}UPeVdEGMM^VdVy+fNB46E$lw--VN zI$rlToqa)avZxzM^NPJm;T#snEQO$L*hp0RAn4Z7xsVX>@m|^D4);yJi^E?ZneNu@ z+-+sr@sohJ45swz?hS7A}he%Z=IIA zn{~2orwgnG$u5acK-%#5tE{I-F;8VskYE(lZB&}-U-+!>`yD~7_3T^!_up^Ny2@BV zx;PW9I?N;bpEu;oM4dJ>Y(^Ja22Kizzo615BwxGxSAmVk)>VQyrew|Jlg`NOPdWD+ zgq}H!2Tk3g6BjQth)NA7$z@O{dUI(z$raJKDG#w=r9`9pMch>}n+ZX^g|X=3yw~5X z?+Y-jVWWtMH1e0he+k=Ii2V}*B|-Ak9s>QM>*97DrS{Z=djW5FB~<789CmkqJmLy2 zSi)1{v%cRv_O07;L*YQ>j{NZWmrav_qiKk&xaZsaS%q4Rri(SE8KF5C=Yz52vkyWJ z2bf9|2LhLc#Dl@klBTtc6Ie#pzUGEahz?qpVw(s#PTBCOEB1k^JenB~sdoRE#o#s- zhME7IUKqqU6i>k7-}D`s4Bsv%OMK8{;7KdoTE}RjbRtZaWu`I~2@^#NZr4pqgR^>6 zi#VG>J26Q0#xcq{G(PWgiGo)|p|7{DmLBe zg(-!1KB>jDED&y|fSpA2cxCDH5(aYPf6`o^hE7fR863#?J3ydzRCef<<6jHo^0RN^ z$F6wJnuvE9Bs)(OkmL$B-Mu3I|H>hMa&tWct6i#tL4`KSk-+aQ4uwCwh3O#cb0CiR zDBCq-Xm?d<^9+D`CAkwur({8gh)I&Gj}I=aCM=gWq=;trdSS}ad|kG0>R^2KE+>^#ax5t+mGu;NqJ4?uwFp8_eDU9~OD@e#>ZhTrMX;t@*M%W zV8 z@<&poXJjrE$n(ojpt&jkT;RyLG(Zki9`0ZJCJKZ@A+*i;j*s^oiiPP2pZ=4Ef8SXI zm|5?yiw+x_LE;ej0vEu~x7d!c9_+E~a-9e%(Dx?9oDi(|@u0-WmIE!~s8xU@c= zd@Z~$Jj*92SqgFG?`fdc29ssJN#D&G|wb7VrV~0Z=7gqxKFSv*%D$kMi7S6pO6IJe3!)*3A2iF@n z)dHMv7+y!;2^fb}tX#M6>XkX#3;w8w<>PiZ4IQ*)wN$7BuIK3;ML9nafU9(G@gj1x z&OX2fKUB+zT@279z_km`7%S%YKd;|Q9&oJzWtd>fLdtxCiRnWN%(*-5HSK+)t*WV_ z1jU3TFQ@!>(7U${5C7H#^b3eBgKEeg@!Kb3ze~a5p1D_Sk}}goO)p^y@t@iz6&A~w zM+SRTFb5mGw*F>?vtou9K|B^7y2ojv7F#iRfS?;a)k+Ps~o%K6b_?TfRv`Fa>JSW7d0 zm2mHx)=|@8DBp^6iIZ6?6p1OBs*#rnb7s+p^sk@s=?WgA-01sIFZb#PyfX5vOahWM z*M(sJDcHCa*fj(*F5R(@vT<_1eq?#7;Nnj+%q?&k$R74zwZLvo(Gj?AbHiEc!<^(R znG5z`;|aDo<#m)#nCQv%wVtQNB0ZIWm?f2evB>iONRM+}3K_r{F+W-l$qQePH=_p| zdn)Hkdj=0fH}glf#NYSsChATx4q%FYbV1&#Ekex)IJ3m7G5S6BM%5i!(JI?OSt}8O zSUYaX^kU}Ng*4@Rf`Mm3_)J+5@vci;^4SQ1SoXFl{_md!v$uw9kIXCCP|Nkz%&;+= z)8(!&hzH1eBB(gmGGQ8T`#LPIOTYv6?0k^8!U&7Gw>TNUm1Ot#O_C+SJ{hQG&TM)|9L$NEb<-uHO!6GYeH zkAbnKO^4*w{T!>I(#{31+rZBT=w-G>XtB2#^F+wLy-PP9eT9u^(x8^|zqDuM!(RTE{HM>n^woLk z4u36O%E@M~Hqx|ZJ{@I7-Cg1(Psp(KKHu6z3}MO+-V6}iwb(sro{Dr0aXefKxLq4c zmOHphqRRSDMP6jn6eH7}ItV(J{<-NIAIA;zoz zy3NUgyJt6E*YDLhH;gi}f8>>^0{XCRCJXYpGllv``7eEu)qh1H*ibym$4`%voi4Vb z#?_rp$6$(NSf4NL%xtdv<$L*X~L(z?4K5H4-^rm^{1z;g!^}M_@58;pBhNM z>t~p#^P!ZH5XnHsnjaa^I#TAjU8BGqV6OhT{4X#s#08tXfEXRLA@WjE(R|u;uVU)$ zyPtYOVs<~`K>W?rv;$~p;FsJlcF<=I8;xq*%Vr+4g3Y>eCXUP0v9z!5Hri-a+KO2DDm`ugHs?D?5SSJux26 zXgXR0@B6}od(LnB!qM?q%|kdVRU@Er|Ss$LD(Ny?);bDJy-x zT%86Cgz&aQ+wUZRA;#zf_znUm}Nl>%z8q1(cy7S^6HWpnKXvvY~4-X5*0Qd%-NmcP>MPo z1}@f}IYWNwM*HcIp_@B(sj9)m5ZO~PGTvs{92(wrwPg%F@sB7*mrnphKxjbtG|s2p z*OYObp&{}d*3_|-xpPZ?PX?rsu7R9R?Qnz7IC zYACUFX4eFy_a;j7$$f9d8aeCHYIJ`4agqO}@ct4^>7aD`Q7+q2n^|$6n}G4?_GMPq zh&oClj4Ays*I_durv+JjsZR8N6~rGjUV;826&Z9|!!v;Ac1lwY=*P_LT9KVLkRApP zxzAC@kRP6ifT=at!i5>~KF#8b?bje5F#1Nm%vu;-Ki1BVX~fawn+qqFjjTvIEq%26 zR+uzrmRZ0X1#?!6958%5`6f1=@5o&5`u9X1=ULlhHvDVMC|T+c-Up!Y@z6G$LOTin zeY2}(A&YUIeA>nhPj9v(jshS^Hop@OdIPKP%6mDD$9TS3@`s2^X~T8Sm;~zZ%N=dC z^9o;ZO&AvfH{xxsL5`PyS4G|(xo)WxK5CsdG9a4@8hdnZ9pgq7!Ho|_tw|uTs>|ht zO}!TBPW8Gt;7d$W_kHYRTnC`}Ilw$3d7rjFqmeL!uXF<$l_Eqx6qDK%sxIfya`EX@ zC&cp9Ed-Z+L8>w(G&h2cqB(LMe>+gzM%#^KJHnn^5jKh$g~>zyB@TQ5edHgZv;!Y> zx1(%FKpui*Q5*3Q;JDuR1F>>~wUJ$G>wvuW;Vtl1 z&@@J(y)M7HFv|6uK7w&A$rP2^RFL6(IR$2(QGKddnbYlMs1~Jjc-<(>oL9E!JKX=JJ&JJ$$Qd zga3=YD(h>4FhTfbQ*D~(z)h(Ap_oMJ!Mlg?-mLu+U3@a8N=686w$Bpjcus53!kGb& zaJZ!&_H)a#!;Cz;F0+}ty)v32@Rv$qyq{CCnUNwg{J@^;JH2DszAsa7}>G zn*H<0GvoeGcpImbFjJ*>p0l2I;5s0${uU|xk{G}bxm9z?fbayVKI&MHvHyX4e28pA z+Xe$&;r+_4J?c=?$&S(6=ho%iXZKSYKKptD|Kz_$wGO4D8iSykor(j~cHy+f!8_{a zB}5ml9xu`BrWs~j$`eH`YKc>-AHY{+@z){agZ~nN|1&iwX?ZolhdzPC>}cIkJ#8&F zLc*t7ai;~k70(UbeCxv2t0aDK!&b#ZdGH5?e;BSx6rWatNib!D69N%WS<`i1;2QiB zYQxHLMh~Wu_PhQF^LsBA<`WkDhKl$SDfuNHV8SQCdUkmJMdt$N@k-EjeI2?o6DhqPt&tK6P^zm}TX(K6WU$+M>1rz@N& zBc8+cyy3?x83)Pl{1S$1i4)#NhjlSA*XXRXbOnfPNwNB{mlvz@hYw?mKiaZ(-Dp>N zGYjB-nU00L9@T6ZKOM62+V{+v(#)>t=F#zi?MreP`dO%>cD=?2KN!CFHm0~`OrMS?{qToP*C#L5j^IO5_==@Z9ZGN?i36v zNpwh#-vUWZB0B`anisJDQsw_YB_?zAo3i!QtZwFV0o>J$onDPU#v{auquFiMllPlncoIdu*G2=o!_Q4iUgPyL#BW1Ux&fhA*sb$&EsgMtgS`J%>6uT9O?e8hi!LMx_6L9?lmBo`rl8a03pM4R(FE3Hp=b%YW~mE$tc zx038qE9|HwiSZ?n$I@eblAF+_<*7&wb9NIids{1;Ugb6iE)z z3EYe|K?Zh+RsJ14$&YxP-i3R1)TU=-Qus(sl9_*-=t?+nH&qPkk;U(~KqKpTF2!Yv3GcGr`y%yo=(zBdl4vAIU8TmvT5qHnC2ADXJI6whGC z#HaboY3D^UmKm!$9go52C5jz)SpyB!uk+3b;%<+nL0MPX`;Jh_cVwn+rYB*gwFNV9 z!W52WelkI02kN*0$1U<+(;TneQ~~l-GK!;AKHNPt8*mYPnULdnL%`9uI2DdWt;A&& zhsXv!<9qy}yvqfAXtKkU?&s{h8fs=D0p!AZ5#M({uZ!#QDIMo=a!#v;L_U;|YCYC2 zC=RxEV<-7KI@{C|g4ruEv+eTa05H-Zm!=^5OVQA3>gS(0?{mJ(&x9bo-~wz&tAWd+ zRPYl_^dS0g{*RmOcrxdT2hF9v-3g(itv^;|3SDyvP{Nl7{MYpU$98lBH)#hC_`RS9 zPlDVxvGftJj0ft8W%Q}^P=XiKE?YZgnKp-|ceo;E-Ay4wk9SBI)a1{fzhT0A6IJ;1 z*wA`otr~sAi~h&JK#~v7$U9J7Gj(kaDzZ*&W7iprM$(-8`o0(%vmNk?Jo33mOM*Tb z_SFeEafa*QHeL(xN?CcWsYC4=W~1x)=$xPX(lVgetZ52oq8@p)f<-Uhwd&Fl8?%5P zNc#lT+7(uJm%@SWJ4p?3v!6lXdcA?2v&ZdUJ~hVByRX2*0gk+iUwb>-thE(xRzqYz z9hA^Il<2zbI~2wV7)HvRf7ZeQ37D}ye*|+gZRDhQ!c?TpdbRG_V`o{Z8bkX4v!dG2 zty)IjOIz=VpT=W-jXd1*2{)=PaocG6Q2R`0Lp)v7OZBY6Jftgi+|?pa<2IaKqShkW zX%befeFc_9lmZlo;*rM|P;X@vQGqAbFZhLafOwZwSDkKt1)^`_nWB|RF_ zsG*oW(t~vR+$OGhS7xk4I##RRhfUR`aUQ3n#%$+BFi1LGRkU+*5l$9PDP;-qq5~d6 z__{g(`uCVOW3`S3*C~e=VSBwhFe#_=R0Lmk!nRPHqwaRtjpo5y)KV;?~%X;4o)qT*nAnZ7OL|4Ff7ElqBpm!S0 zowRK~CQ;LDPwyLpBeqh0P1dLjI@LasenfU^D2X~5#i->aHXKOFpQXPGMY`aW$= z($5)1O2?P`##XIp9pTj_l#1kp!|6JD8uEa*KpNl$QETAE=?N)3FMd>EBqou44i{g1 zFC<7ItoQB38jfB5M^lW|+13=!M8PphvIVlC;Z1=-$PNx-7DMk6uU$AU%eE?HcDYAF zPF&Jt{>}&McJP54?k?0^(IRBY|rWbnWk2pPixA1Ve zz90{Ic~GjrNF93W43r`~n{+PIR*0_0P?{;E9y-2}jZ+R3xGyv=dbHRU6ZhYhqcl3| zXp$l!Lujv>;=L99r0&O4axNV*asJ#q_YncBglp-N%AJRu{yOtXwr>FYf5{AeQ~qx4 zNad=&VvHHj13#z`f^@Yx*KIUY!E)h>YgJ8A{PIutw=;<1a#BGsgIp7JD`t9a@qAsl z?s}0==fV_^@|3y%5U`v^`hnVyY<{o`+@`D#Z?@=+MTjHX8)wetHEa?2ucS@MbOxOJ zt~4xCR~Py@X{dL5IN9#%V!(`ouLNLlQjYDO@I-{gC|}xfsnAPbq5QtErXORy>(Xc= zc<$`8r0b!t7mlr(cH)H+oV9zS*bx*R7YbA_0#=Rw>Wc-pM*H?u5!tg9ny71df*9S5t(I8r|ISl2ld=IIYDiW90NP z9p}6mkb~70JKt`)DZOA7VO#LjA@Mt}QI%^_ZB`_&Z>XNrB0rA5vtYn`hFV>8uQF-h zd4~S9!S;<@{Gp%POz10Vy|C6)#R}-Va>8#1rLsIZ3An7dw+VHO=z{t!P8L7gu@^8@ zalMA?PaM@}dR)sL5K{BH+!C=*=l}O*LHVuKd>}MBbT|e@g^*`>P|xUKMHQ3>NwJWc zyI>ce4wDu7)QsZ4L2xXTpS2QS#5Bzg$XyH?_m2+x#v)#k0pHE`B$?0}09X7LTy#cs zy7wXxizRt0pU<~Ad<^_yxw6!@St-laDp7lOH)s7d=bO}6s#f9=Bj$8=@s z%sfAdbC;dO*%WN?&MChu zaJi2y+;m8yK|nkacYK`l{{Paj-4Xkf3e{CMPx$S_O`&iLn^9hMR)ZE|a$*_xe?nuf zKzS$m$Ifm8xNos#3LToB{d%y8m!WpCH)3^Bl=?b_+D*q75K6fcehs=;ri@3>FsZcb zEvqkE0^1t5pnqisgIiI6gR6i%l|f002;u3g$aqGFO=nh>o(7!OK#)lKZo_Cj)cyO4 z{HjuT33eJUdkwX6B7#epM0|w262w>o&Z*@%+ovEw7i@a@u?vc`NTdYpj!jasjyR^a7cZ}>$SiUgbtZkkjAN9By zyr*FmJ-z&;4l!b=kDo8Rf?ss#NRgA(|6Qo~Q|x~Fm9+2I25E_ds5;z8HWeMX_Z3En z>@wN6^>}DAz!I$4-i$QC_O+=PxT)k$t9zb%)a*h2d-_`Jc(OKG`TrHR{U;WPU(iE4 zz0J*crxI3baD^rj3)X}wD9zfv@q|vnpMq4kAIz>s%SBG=DnJ>7! ze}^Jsw%90dDcfYDduV7`Hj8?Lg`Jks!%iLnn${Z^;n~AZ}V_ihb zZa9{bvEeb67@4uaz)OO+4B=nV!46Ej?vizrnBPV*bIr0h5yzCjxJPFoEHc2@W&z&` z(wZyc_!jFLLXUdAI{OLNytxeIGuDak+%q#=;+(RD!kR8Nl~J7%&!SIL2&BuE7bc6U zrAOPO1N8+;{0?#fYu;EfDIY*g-U|L=3f(j)#Xz$cTKu6hV6sThlYWJIsHpMN@ zx1giuC`6m+S8%Xkp5C0#)Zy9Hx>-{WRJ#k*|Gziw0D1J+j7Pt@Nx$`S0*XcvP4Zcl z1MN2Qkcf`iU~NyjA2qB@7`IZ7;+NY0l6tCs@RZdx=q3RtF7tgdMH;Fs9$;2C9Yg5^ zdS@u(7oo-3^a{g1V$F|@Z2bdt*jI|O$==`zdW1orI~7I5`taN@@{25su9jD=dDhAE z0c({UScvk{d)gnOWrHK%*L)4wQp##kpaUkA^;NWQ2#KHSPnaL|^;u=&c;LAAl{iXV z^j*ZQ&CL?%l_q8;OJ5`zmRl6E_?^vn)a4D2Wx;CJ3~(;9)$_P#&!JRg$`f-~U4_1% zT9kX z0sd;#N|>Mb{zL zx5ZH~y1aN+T6eG9Ut@bN!jaQ;H;C7=AT;MvqigN6QB*!d=et$Qe*3@f|9#a{e^-K) zDnOP$OnA>mQZSG{fC>5QWfe<1Tm)$Evj82N#TkJ&_?gdH0e&GGNDq}c3d|o}zCltH z;vlVAH<7M|y9|&US=h%Spv1_X3=}n$?q*OMQzv#`{34Zyn^g(cC9sRMU_E*Q;iAo* z$!h$(WdRy4(sLw$f5l=C*R|soTiowqj&uS2TjogOH+8$(VlVFlQl`s)TAU;&V(Oar z$Kdb(b3qhQCOL_C4MoWdX}+z@aQp8U>uWNDTl%Z=w;EvqBMUH45j`MZk}X85P$tiT zMs!k1@ZSXe_>D!Pf@co_DvF5OOE&Y^z1RdiAG(k>7?}^PvDcQ}i|t&%+@X4f>rSQC z#^;%|ZCz=!>O`iLr=Tf%T$+Y@M;1s@00Im3`W-8hE8j zK#3imbFC+S(N~CfI$5YvwHxlHGo)_vSb(ec%m#?Cg}A!6pw zTg9m^)bG!`VM+sXSN6RJ+lg5r;1CdF`WRWrZ@d+7#&T=?P1%+cXlkwcv6UHwx^o&l zS-VV!oiW}TP6U@*nL;`}F)CyDsY3CJ+HOzWNMXcCHyY{y&F77Ga$V8xKI4P~IQNuJ z+^87MvmLNX9hFlQ&=_A(7+ZZ^u=8Pcy6&enNOsq!4PYF4`cmnL&p*#CsBf|f0y*KH z*ZhD2ze|bU{|ax9{U>$G2UQ&2`n6d1hi3Y)iCB547 zXoNx*RoeAi)vdsg^nPeZqJwtUvwt0981cL4!)YQh*2&Rvc(rm$MZ>+Mn+}DsLZ4?? z#_m6T_~#xEgWPVov$?8>(3`v0Dm%;9Z!cg~lmJQ?!lhDSS^vJUC)s( zS}y=EAKrj$uX(dRjP5FjNVF!t{I=ltkvbVIYUnUlkoVTkO$ZGrz{G9ExxY<1xM;e} z3n1hy>GdK?R;H{9?Vlp12FRjMN->G$ysWNIeLnM5qPo3NpA-#r1jq4^sl>bix9IW( zdKh)zp6DR%n}bQN4`~#upAT|rEIHJWPMb~AM>PC+sl7J$oZYLHCDQowt`fW8d`%VT zQPFIl-YVD`AEzYj=93xeA5G9%^2S*DVUEY<_;h3J>5oVj&A-fftTJ`Z?JHipEV7AUvG(Qk*7{E2!kQms9M*J5uaV zUzxaYF7Qz7xj#N67o_@}ZsRatZyMPmnobAiyjW|x2(AxkW!~MSaz+Yn`)y6Nyf$v; z#Y~1#!rKVh^)zQ{aadLOX@4W{74%anhHvWN;IBKHUV z-aj0BFy?+s?9#o2z(vilK}|c-Ys<~JM|O#tCLzhWW15Gm@MUN2q^)RX3lWbuww#}eyC{dhB z0W0=NBpNEmBa0z%b(>HTd1LmS12lst85P} zPi{zx7>dhEj8FTe8-)0f8k=d1w?i<r!LMl`AfjI)kXX4xApn2G6Ln|W~}v$dp}N8h_Q;we&oKY0T+V&Z>MLH z&Sg3`IQM_ih@TlQwnCh~)8HJvJge0*q(#kzW2gNL?rVa~2XdpzH;?ykTd|EN5GdM?XTYN_^?s8-(m>{Q-CFRB)eRHR}B9p1?hIh!(CL z2rThUzR)B8-qqt{&ui!gXe3&gn&E$vzDJk^PUBg34!VC3ur>qy3UyMUKb(r%jkZR9 z8&S50@Uo@d&a8QA^M!eWXaA5UNozFIjEFUR#1acI`=4T&dbHGj?nG{B#11y$ z_O0#M_3PI+_O^OjHfCF_TwSHmjl?aiAH2?q{vp|LU%e7b=H$x1RA-3uPpvzQ8{%VM zNWgWdlR5#D#@%Jejb$bMLeVsJqy<*OO*^(Y@P2=c#UQ8Y@hQ|J4_x`Qj-ve)x|PaB->4EvD6ML7B~?GZk8d9Fly$;Z_U(B~(6&kA%w zug3vivicxn5XoF;h7rY6qS?%mMZeXj2q zn5#OuPq z?0mit_+4!ou;XlV*M>o$!xPJ#k9T}ODn;bGLLXtWOfj1abH;IgDp&o06aFEenT>4( zM6F&eZ!{n^n#XC9!}>!r3`2;x$2+FnLjWy;+5C{ec6^};dg-JY8(LNaBcp*nm$xFZ zArxn)_sQ_m{!YA_LD`E-c}vbfey?Ke2C-P9L|N0TSGq(_P$!4|K$1p z_D%}*w4%+-Uc)N|W9k0D*^~>#0XCZRW+61q7QhXCv-N9Bl}1@lHRHbGDdjmAqIA$j z3HrWebx5l6eOu7|3fRdQlXuBTqt*6gQqK*>*VGOO?orE=3&}!CrAEZP{%=do7KjX$ zkIwVdoC2p$A5hn}W6C@2e0m`7w|rC)+V40}v7H(I3KVibq^qQTF`i)Ro1^G`mBO(1 z4Jr7xnxrJUmA4LalD*y`g0g~N2-mlOS?r*+q zq&IHS7xNyCu!qdpgajMsF3WcI7d#00%rVjq(VRhf25kHf@|p!MfnK2!^jIN)i=(6Io%ahepSC=8`c3Dk=Gu=ZfoPy!p}wo7I1sHP zGP!y=E_#moV3~OBYJq3$ErxdxQWvCyVRT|`v)V&xgp7eiTp$&X+j&XmwBPQ}9@fZ& zfs*hh9sMJa4sb_BO^B4?K%HHOd*KCdp$p{oNT7mEM93U|!g?XZm96>4z1tvhE9EPG zZqQ0MJ5-t}8aOGCuV3k?khdNGWmbG4?#pZ^OclDS_^pmnZT0SZQx(3>Fa9c0!x|m& z7kVnM4{OY$?@fw=AD)5AjhHQ+>gXvo3Tb7W6^nP3_X`xOD76M%#MXw`$r~l+`%6xY zR`pX1Rp>ZH9b3z{YoZ5y4BqfrxB2_x1ltsB>~Zru*P1RbU5>LEdE*^bGKJK>EjM!7 z#^0-U-t?-U_!+y(VVm;Zygm7c+7f&#VjI9kiKTnLcJ3S0_u>9Z=v`sPu8=<`jZd7~ z@nSyul4xS#4vhO8jxe?g4?qMNxLL^$qrtDXD+@eQmQfH^Gw>fek$x^AwCPXsZ<*)d ze#+N^28i@fQ%mdHlN;^}nx-`#SLkmKS0%oAcYjz(^4>jA$2m~#Uq-RERoP95C2@D)tJkt=~#dP!tI%(MCmwK}%GRk@JC*&u*BP#picgJTk_U z+I{8nofO~?6m3Sy&e9*3x0bpt@@2NHZ?oL_05&sCy08rIELTaTJ`Pdocc(~sW|SDV zw?S&>a}@8RmTElv%X3#=yTuu7gGLnN?4Fbj6)0t#qMIv;=K3Z}PX&#ov)pKxDUY6B z@)4hJb#P5y`%=9JHkxx>X@T6%`eu8d76_u5o^4 zD_TD{#Oo?G!`qgE7_80GUuh|e%-J)R5>YTX^Q2(>$?-RdLlV~4>{odDr|P9~|1(Ir z=!+)3W2*kD5_zSxSYZBJ+fOwYtD|zJ(t&PFAa}~)-`<^9r%PQ>SGsw=V=ZrH>^jaf zJ~j$_WP;Sz8u6vQ zwY6kas`_N7Rbl>k``&iQSVtoh9{fgPWb=S;cC9^m>1CW}saqst95>^Aj40vc>&;SC z=!A8^8*<0OxvN+|aQ}S_Kor)nY4Ml)I=bcb60a8-Jo5T;_B8>!ktdZXM*IhoP4?2J z7|P2)h5XUveYuFDPWR4VXSSQo&w$ZFE+11hyqvRv%Z^2CDcZDA>f{xy{P0vru@~Qh zL4{FNZIb`u4#wrq!Jy3DTbWj!q^%`3bd^H*UhvbXxJ$9%fvkb@yt}Sagm$DR>FUls z*9#-5aqOaoUsf!9sO3b+fX`sWi4T5yd+K!EMzN7ay`oPinLiC&7#TDB82Etmhh2G72ds2-6pvDWU!>CUz9)qWjuAxoGoFa^ zY`?HAV7AcN)SO616lPTizPG&R+2}+qA=tb`cj3lfXU%*RCk`Q=Xjcd39=5 zY~kT{Y&W=wnsWM@??dl2qwqBgpiLW<@2eUFc+$O-mvlx6xAq46smuT@>hp1;A@$Yh zb-v_06+(l1gagn<>9MciTa?k1a{%&P=lO}lc*^;G+OyNQ;Q9ur@C|(>bYceOrsd=R z1Mn4E?z90M<~mWyLHh=X3uKHIUj?=g<*p6{>XkTuc?gm#X^$I!>#r4Y%UYD`|L1-j zu+aFlf7nQ+D0+!=^YF}ieo9Rh2feJjH%d3TXSL;hHm&&;J+Czw;Klv6O`sdFta?VwL*GRBd6%8_L67ux+}l1! zF*dY2kdha2MYq$HzffjAUs9avy?2L8F8^YlN!rGU>%I?P*J#e_oT@~H?@epIx93MY zeWsuzYvD!n*3BL2&2lmJ zdF7fI?THm*>4R2em7$|^qg$4CAT`!bk9U-9Y55?_lMlukcZID=%Qt_#+W9$s-NIUQ5iDEUKW{- zuJXAuwZ`3|M|vK&;f7GH{;WyG@1D~@wV>lp|D$W3t&D-#DAuzzf|V_dyBL`Py#g^#dwb*W-z~k& zVNf&*^l_qj6|EWVg9p2m$MZLMv~oJe@0I~itF|#|r-zT|Z86H9%R*u}J@qt-Cx$s^ zahfG(Ri0Wmj9~MrsV@h_(H~=je^}tgelNZ8y`1uFL3#qayR$0Ux>cspJS4_-@7Y@K zf0^fX=M&{XdtR}cfwU;7_|#bD&bW?yzdpOU++$8_?7_v@rJ8E3Egi}8(?dtkiw@Y~ zI%$t(M>~V@cQMi9sWJX5Esd!!=IsNMw$l+d>UKpav1xKdXYdaVYhV5Ju8P!rzp2u) z0Dl2u0II9h#qNscs4zB0L8F&Sw`iyO7f?veNplF^a@%g&Q+o}&b8m@sU!-yz#U8uC z#QsLniHeXPhCi<0hm3|=@2);~HqVwhN^0E`gy^i;P{Ik0)lYF-YVxzk4>wmA`)6v< z4z-X=6iMrf2f~*Q@2Uw|N>Xkqx|00cY9Qp4jAG@pCm^YZmp}MN?yXdk?=RQU8r1#h ztY4^cQ!sh#w6dz#8))z#%UJJO7D zF3x`Hs@w*g++oOMuumrDdAX zo=vGq&qHiV%UH6!T|e&AL+zx^1Sk1*CymjT{o>m5*SDwB#3Hlg5q8D_KMck$2T6>$ z%#V2TXX*5d{P(|TM`%;AqX8R|*92|bGp9Ndf>5GVBTap0dSec3FB|bJ>m~-?*_qAR znX#U2bgj(Zk>37`bNB**6oI+9<*7aYeSUmSHFtK_3LY82-= z>)rV43dOjp$EbdCJw7;CSr)g;I0tY3mHA!B3j|l3R=9O!#`Eqd?G%G*G}VBBfb^$& zb~ATRsz#Hp%Oudb3{Jxu_w-Er1`7zq)#!bX@tV0iez8E4o_^C4)OOFs?%{{Su^wyZ z&d8;l3`Ot!Ev5tLlzDUoaeoEg8hmoeJuAYZSFOXzx>+(wTkmo^Ir^{Ix#ri3E^XsI zyBqJU_kNK`4Mog0tjA9Ja`_Vh$Hv+_XfR|ndCw$C>9 zRvzh}pAQTl)j&sow{%tgRS@-AXJutoR9?QZ>~cf|^nv(3;r6Sl%wLqkbM8e}XNK3L zeyCY@W$XMH%lGt}$cVMm&!2Di!*pkwA-NH*mZw8i-&+(j?p?3KoA#f;t)EyZ?f&ik zNo!xC;YK_-_&YFn;bSW{_VPD&2cl#YDZK@o<&G;J=)1?IHT6zsn=G3xx{&x>5NyC9 z@|+4sD^sSg^7KPo_I<$I{bOKr&W(iE)$AqTrEqdL7@nd%f8N}$@OX>f z4|8@4r;5*n2YD{pUAcxNO;=MsOgQ@1ZGwF2656^#w{?$;;-bB^2yV#yy1@sQ0CNSG zxo^(jwSi=}VkTFj`i^!=!gzK?YZU| zA!&8%@Qf>|s3C^aT6RT4{4#!rhk)N(8EU=%R57#kmWkS^fo{F2HK%MMJYVl-XS{1Zuan=!xtp+U zv#0=V(_W^>p%2r=1{C{hsK|EQbY)JsZK7i{^I) zqD_&bA+Uhx&k<5pKp2bek69CwlwU-q+yOCB{$W60UpD(xNa59|PluUy=x?F_)2X9t=X|8L_o}NkZQoIYO`eKnxGU|l z5oFe{+%NMV&YU07t=KfZ3m*7p>I;s+9=$aWHz4Hg+_J_nBgPaX6 zc?DJ*NYeg+lZSQ=B#JIQZ=Ue<%p`5kB8J%Ip;O-Gywo$<&bE(3&Jga*(+`oo??ff{agVW*2Q*c~h~nXZQvwW+ePVZ)J4>GaI7E z#U#k*KflV7w1e8E7 z%;=I9@*ku<=lH$$z7TV#M{RHO%yrjK4G_AW{tllV{KRjqDQ;OgT5@KNOX#_$dY2U$vC0B1! zW=f${(el*2j!>{cCu;Qg#E_Hp%C>~~^qq#ve$u{a*I^Y;PqDRVQ}0X~%J6DiZ0GIW4L9X|j-dDGKLHOfxu5Ipbp8v=s_0tET zr|pk&m2FZky!d~-{Oky*uG`o7*I&O4z5=j#@bDq%O`&f^=l5+qmyC=mji%HBDm>(M zbxlXA4xa?h8|?hc)r=bT6?_XT7DZ$!q~|A}M5#g`?aP~WZM*WhoCA((B;}4Y4JL!+cZB!d|+ar)MqU_gr}M-xZV_jmVce89^3jQutqAFd{r#S+S{^C_wpL`Xz@(VRRj zAA=cf^Z*WK(T2T_mFw-BL0G^=3ji7dqOwiegNn1P!$pR{vig&IXcTeUZ@tj_gD4t9 zG}j_f5ezD2G{0o{+TNTG=n^<^t8BXTzZd}ese@S30;c*TJ3ITbmge5Qjn>81OIl_q z0%!|})3AqWOSLDOa-^RCtQXL;xi3}Jl*~y|TL@jK00F^NwT|aYnqvg4Ezs>Rn*EJM z-QsEa%qxEx#{2v6_6+-yKRmI58IYq=&?(ruJ0R_mzC4_E61fQwaI1?cjo45{#HmA_ zd-8gL~%FtuN)}MHv78fqh@+rS#IwAEO;Y6}Rj z0D-afXaEP-RPaDDaU*#ST0&8v6s}f|7eG^{CbB|-{#NaDKj&HbX_z*(Kj{wo2Tc+$6g*D9%eCAK*WU+Y4`M$qSH8{#8oHU z#%E>+J9(a5ey)5cv@|CpigdCJYtnK)Ne%Z*khwwG-t2Xeh~os^{Gxe9z<%qt{ENRA zTjiKHTop`}bTnQH`}iaZBG{D4)~kOtUSHZ5(3>3$=-@3&_SRW=Fjqv}0nwp?4mrF2t;y5z!@mdI9SoaH#cLZF zhS(H{xgj#}F_88|xWC@NAcm(3%MjKHSJqVxXC|s?QF9vJw0MfBSDr6_%LSf~We@l&rCzK^srZe5P;q@e!T@=%HLZ9`W42 z1HxdIoJ05@g<6kL_k-wmN&(eIKZ5pq$4NJTUrvBX+1l#Y;!*`WX@He8EI7VEsSXn6 zwYw0nVK4^0EfPsM@dNGZuJdG6L-Q=}@bwQzz~hBs>z6e9yR*h5I6+jzulyWtV%`bV9ZflIH@?R#V*YO5f)%n$>>xSOW9@pD3s;B!Z{W!3({TDx zv!tY?C}no*6{;wgS~+%;+I!xZ93Hn|OB;YBEHv`S=|^2QIbFLGm1YLy4;!~$No9ua zkg*ccajir&BpwwuHbG1pG2bcg#}Zp(lN71P;`f|MQQGTiU_ANHHot$yPA9-pz!o^0 zc6$*D{Q7kk^`LO;a#nNG{X}odvKK1){@cOBeZtw1w_$6?Sv`S!wH$&49pO5B*(f@` zy?rRz)7Jr~&JcA4a2s{yMAVaFi6PYeGXQxdz{=C1QVBA;!3H&nvjD{4(s7KC*t`^J zq?aPmxmdkNFi*`{(rTb0U-p&cs||*2H5Z^tqao|U!6A-xzf3USWg%Gh2M&BdzH<0x zReOe^QiqYi;SmyV+}jG=WnUveSbdLTp1M|)M%wxL&~V6ghF23BSf+Bll)W}`Rmb&v zTBj=ovOet~{MkVgV&_gybo)$I*1;l}8u~=DESDi=8jDiKjPr~oHt|B3cs!fe%ZBvT zATKttDZ$8!!kO^B&@j%TpzZ@TW9hYVN3ZkyQ2Tqiqt8j_;y+lf;JtKvtF_~Yz6q3Ns>jG`-Q?`@aPDf>tOk|YXP$W- zW83Ktb^c~&aMMhp#Q$u>eAf+^)31w4&)HpiHg7fXqYW*+#ZLWD$6aLp(U2et}bY>sy!M4JuY&TOm%K zRV~YiL;p;uwAlYEQxe-QH&LQLTvb&Je6m!^b<41W!ihOu!_J!7~V4V^HdLwNeZ_EKn` zjDo7oJGX@nBn+yk*m3pYEv7abunjE*ROyMqP&p|utSAIxx5(*V5ymNIQ!zl7)5L8r z=49e_!T>eA+WJu5%UWBCUVjWhu=kf8O? zxtk6+Sggv}u-ntH*L1}Y;sUv3$C|+RHWcQ(xX`#u$NFF**_iF(vDuei&88h1(w&kN z`sv0~+|TknPZ|*|aixv~l+AXzDsV^VPA5Je$TX4R+}+(j8pM@E-QpqJ`v&;K9%I&O zo^%1ZCaR6Zz|#*}5Pj*bX5!#(Zgx%M>17C+%Sc~(;{~cU2nUdL*xqn~44mZh_CDmO z4!IIa^0Lixv;8Q7FTUHKjLNstp&8jMVj3p2}t%YI0`3yB|nlSlctryun+foB_VGR0e?N z)=e%;YwQd3ePSEsEJz5bo-kPShAgOwbVlL0?$l=R|ELV$x#PP_HtU?=A|;_+di7MH z??B!D6G(4QT5!COI>%-9T6Y|X(bns|E6*`}eunu(fhY@-TMnAj-u30hGH(w$5)qRUS= z!?zNAi`}nDLzY!UwO6Uzm({y_D9WrUz+0T9^?y7^(N18leAo#gp9^Uk7VGjp2h@Wp zP?rx8!&vIhtsUAsu2`%?A+)sOGa{?C`Z1Wj7QYSM_rhGD^E!C0)4b!o-ai$cBsp4y zQU){s;@I|D$dwHwbWiJz*hp(NZd)n%SY%JZbN-22MY`JsL_5e2r1p_NXr0spdTF>=yGj!+W>w+kzJnmP})XV;|9m=d81 zNTgSy*SL^~rjt2c6a!6}O%=ooUp6nTbpX-VlfZ_7HBbU!ZoFe@+pKn4dODN{CubfC z3r(}7Os8cJPY4{WW4c;~ur0bfl6$JlWb&)j3^B=p4PE*)W!|C%t2peE^^>%wA}BqU!SUX>{tGw0+|2-VDJyO%&?u=IXO3yXM>=OF}PO9 zE8S7*W55hXTOF8S*;+m2rJG$q`{sCWaaRjlJ43EvdymjI#E2X1V&@&K{1>uo=n2|D z$`AO>XpLFP2U9Lo@BSv-uj9pj-FzHQtviv_=JJSFfG=R8Em}~%Rrwj0+r3?g9bqJI zC6_Y*1m;CQc9T^X{NVw7rlusU(8+@gicv4w19~$78&YN=}Bw zgKJr1-PzL%G~%WndHL`f01Sr3{={rDAP)LN2i2R-}xwXF*`So-@#XSpG#EWXqRksd+R+ga4p@` zr(2k|wyCV)m^1=lJiW2!grv#V#Rn36R#@+0pbJ2(5#*?+PeQ!!N8+WU)MYjRTHHhV zLDEf!^1t>V=qF)be-F~mtc7txA~0Nk!LaWrdAG(w`RPKV!LzJ6%(ru_Wc7RIR3+N8 zCie899g1qbC+T_vjSd#|#+omKz+r{~JU>N37oY7?z)V7Q=`gaflTGQOSBVr`v2*m> zbWyIr4%wS>s@lD6(mSqF9Y<926*1;D2Qu+=injplLLoTP1Fm7NdJpGQ%XmY4$*t)g z=OsUiC3ZS1}-Vo0h z{s7z>JclEtgigN9xeN-8$hp;ecBnip^lI?X3w3_`=;`eAwN^^a#I8SkCxUo7F)n7& zB~dD-O+E7!3{GKyS#%+NfT+I2g7#$h>tOoENkQD1%_vUJMEI+X_FR3OZ@P^=OGYkI zba~bBE1sxMR7-x0?>?B%Da#H{t5NcN7Z_ptmyA4qV2@q6mBX{)YnZ(}GMms9R8aO% zRgK)p3)3Ak^iY|^(Kxw88-V{M+aT?)Y(HSrRpHEpq@p$flr+jWbE*FDWBH*6?JI(L5GJyGO<$)%F-dESDg2)9mg%2_P>fhv>$7bfKfK<$eUn@Y&(qx0&5tk zw8u3ysytcRGSx#k)eZLEO-*5|Af(2F`ZBrBBHa^!?gJt)&Ktl1Sb9iVdb@U8{xoGm z)KmYp$6;>;QGV|^)FXBoUR;K~*+NbVuZe*ayO*zbbXRP^1eQ)QJN<(FQ)koZGbjgp z4!#>VOnyjqRC5s-88L!2Z^aQ$JwxT4#+la*c%|FZZPUjt<%L+C=VbFIU4TZKPs`SS zPCFE@?@e__m*(TG@ZQ6@==7aazUlZby<71?-Hc6c)MrFdWsbCmNJOWv>@TUv{`Mz# zdv9!^cVhgp#X=Ky0FY%!>0oKL8JpRSrVA%&AWu%H(T(3PWsp;{GTjCUko4on3el$} zx*hr6d1e7aKXQ@nn|0cp`zi;gP_Tm!5%*&IT#~AtqQq{Ay>BF)@A3oQR^R08Q&;pt ziw)V)QU^Sk){|tXM2Ou^clZA&s+bR_b4BAAD$m6%t?|4sa7ZYnrQLl#T@tjMXc_HG&wUF(|P0x(MKc;dbyOXTfw zOu`&AVo?X6@qJQov5cp`=fuf|ua6yE567u`p~AfCPk$|^ z2=cchPNj6PThTIqfaD|T&dr7FnB#1-=w|xbTe`fS`KBG~?^!V*lps^WSX#FgwS_eT|5 z@8Sl`an(HTN_)AeDBUh=W3dizoIYQj-k#Gmr>`{KyZo1D)7pwxHj)vYbG(SIARpyi zfKe9mRsp2V!IIpdWjW}2ZSfX^vR*kP%M5>(*EOcA<~CjHl6@t$A_tFySh+BtDawB` zj+fH#MhcJbD9vF2a#?TgZsU$5Su}!thnF?v%tJ-ko*)hm*j-_GW3m@u=#&eUTK!>k z5^rRQSqmNCWx2b z_|abFXE;yC%992E;5Z^`sH@k9F7<}Qx9_BtWHq>hoD|JxI(KBfq-1xns7A6TZ4W6l z?|)ll+c6nlp%;pf58g*g>X2J74D>mLYX?9D1{!>_e)#RfgMO3UFCTyuSevVl&tCJ) z!~GiNP`W`^!|V|A{`#uj`XOl}n8EEmx=Gyzlrs0cx`C>P5hqAp!~@mhIT>~CgM4&^2CHrBl`wg~`6S1t$CE>^2>k>PP!BJ^?=_&hgE zt{Sp*$liX%c?w8j{*h3PA;0vqJc|QatP>3SCV9i$e-@-bR$Y*PW4k&7%&@#|_BS+D zN~fwIsz3e4nLDAQt*xkT{^+BkH5Q;24hcJ%t&e^-CtzLNURt33aBqV4j#AZ>JD3aK za>!V=%bBKkhiZV0ePA+;yq(!l;Wx8;tg{zS5AQYnJA^Jw&y?@V;vc04nF&ZdWL}2z zIGt~ARpos`5pfNJjwe4*w!ugXgHBOtVxe}1qYFRK0*TZ=!O9AMWX3RKI^qoD8(@GY7qx^l1_ z<)g+0{r&xV{v)TRhN^VI<4zRq9Xh^e+A~yh)mc7w-MFlc^pZ*r>!H%<8&f_}+-J7vytBEjz+4JeB(0mj4|9m#YvDBV=@V9ev<|&lN1rv_m`% zAcXAYU#+VU1q+;+@`;Iw2kGf-@Nvow7?Tzd_@rSwsLWM>`MB?nt>(qt8zqKAG2-hv z+B;sVrj%nYoM)52XJ)m>XMuCA_y^6Ibpv^7RmrkR?^4IA0b}cunyeX>5S&ekZX;9&Q14>bg& zm}tnbbSZzxfdos2l6<;pV4!>*7qaNSY;cxP!RgrP*yPdKZ?DYF%~5Er2v}A}X4p|gh zI-B9h{Yovw_7o}zdfGf{jZGcB*#eSshl(?*g|?^%n)uzvArAd-Q||b?F3Q*b_hOR& zR9_K%FEaiVY_4_fiAnu07eTkvnKG$(`0psSgyl+m>#tt_^x<_VIBNw2HbB#P`nAUk zUsmu^NJPp)&zhSzUcD~-^f!DlGX4xS%>Qjp$DqY+F$mnHQwdCDV{7=4z85eZ{NKlQ z;!djJ0*@)lTbgtX4Htn|cy}>}JPLLU#;X&;542|*{a>&uu&?<0A5g_hxP32HsTnV3 z2n*kv0qdsrLHi^9F02{I7=uXG+qSlbZHb51{@=kpCm={j#e$}WSS6QjVE_`W53g@5 zf=xLC4nn^I)$F@IpPR7QL*X|L75+P9GNxQi5%Bme8pL`l3nN9(Uo8eP^B>>C4@EkI zDOLZy?7j-9uZFKU9sCH*Olwz6GB_0Buqt+{C{#3&gKhe9TDJJ4q@?`-4(ZJ`I5Fm@ zEAUjD?n6_2axCg{mR{&5Cr*eD{7&DBMP+^##GtxgT2X$xVsV&z{E;#4Qqh-~i400n zzdCfb`J6626z;CP1dx~ufZa@~q-?q_YV}gMJFg9JMvx5^=Vf9hUxL6fh%xW}@6r)sN4@Id665eJ%fXFP?(A5p>u7z!{#7V@BQc2MGw7jGpqyR zXTe%QMk0JR3GBuFe-OwZ__A|iD;x>|LL0x!g=1kR(E4}%JO z5MobIL#iek@gH0UwNL+Rx)bkDs>L@qH}_pzrDD*n@putI{|KZLASu=cJh2n^KxTP4Z=_NEu=wi4DeY_ zz_?VlM&&`I0fj5)Q48ae|0K|+XY2C-{1f9@04$%^S9tX^ZS>m9=)p2&NZ)|&tl{p` z{{k7s!@xh__PHcB6uC69<|n#hk{2NgKd4aJx9caXxsiMT9w@&6lEoTfe#M{&5z~tI z?7)MwAwi35^Vkah=sW~UPJpU-`0yc64Y3_~*<4WwXYp)E=rPcKxh$7Sxu5D*H{D#W z#4LM12^XmvI?z z1{BNwx7`kvaGPL0xPREs6SWH}8~{GxF`1zdqyO(nSK(w>;gv^5Sh7vtlodgA;qbyp zk{>7m{I@6YqpyHJO;?Iqr4nN}`Y)BSuYl=OXqOB_KYgeyT_nV^beQr|N*!*=iMo^`1NAW=6j2;OEKT!V1VcYnL^Y zM4gXG&M*ji1*eGq>CZ_6tioxENk#gk(j9+>hmV||{!G^87sH&3tF_>V`}V+ciTu2J zUw@Yca!BI>NPrjV;Y6kX;fdOmpsJW#qs{hne=H*PdeulnGbGQ;FE02_cv^m3q#&Ib zC>GZ@21;9**wL$3uU-Zwt7_QD-3OVOtNxiC_#2t;1QAQLMfg@oc-UG{xyn?iUJe)) zRVsit1j<4n=7+CN(aVUmMgH^J0TcH2&dXOo^4bq5H^|O{-}(HHS1^8>C?%C&|NA#c z>h~VYEZ4U902MVUD1`i$iTHSR`F&}lujB-LrPdlM!&2CsVT9FzWT>z^$5_$Ac)^up z2;X1)r}nD0zWyeNR(N;HGSnmBz-y!-L@e<1t=WKB5xdz(7159^r z10rHSq&|Rv4j6C$GHP3Ub?#$UVu9(sp6X8zNAls<2i?KS_Ur5G^LY!TsMTtVAnS{P zan$HP^5!n&?_LH{np|@8=1tHkpvcsQ7LZo_(xY-Z_-0+csuAW8*arSU(?a+e0+(dz zJfN5wYmBKn)I~i)ej2S1nf~b}BK%i{J`@NXyZHDN2r2_7&OZX%kRVk)-a+6fP7fr+ zT)wi9YfGrUV=w!X_?@x_sCYalh|6lQUH-or?^4ChzYm>wp2}*|Wp2SZq8@kwJprvz z=Udo4uoK|EQK(I;!0YH6u{ufYxY9QVu zQ=$4TPy-cP;?}{BNgi!zC8vioOrDS-*CAYZJzO?NNTgr|i)0V?pirA}Uml`Tv(V<- zn5<#Lx;heVF90$OzFD|tbRlA3*?lRGDx)pZ0l88|UMK!mAyPYFxG`QKWtu@Tgp{Ee zk(;UKw-x5dw6qvcT0ui%#C%qt&n(_@qCp8AhZnAvJKKR!DLA_$l0GeF%4P57)Wm&F zutsHSQby7Q8gND*W^MNYe3zPvSfW^faJRvo`KH(6J2e_g9jqI#{HTeGL(*q+Kas!9A;h2T-?Qs zUe1dk4`ol>-nz#9Q#IK20KxSqgjcdxoe-4>5tTM$*LBnQ1D_$r6cWu9!jwp2Sq zS%>J{v7bVX$P4XoaR!znuwgDR#Qhx+I>w)VUmZ7S;qBs{)}hxC+Go&B-y9MTJYb@y zeX3}I>{m8TCChBO&>#psx0%1COU!PH36YC|^M%pzp7Sv5?e(!|4o}59`Si z4q0R1OA;SM{bOM2_3}QmRqa))?I$~u_)5)^*zaX#1o}Oul0b>To~F@FwH*n3!@+{RXq?z*!ke$55$tI`sPj;2l2Tmty=Z940qz#-J_US8UGwF>+ni1?_ z%|=@3RWlqM^2zc$%q#LC3z2~Ks^>dRDIJN%qH8W{z(0+ZU)y4V%S+di(Pw;>lZ0x6 z>FrRgdp?tNTb>P{Dc5_lS7n^GOF^MAm^ay@&})t=)j-$=^QZb5o`wdX_zOht4+Az` z(*TuZYICtv0Tu{7aOCih5HUvPf6PToC*REx#Dg%2l5=&L;Rj`}k;0|hh>-H%0>nRf z&}o~yX&gbBXVjD8N_`pslnFxljXEA@hc{^0nmvgkZ(YXYpK$EzmFu}&t8^oI(}?by z*)O)s>IziKm_5&ClygFB^#J`nANDcq4aVaf8gROXjn2}tco!1;fR96hp*5FUOtGYu zf=Z8#X)|`-be?Bg;~tQ-)$?|&4lNSc3)PLe(dm;=UJFDf#{7g#5La0oDOboG1TnIT zZwo0?Ktcfv6bb40y!}gBoN$UaTV|hL3F8(|13LnCOF$?P*ss@1U}*|efd(*yB6ki6 z3K~gjID}8N^n|9PU7T_%LFZ6gPoStMq!!XIOUf6Z;M@yY-dHK8gj=-ny9Kr6uj30Wb0oSa^4|_;Rkc}p?TCk?^)^~lz&>*(xf57>n z-PYFjqvCFjWhq*qvNw1?P;BRbA}`zn8&wJl^kiV19hvuVxn&Fd<^X6q<1vo zStkzjQwvgWwoI-PY?)%YB$m;!ThghC2*`lyZU zi1Pu3ja|Od_vzfpblRRK*&O;o3|jIo6JWmvl=i7$8TfB%Pgp%WD;`oa-*py}C-!fM zlILu$hFUnW=x003*`foH`B2egBoFGpb~7ju)5!$eKSB8*q<|S^#npQKsWtP@Kbw=K zu#5N^PoQvnViug9w8;Fi@2V=fW%w}Dry`OH1jXnDhN33It2t7T<#6JS#(4RLN4r@<{^665mw0bmJ{ zcS3$kuMMPy$#+5Srf*eHTE`>(bxjE>Y(>}q8L)i`1=X`lx$P1p+VYVm;)D+GgE~w0 zFbjMOC&JS2fWkFyJ)Tdf&ukvzzNAVDL0My=$#fqsWU4~`x}5U?zex}=)kWdFlyJH^ z;o=b1tQg<)L!{x3j@6&4kdJQTMI3cF^c?B^fuJ=N$CS>E;&U|Qj=!|2JlL?4zDj=y zG<_}j?1~M#KyQ7n9}%GFCcwjsR?oaWPQV+3AmYyuxh^*7R^IL~k>hGf_~6`K^!fWM z6dFIRQ+p6Yoqf|ZY(KYqWjb-*MCKYa~r@0=IV2_ zU=qN6Q=*#9vqfhNnpmGs0fkFxWVu?cj(BPV@V}9%WSsNkPYPb@hQUk~2vS9J1%%rT z9DIrYUc`B_i~pWrqsO7U71#)iJ(=oC1iWX{tJtJz#u{$X%iF7&D%{sb9Jx+)zycJX zxQ=zCtSJ(r>e;E=P&l^*QmD5?Jh$~qQpR)PTb%Bt<#%?&JedUt3B7&-LXU`>_M|*@ zh5d@L%MZ8yFZ5CyTof3#pOGOSPj<6{|Yu2d=ufrUam%6n`m`*De=E!{{EtjpTwyu)7)+45@?m*zs`9s1P7mN~{ zN2$bd*>w6E`6Q3KJXuVyxOmeY@=5wj+^|cUXO*Tw zSpi%s4w;T1L=HOr-1=hn;@E`a0#Q#(5kYvfZoi)Zb_W|%<&FjjVFqw-M)2biVmfu5 z)j|A0Q1?G_x*>LiC7%lkX-rn`e}kQM9VaY&@CU>VQ0vmVJb9Fu8nK=$CN_!j0ySKR zFAg6f(M4i+@XwYiZ8Ke-+_U^4Aky04#ej!9bgIo>hw`5hHNwlpef=O;wy!3fk{LeR zjJo^3_#%)cEgJZrL3yc0Q?{z)Pq-lXdQ$KZ7Jn8u@1z+aPaiD;Vs1?;Q*J2C1mabp zFr|~_N)^{d1G)6RxjrY@L=wL=l?ldG{+Ss#EK3aI2PScq)0{jm`BLC!JdCrwGVhKltkhs?;D^Js;Y+ZF0G2$cT z!QgvG%Lkx%*#@q}{sB@(3NyE>clAT*uqDk<*){6DrVjyZ=;v$>uO@_uj)&}|sjyF7-za#FCYKp*BQ6X^ElTkG zo$@M~+f1Tx2mntYMyB0HFF46VCVk2@F`1@FC5TWJAhi=o|2S9?;0FqyXGxBq5AA?9 zsX;ps01SP~s(*1n{W1fJ2%t9>Tj4`T9M3+P1F#C?*A^RM^yyqR*s{~gp?R9ree>a} zVsjl{9P*sDQFj6LzzEYA=)@p)y{iIOliyG79zju z>hf*I(j?$Z7$@5+@AZBb}UR$pn66eaH;{rhws zQb!TouhseVY3Wv!X!Z0sJ~XW2O&H}DJ4Xx(PUMge!qj%L>I>}*9XkeXrwdM1+JPOp zdlD2dtjpbdrq>m0k)T=2Ff$e7EGT=%`!w5%fY4aR?Br?q@j9r=9IYBv(^TdpC4`A2 z&og?!O$8?`ygAMD@}#V<0_-l~B_W5zs}yP{yOSP|pYDW2GcQSLDPBP0+-Ig12ssbF zE!2q_VR;+-!(gY^;;^>}^1qWunLjt~Hpt!wVH&QQkK2tn6AbpT>0$X^#23j=U0Y4+ z!8cPnHOy0}y{(Xm48{JT%?v=nrI2O-@y^%u7P`SjKlPt5o zucT2WMv9(6(lxg*utLNsHJ_@148TZU$y}y~-zVU#{ismBDeR9=K4tb1__={kfBr2n zgDjxJm6G|}TSt*KIzqE2yN;yb5fPrJV(%$3+0zAeOpp5 z3Nnm$FGkp$>WRIt$T2lF%@;s#S;LyS_Q#M{yX+h-pTSlm(0I1XJ?*A|9EeP2$Yylc z?}SA1hinIwFqjk6(WE-92pl1&!@&B1#84ig{kZnQJm8oO#$kT@&M2ekx)0Y%n8eUv(n z0U|5BI0%qbz0f&P4uHVcu9~4Bcg_qbRb|g4W(cNYy_)tmuZ1Q-hU9QVC(yg1yqfbVFjdK;c;BlxqmE@|gA>tuY-gwf=xtPoWzZShZ zg*(tDtHm0LH@CC~f&epwLMJJKGR@xK7ztplKroEYu9oTTrG)FGFsdq94yeeff~Be$ zId3lJE;J=Cmj&Nr8PPb%&bm#IT<7$C>6}>U?xiZq|5lcJo|Sgu*CeS)`oTL;i)=gl z2e85wK7b8-yGN)9O%ODSxj$-=ZP@yC>-i90$&Xbkl&!tMrmj}%NJe~lw9s`s(F)<8url4 zLjN&n)=*1N{{C<9r#63Agv|?i&AIg&rm-mtE1~{j6#BH-P(?jY#;G7A|Gk2jE5IlC z@Yi-w#9p*J=3dX}Fl5353(+00q|y$MT^u!Ok>U9>$P?`%^G}kH!)LFm4;S=14xMu@)0jRpzUlh#(5; z{L9MRSGfdHuOMuN1*ygtTI03j$6JiK_Jo$LSJr6crVwtdg$)mI=P~*sVl9-@ICmlF z&pd-8I*#idHB}ToD)q{jK34g@Md4bE{OC82)h_Ny);zVrk61omayu7*?wz4*lB$}k ztez|3=aj$(y25&1E>)I3QMT71C1N^BzRyh3pV(1}Xg85gs%uQ>-omSYhWQa>(JxRC z=baC)9jOPd&VR?2fA?+`L}|5o8!U@2=2|~j)Uck%-On5N^y&H@(>N#*)ds9}iF=w* zKztNLe>4}NC6(SJm=T2e7CXRQg=-D!i1KTbhr5vASRrI}rny|=rZ!|8CgjaW>hoShzXKzSL7j%S;6Sk36~+%Fe$vSTlI3COoW? zKj7(h@qV85@s`G>Cac_Aw{EQkPqm3TND;Dgf*|GDl}YR5eqjb2UHMVS?2J56Uy)6v zek!qhrl|zJq{SpuKafSqryKvL-xL&XJVkkVRsQl2CfFP15EVU8+^ zpA?t-{i`XOxp64npB)vK3M+q{bI}037 z{oWf%5YD{}xWIW3=X0t%o$B2Gi2fAj>W z+?Z)t=bZvc;prpCn_ne!F;P)bP>!=LdSCy`)eL<$nJGL;%`n#Yb%qxjEF4*)^qe5) zy_{E4PmBnpqa``VCX%Q)_p|^8^WV{1CmY^qZiMLU(6;v7^*a*pjh5Y<47KA)!KHn- z0i7vC)*O6eSzO#;3;KQc$f`*;ZO4>P66IIZ#1;`SDxp@v=Rx%~- zLW z$r@@Z=oM;@QyVUS*suf=uPt{7Zf~gVqh&omyaoSt`uQg){AQGU{^heEcJFgwWL{Nj z*D*HEytD%%vUiWD3%>G&JkGYWd}V@Y8eT-kr{?8*&*UV?s{%#i$krO9w1jF74q{#X z{7aPPdJMZlYFy!n#?_dFi%a3*&MgkNV{v@|nLqFFEZN9>^5hBV`G}vKNstQjIODQm z=4tSz!n@OiM`gP*LpwKxYEfRH6W!pMf`KDPSbW)f)j^$?|7bfy345vVo3aBmMIoFcpk-y?0SC*T1|;zKp1jgrL$g9qNl&fct@ z+xO7qHJDT6)$Yc|$tCaECSPDng%mi^3FMvg0ZYiZ7rpP;DVOANT-IEJ2KabuE@DpY zk>ak{02}IE=n80$buS`TsM0q41ku1HORUeY@=89YvI$rdwZHrfIbj3lApWEf**`MA z&TVPS<$wCJxL}+tV~xB|1XxZ*n*D!leRV)oTlY444X%iY(nvUnASsg4A|MI^(hbs$ zNY{XYA}LCDBP}3ZqJ-qo4I_*+!$=Ix@a;3`z5ed|?LTyYIp^%N_u6Yc>sik}2Y)iq z$GlvWY3iO0_|Y$Iskvo+e5tu3;Wha2mqFR_h>9khZqq2xiS&WuWN9;Uh-2M*stAEU zMGtXSoSwtk)lvdEN=0q-~s>qYgw&}C_+(3$>dpM%$B zzBsM9pzWYv^0pzE)eMJ|OunU?mW2U7`pa(lIE+n6nRSbeWoT$<9{h1Q{$t1oeL_(~ zR?!9sm0Grd+_@i=KbKm1H{7-DAnPYM;}1o>p0~ub0@wa?G_a{x-+;e`@(yqhOa9;1 zmlsbi4nogI@Vj&CcNqNhC}(Aki(GgHf-i5cRto1*=R^!!1m8G+3%>&Y{4V&MQee#U z)B6sDoczpZ=P8#GEk3yE(qDIimXqI|YtX{{Z~LUrK?pCDX@ac5{N35sTm?;&ZFbIeLJ??0hzQ~zrXt175qNai;StX?-XYt3l9v~AlhY?C{m_n>%hdX z!l5&e%`=1l#@NzKO-xK6!PY|ncy|`IUNgXjewpC!Gks?Ka3MU==7b~og`ZQ7SnFG) z#-U4Hvg3t+`>$LgDhJ-p6TUfSQ)D)L`SRu82J-tPXVkHYD9CIQ?`uff#o z;^~fmjsPFNQJgvgdKkx{;aw<2B9NI~D_(NCFj(<=%!%|lIF{hodUHmI@7sEA_TKAn zYLfn+OYg$I&p>x1nP-jQY}y8@z+F~p4Z53nd&j%)2y;;Oo^&0zgNEkZwIyC7w0x2_Eaw!dAWd?hSS7;2nO2 zL;wXb-+$I80biPc9EIU97b|{oyZJck{V zQn&Q(!gX;_WrOM(exj~Ij5@{d1m>UPEoC_f4Ql+LxF*RoqxS{4Y=nm|db?W!{O0Bd zJj9d&#r*gG9P#Tz94XQBBcNgZ#`Vj!wIgo)n5Uxg@Ww(0cm5JS;5>$|O2fdQ^xwk^ zK3u>{IQiH}k~CifugX0j+q>a(QECx771Suzg1D4~<2GCO`hyu!}GQjx+h>3|INBw7`lHN7`z6+cF?cAOI_DIPG z2?EaNc;t`$$X$>Ylu7f@3fRhZL~|*#KAM17=R3&nG4!v54NOPYTE&C(e|)#*tJ?4TMZ#eMw?oDy5Kw zjN^Y|S-?BUS_?XuOg(J#_m;<6e;m6=ypJ~L1!}~(X{&hla zm*D&@+-;{hA;@8VmW+q&39su{%OZS*2Kr91gP&g?wtf|6{$dP0oTmR-v8c*W=;uGx zQqV;<)4d-*A$kefdRKe8sYgcsl*g;3@ug!NT7DVw_vX$Z{;5TQyuqxoXnJ~jfZJHt zZFOQ)C1d3kt-Tx&yZ?8&pV7w7;4P{vmpG-h>eXVmpb!Zdu3uws?hG`VYOey`+fCKp z#-=~Wd>Qqo<*}{G*+Dhy^zGe0mVi%KFI;1|`p@mgo%jLIz94 z2ig4R;Oi_bs(CO)0Y1KnJ-pfHs^_Vc0y;KHrRBfVv-b#la^^VhJo1Rh7gp3z!ulM8 zCN^l<8OegX${`0i9;Zy5=fkFQ61I1HEH!J@7tR>041Y zQRLr{rTO8#q+ft_nK8h^RsvT1mE}M<3IV16a}o&Hs&UgNfzmv{N4etJZ+-RY*z$0m z)d7&}?sUbk>hZ%>49-5z4zkRFe8PG$@HmYBnM4_mlLijqzdT{r2e+hDgHN{A7|o3V4&*~NY>mw0{{K8gE4!7Gwo8+?f4M^ zRr9ytPk%Ne4BTXhqg&lGT|8q@7igRJ| z^~&ygx_rlh(`Bi^mw$2s6RfaOVK zU%XQy8erfP8>$S+&|k* zjC`Ja<^YNfrOTp{8r;$X7Ts4MTOl}e91LA2&pSff39z|u-n=P4Gw`2NrD!JDgyT`S z>>$V}{#qy_Em^Dw%iuRPji0PczO`2dj%RF z*4(WtcMJDK?}}#aIXWVpB_P*X8jBz6bn`DpgJ3I6LVJ$_s~r$_(g{6b#O5aBHnac7 zXrB4w^I46-GYFfnrdazwF;wS=*qd5q!-l?M4;2-&H27&BfQcJc5xD{$@Kt6F#txX) zpAF!1{VTPuckuW;7G%il1RzoTKq-wtNF4hU#$#_gV4Y$Mx&>Krj(V4UD{$Kn{>~Wu z{{b2EDDIO<+?VVhLF1P~jqScMK=ghvZV}2IVnXoonYxUIl?YUfIG8UDb(Tkv%jLO% zSn*#2>*Sy~QDi_kk@g)NDamV%;QrBAvh+k7iv~a6{C0dMPJ>#ZGB(&_649TM*6Num z_qzfl#{g1c>j1CT&|2E~?+x2()M!=8e1X=xxmvPnK+*E)UK@ZZ_6RNy#g!2RI58kC6fr3=UkOjf-<;DI_A&@B(} z8hEHTU_Zdxy-r{r29_@cw?P6l(K_Uhi+~Mhbj1s11COt!W_#~tcO2;vTqa`$?6y)Y z2>1rw#(%*?V4lHEQ9}jDo>EBr{oA@-LQ}(AL4qBRJvw1|7MOTk36K^$O<{bM3Cxp) z%A-kHiHZPac11ms6%4&oKeZ);ZC zxwB9#;tDP$e}Nu_$ZqsQvMjMS}*Us1`K$|ecN*rQbS!p`)*AD zfP-7$0rDLs2uDE}&UG?$SRBMAa5}%e(Bz$)A#}S&OM9kqF`|*7$p=48!eS*b2Wr%D z@v;D=Htc;v)J>#=`k#0P$wOKXJz z5i%e0#$l{m;K`C%x1>LCB<1DLv*!v2Kxcfx1vA~>YIrZ}d$2ly9UtA6k@;Xdnn{@* z8SQ(WQ}?OkpTpsKuU^i*o)tJ`M!Z8-$L{)%0rJ%&U<~q1M#o@-rW~-&&R>ZIGwg$u z$80|W3Jh9#8GN2U>po#ZD*9I$R_)jxXxObR6t9$^_PH~5DHD=Rms<3%iYlLt}I1 z89q1p29TXN*)_ubOH z_U(AE2+o0Tk`~Rt*FX9-9$gV@Y%k9j0zxtC=_h5GT;c7wUGl;8&lU?%F{a<#_I~kHr&LmS3t}mLcTd2R1XRR!IL~=YkgAVTR#X z{AB_k>e4H8#rZbYezI+Awlbae&$;EckUk!kEEEeCf&q=#j=gnDD5l}9h5>wZz{&6( zxZLmNxB3xJdMZmdwMx;$^0N2p7wI8X`{MZK#_p1cML<&k(k_(>>6K*&VILX;&rqOK z2C>(l8AlJ-r<@O(Ugvy(e4PLs+mf>!gxIYFGjD~Y1bQATspvsO$H| zdt5yN@`dFnh+ZCVIV_0bQj0!$_@%6ZpH| zozi{7Lo2qW@O{kvnxws;Wu|k=4Xk{F(ig|5i7;J)v+?wI(LMpyiB??_pYb<|5v<`WAsg`YS zuYcZ*T++)_;Vz1riEE2)4yNqPtTh23S!kN?Gu;Q4gN` z%LG7IKP&=26t|yd*fjVGgKbeC@s!M8HpVk*sjY})LGH-Mv$M(fMe2{FzJ`U#jeh$= z1X3aAa)%=kE8ueZJQwO#_E9W~N!S>|)UF021Byqwu8Ymu%66uYmK$o$7uVGoPQ}~p zkMxp=m!p}8C{4%f*4I1wN|-IAo+M6`7c((r_&C%i))`{UxsfeYC~sSlss$}^xCzGh ztt?6XFm^3yEzr)o>0o*zek=(H1;Yd>{>oRIiz-Pj)2($YCYi^entuFfZQ((_Mqz(! zU zt)UhN23+h{m6`QFSzc>oyzMdLUe2wid$Ww;{6jPeK_@+^$bux>bQmNdi}l^UimX8R z@r${SxDGSpuG>`c{MmT6zZ4-JFU$!)0((HC>?@~VL5Y!PBe1JJV9Stg^~4aaMx_7=9Na*SxA9ZF$*sBLY0@7q?+w(Hz& zYGWM#<-$NJjm|{bru~o0IS!cyWKPSSqWk^T(=|G4F_lfC9VP*0waKJYx**UIi*1D$ zY-bD(>+SY#7t`^OWREKuuSSwSf_&3)^V4XS`o1eQ14_WK2U`c-bc-x!@0H4nZM>S# zR^rx%N2cmoWREg0m!T9)Ve#4 z)%$q0-hB)*uSJku%U2-bUJz~jr3{%HB| zRNHEUo2u|__(4Kg(7quxGSNSNXyT)|U9Fe=^U)u>^R5{z49KYKEZ4htE4>Zfm$5#J zS>pm({5D`Ww|Uqi%yh6K2%_kIk$~k|0P*TnJ6loQ7%#O@TFMBBKu=WhLOX_`cPu|l z)W}imgUqQ8U)6Ay`AbZszCxs+wic`04;E#=h*z=_df-8fwJ>S_*j^VJ;NK%Yv10VI z@-v|@d*_Z8FWt~Qe<;k>aIxda!?UTzHC{X_GZsNrxrt%XQLW|qYeCxwQOE|tpzL1l z(GiD<1phM~135qS>V3)Hod31`13wNuc7u+$0AXUOcq$h6W4a*cgTAW7HtI$C;+lnP zW@_q#=kN+UYl#34eXug6$F{HqU}0gqDjXCDHY+?Eix<;OcRdL49`08`M1b_1*Ut3o zey~J@>DhwhM&`QWA@ZvfDF&8~@YOWawKx>?99n`KAsjl&3u~m<(X~3tzP;rl`8wNv z9}+XtG*@a7PmY4B#cmZT9jx>SYf?k!O;zpi689ts1eD23=ggGdBd|ApT)Z1=GrR*i zt2Ev%w6BQW*t+r}X|0}%dtv?)!#X7C;}Thv>G6y^x0q?ddG6+GQ<5Tv)X$mq$EnQ@#Et(AFS1DP^ML)8R{)x zT*_dt)Xnws_B`&9)W^pSu7Zwqj=&2RizoL&(00@fz;Rz6OY~D~>zZ*omKXLlM^g`T)E!ol(FPt?ZqMN^ zPY>KFsB8vRRS~3!(~?JRCMJWu#RsByu`YGvptHZ~xOA7N3n92lS_HPonhld;q3B!U zH-f>fVzOL6T*zp~+~8%TK$HRKzfy%NgZjcyvA4vo`iK*yhDt-2 z`&UazG=BVZsk!Od)S5ZNtNK01_|$-kEetzn)-`2W33et$3fMO*LxLEwnFA?!4sNOn z3a7_ah8}W|nnHDG?E&_ahNKsR+_gm1U$;Zd<;pgc=aZ|`=>*LOe%7g_2M49pV}}i+ z<48$AbQB&KBVM+iyvpJE!!JZ8;~TnRd_ zlQQMaPT=w6YApaNH~paGM*qu?AEuC7g(?TFVadDcIXl8?KuaYq;EOxpP@uTO%LCaK zt2yrUd^U)i#fu+RU$)Pbtn|tEE#>)gLs|BK6)6P>T65*TxYHYbAUY#N5luOKuA5qH z)~(xy-=rKOQ>$|H*;MC6XLe0=O+=XH_;_5VGN|{b+*v>_c>ik{^3!bNK=8<1jA3MZ ztRuC^`kiUJ&|-a$T{B`kht6qpCe^`lPw#`TIcs9E3;FIxac}6Ncn~w&t2mbYww3-cvshP96Rj7- zTP0f4TXUG|QD#2Thd^MM!Uw`b3^fib z3ui|$A!tq*eZy=aF|nAy7YCVE0b>^)3T(N@W{;QEvS2x7oXvRszQP#)-4qUAWMG`V zE^60U++n|)*Vv4%r*oguv60jFW9q77R;*9@<a_X=E+DK~Fcq@^wP)uUlM!r8C2PMzeYQfs4QfJDN>Odk&mL5kP<>93rG7Y@J)pYHUzEhu(T&30d|QuXeh&O+$!g7ZnI_(;c;Xms^K?A(qa;vY?(OJr4Mk6 zGp2*a^rCAlBpLjIzMQlq?a}@}&vi}`ddqm{lak6!JRQz?^%vai^%*D9D|)_%OSXob z8#|C^`qj)zsDw6?1K!t_Sh(qwy80}cnVo)@GR&#P_pWQZq_f}?J!_B;2InQ1siBYc zA3SNsX9|`EA0NLP3(IvC?U~yr7Pzt9Je`||zi8h{C>4ZS&sC2Mj+2sR9Zb^E`n$~# zswhp-pmsgVld#(Rb;yfmN+WP|E3su>JHSMZw3tMY=0fM)7iNJz zD9BJ`*8Z%9F9=ecem%84g!9Q6#(ou$qUO>Ode?r7PwjrP0gO=AS4VlK-bjLAL8)y1 zGE7v1-9GP_aKlS=pqoiahox+EPt6mZO$Z#>ZW*`g>T)wrErC7G(OD~%F_w}sHeona zc=l6M`&mhvW64FHGT2&rY>SwIeE7CGO0(fzVBmMnbjp`) zM_g+c^!zX)KB4f#2325awNsq_y8R6csBk5|5kH3)NJ1R|E3;{&3O^5Wo>VpefCOTU$OHw!d z)8rn7K}_7BrP0F9oA){Ka|m|smulDc$Vn^ALpZEO{RH)ea=%ql{+-qfm$~cS)iOI{ zK|2{1WZSP0N1Xd_#E=yHLF7Ll^laGRue#-e!+n(^Pi*E?NcJ5OX|4RhoBb^k6YuGo{9u;z?x}caGrL?^%=gb4T&!BEu{liaygptxr~A>5?7%* z3R9#AFX42vvlLq5-NK#Cc~tN+r+mW2LRehfNt6>NSK2%hZbB?k9$9S1M?;=P6!Fl_ zON_m!lA~9oC6o_06~bXFYObl2d265tb^W@MS@h^9iGP8BB$M@B)8d=%=$UCrhrPDO zUQql48pJq~#!nsbVpo)2xWwHb~dl0gnR;WqczWFF%S{T^Pa;NRQ z#d6x=jQ6Nkq9fa;wwHSZXI=~8D_M9+M2a87;le#D^jKNW`FmmX5ZC#66edj`9qLP6+9BeElb~fLcE!LlKG;T*};7&>9+2M@16BOM`zj5L<^mDmg4sWU_;z#! zG6{dxl*>{2o{p}6o3*stkGeZ}V$X^|T+!q{%XOtp7+Yl3-&yfW&i8sh>t!n>#3}sl zDgcL;>wsoGcj53x8n@#v7}p%lO=CXd>giI={7$Z$-+)sY{JG?;eiA4=cuK4X=jqq? zmnY8vf@fk{C;nG$eE8>83~=I9J_nM%vnZG)IC+?Qv!9_PNIV{qW) zI(ukuoHnv{`ri(izYu+JUGdD}GRs*{7D7QUlA!_kgTc9f@tn~k!+8}l` z5jwyQWi0^pol!;a`tQ^W%L?enDDdGaoqx27n`Hq~Vo#66M$G`sJ$DG!h^sfo|2O9c zo%WL^R2MG>b@97L;5lA$P(J9{2qqb@6-GDJlTAKQvgd_l%!g4bLgd>;JoyG4So-C- z^LrZ8a-$2W+m{rwL0Ft_r3WEtm3}|V;%yPLFlWFR;47MtcQ=sW67j~@^4ihFLk!NZ*XLg1o(GLaU0+| z$)#}SaB(o&HQRoFSnMM)VAr?1$gbqoOOV6?m-#v6Huo>j8DR^7RgiM?AM9Wa4H6-a zh3#vJJokM7hu;CpZu}<#VnS)O-KVuH$cZE97><-L=G@tMJx}g=5LF|P(LS;I^2<5I zi_3F&8gzWHk)vzrm!mkV{*YsSPI)8qd0D32Tu0%`0_XepmmzMcAAHL9I<}U*#O8pp_`Rde#@+h%rtB2t}LdMQ{MZ0ysCVI1c=lqkEZ)Zp6 zHz8i~`gKLK|EY zL*#Io+I5~V9jP=L+8e!XN|5lBdv9i7-C`S_gYp5lHUI5!TMHKv8H zT&f^08RH2q&ZBGaP(_g&*BY;I#g3ijirb58eEQTnv`MdR?%NyQjcapCD{h19yO)MH zxP(fJnulKqTdezVoQ!4&O&&!&-7UV+!rH=u^J!}h+cQ3xN~GzytZG7y@-VjM(m*tN zD{Sm;FJ^Y^ZD}FDtyS$|4L0453U@1r%^O4oqRzIhW3q|f&W7Rl68~ZIne5e!kJ3-1 zzVbbqcuFB1csh!PBqDO3#=KN3<;I*`I!{sWU5pE-auhem;6k|~Wlg$B;L2ABP97Vp zB&O}_0{~CFmev(1uG^?nkjL%Wx?u-3w|w+ir5>cep6U|U4O^C-MQQExY6mZ2TFgh^ z1@)%{?N$JGM0He_tNLa^1Fm{Nv8y{|u?b$f(iha<-XYPyc4&^oRw02B?jI<Fe87XhMwwd*FDH3>>)vqMnHv{K;yp=@ zxm(M?YvqpXVr;5(jzN-SEIKD$Q}hMYt&F?FGYmQ5cMrIwoGknvF61a)AQ}& z0`;&_S{&RIH*BS^yf%JToc2uSc;-{YV1;DK+fa>=+T@OZvPh6XUA%w54DWPE#gq_s zcZS*jXew2A{jJKWai}h77C0XyAM6#}3h}@)iF;z^H9BvEk!b{OLN)}gZAg88b=w!C^a>py!UB4g(6wj^xGIQh@*-Ffomw@Oa z-nQD;kp=}H1wRZGXOz)oFGO@NaZ`3?_kHSDdgzGAu`61dIF#UoBy zu-k`SF1_8i==boHu}+AC%*29TV}?5}z?+|JH}=kf_o){%mrTeTys|DT=E0vDeabv! zq#XAs+!pk;OT2{XU`K@<)q_b^=V<}CeL+NVAq9#C5i8-4ej?MA)xxBbi*vPB^3Gq*4&jJM>4rSbg|Oz6cDk$92$(UMV9C!fQek1I5B?~tIV-(vgZZhs>RAb|2@=JZi$?g= z0Vc2%ao&r+CGvIK4CcHrsY+lmM9Pe24!35}8Pk_DDVBo@hncUgYL+4*Bw(Z&e%XZV zS%ac~yUrsFCdN1)0!FQ&f)JV|@*m!(U&uclB-^K)+2{Q8+841fiBfhKmAupxe!+Z9 zhop9m)cyW9lAYy64qaSzR3Bs8+Ag;)+dI^}xX)w1jjXvrG%bqTEUc&Dh2Mb5tkIX# ztXiwNNakFAR#TEFj9v4+3!88Ow6L@7`F-sxMOpo;Ra6KDry|NZeU?y!_Rh-er1slF zfsfXOQb)POp${pgfSxIV-2lV_g3{4{QtvbD+(!zjtqu^=$pKtob`Of(>GjeS{hg1p zlrIBM2hkAQ590LF*YZlh(#j%?Ab&J>0GTXp=)6BHJkNao4zAQJx6{tMo{TySe#x;4 zw*Qc0Kx0TtI-iT;_hWTvE+@y@S7TB?&CQ9_P!I(ocEyjHv7__tb-+JXAZ*9ol2_&Y zay5dx;^J2opxWMWHIQ#wfxKO~4G{+R_ zcxY3|ErUCf-ZnlM!fhZbIN_?CQi5wWQ{Fy?X}(QY-uQ6BoF)UvXP?!KAa!mJx#*y^ zn#+-{D4ND>il*WQRVSWgw>5`{6-~wKV>uqk9F$YA9lF5^E9E7L=|FI+C0oR-CVX|C zhB!|uvfm|gN!@&4!@wS!;Sl5-$ORnWjVl2(+G-M#rzRNdt=#r(f}mS?Mp7C zp*rP1-o0XA*hmldZzwt4l*LtHTU#+LQ>9_hiyJZE@GJv*71h|lmsK-vl=>MQb>Z8j|!#~jn@6YDWuf!JDIB6hE z0F$L&%;>I{+iWqDxmc}9vXFE~To*VpW{g%%H) zOmcs_HWR1C4uA3Xlh`smavZmERMtW`|V3nfY@qrP<3XzM-N4V(hxk&dp6x z+%aSPuscjEfc^QSUBo>;Hi{=U(w$+hEd%Q^MePFm?nOyd$*^aid5ON;tp@p55BXND zNos$amn{vdX#VnsXIQFUIpuWMaR*ZBcN92h9@Jgb${i$X%p7@z@OAVV1N2Z%bkqDy zDgSE3y^;2Ez~Dj@@O?9Cu^j^e!I`=R=Cuks(ud1shqr%>lMO=x;2yE#WTV2{vkg$^ z`HC&kQ)5V-^97N^qgIpW$~&(~36ple`L4;GM_)nJLD=gof9i?nRZkh8-GtT1i$-Na z^;52>xTLU%*>TEY--COJBzoAI_bl*TTuX6UJjSE5O32(*>g5RxvNk7g-=KC%NH#f+ zY~aNxN1AEb{MDv^W&6;rNsg1f@tR!+qk1({w)xAQVn!{FS~cgj6zp)3`1~QMPCSsX z41ADf(plRxE>4%FG!3+4bxO@O_z3(hQHWi@TF8!SI>sp&17f&&Pzw}^OIL$rs@LBR zC=B^}xUi)yh+4L$X)+Pfl0^8A%*n#cJd}Yh>ue5#v{9jD2FILi`CHv*sGivEb(_&I zZwA-bNw6(1WdSj z?bW@p(=A&s32k8v3PfqRu2efdw0hKBbvKgF(pbD@tzrCeyO89Ot0VepDFE z92Rlnut4sKf)GB7bos4Iv3wuk1=TztEl_H5kbmIN#DP7K4DABB(DR)6Dj_0m8B12m zAPzbS$6FzFM|Z|5j?h_@`hj){Z7oIc%;~r40nKe&b!)EWm1?#}-_m$fKWI%@f{es! zyJ0Ne&Z!t(E7T>h2Db+^G{njh9qPSonYr|=G@#sU;|HciXzq?4jDkEUr5V-DFF1qa z9WS0vXyAn_i57kn4f42%NJhNa{W_mK+VVzqRO+dtVJFu04N}`Vz5?##Rc^OislnzX zQr(3vS<4gb7NWE^BSK7iTet>mfntq?vA-(jG5kvV_I!*5K;Rh@mHSYZQL)32s9R- z2AOm*Z4~K&F3XS4drUNvr&AFvkFGxfU8dm4A`aZEwT?729k(7(~?Hos4@BV?TUqc zO2B%{MxZ-qY%D0|F8|t^Wb_uRo4^t68#=Zsr!6jCN6*qR8(u>Ah<{y>s?Hi@^^xK% z(fXeE$AG{}Eeiey6jVhW09x9*?ZF*TMPmZAP0pHir$3uGK6!R)p%5&F%|+&pKBWtx zS?0B6zJ}v}nIS4J#s#_$LHua)B1dTE2WTMP{6&2Mwpj!n!@Ah0<*X=JNajPl0~n!~^zoeG1md&`>JSeOCto8pk| z^mv%0;Co2O1vtI8du=Vc4ex}QrWz`pRXOU1xPu6fITy*Ai4sRAWOLh7(i=7wjJe1e z)2fjv!bjM=01dPk3ays4BND(;F7~#)#NlY%hn%G&1nAr#^~nJH9(DvkoYMPsRN8fx z)IHRsjBP%J{(hR$(YeWabW;-z-2Ch_rldEBvafrw>qPFOyaciOpo$IN|M|Ki%b?H3 zu|)5s$3o!<0(NGw2u3q#t5Blf)TT2W)OsYEtCaap+y6yZ>=}fjaJs0R+q(uQPHT0eVj#T4fp$&-`pDwUDv3L&^sv zzO%lsm4}dRO$j4=T{zltyvL*`7r5V15{tR>`p&n163mpfnaZsY^&A&AL!eIxmR zeJ#q}uFBxNrp%|MuHkr+kJ#t>2((bv02%d50}plCSz7v;!C_-)q0EP;k*TShP333fbQoEs z9w!DzG{?v&TU`VL5F!V5ENZ21Tzzhc_EZ|fjtNffMGkNkOJL*O8)``Hynf)k`T!RI zDK9`XQbxl)94Of0h+!%ku5uhnPMbY7ImylYw5cU<9#GEfOSfLT?HS&1*Rly<;g#_x z`u6=%ult&QH+bCk?{eGy zy`fE9cyUt|)+@L$+0M&-IiJu=#u5#(dS=TC&1~FZVgd)t0_D-r-^(Y^wbhDnj>QW<0Vy8ZWLTGpvcbNHw*@X_MvC zrB=IgC`Z|7s&R(ZVB>*y%gxXJ(LU-nB@Y=k2nPj4 zUQM!?osG`a6~H|bhR>L?soE}sFv=WcM4T4GKpGx&x_qqcraHAr0wVKPInPOr-IHdd z2m4Y{it#I$Q?Dgm@@*_=MXVl0Z7N&ZBq)cmNlGZMoiJ3Q8vt zI1j5}dC_f%A~zs+a_oBOW{y(M0tF+v4z4<(Nak|hIV};6n$34s^RhKt@pMRu(uU0q zBn`O#f;?y|3^1|M>o&&vq#HE3YJESXok)w4uTULoUCdV}oqqH2erdh);b?D}xuLFz zD0ZYg%3$1iMojPHlWRWqyX|}q?(jGfwcUk!!cTR+?)i3isvzOSd)1=lxpBcP1J~ZP zaObuGy^v+9BJW^h?-?x!@4YQiDWx-l#R7UXE8z% zP0Q12+^re*@QRa?LRW!X_MFz0im|J67(iVm>hu;Ka;qK1Yl?~QuGLPW{@M%$0z&xe z<7&Kc;aD~d8AS@aadE1?5}X~lWNv8njvaoj%dz!{;h0e{JNq2k{eW5V5ht=j!*`YB zwLh{`_y)#=`^R)Qr9)DUazfo$C#EFc8F#Z_$NCTmm-J(N(O&c$))0!NYd`n28AoOk zgHc^eKx@f;ygYLlB;Q$l>#o^aeG64&)z4p*pUu1-(c+bczb!X0Cm_xDy>aEOdBjzm zxe=CMvwTzV?@{E#h+6$M4>Kl{%XbQP4nghg!+`Q=IY#`wE7mfFVzu&zoc&KxpXakTCSqYp$0YN zn$4So?A&O@u0XjqnV@?r+<}%4N>QST0iV2~x|rF*mFXhH$*o1tlmIhx+;=?vrMZr%v;}PMeF`G(aTAeGNINPfN1Bd;aEB zgV0pshL7Gm@pB?FKNf+>#-p13ZZd*9U z8UsMdmiRZtkxS49`HGdeI8j^-I-SxU`d&R~9ti;* z40b|m=*Ns1#QJd-Vlh)5Wt#+gqMDthX&B#A`5vp2g!AjGO=iSDUqm3)Qs z2nNFE81^4_X71F4lx)SjKdci5^pszBfPdpMC*uX@*0#$GB(vax?F0D%zS1wV6-WcD zhq@|!M5A1-pTe-u12Q6RNC}9zHN@4;$$m8=eNqD$-99H!EnQhIk>*?6O`L5R)*&y# z0EwnqQH2~8hq1laR-LM+I|d{?(iWzU8POfYmzuo;cGXO|noY)eqNfssWuts1GWP|h z>X=0YC}T~~qFEJ}hS>L!H5XBRD-9mS3{khmCEW&zLb}Kpoaljf^tFNm=czTU6eo;Lp->D-Wp%qKHm7;>$CX-d?W<3>w^-EcAtbzGfWKGxLul|<43SHGdNJ0Uz{PI^NZ=>N0I z(WY!)p-ml7`Ri+Mw)Ln*r)siq^t6KClI}xaS^BHF${Nb{+Z|q|j*HBsFM_u76xIX@ z&^rkYogWSJoS5K+orl-c;AUqh+b^{S>>NnoD2#AFcP&D+4<{EUrL ze~PM{*~|)nOIN0kQnkewVc5ykZbt{af2hdEK$cfQ_g?HGYsd5Z+>w+D)lh{w=3#nx zln$w&*TJGbRNntYmYTD5;3U!)Jyhu68Q)G;grP;k(=r(!NwNjy0qH5pMBHWw5mIJJb{iC1`=#fi8mR9admC+P0Z z7EcchIEsfEHNz=d539tUtSr2u$}3W$E7I9@}vL9I2I^Kvnw~#M6;p_6@9KE5j&LzNfwan03d<8J(xpA=tHECHdh!Y zBq1s7QO=vW9T(yld0V{T)CYj^gMO+|lNNz32>q(hO57isg}Md&)TDMQ(TIKA03>cV zoYKKOO$8a8i)f}QXn?!u9L>&2y#RCy&u-fHGO$&?j(J8zNxYtu9|fwX6=WB3D2hwX zXH&}z23AIfE*BMpi(@yWGi=dIhzc_Q4jJ2p-9(W-NDMtmR+I;}lW~1fAuYXO;|-hW z&vSAXc%x;YZ7g1K31m%bH|IV>nZYe-QMRNa%IAKAGeTF2u%#WENnYbzJ2lJ9R^JbM z86;F5{NRO|Id-xwBjW)A=Mn563$H1`(V~DbqV2hR!9m5D9uXj613{4qaXnZ;+GxBtpF0%{rVlk9v^(`9G5i5yx2#fu< z(AE-XS+L~~QmnWsP+obHqEHHZrz-pQTH0087uG}0MX96T9>lhvD?hK!rmdEr&-$pT zq#lQ6QPFNcf0v)VB|D#RWFp5A2TCcb+KNdFIK2e))L28&Cm-?98>HTB9dWcn(U;QN zWiEcyY%0DnqXbd;v=s(K+1Omd?=pUQ95zF)NA82|4XCmiE!;iTja&sDC4bms*AHS9 zPYRugjTgZRLx|Y4~LLy z`Ap=WvK(kclygf?N}~ffW(7d@1%umpLl#|3%IB&>jdJdm$(S0uT{c&}22H$3uIU0Fkdk&5=C zbWhkxRSm+qK?*6SI%`nwS9_)U{-haN&HT#Z+<1G?)}T&ld9BYb%h_sL*8P@>2~6(35;cY@OD|-9%YA>}C~Ftkq@|(_gAFMjNqG4(T+{YIzDa7} zNY!+UY~AvFl{HQ~eA(Jy&r|Reg>CE0Q%pUZS}+Z%TLN{DQ={DR zW7_p(tcup8@l|(}wWp_9qtC--x~Pg#u{AA`$9^~R9U`tOEjrlgKJ&9XBb8W4#yF_q z&VaUBj?4dn*&#v{h<@OUo-@>SIyG|eYEst{9<_R}@+`3a&(|*f_c1b`=6V6uGxqjN z+ZdsgPW!4na*SBd`s-7~{?BmAG~$}6gDk2@w5}!+WB2l7K~v-Fdis3{H@8nW{NJPZ zCrO%FGbJnTDwSOxbD6C{lBN&HKKb5jVk045=sjFiCIFst?icxp@JdUEwN$Qw(9=p< z#j;Bz67DxOmej6ZXPF!On^5=70E-=!i;>FHttt@{&#Xko!t&>70)Yh|mWd%Dwd zH{5Lo#)M@BPdO&yTJ*8?b&W0k8Ywf24~~Y!k8DMpdPYu0sk}j&x#;=EvNP zScdL4gYvt39K+QKd*alV#EHYen5QF%8dh-ea0?M5cIaWlO#(FNBt*!+KZ zJDqM+YYn4Azgo#3$axN*Gau$oS>e`nG^kSb|5*08V^bjk+2=TRF7WxRHSZNG&be{s zXmn`#|D)?W;HlpK|23$nG-O1ig%C2!suYDJmCZ3DTjnv&k;2Wa%&e0ndvk2Z$X+4a zagx0`#&I0yod5gde(&#pzxVciKOQ*4XT4wh`Fg$Iuc9+=+{=gQ#mt=Cs~a3knVn9s z!aDNu8|s?yU2mZJUE%ajXqD&RhEEiS9R5y`@qph<)^r-dD^h7F`mF94{Ey@4*b@+?7!lk{AwfC%@LNOi% zLRzP;4-^@`dZ3zvu6y5ui`ZZ--YD5xTz5sE%81(H{{D8ezaK}Q8ylt3ZqK9Hz*j3! zpyh3cp`CsHK6I~3yL~2E*6SV=ybQ6O)M^tfs883f=BeI3yW~X_(=EVaNWm*Q^gI>R zAY|$D+tvYLMW+XgHP@dPhgQ4GfG25rLiaw>uTF*DzkcV2h;+py9tc;}FQ0-`)J-NQ zr$VgH`zep@q}TZxT2=NdAKD(Z3wq}yM}%S(x77>6Y4a4gx7QY#w(vJ(1mQ7VdQ{XN@eu-O3XWXe{6sQBNT-^xb*3$&l%#fTQnu9 zjU6Y&DQ^U*_W#Ex*#ML^zu60=tjL5Jv{Tmg_@$?>G;I%-WK-k3;O zG3YPn`)($_Pc~W~?b$NLFB>3HIjEuC)^9uVh=0{qywi^FlR!wmv+a`+W5J>`p+%a; zJ`ZB;8Jp&h95lP=zSgi7_qxVinZWe*>(}xwbDAL&0$c3DVF2MvTn9U6u+r``r^#wg za2qR|#6Lpr^#0eQ)tKGzSadHk7JQx5ldo}KgT2-`m0sWK;vk<1_pPArA)qz0=2%sS zW2H&*NJ0y>Qo8Y#4=_bBGUoeMpVvCvHqXn4#M*Hnfn<8Qnz)_ZcAS^g>V3#0MdRJD z;_;fBA_Hnt!~KQ*%N!-DUCyMUkecKPA$nfZCFoJ2Owrk<=a7$Yu`R6JReANvf`Rvd zT2f_`I$hY*N`NC+Y|qLvPdy+^|Kz{!8aV5|Tl-7p^_#WL&1l$Jic z)umswG!>`@vuuUxuNKcgAG1x67l0I)^$nbBe6QMxCweo-GrRjSC3?C$l7|N?TS*K= zZ+~NMbE3ua-u7(i2cca@|327Tbq_h%;?Uy54jT;?#E2*6;ZnKZ0n_BY-i0HL9v>kmUEh5QIQ{x_!$gEB$0OOSDuWWM2fIw*BBmW}6t@_2$x22S? zBao20iq$u_4!|wh@?I(1*d```>t^Y8!6C**nHu2`kkAI#o2;^s>uQ@|M6z! zCiqUef%CElaFJ*OB|7oCfst6>}QNRq6|dE=+u#a z>U5ub?VSmKEZCO|Dw;p{Pyts|^}YU1m6t6zRj^mokysJUb+~`yo9XM@W_jPG9HfnGZ9a#DbCkbq=tNAH+nn$C-)qe9-x*|FI%x#Ad{rxG?_Ie(~m*>16e;W1} zq!KGOL5{HIYRZN2z*nH5rd#Yja~Azl>xSBkbJ~yOUD7;Ib={ntr*zT!o;hQlx(i%L zN3yS)E@-_l;bh6r@m$yFlyzG6nuMgM2a-!Gx#Rp-K-DbKq=*__c4kHNpm#uuZyR(| zqS6-uKjyE4O>r6*&mvy=RfFA21o>e} z0~xkkZ}~)T;=&NTGc@Mo3wva;*fnU&?cs2i{X#*0{Kt<^%w!OjLMoO=x%k(2%*I&(Gz!7kSKKG<@UFm1Df}Z2)IuS2aF!8V4tuu z-;K)UPlGb<4@A%W-{k-KLU0hi9BNrVK4bVt@yqS96-7O@Ge;w%N=_>FX?_~MGzj>| zeKHdkY`~JpWjh3zM-M}1>lb*=WBwyJ!C-T&U%g*=?T+pPnm$^V<#{pFdnRuIV z^2dCFH|C`6|EUDq7uI7kI~GXDO0f5t+^eUJtlb`3r&Iys;4sM#dAsGif&8W%OH!!` zHBo(UDiZn~6K!Us^xlN$Hx9>ITeF4!=+EzDR&S&e4&{DT&}Ez`Vg=klp2M~Sak!$aP1i(H;~(-N&!l%p-IRvq z0}^!&97fvPqXyxf?546}^)1RIN<+Y}p9)kJ4)6Q~hY~UhBkc;?>DR1l=-Ixp+LXyC zBMs~XPcBx}`nnsqx|){G(Mq=OHcQS*vFB==~o|9PHPiSBg#i__l0lNIPVs>6e%(r2h}8(Z7vI~4SQ=;VYaf&^1!huCjrkp$Xp_X#yx z6|?c8HOE0HO_Q1l9#DWI=e|^Ms>-zytn$zPxzg=PktQG`OM#>n=@Xfo&94tl5bR5D zDDwGTE_Si)h1cs!Q|k=YI+{2``aE`8x&toC_V(zj)Y{DLTgV2FHTgDtUHcYWUWD@_ zZpKUeu$iu@s(_R`?cjPwg>m%Y&)|B3v=a~Q!O#V9pm-%p zo{z8Im0}CdZObh!V82z+-^8J1OPkf+(Ge=cn$tmMh%%PlwNjZ_9uGL)m{#m^)F~un zayP2clzvK;y1ehG=py0{QD^-IQgE+X02GbwK7I|C`e1wOvHI+c5m z-4(Nx-DW6B`*=ylO({#0{?kpbNk3LUrt)H45bY=AjM$es>X_Q&RuF0I&-S0Jj%9vu-*Q^-iC^uSS4Nzm>^m(DzRUCF&Ce73^Q!PJ0l7D)6n?y ztz7=%c}xWhYp(C1{;NcwG6S)mZsfllkBc|sQag?fM z1t{AqtXgPP`uV3YWlO(GvcL8D;fnIf0@wX-65V&Z?Hqb|Wp;no_HTI;&uGyJXvLY{ zq&=JHJ=SSiZ-BDp$+M5i9UU+n{7Ga1an?j>X+upV`i|(U4t}huR3F*zC7e?eg2uw{ z{>#hGFtA5FDdMK(T%wL*yV1)Cd0Z#r$zb1dN!4Z%`UU?V^WRHQFJ#|5|Y_SLp#`;ZUKrF7`8sD$B$2y5cf zV4Eso1io&Z{&l^(N|JG}sAxWOjt6eoFDXPh?v(*a9Y;@3zVz%C@!R>LU!Nt@3RRzPstiQrXp_zNiq-po08-O%lb|y*Kdk$Btc2n+3v;uqSdV`x$k?CnA{jK z&Qp(J()#Ac4>!8HmJD+oV%iKj$_y@BRw?XWcwb65@mjTwTk!BRtr8ZoqOly}YPDq> z2NPmUohN`^mvlivsh*!EU5G)`&iUb!m>iM_Hj`od0ql(3cg{9pt zYyk=dzh?B7)x1dSu*c$2eop#2Ck6n27Z;z;U@AoquUi$`4K8&exz;zYW@dzU{xK|7AOXZd6 zQT{0k_~DJN$Ug%fyaz4%%l9 z;T@8~x!EF5{ilk>8{2%ohz(rB{A%qT-g*nV#C7)d9gG)GAtK6`e2T%6B)1ooFP~~& z9#8DVz~@$O?T~u5Fz}kjh(qC1Z@vQ8?%&=+4c&)3$m4Pwn}d0Q+OD^TB3C?=BrWHF zD!Z$vA*|u?#XD7rURQZs=^@`cK0m$`myzRFRC@9s@qqA{iOFR(iJAiV$2rj+-em7O zf+pG8hZwL_zvv)+4MFHwr&saGEJPull!`9vpcTSD1aQd=dVism1N#Ob(h7e%CD7!t5m>XLie=tAx;<;XWg<5;ZhhZF@H!QN z&IE_tWCh_MyL2a(O;0U!qKK+N&#N3Z6UMRZ&y{fX{1W8LH140aF-zViT;rpvf%$>EZ^1#Jrn8w>Fd>w-ioaIr9m^0lsbIGMy1veW3!Vm9-*O~TINRP=@)X_R zJM_mYr*K=Z55!GedciN18*!%oL0w8$;fq|mZxJ{_X%gG zXJ#t8rjm=EzS{$t0@MF4#NajS`|Fl3A~b$#(m%P%m3;K4@)mjdkSR&WLUGS~g4B}f z)BXZ`jn$`mpC2la}tHY1lWt3G`AjKHGig5vg`rU1G64R9x{Va-+~X2ti0 zUv}6T*v-cFjVM-&xt*^IGM%o8g~^-3RgizskmT?})ytTQIZMj#7MrKJ4cJ}g%jci< z092dEWWOTOYZo(kIofXdRyb3vGlU3;-rpxS=M|&MqFn6p90K`_dDzpN#YOsW{yp0LDWw$&Fs z*5gb>?+*}w=Ke$C++QwbWI5hog>@wVy~<4f$2zc}(=}*moCtWnROVq1=^0k7xs_il_P+1Luk@C7$s0{5 zCz+bF4wW)h( zWqQQKb@^`Ak5YnDEU5(NVG6itl8H8;V`kCG&GkU$utOi|3eUGoe#fyJZ%0f=5A$4f zTQjdPB ziInlB?bTS7PKZNbZ&!`{Oo{VxA;)C}M z=vUa=_y%lwZ%prDSjiRh^Hq*GlnnUt;YD)DATPB)#ps@qSLAI|__j75;|2Z$?X51@ zKe5Y%lQenz{A7*og5RzDnzm z@+o8U-ToRVDYZx)m30Kkhy2Iws*jpjwvZQGtB4QthakQ;1pF&79kL`njOd83O?yp+h% znAM`U8=|J`{Q^!zNXQj~I4Xh1jwR}wIxvVq$aN*yADKvUg2-`=Ps2kzGou*Nq@d&O zVE5qt9$Yt0Zm3#T>(O(gi*b5v8e82sQi40Bd0d;3?~^duEy`NHl*ebSXpsD-VOa#i zYJwU5e@g42B$f%x%-r1E@_$WBe9EU3rRtpqC<(dXFW3Q4>`nB0-$;>fLP zH*$Z41};m>a>S#Q9K}A7fGH~}&WCR_M3)b&Ps^8H)4bS`uq;)z`jGwFw`;Vd6d%lk zvphgq;V19aJbAoYAnfizClQ@FRwB7v?u5*`OV9A2_HwvD0yUK~%diGAB^Bh-lfK~^ z+C9}_V;s>iNAnsDjpi{(x7v}nH${$Lt9Eu%RG{Q z%BL@S=T~RN3T!~`!R>1UUhc46k`iWWIM3!q)+m7|EcjuKrj-T#K^5Z%d>l5iBk${u ztW?e*#?4fNt#hy?&rRiMd0nXn-X+K`BT?ShUF(Dv69RkdCN=;t`fm{Yegn2|k^4h6 zW;N)K>M@uL>(Il8n`@O9PTqY+7mlSi$d9Tg*;)(I&MuX&?m`hKj=9i@=d&Pj8)&Cc zZT=UOh<&eL#D9SUlqD}h=8M1szxuXc<2BRo?8ZOUA>6VXPxT+$vP+eqKD)D1QK^lGHeo8hQ}2# z#m7)2_Qk2N`6lA@SY30IcH3q5;>d0`pgjIQ@@_}mHs`CH>y{^h6=wDg$zfDDsL;5& zRJ|dHRZn$f7*n=Y3sb;R)Hu9m7qBzQLY!gfFRO)Ed(XBO3Vh*izz1Tm!xq8shKm0P$+Q{hv zw={?sW;f7f&V*Q6hmbhnz`;Qk0f64PmbO+#7j23z)0TdD7n7MYHge42q;jKGw8&OXIRQ9EyD z5+gtNetJ}ESvqeX9!)V&p?*yXK=y)ks6uNj>fytOeZD<@f0j<%^rLpkfVOOijbtub zFmW}CTU^u$r2b2yJZPdSqdlquqdr|Cf22-QPeSvWXK1)Xr6R3Yy1bf{UN?{fsH=jAM(p^LObG{`& zqhjrl9C9!5MB<7o%3@|(t<0d%9>U$Q1e;s`{pebI*~@A{B`ajC(+n!!G@T4Qxl9sUUXfT=4=$LADaH_WHBX?U@Z5|*d;W&~ za5|Xm+mpxCd4p3+01*>+yCKO^l8O&z~^Jeg43ZgNp>TETQ>%#-p0AXTH z5QV~xzYU|;D&zO(@(Cq~%O8(eNkwz2tWnu7RAffiUxzz&FsWhtB{|Tu< zC#bofa_1*pWc7Ny>MP(Y8wtv;4JLO-iGoCK&2Za+mJiR*>y!yF-m|X;v5C(E?ti-F z&^fe4;Zh%e{RHkC9LfiDaoylp7#}~xsL1{`)#4W@^`XmioT@e7zlYC0{pTb>XYhHi zeA}!;{W^4`!XLMg^HJNYhX`5l`=FY~6Xcn@{m;$N^Ss{F^bZwZkpyXWX3}1QaIa7V zI>FzE(HD)AtCiaB6bPQZ0hQ9Z|H@Y~wX<`5sL%@Cpc}SKe)uH>$>xcGEJ|xl!Su^N zQO66vouX#(`W7a-L!f>L4}hTJcX>=2TYmvYti@zhc?!Eme-xDNui3e}!VjIYYZOYi z(ERpvf24f=nXc|m0sd(W2Oq~GBVxtP|C-9UF(fjY2fiuu9bD=UGvI;*6W|cxwpg^R z&*pm^%(9wmtGtq@dlc;0PjlseKf6qXsxhEti~x90WD89J03BEd5SEW1fXlLtLFsr? zG1us6MtNu9;i8%Os(m&`iDdr`{c5|hfD?Ny=)U}`4iI{ntheCAj?jsZx_P@IC4Cm3 zYWNHRjPTUT5IM?DeMsZgbCn0RX3%4l?hN3Dys=-iN6<|mW z{GJ|#PWrcT@hLN%7R1R{ROs0%7COnPRS)T}s)T`wEnY3P)Up9J6#$gIQA!C?f)WC5 z_05Thd3A!wRY~t(TQvPzoCw2ewXV7M%hD0KajkpVrw9LvC!h;jE2{(EutD%yE)Fv; zz3ofe-?SX;;s!XvwZ6wEBpsfc#HKlIxCh-?{!Zi+(*)?M2R(tjte1fs#l9=(Fanux z$Btss{y|ny9IK+)Z6U-0iT8R#zZzV0zq$5Kw$au`ITxCY?8Rk3T(*&GEB zJf%8a-_ZXfn6KZJf`H?L^{>(yG9MH|(Sj{2YNm_KpI?y(vX-k9x>o9-MZ>ND7vjb= zy_2&ZGqt?7C%pg4`apNVn%+SFD!Kvmbn@vi?*b8Tt>MJAgnIFTP%6CUd4cvUobzt~ zQ4vV}Jh=Qj>1Ry`de)FvMu-EjrHvflpmn@L>znY}#)yD0@dM4VgtL3H@Egb%y1Zh> z(M!^O7T_VLKkTpG0?htuW_ga{ufMA-L%k($CHxbFf(|tL(poo8gh+6G5yZbT ztFt90fCTy7DH3DAC3GG+YNI+O>%TizrvVB!NGF%?frEY%Uqa`)*eAe|7yxbj5eK8E zD07=bE~d>>ErTk_WpcO@r~!~ZFj(r3eWJZ62d4EiTRbFz;dlO-mVwSCtO4g<`qV6{uR*(!dd*S0+|RKE9D!m z;C`<3{?i@YyGs*z(yGQf2wjD5w01lk&)kVvsa_r-436iAhUQbFl4cta!ze^1wJ zzwm!Q#XUPU!}m@A)v68 z7N(a$`I|}84QW#_uM=8A!)In-r7FB^Hv7%b>u{=SUR7vmom!TL=5p@$bMlP&Fc-51 zNR6p0?=t~8>`W4G%BOADDFZyy^GTrgYVEuq zDi~}tc=JISP`0kfhvOotLy=lEO4!;dbvH&yr^!8Pv)cxO!M@Qyj_tG98{+0HC4(h+FBkZL;4m2x` z&lImhi@)DH@fF$01t4?3DN=@x2!48%DUpRi2nG^X;B9=4F@g0I0m67bEeF&1oY}6x zkSZ=mQ+K8Ik0S!+G}v59dbQb0E5XJTrdVTo*>Hm{n_v8$W(ekNmi+I@NM`ttaYY4&RoeQ%b z3wZxO%=kmzJ;H+O2X2)x;K#8kqo4A6R708ce^)w!4{2(efgjglGOlul5xR_2WP@)L z-t%}}3wRUr)B?J(e~g+xQUKm^dzDBe0vB`~d|NYjRpEOZiN0ed{u4yoD??BJaSZe} zSX)O%M(8}mcMdl}jq23GsEM)JT(Ku)(^Y@(LEVq6H6wcd*?JKO1*nW}?TY1o-a$GA zj`?4t>^pj~jlmBH0gb>wp6AgiC!(c)_^}b_!vAB|zQh8z-;*1%{55mhR~>8yz5SnC zLSpehChdM?L(MvYD7tsR{a21e4+g55hIvmmB;qYl-V%vk_!hfWZKA&a`(i%Y z$OO>f!*L8g^Q>zQNYQ_dqD&oD{Uk_h85Or0UFV(!uY}_vt6+pBH^6r9L47l;*Fx-_ zQA#HjB~)*!(ce-D)1ViunEf1-5wNE|{(eGBnBgtRs|TU1Z~;E?jzt&%3sv2{*kh;(^LUxa8_y2<6M zE!0_6m(MXm7@1LWT8ZKYE&^qa(d!%#Jm!hA*RvD-9(!TL!ik3%dO=U~C};7mol}0Z zMDfG*Ev68^Dn%EJ)VcTK8M9oMnCHS@$??-M|JX@#U8F>pT6=4Wm*ZBa^8>_k5q4G9 z2@yyVGB(N^_&j0YY8F{hwhA=j$h+b7SPL7 zQ|+IFKvLn5@B1G?t%>~z7z&V>XSs?ncI=EYI>NLmv?02BBW3xbU!?csMXtACTWX39 z;46roggq2Mo}4^p-;Ml8|Ea4>3wHFHM`TwaieJkjiGwk|KKp$*Q+e3w#8eSchdQonVcppfgi@^emCsT#9;b1IS&WP_S2Rv!5B z(dWCgA)Cy*uxQE=!s{beNe7CbzDn~GV;X{A%PMeKq+BugQMmEkbLIm|NkX%AGpG^1 zg*l94q)!V;S{$`9-d`-24#Rku+)I>vcbSQ4$VD(PBbTo3a)w_H50m~Bf zVf|u8!i?5mYB)v!etUJ;?Xo}OE%cF5>9D;4^W#3QG_u`;J@SMrK}JOQaeKxV!%Mc^R!#J3D0hCTfuV!Jd6)0gq;y7LRrxg*%z2n z^HE9Z2RiOhgU+Z(IaBkky!7%rp)ZUT zV-0!hcA@!yBpI_E*mpWaE-j(S=feDrlMNr1^tEM4pUEZz`Pycj5z;R>lizE-x7QPF z^*w9VMgBS!H((e;okp%GtYhG2>lj2Jm^Fy0r>QkF)5sWVW&%|qH2-@pS%PHH!jat6rDovR*(U7D-KCJY!KX1|5Xj*3en_C!>>*#;B2X8za&q|h zVc$G)B*WW~g81%R;7{4*mk;IgUa{ProT8qTCwISfpPF=9>*Yf2?(#rqx6#k_SnB$g zX-E)tY-C3)Pfl=svqo-lHU4PnnrHqsIKR)m>M~-60sJd8;&ctgWT*hw0jufJ}MC9DxN!pI~*i7=3}qoWZ{?6GIWe_zLY6LmyO zZ3bt+0sk?^*|9(`u-la;v8zk)g`Sh{KW6F)3a}^27jlAtt)T_~nOF2&c!)7no@G6E z_Y!#T(=inpjbXu2mtoE@%rXn|##iK=mPrQtQ}icxWo8EIW3149`4ejLd;Lhk0ibvi z6a}(?DG6@veRM7mlmp8fx-UDlcXS>%nDJ$BYXIwJhP-KLCQP-zYa=j8V&dY@7_K;R z0l#~1K@})l>;RqZ*tsaH0Tr(d!e=2m`$u>$nsNn|V8}}l36EZFrOw{1^xDWh2!Xvd z2Zrz_|rWLEJ$+ zbLG#H%e~myvH?RSQ043@me4T?2_X-n>V`D#k0=4wEL|>NfZ_>D-V1=zr`O~|z4#4@ zwFG8^rC>qK35&{ci;2ED;-=`yTn`u31TSZ|ZU!m6G-E@=kW7)h6Is#@ z4a^lIjs1o#u!TYr|E(ggin=Uk7p>Gu-6fMP<5WxI1}> zhq^-H`o=FGAA*YCURi5xX%glulA(mU`?=+a7)3{-~Vu;RRw{%BRx?_80E4+#plx0G)6D;i6NE3k(w|6RP3JOYoC~wjDZ_l$8A3E$bX1=`d-L&zCl5@>KVEAIKY38cksotV!yv>ZW`zrVRZGJD7}}Pa8IE7t3V64wB6$hIT5dlc^9F% zUlIF`Z%Q1+*$db<5Are(Avwz*;|P2?u+8#BZ4=5&J9XbQQlJGG!1~=>QR)IIB;_Mj zrNlFRGuBp^+9qiIP?FkYL2)y5QB+M>D-_u*|LEQ!KR8VGqBO||1A^MG)ki0;RCkg$ zl!TfHl-^{K^)Raa+AwTT^ITzg35IYxapf=B49+#iA=$RDr+qe8LAr7;C2XJQ6|Z(; z$ONDZ6(m)tQgeQtNyD|pykg>)idz#{=Xk(!K)DWN;Nw^80rm+cK&N?Vg5ToZgy6h7 zpfnl+n*J<0t;smdqZj`U^3}D~nd6bfSy;nc3^DW76kp8ZF}!Z%Edwm zA_(oCFXPnyuAgey>NQTzqw(>(x-BxP9kLWOy~b?qmYN^-oZWOAm^O>oU3}ZNMisBf zQ%%=kjEv%G_Ifb1Ll(Yma9shdH;86Ebw}hordU0|a3ru{IKH@wwV!cQJ>U?bLBnTbMz$1U1?wnuyaDssj`Qa)z8U{YVWe8QJBs%p`Kf zc~hdD3M&<#L8S2cM=m>IZass)$l>Ef+`?G!S zeG7*%4F92!&)UGzOHX(?w1v!hhb}UnTciuj?i1;qzg%=Gs=f`#$E{IbW{BXS<35K=nYN`6eI)l~dhj(?Psz|7*oxaZ|KZO}cdV|Oa z&#dDxvDgo`C~sr1sT{~<7;&~D%{jNyyjtW4q9EE#f2b_ZOngd1DWM;pIn~Yf z^4!gk3#5A@4p`=4u5QNI=FrE_s_ZwcQ{}hj+34=|p@;F)@X3dk@krZUx6l3484NcS zLZ4OGF?bu^MOIC>|Ithk!RdczB~@*?_%Em2v~y{<+C3g5%uDBQIfR*#3AKsak&B(k zSe#I#PLVpIVyMQzecD7^r<$(HfT%$JKR>u#*xast@RSY4JHfr9i>eWyKgIGZK&a}3 z{7wZXeM!PEmIvfhso5)aGMx&oe*s&pPUhUJyPo@KK(6~u%K7At%CV(c^i(I=v5&L` zEi67}G{uRSH7ZJqkK?<0f@UxV56}Y=L;W29`(6YBWb{E5xC%A&9MY}=c9m<5KYwU| zl#Uam7awdAsAS9d?KrB~DzVFk8tGn-rk1g*X`Y!E+G!wLpQLN3!{fVicX7BJ-f2(% zDAZ>Eg0~?vNyw^rF^ID7f}Y_~J?7#G&4*e29V60Q#bFt)_o~o{k{XZ4_FXMaUIY=$ z_rx7K24k8&(#{O|>K1&2eZ{k3g9NJ0t;d=dx8d)}V>2yYLo@GTC?bX8K`wI`#FM=y zympq-%Oxu~iBBfmGSLz_q?)qEI@Yuu$%6I7mFzLz9+&}%Xeenx*039B*~ynbaked) zUO8&k$AFA>WZoq;%LwT4QSsdPx6j;Esz>0cFu_DR4|G zL1=!Ww7Ij`GPrD-w0x%7kR@bIeR3d!*R#;FsEl=Ic-Owte#;x8=;H;^b>v_HC4arm zj3Sxyk;t{@DMwEf$=DFbcDYIw8=iw2^XlcWY`hDC$Q<>sYWmokD9Bzfw2<~mC-eD5 zkXJDj7m2TDQ{(nlIYHL3M(QelC6^}aHItMxDB*VIS^)Kf(jd#eNc|zWmWZ`~8^C45 zV|3)o!`R*z*NV{?9?4GJVXzTp#shUZeAkEFF$Ibal*al`ov6{hoa&u<;Mg3C;_)j> zQI`bY#4s^zb_Z?G;ZT1JQL88l@twf3mkb}dm>Gs!$)%v53GQTgo5P;U4jogT zy-USATQw~XOmxxzP;w2in&BiP_yTPz6fu!184DvVi9w6;)0y5%XjDcT!n9)8v30_t zi!8TjfK}sJAZ7hT+%?6Jm3Z)`Vxb0GTHP1~`7?=JopC(MAm9Q!!{+(d&7J&K1KG{j zoRO4`8I2+NdlK{onLX!__qsV4i83z|tsh%^$a;N2gkp)>hRKaGd?SxhugyvIKq-69 zm{GKBuLdsO^#3y;f*d3QK|~mFu>wDh#4vVHr#h%6+q?FQnP<7HOo6*ib!?Ac?_ZYn z9es;I+=QZqCryMIZP#7O%w`zm34`IY_FaI(aC<*Zp5pZM^NC~(L7M$qOctLXKNc)o zsJLwo)*IAbatG@*EPK~jI&OnihJlSM=kQ&4664g(ghc{WUn2Zs4;4*a3YA_yfp&@2 zh#5I1IeG&6=14}tQi7V-3er<%8b*9zOYB9|?{I$>_>ocL5p`?pNM}(NQ1WzJn3?(#67e8*t zAm`WbVWh}jxnzc>n3dv-^ID(MA{)D|Id(6ke!lsftRK`|&sAVW>LWV0I?Y84Sd9mb zufvmeE(pCvg(PQTi&tM~g{>3FXu-wx)b;gZmo~KXi2kM{xePh4(5R0HDR9JmPt`$+ zH6mOp#}If&T|XK#)1pedDG8zl+2M8CCfUn%kyvm{$1`(NnmmJ$UKi0`8KK;;Xu7r| zh_Vl!u`)Do%AhpuxlfW*>*Ou~^6a4r=t;8eUO0TVle#OjvtGp!GG&qLK~}XPPQgO- z*LqRk2q+RYAwHM9FpBDW970qx8#n?{8VpwUjuqS_>OinE{Uy(p<&tlw!(`7+T<{A| zc~?MMkv&m^2Z5Gi+?#{@YZ{{teTvR)Gb+H#DTt=2?(JgvXU@_=S3`Kn0(i7PH)M&E z;cF>E-hANPM!gH(JcKFW6>ZOj8RiIj)NjGwrVn%+P3Nvw-&frnw5f#o?)7-mZ;QQQ z;i8paYukl|gk_4yh>~U$24-6Ic%zDUp<%%HdpXzUm>a5X;F+Fum8tns>z2V&Z;hv< z9IF;345xQ@E)UGU*9&9m1+VmzVCt*J%capb62{yLoHEp^M}Ez+z=aB`TSBobK?n6!I)SX!Vw>L(?IMgMqGr5Y9!fuxXDNOaMKDc0%hJbdUzEF8(VY8YBUb^Q zoiTBbQ{X$7>AeY;VvM2QO(HS6N(y&ag?HX5<;J57beBcbGVH@}*=Ftw3jFEBRj#n& zD`sRJv!>awiuunm)RWUlSG#FsH*0_~MJa~bC)f7%BY;6-sKl7Jm6$p}eN395p1TF5 zX_bhRDai;)n2+Cio%AGOf=zy%Va>nLRdG3z%S*^mZVTh0h01NzvW+yXXziC|{>yN) zYZ?lBfq34wVxf>UG0eF9LblX08&d1})bJ93qo`|ew`WY1A zX#VU)QJg^$#&YRIsL_;0=?H*K#$pl^A=rJeGSGc$1r~Z5xsI|zRWGw3_zbB*m{Fme z#K3`6UwqYv^=FP$uSd^rZ;c_HKf`WeqEx>#9G*RsvBI1uU$&x_I}6{zrFTBv)OqGC z=W%b$Q;Bcn8K-m7ryZ}{?0YL^T051e`w)R+BAIndL^x9d;*UG2u!G3zE!t^BMNrlU zuQD9o1!F{R=?YTA*n3p)T5$IyR(iC$7|n{IOorremGQQx$%PbhfjH6#Sb zMBkfbsXM})V?Q>quJfMU+~2`4H8Ud%bJc$qfw3X1Iz8ZTx3CS;6`JuLO1By$_ zo+MAfkxWFyXV=AL_ho^2(=m$G?&L(lELS1Yd*^ipJNn(Z+~!85?iil+XI#wPOKbSi zAcWfe64`_umW9H(C$xtnCXjN}{Ro7?*_EKhoek3JBoBF6w^TF4@ncv8Wo>@n$!GG+ zeH*!c_uHen8U8}P{u2o!U5-y9e{?2eLWmgVb@>8>z%#Nq%xvA#I7@(|`j3IcN-be( zIM+8EbpVrtCXKJnOdO-mF3!fU#lCV%zxEU!{8?TRPz)MAA=x^@G9Y4N^pP0d$zPa& z7KxTtP^%9>VeneIboGheUeSF=OF(|vvNse$hVmR;&T`NGnK0eF>1{}6ZuCAxV-%c5 zWvt=dbLp<111=q>`_ETKQ=n#e0`vig^v+-~K6k1bfGt*SUk%rH+_4`m2xE~je8NiK z7a?Z)3PNj+5L4;O8C9WqQpbUPt%1~glg0^Wbn^?~5~M&sYn| z=TZ-s_wu)n?OuP$mZwSbeYs+W|4bp%n%fIR6s(x}?XJ6I3j0`I&2VZN3FpeB7jI^0 zwV)**>JntI=bcw-C0(I@-ZCN;x)hRUj88Hw5Gp=fx#cpNemacA&`?cg^!>G-zWzE( zuqTu~v4>%Jk>pr`joXhyl4es+{Md>kD7me9*;jV5=%6;6bh5eDXD-Z}if2qe>mrJ` zk<1--H|xdmMq%@;>*R%DTYDy*cei;^UhiRX1=;3#cM!*c0f0x@UdvT_Z&1?XMW4%? zOZKYHiti3-w3rz~o!+dW+f(z9!H3WeD^~~%HJgA8!GI-YU|PAq?*(O`KOH?s=(DDF zht5{c_mQ1;=N|n!M4cj1UKQNXd(zpkM`Q4-@!5LC4I0})0Z34)+_L!xL`@;cF?16KzfwFi>+Iy9YgJDBOVQblMS9n}XPgQ^epgWlr|73=bp z+q0R58I)Q=#9!A3JuJANQU2!F!2^VW&mcYiP}u!J$@QI;2IN3F+c;Bo7BwyK;gnJ02> zpGcPwc|Gpps|c_dOOa|y$BSDoJ(W&4EjIjfCU_X5gl&18V@TxxGir?~WU9{-Jp zX5^FP{e+Nrfdw7+MBI;?$Km(yPRV6RIPZkL@+HAN?n=~ z6q7q^nlr{igdlx;3d=N7LbIL9y>y-0LB(GR+?5I3abh}PI=Tz9tR%`;8cZXP8fI47 z7fi*uQCNzp$(W&@&5Qa(4pUq7B%Vp>%=ujmsFXu zRnJ0s$eIEu-r5t+&A$t$Dn56dvQ464=1=dbD2c;9LpXxIUhh<557P=#m7FcS`3iA$^K$>B84})+T z{T3lGPjYIhBdAEohNYNbp1r;6-8V7C@=1CVk(uC`d&e82jz~q1Bm_- z-FNJOG2=-cy<|goIcr3=FmmO_=9-s;B7SSCsbxgpRf9arKEY$+j&t52sWf$rXkxD2 zJ9GSGpG$g4D*gWEk?1oUT&&>@KgF9za9o8V>bn@rc)j8-uL{;mx#BXW5Lb!eA1lLN zsy}8d*1Zn-a(_lFwchw)V^5JkFo2mQJYc)<7!o+}^QY47lUrXhS>5^1dq#ayaV&Lx z9ZMaN#kqd6pC&1JVTCA^TuCUH;I_%?;Y;d zzCvR+hscgpuvJx|2PkvKIvw<{02_^bJ_c150dHKLgw=R_RG5zall@faB0V4zmJ%o}F@=ehD`@i2jkC`zKfytbG_E~%Fwf9j^95|L%`Yp8i zTktM5G-okb2KizN0=@w3JMwUSEdXiktxE1PSnXw7FwZ(kWFz>a3s7~Fl4;-KZ=ovS z)xe%gm&6ulfZBe~Amo1VzRQSO2&MRgcGX2#fiUYE-t>%hk*X&=X=spG!W)$EB4I_z zG%0T}!+cmy#8w@pw7ZrtnzaA-W@vNvrrI*KYU3ZSkZWBUsOEW>eT9D)*7)bnO~VEG zz{YGmh2+x0nBy|B6ioaZm>C<_x0(|2&1^aN4-)fnmMvcaIor?F4~A)-LGqM3CC{3K zWuV_QH8zSN_Lw%c3GkTM12l8&QGBEY;3xVZ?%Kxei;Qf$dK?BYjh0l`-%>zqNpDQh zhG>Zyj}9KMc|k5a%sq065l=o!4$x;@r$&|3-e)A`m^UlXn2+Am=3vfdNiYLY7BWNt z6An|X_2YrUxG*rWq|O6!haeWqefZ*f)Yso)`y+K#1v{6@hI(nzr%#{8YCNn`|K!Y} zKqh`kQ8mAyAZZHj=_x^aND&Kes>4cJr%!|SN^I~)^@&{Hmm-GpFG~X^tL*(=90es7yxg{6_CjdvVnIUT&Ge&J2i z?jW$mREd8&i*dhj?%%eaiCL5nS)dwpdWmc8gBaI0_ST+5B33>>>MfOR>+ARgb0T7X zj7kG!GO#ex=DMVS*9o<@gY}x`yu}{ye<)yF z0pdU*zb{ZfApQ0TmXtrr|KGH951bF+aKrw4%pwm0`f~M3N0|sp?MUDUbp8o%fRx*? zTmt}efr!P&uixRc=#&!-d|dnM|0LTl2H9EetAXiKkKZM+vxyJt%vpyY>GIn688=Z^ zz1Gzba_wS_AAzzLMaca*r|Z{N1h9KLyKgkRNAEA2(A_e_tH$E^pmnx-P&##TV$0lX zKk8#}FQKYhnQCsjNp&|iW?ySk(J@c&+3~A_hSY zwr3vkc?kzEU_vgQy$A^b-Ofa5j?{!2$-_5L8+nv$H>=xJ{$Gx&RVE>>iY#`vzfA-MEz0O( zTl!=-o(LY@sRP-kZhpaORx9mt)R*f_WgC^sTg3;|8!V0xXu_@ z7R7UMXJ!{EueR}w%kMyn4iXTNlp?d^Sa6KWYl_ z1R~XF6XQ+z`wfE}me2vo!I^81h1p6Zd2GLr9;BPsNUfQt4}N&l}MT^`@CrskA!dCgn&yB$Z9Qy#Cn*v zXikvKrbR6tsQkc5SV!zHH(j#Id9rOXT(ef&2tGiXu<;f~o%Z`7=fZnynL4Z|RmjZo z7=np62cfpdLwmTa(NXhh#iQ_ZO-3}1kdg9ipH*dJlw6w1NVa_E4=v~qbg|U)D2NHu zsb6y^=b6|jYY9X4tHpP24kH5ImeGK>|OVw0)-!LzlKY4FOVM zxLkWc0kzCV9`_B>(H_l6Gf;~r*q zis&tAa->vmoux0?^pZOHk1L51uj-n1|B3nlBUavN^bpVvd^E8Rq%|xV^NvAXJ*R-8 zQBym+V1QmR>mE8#BbKgC8-1p0YJskTmN#CN{uZAq9YMA0bXrCFK9E|Xr`C6y@T!$F zeLyAV-mj~maSyamz?Af0=I1}%+g>$ydWD07_Cu<7U78MEN}exin{lJWjb_kd*NQc9 z)iB<1!ox2rB^uz75cJD(^QgnvY_n|;*Qr8YSIJ^%b|vajg?i=i{;uX}{tj&8>h-W| zX&Gj=P0i-NKuT>9Tw68oJMcuK@Qyy%6BAN>+S=Bw_nQnE879uTTj_OhGK zcc{P6fY~NavNQd&ZIzg225U+_N^txEZ1#tkcvDEy5g87iBq;UJp3Z&scHi+p@xJVg z?;G19ZsKS=m=S&=NU5^oUEga0I3Be{v{k_VJWJPT&<_j4`Q6i7>&@ajLVV#s^Vimh z%cmH0TOZRE;ChDW_WQZ355g^ibLHYM6|NjoINR5;-o6a6N5bICy{`Bc7ocH6 zkch)Z0DTRYvZQNYXL)okd|x*sO=0+ARPD*Vpt7Lu^qMo=BT^^=CwQFU6qxfn_-IM*Iw7EMv9AHd0kGKV4Gt z^YOj?ErYT;*j1*935W|dwv+mEm5rwStNX`zkW4K>a`9ZM7f#Z%!x4%4<31TL|qyl^R1jVGqUuRY751-)PTBT1`_W)w`5qgu}nEB7! z1Cj63!=y>G=D`aqG>0t5Zh0F-ut)Ppv>50BPDN@MKg@Dzi#O*XO4t3T1{%A@bDVi% z=e?xJ3pss2pOLmZh2=PWo^G=gXZYUSxY2?z4;2&?b`1z-RZ{?54wP_2G;enGc^ zKRMs)c^AuSdV6}>VY}13B9yT%pc$YN&y?vmHTqkQOxyw-r?|B_7RGX>f0V&`y(+~G2BfEl^n2?Ugy&I0I%0M9s6N4OA8!3l$ zdB#8ukHM(_QGM9nvxwiL8p68oCAjs7@&BhqZn;ips(_vw-{}&^IzW2rV4Zuhno!;- zF|k8k4+C1{GT2!|gSAnR4y-^vkVlSkI?>U8tLwi!mIxE!CTcBm-^5 zSiq|d#@7G>8+l{Q_S`SYWxVD2JjvMRJ1B*)9g%90`{q#-$|PU$SR{sZ#rznXv6v3{ z94#UEntCppP5Qr6+($~f^V0h8>xz5_HB=w)|o@! ziw-}bY))iJTQC|D*u$t}{!|4w?=Q_&j zO&BG$ zx^96b6;&vL62AfVFy|qWfDA^nqnXEd61nn5B39fT3&EqG}i$EA^#ZpVdZ9NNFK7^t)1Ps6G&9R{&z z3J>?QnH5H{`z?c2)^DGGJn;SHGA{{|thV~={SNUkp=n(J9J=l~No(5A+j~kWjN5&A$o9Wi}!Tn+FUm>KHyudTb_}T!F-fzLJ4b>h|%u@->hyg81kEf+ssI zixrCY3GDl07WqI+)dzuN=_R|Jg?Tt@5^^X09of#)6DZ`hdz&b_sQW#PFgrv4;5$9K zjI|G^jyHm-OC+vGQ5>jT-jhU$@7fOfKl{UAI+`el44!f7`lIfs#KwI z^HZV*mjvAB#_d)-vH0tGzqYW`Z^~Qh6#Cpa&OeQy2oCoTJei3-mG=UpB{tOx6|DM* zGC>Uy_&c*H`vuc-1a9;vPBSUueWIB@+4O9vujk(}w-*s4+vZap{g^_eXL4@DC|`2K z{P;5AQq=9cN-nPM4RW{jo&uys`6CR6V zP(d8m%G(xNmKlb_c$YKC1&2knFOo+WL+D|Y5pDC-Fl}@+tU4p=i{~Z?X@M3^3g&;% zaJc;M;t$WhXSs9{+7%m4>w$XW#^1=JKACtn=YXDEVhCS-MK2xz$}^Q<{{5?cOiQNm5 z{NK3-zNppOAMf2Qfy>IeqML_pg421u1QrWU%YaF7Elww(1!@&j1AH@b<-7G+O>VXs z=|kJWywrk^hSOHaC+!H4*j5HL#9g;5*ppK2^#wkOaqDRighn#bkT?=IP24KlFX}oY z)OV55Je1qoT_NH^5(cuM?-(dVHj*n%z{Gqi==Q}BvhY@%HEw%|Hom{WCQ!R)9I(ZS z1Z4h$cOlgvVl$fet@+$>XK|mCX=ro#FC~z{lgH`~B4y4eVI~S-B?NQy_sO6o;@O1N zShmh2T%4mU@c|`Ze|I5>Twk&jIc(2Tvihikq($w@C<@Cui~`0aRyOmHV;Ni+b~G>r zsKYiIO)@A0YLPNihQ=-b^!2tY&vW)@&x73O0vf9P-=W7FVyK5iJzJP%kXs%$v`s=< z`_BuHiz}Z``bACi@!7fLbRh;@y##z3>Z@ zTvEbY#9k{kcjnR#|G*uyV`r6ET3?~-58id#GheaGE;?3_U@DTypYv{~vU`Iu!@$h= zst8e||pVDdY&jK}< z|8GzLxA^`CB`kUxe~f`pJ!rC?Knwt1i;dO&{7LiTL{5yzYqcg{GX5Wq8DJ&eTJDzo zF&VKLx!#6(|K3T_B(=cTKRpQG&w9HW=YyZnm^<)e7jxT8uTrWXdiiCadu$k#T#@eTc< z&+!u`@{8JA!ne{qFJ@^TLaH!t7GN5UHvA=j%!Kxoa~MR*=p0>%0IY^2=dYY~SX+8X zgxg5ZNcc?io?pgE@KhTm6kKOcYQwcF{*QNM7;59)P5u~igTan%b?o^@`VAfr*P^7? z)$(Cm4en9g(Ztn)j6{5fvP1!sqs6+lrh@tWxi>DuI8pJdk<2!*)fs)R)2texu6W@$ zJ}cywsL$&yt+KU2&K0(E=n_Pef?rCV8ahKk;;F^2A{^FGLsK-e5MkF?H~Mb4t3fBp zrt*#i>M1&TG7`!`P%iJaNU4qtimX5_6ZkRruiGlm`)Xb^4eT9%IkDUpJ&G%vL$N}* z%k#YTd+*;hXO|Tuk^IY`g_@U`5mus7w@Obvb7F9QPuQDM-`^LYD$qEOzUs$2zbh{- zhWgT!^SDHZ32${J-mE(8u1){8Bo#5Q&R+ck=OF8J zLqJ(4sj?M5@xGR0q~ttaf~$of^=xbw48nBm9Ox5+eac=lNV@e%K))bTDs?Z!&w6MD zG@lP%3wC#-xq6m$Qf8PgeMu$-$c6cLm9P4#C41mxodvc^WF?-9^sEmV zI1&#Jn$J=xRixuf~AwAa*hqbf_luOGDsv%Z4_;w_N4A4EB)#o4N|%ANF<{7kU5V ztNA|;r$xrQ19qj102TpY9DLQW-?H#KpHA)jXMSyZTgfb%&55gH!!&z)dr=rT`u9#JQeMBScD>C+t=)rqQr^~BWnvh4tuMk@AV*a< zEqYIj?E~vh$;;(Q%1+NhwRXvGT1|T7dTZxnUOJu#K_sUr3rN|e7t}~o{EOv--JQMo z5g|jfYA~o9s@LEFI%s-Dl~JI*H*N9!R#{~i6V4oq25RAOG`8|%B zAX)|5wZL_?Vmn>sP9XzlW{{{Pi%$0Dhe$1wSpPM3ZQIOcGgFN z3xS+^u@A~iwo_h}imO&FokQ)JwLG_!JbV95S5g)8!>jOf=@(r^W+K%dUiROg@OE0= z4=wAI&c%0(tgv3_|Ec)Op3TSAY}Mx>zk2Xs#YVW*29`i{oXO!JoaxT9I~#}rJQEVwW9wjOh@z9oi>QT1pq=d!-$ zg_MSG7_fXCXMe|fg^=C$L(`(~OU(7Mze~rD)}j>)>$lzIV?(+u<}5JQV|Qa?-x0!w zFx6+Ys%vui+DYx^3EO8>0_28ltx}qWrAx(!pIF4Yvfw9HWMAa_++?s*?QxMz92pte zuK!dgbaW@gm%rhjOLbkH0?^78nCec-OAs{}W7?4us91V{eR{Q|1m0dS$}wURyLCyg zMGhmG+@$=T^)>$rzRNF%#-s2$xf&l$Ubuq$+zJm>kRA2nHZ_RWyG5^u zHl7NbFuddIKiy-qI#l2-H*#@?*PcOXPhH6~du>by+KuR%QMsy$slUXhF8>0fzfWA( z`=;ssOC({)N6iZNX3J$VQLQ!=p-iHTy#RaVVC2e7E3de%Ed{Yb?X(H|n{^Til4+20 zmu5-)4-@9EKLjVMpAc!29&D(0)+rtjs|q(j82X?>QWH)+doigPQYLT6?K>w2d7b|t zn&mEMi4n8MbSf4(I^57nP@)<<<-5IKccJ8CawY)6gMF8=J@3dzK;t(q2e4qR7L_1- zH>JTrocvv4{V#Bht?I%{pD5yNF2w79rn?;^mlD#%%dpt?bMS%W+)dF6mq87En?A(= zIn5-wTOUhxSst)=-CkJAbMML z4}pLjc`>)S zz}KwYG)TC2>|u*mvK8Dxh&%J?ncFX!4jWvNXvcr;gX_^@UG;Z!%o-tapp0&XE6sgH z>8WnnV8L6S=H>Su;h!3O-cbsIvY(a8`tWV|-L&hi&P#=wp(P(2En+y?H#?fFplRjT zfzdX1G@1NA5C6x}Pv#zW99n5K>tce0Oeb^kv87~keUV(UY;gi2TJnEKKRGw#B0pJ} zgJJB2P%QOiVwCr1m-OJLLv4tBQSL=|;ktECMgQ#=?$z8M&uWGM9^HDa2~V^-FYG6$ zwCH(e?s4mWqvXoXRLPT%^>2h0qSYH$&%fp=eARNK!o^j(1+-hthu#kH28 zdlnH#cifZ0NT2cHRb%zO1$O^v?$N_T28LS&uBD!yEkdJ_GkGk}h7VMxzfB%o1x*)V z=bZ^NE8YKkjDe%L6TQ)i1Bqjued6&hN)KSZv5g!3~knxMUr)qqkcx=ijobYs9%*2aYmp5$1bC1 zj(?xh3F6{$HNd0H+_UYX9Rb( zoJ3mpmeS_TDC(2fzVb3^Jdz$w(CSo42+S+8Jo+fK$RGna5=i_IQRjHhO148JJ#oV| zq$Wq|JQQLRa`ec4zu0xx{|rNWO}hZCZ6>NInoHeh!Kx1!+cVEUcio|Nn3|l<%#F%S z)skN+>6DM_lgYAAP}^#`{q_5E?w)}0b0N80l6q3?MTpEi?vK!cj-(&{r^}~Lm{?d9 z7$9v&MpGgXDc7y9GVj>Ro|}98h{dCEbs@py2Su~#O8)g!y^w{at0kNI1cC&^?UIs` z;3+2>&5H zq`QoSLr8NY5BuwswZ>ruTlq^~4WCPNRwZQZ67B+g z#%zQQlg&AeHr&)I2jFhYSV41b;j!5$1#TDr#}T{^m-olqZRJaOrCV99x2yjZBhlV} z{PcUS0UzXW6DfHVL{XAyh03O22oCs>k#Dvy_Y0UP`^P3I0hd#bFdYoQ&X-_;2Rt zA9ko1Hzj%YuDdIwrd5>Z>^+k9qqmj$@37PT7@}|+n{a|bfVdvaev`FTjJ8)~$g*6` zqM=g2JsCOOQ z6|Nk?FRRa$&-HRYKk#gvcQl>W%fTB8CTWRG%MZN%?25XQy@!;FLo_lhBuT1CanoQ~ z1T6oLu-Ky;vYP?6IRLI^OIZfplmSNPRV)Wius_AD$BYGX#EwjL0CL zyaWqrP4AVYBv8hdurdx9X5!KX>6R9#zWQig&{#-g87rtCHxjq2F9vssI3Ai@VR(!U zsukS)j~aEwuWE11PAyLV!L9w^+kMf;WC#vBbEL<~2-`GCFR!{$uvM2SB~IfsStSoI zZ+~jl=-7AIT3W92j%}BGF23D)YesI~(040;6Lss>3SZci(}AH+ItjfWb6LaFW0 zEMRFsLY&SN6XmjAfA+q4&~{TK(`+ES>9R@vSq}Op}lpc>X|7!p7O2`1Y{+bU=kQ(Y0pd!QG^2!99p1p7%K;ON% zwxf{D3x9;g!)B7w*hvVyHWg1bq6OW21oCpG#u`GD4tlz%?8=e4_=QsCBY*d2V6A?f z+f4%Uz!Jc&05h7&*)Hku2Y{E}X7ZpidY$-nD1#*fddB$DEO4%T6c~nC!|( zDH}|N+C&spqLBW6EVlu?b@Fk$>?pps#8F{-1(maeVin(bAVUcVD$`ul+&jDbDSqzf zN`uywABy+y8@(2+$n+a|W$wy(olh$c3=Z&-S*ZK0bM{#|!?ETIM}gLn8%{c!vhoyw z&!K%ngx&HBS@ol;sZP(sM!R^7wQ9RWo~r!Me64^sH6&yc)c|4807*pg8jQ_eMpWTS zBxBs?IHAFZ{r2&*NVm{lSm&e<+hAAhl`m;lqc=IUkaeYEAEs zcT1wgadrC(DSu|>{KFjoX``?wC!QA78ph#FhrFB7jJ28R`N@?80eFf8p-&K9;PIe} zI6MrJkCX56>yRD}#HIEyr#nVK3k0?a(Rj5e{*Rp`hm=x6<-^`x-j(2w0?9UzZ^hq3 z58-3_{jLXt$SkEYo2fg*W=_d|q9MnU4TaBk)jUSwAqs5NON0qR?wp{;$2eBOhgy*5 z%w|pR-nI=mJIbB1qU_3SpNRQD55q!BeDeg>ljQSwaea{SNZz}rlV`WALxWb^h0I8U zf=@@x!v;4U_q0;YsuCVLmJ5Zxxmw&8)e!xNL?A*%eL~$zgUiW$Wk$y&A4KzgQ8?E! zI}!*mwDk1G&}(2Iv1*@=P?j945}d%2K~>7 zN}X#+evSx29b-t<28-(F@wGgiPUq^?HnRLk5RaJ;U3xRlroxW_pmSn}H2IjeX!woD zMAA~Vxox7FUNKUBsFc~5WNBp|R>fye?iLnXR9G|NA#FR(fBA zV#={XNb!N;JIg8K`JaL-md$(Dx6hhXd~#DuuW{w?ELd2Q5V4TQ8;V$Xrkl{*PaN07 z`BR$$n-;ELLN=$3%^n}(3H!E{i!E9m3=VEQ?-Z!P>GOiHuBS!i;WIM40Ww~#YWnvP z;te%Cz%zc`@2>iNIdAQ=q4%KVP*Q{uQNDcWYQsLXEOO`GN+`a(`rC`Z8gh>0~aHICD}vS1}hlh>M?N+wTm@ zI1kpp%*b}vs>&=)ezX7=;_K1j*i{{y#ks^+`C*$aTn&}IEV&jif|N6&`QI!IlP3O0 zv(^a@hgGrt37lo)EQ`aMM?VYi^9Pj0-d?c|y~F)G(D*&RV^5dVl75Cw;=}OFQb0$D zbol}9oFJAPA=_`@ngKO~l{t5u;hZ@WfcrHE5234R2fkf-l5;Ah;*#BUnjT=k&AflQ zYqYpcGpeDB(XeJ~0jujxB9TTYAH7StHzUtQ_kZ1h7t$j6moI;-=uToH@&y3Ah#(ou zx9vE;-i_bzq8Cvzga`fJ?kgNhDM67-ZWOxb)aa^@(@Wqjg>gDbbZQ-;NpBSfRM)yz z-V260mM=RZM|UdggM+`#JL5Mi^NRxA{AMD#EEHd4x{=D0ag==JV0M`bjzOZD@4KLL zH;i@$%tcRmwvICqkdC&!aQ(XHydj|@Qy{@10#3e{;OH1ueNFD0x(JxoFVhs@CSGsf zq}w=1`PRE)nsZnsY}hj)SK=XmulvDC-bC!OItwB`g}2C3DXDYmXUn#AN1pFG)C49n ziNUM?%f~Pa!{;WqE$C1d_@NTCUn~3tkbo=&^o{;Zx1W-7ESl@xkqinh=^0w=Pu10} zwU+oyF5K-uw=?@wb}2SI%4{#{7vUNsci`EVv$s{Rj^-7rM{k=7HL_gtjt8diNc10k z44g7tj#glre_2=(=ChiYL;6+c1_^{#6KfjTR`S&&x(wmJL&~Oy3hGtmTu495FqWBs zg=5-}TU-))c>Z|vSM;RD$F9G=KCRIdY1OD&_$;=2SHxCf0IWXFrW%!In!qe$* z%bJ0GW_u2;{@+Gh)gxn(G=`n(t_bmImP~2V_0*AcNdDTj`XIu8L_rlaop)t<^Tc;) z9(7vZE6Zu|(ujedyGTB(2{pN*-KCWL{U&n#1)m8-Y|cYsxtte2&b)V$7*pg(JTR+k zt$?ylDq^VzqsY`xjgq@L$)kk*<-;{s10;@Vb!-TX@USjfri|qO+vJx@P>J^o^@ZmN z{Wa_EOEcq(+`Ck&IheYg+#XGK7EryJfk4&GIByR7R~8{Lm}1G!l;f9DIj&yo(+*ZC zFEY{KmKL`-#13e3hAx%Bm;+>1=-HJ&lN>l<;iUDgeV=&+U`x_5Yr&Vr#@aNC1fVVW{~dY) zu&|bj5Jm>Ohx8mMf@}H)*Hl2Q-XnG))-X*eJ(kc@VDxxUdQUd@vg|2)eg9RYQJ=`c zbcg*|<;LGKi!4&r8e_J)=XZ%Xsd1GTC%$%uxx4gl&YfT66U^vG88+lcF;cq}k%ONN=zC)l2lYN1wn21< zW0Qg_Wn_4z8d(D7C4R+=nOh%zE-CPFJD&#;xYGj7ZAlVh`dzb zWTv{^uO*Nk;!W(8@BaBS`{+qYbw|S(>L}?`V}5Eo73M$-JjohR=-TVG_W3T-T#zLb@&)cUZQRBN!u$XlMmG{L3AYAa%~L{g3YLe|`N9q;h2~(ZTKEUyyzGSRyCy@;QLB zUS+9qg8bdJ)@3*AVNr=u)hz4H0vw1F;YfB$EhDw=P@EpIGcXXHHRvb1le)UPTi-4x zeA8(L+LZxVQk=E*F&F5d!u=sjKqJty>PUncK8|0Go)rl7bPor^3U+YG@xSutntpK! z6|DVi@Ik&XIQ1=VDotkYq`}Xy2PV+zUto03k_@8j>U>;hIScCxbZqZrbFKa!RN;B8 z{v?s;n-h2o6CaXVg6+zU9BXS<%tvY{^ZBBW*zvslDA((OMu~F|n)E9=p7&4w-+)PR7!tEQkvMc#rgke?E1xSQbgpFgy z928SWD&N~#j~ALvYq<3x==zqTZkKNd{QS!Xv1KW>+WN#*(nC+H;S5_`pdicxx~i4S z!nLuSkpG<3k3k2&3GHPed2!N{Z~TAk#i3}+lf=wq?q&aN0`@~UV(K!C#a6^F=1z z-eNUK32P$%uxTL7%4@^6u(cpQEbJFUS@oiNr}4JX==sshP0mw5o)3z+6WEuw8;X4N z4te~qHb`h-^MwwGDx+p}eZsATjVZM1p7v6=GgHLzmhr_f$Td_xTJY8O+;3P~6Z3n~ zdbRkRP2#m7Y}rJBlb`a#9Qk0?@x#{!O@;A8D#a z2o3H{Lbvz1npjV~5KuxSqZ^yj#=%1?&C{=D@^GeT-x-;|4MiOMZDozupE!Fdug9*< zXLQa#esb3TR>2l`epfR;N&uNwsQ7|YaAUQ4fH0@@m3Nu8v+CMK{4|Q3UIa4> zBx7VpjoIea{5s~jLCuE`qRQ*5R*r;Ge=J6iIr;gC#mV@Nzw2qOl0UKpc061aEZDd9 zzrq5{H8eyc#X?ub_zJj0jq=g54aBO9q!Prqn80cD31DK?hEH^qy#-MHrNp+N!GCZ2RQ=uC5JIBgZUh3z`1p`5Y`^2pah4LtU7=CA&fs?Z?xyhRuiw-8nr!3F z9N&!)b`wVyynCZ6$?p>(5B19W)LD2=c3=9iGzZWREUx>qH#A7EX zZ9q3KI{sqP3uObuu_aW!M){dQEzgFf{2RIMmjSy1is+nte2kAiJ;qtgge_7Gnr3No zvryX}GSIi$82hEdbgDgnrLwhdx8A-{EO|kBq(@EG#w&(@>Fo_7nT}JE;ErNF=STY;Mb5Q)1A(!>4doJX7n^MArc3oq62B$RmFn z-+BI$`g?aSY8HbkpD`+_Uot?ao_&+unc|^B@;XmPz(X>B(9eGgB+4jiOjaI$yXXT- zsr=!&i_0W+k-nvBJ*eX2f}_Xv`^9dkehYbWU3uM=A>3#Cg8EPlpV@gp7rtxm%NT(n2 zxa}RVNhDg1xLm{l4h6nq*T((8-`U%f!b%hP`!yu_>%Lkir$KF4AwhLL-b)8zzRa2}pAv z2GGkmJvwRyr_70g_g@Msc-yUbf8IFSr7GOxZ&@yk$jP-QeiD_ILDNsB{{6!LvvDDy z>}s*cPoJj|I|shg=$WF5Ki zS*I9Mo1&;)e(V=!qZc3?`Jvg-o(P z)gkNroX;AAt9gdHfI1s8wP&#zN}Uq47^s9*K6YZzNiGOZzYsI6qAJ`bxs32J7;Mm4 z_JZL#4r&%)`?=fJe=zW^tN#xS7aXp(O=cYq)x3o?Xo>r{lv`nw-->*8{%rzBuXe%h zA;yEM{0vgZq{9~uriacw<#}$$3G>UKC-a(_BI9mEe&0U**SW~>6z=fhB<^ot>SW;G zdWmSEBvB(p7IqMjs4$^Xt8kv2a4P!2XsBN7DVae)73`DUoAX#|$ zZ9LqM`g>S|wk7CDD_nXi01JCM6NLY+#ija}z~-=t;dg-OvIfF4fD8HOrsl=*HZG8L zoJS5oh5ecdSuZ_e%P({Le%Xky`;#~G+vtT#^j~~YQv5RA;wzG zlRqf6B-eAvRqbutsb#8?tXf6{@3yxoE`+ysS5B|x(6-&bY1d7nm$|Vc0KfwfTe&iH z@0@Afb0uDQHM}RCR~nQ8-E#OUFgOv@W!7B3KPfkXAxxGQ+=alMZLE=feubL&fAV@x zB_XOE;s$G%f|t_YT?jiJZr61|cti1|W0vg+;eOdYaD$PcuQy+bh(!oGO?@i{ZpTZNBjA)uvI>MDtc|hyc`m;Wr!6X8zgD~Y0 z)GvfjYPso)px)`P&x=J176zqvz6AV_`AFRaWQ>n?V;IyEieG=06e&6WG+q^cJO3U_ z5nU=!*m+(vK$CmGd0APe&~UEF2G(n<2zU+O9e};w+MHZ}TjmFd2vX#t`DDI)ZaUm3 zl)yzonn%s0FtRCDTrgGw<_?P~U_+}G*2O!#wgH`_iEhbqWo`tbqQ)DxOI500Z_x<) zS#qbd{L{vxs+s-@p9I-cbFu+WK~zylTdLwMnC}TG)V9lm`v(F?c-7AKG?1AZdyS$| zP+;ONZ4Pt*bQ|o6dH?=g{mukrJ}=33!uz8+%Lxilgm-_rlg$VY>w;7bgMVu?}mz-UNn(Ydg}$a+}<@B^*wij+Pb) z_J12qaMz-#oCv`^<-GTeW09!h8xe#^8A+)g)c^Ijd?9bOgtmp2c~!92`Eplp#98JB zKiM@ybR5T0ibe%BqOZbWBp*_y{67{mcE-uq`*0V=@b0{3kZ1F% z#5n00TZvIQ@_7XLv#Ev?EQ%>=Guk@^X#-u|JLY8X(Ux)Vm6xjAtyJ;l%VRgK1;oU< zKPwcb8LQH!d@h(PfC(HS$vTqT6-=#vxeNctc9k!EyD(vuz z+SPavBU3JMe8d9$4>gHP-hH`lP|&5H;Hv5t%LtwH2>6g=DFi;B3;85hh5?D6@$vmSBZA zM_LD<2B!aKn+hM)f(VP zO}Tq-8G^14=`g>C9n>`b0@$^0xlKV^q#4Z!%{ug7sVK_V< zaE@emMxxRVS=H>%i9=+q>e@3e$0``pFB+ zP})ZJc=Mqp2qaf#&e7-SOB!QQJ1h`>t}C=b;^^E|sI+Y0Jlh{PN!ku|PhB@H@jVx9 z6n!c&>y)$blDWQCz=$}uqm$@--ElD-!hDfukYnU}r@DL>@8lk~z;ipuX2k#cSk~fN zdc>Jznv7#Surm3CrpJ0^fMjQ;at&_CV-3H?zg7V0;)#5O^()$shdg}1-SYM8bN<@$H=&jLDi=at)ujz#8-9M= z-*vq?EGr?A0YP*Q2;SW?wk9Y^;NK$~9E5HT{24!Q$C)cF|Nj`cxIn`@EwoN9yIWDOQKpR1< zNE1v9LpQ>raVwLX{}J!n|Fd&T_!}U0*k`iNNFJz_H;X%T^zxs62%c_BdCoW-*Ic<2 zQuT~K!pnn@;<4<0!YhKg$?ck?6T>qI*Yv!4_*BKrCue+v25PxDeL+Z|l<=D2Y~XB1 zXyJr}Rme@^W_z?~BFD@KKXlbfzDKf+J9Iz=C75I0Mfmly;@Mb9SZf}`zkVEvCfgmD zx^9PNoR1!Zwr`VQ&Fge`bhWLRF&ioH)Y&9uS%KDlPfm2G0Atz*~~QKd)U4D%y|Abv=wkunw_pSHYCX5{nQ2 zKd!zzkj=IIKV2v_dMIi;J=LK_Ys8+N$85E=DOze*)fQ2sT1s@%1ktKZLe(BIN?LnI zi`ar7hzN-!zX#|2zUTctf25^<BnLx>(o$yBq5+usUeBfgrSp&z{=a+O1~`cacd>4)8fbvH?! z$Nk{avbB(Y=WXrrM2p6mpw2qBXR8Ai8W)5G$ zOM>nX&389^?>_RYn{TiU8aOd7y|*gzi~ZKw{t`uRgFid4gJ(Rd61?N^n%Sa1EqK)i zXYB7BqEz#T()tHMb-#i`S;qYS8fqoR8xTAO(B7|lllqjm$dd z0DORnyZk#FV!pw4{(y9fsdfcQ^@}1Aml`odHa3g6ar|8mPyUVFI%E_WXRNg%{OWk5 z=od|$5J)>bX(}wVj=2etajuN3n}k0h*NC+_qO=AlbR5)|A@hFdLjoJ*YWE`hWT$;^ zjBuX!-2*3>G6{8S+CgQTEx%KOQy~As2S>P>;Vq`x=}^#pe&bNH zUn4nCxr(v%OThq2{EKN$c%SK`o>_~P*EbhW-8fe`SzA>YOa5cXYoIKY;?T-VsLqdl zTvjw-A%**^nFXYD4{T+L9A}9a3%&UDi8RZ%lzLP;rUtoM1)xOf&8{n3-+{>XfbIkB z>04pipU8d}xtZd7t5JaAPygpo0c5d1g+xEbb8-ro7tJt9ZnEgj-muOQF!x-6Je z#n87ZNdfTmOn9LOe)ih(VBo{IPX>+Z@U1+m373@DOfgBL&+JI;$_@i-@bTa?0bOgu zPB06dqB@jsSbgy4qkKJ+N4tI@uXdMM{VkvUvX1n~7TG$cG(;_z4Y#H$p+39Se?)#G zz{@WtrVZ&HIi5n!=(2))h&WU$^gVlOc7 zm%0hR-N-u#8-Tvt>rkuS+w->$jNN>H*WV5ibrS5H7AicXS8m64euN@cO;W2iQ`7(W z?;8W9{7DP{*hRMl*jq6fUsnmcR(J+reSl3j`7aM7U+2}40w^N14ObnVfFzTYk+$_; z%`rpMpN~F2djxXiT+C{^+p!G9yT|r|$sby^yLYQVo8qdKD6eJ_>hUh+g`S$5p`|&i z7_$8QneX|Al`8sYr&R+AGgauE9wTL;PdtvN`K@fBd5PRdYXIlG4$uca)JIc`R?pPN z?(z$&LW&MWG<@VeK7(wB{*zJcV;J`T6T-K~wrV^Tg-}KZvmG&B z!agEjH7*(V*(40zmC~!$tMu@^t+W>SM)YUs*yAx3FCWNom1mrf#G}?M5me~Nay_&B zq@VkT6)%3)y;7P>p)b=ujHpVn{gRT*EG}37pvYKW{uMV;uy*65s?=tyXDBy-M-ddv z_wy3z4Ll4=xgYnmGuJO`&U@3`)F>fu`>Jo^X%vj-k^-Up7d4;rP!BFLmu#)HNuAr$ zPB^!m$8q~CPOV7qnolO=D_`=vqj#*TbzP(t zQg-lk-!Mh&~89=d{Jjve#Ky&W@64jh7*$4Hv&y=sEs*3XN3D=Yi1i9n7f#i z9N>2DcVN|98sJ%b(b)K7Mgjl5W0(}9x@<>!%wkICFdNRcu4}tGpDMGUya(U<=YSaT zT8y&#s{tHCx!JMvp_KPR=JV~}f|%cZgNd2+8K8lgaAK6wWkclZRYTm2eM?zbu9%a} zxKK%h*F9O9KYkL+b!JG?OwirMmO!e1JlAdRKvDi_hT@YGv?s_E0lAr-WJ;K>l_cJt zP{oT23Sul}x_sM8KY>asX7O!;4blCe;#;(Fp()J83(?)eFBoCZn04Dm`v=(XACTz& zVo$x4q*Jc?1!6Xb2B9@u<9L;2H2qagfa0d`9}{sh$d(q4Szb>id5xyAtw9hGDY z>>Cs)=14MvDnFAHccen9AZl#@>)B;@bwg)bU-GAB&1I__U$$n7>eTL$L4JfCX zp829S-dq_1qouyIGCya3>Mx8$VTsMZE{Wo*Hah>5Gf2T$Ki6=}Z~Bvsxuw;(<7H9b z#Tnw0M;%FDD@>_C6lN4+OcU`aL&iTu0p!v2l0$7xl=81%Yf&B-56>qM^F3rF;mU44 z^}VKNajMeM{hxsoaDNJ|to6$O9j-vsr@AcUA~M>nVzQgxm=R0;%4ZFT)lOT0hEW?B z@Y5qn{xMm5$5X*{pYgN5L|_isp~mXGkWlAa+&z9tx`TJZl*7@YNJDQad$tn-oxLO6 zWOCzFz4;AYk)#UeCoN&M;(pa9gQcl=VbekjG=*=b3B7*J0X2d$wz}XEd4l_p$!{*A z#*ity${ozc)r=_jEqVMaz1TnI8xAu33ZyZd)(Rt8j)Dh{iE}T+cHL#y$<>YP%HpGh z`A|ERR5_ndlMF-B zqDNUhLJv>TL_#@^!S6JfjNki^{16{)_8I6Bl*~y0%|wderoW0~jjvt!ti;EMuJ>qF zT}{NUfF7Jj=WcVM)T!=S_@%>sijnfomo|Kxh)q|a3aZT0$!+ZH&Qit48UoCozj==g z>_wW+hwM(A15hp{QvX@$Ze4d$#a5D4Q0iN&n7~}-AEZ(Q7cgld_U^0r!`vy$&Snwa zv4-3OcVFmwOO7kSyTvz&SZ4$Ox+-&>8nNuT&Yim7GCCFh&R^4guB+<*R9eDeh7xAt z8xEYqo~U>|1t1`QnX&&Q+Oc0!E%iDkb;G%WvD~>8wry#|HV?nPHmane@mjNfy|Odd zh-fh?u#!p?bM>}G4_$?se`ovSg@v=Xj8o#mlVM%;ZsTc&%f)Hi{siJ@6hYXjcgGfj zStP64*+&VSEwN5DRmKD{aM(q6nh)VEFva;4?m10cyYZvJ%(eQf ziSIJ`Fk{jb9R_85w`7ac_`(EO-NCHx?kwh@1@IfmJSN$f=>Nu+Wj7E$QuZa$O0trs z_`vkPZw_>y^A1&ho~{GB9~aZJi(^UF^xvUB+3gV$sgk2#r9SOKww+;;uM>iZMMYc| zBj+JUu6M%sLv4Va;W{?g`4$Zmzji|Zjf++au_*mu)e!$<S6mB0F>He3f^1?2ID74l-}*1IBAMVNMsKP^fMCx+{~h*9=!c|BorZ zsZOr(5lI2V?soQT6!spib$=v*n8n&JPd(QeuNr;lP^KWH#U^=(<0*fTTa3t;xz&Y| z&p(6RbtMslgx2**`jb?-GpA;%UY2sN`lm#4Dn3%S4=gVUt$Hp~sP~=iw-sBGUFO;96L~y!EIA$*2uIePt50>6 z$PqE!S-gvb=3o|sb9LOgfu?XOR11uJBiKg%)GW2JP4>Wm2GG*9oz{{?uYr)=&9*B-BTuAgX8a+7|w1jT>a}u=b`G zwLBOaW+Z-2`Hg7;1>*i3m7Sa@>T_o^|6Coe`_}X-g&(24fmiXw88ZfjopYaAbk0nN zJH4epUD{Bpm{VUQe}*C94F%6P0K@T zO$+dSeWlZUNo@#MmiyZb`(vbxSX_n|$G&vlxXKX)!#%MM(b?Hib|6*_0a$eM=G{re z_;z|K5Q|+-eDaa@WQC4DWvGF_Xc$lWz`L{Q^K)t%5$wCm%9-lxv&2mSot9)k6Tr&$loD~9kl0=+N0K(; z?w?++;Hb2QnPmpz$=l}zJVov}KFmQD;;~J`sQLgO3Ogv1zb&8V&P)mgRS zmB6C<&QY`^Q0#i2aT#PTyMxd_sheGSX;oBw>#IMXN3=eu|~ zm-kp|t$Tny!&1rCr!d3^K|+W0+m0(6Guy9iS0#t{R$_)RK1@UKPkF>n(_OI4uUfq0x~d73%)Hu>+)WdMn@xplX6C&=@7O*TNR4RLEa^t7KDRvjkc zgQX;OeQc+{B;K*h)J7I;&|Q-JE%klVf={HIJ^xbxkWG0UtX%}jD?T93_S&?l8~VKh zS5YhBRFXUW{n86H*rF?dYe*-KNOSt4oQPw*bz9l}>mME3WzmcqiQ3!UPug=<-fQ7l z2}=wjtR1)+Z<=5j?IEWYbx4u4%#D>E^MhJnYm=XR@KVCE z@2b&(Kz{#I*~+}hP0JjP80G$@3FZZ>tH6wZm>+tYOzl0PnSq@Yywjjt<>O;;FjXST zhr4ocM%yQ+=?^&DJjyLST_m?=^s{!rIIMFr&3)d1r4IH4gB1(dXmZS@b(P>_0b^~? z$$96Xn)cSn(GG3x+Tsi*!MV%}?!c-4_}nqwIA>Mm%~4$ka4cGMk>LM$`=&3XrObP& z3mmRd=8aGHK!?k3S99TFghI)cRyyd`LWzFK05Bnw!>+h)hnV(p_2rx;ZFE85XsP_` zAh^~@}Si&QNO>uAn$vk&pjV|i^eeTH5D#(EkZWo5ev z4aGrAR61oNj?FG<_P-lQDZnC#-U(X&+Qo>A1J!4;D|vxLIbQfqz%JK$>Ncy<0H8wR z4Q~jP=fnf5pi4cOfca`~1a_m>YABq}qdfOM&;e+E$rasSn!5~4HGmAA$GyZg`9S*1 zQtx#MK2VCxnxDZMeLc_);Oha;{}(NZjMUiHIuzAY4fOxE9n;d($v~`CgnS9Dk@tb+ zP;aT6?&hAY@e!7*P@X)5dNhnGH}rdySkU}O*p8`tJ-T&N$=|f`=&WyliGMTnlAoLb zm($x2G{D!mM*CCdPJ>b#^-bi_sQ|apLjXfFzp=($z$fuNQ?~O{sdRn!cnwW=d-$Pq zSeYUJF|*XDE!#8Yll5+KXzxEwKXDhR-H?t2@R`fi+p-9PknXDSodnR%Rq|J{bw^2L-3?V&bmnIQNiMFf0o`b(-COf%fhKdN0?QM> zsDJ<1&=ZRC^bv`o!VA3IjiO*NGz?Vc)@uQp{Folur4Xur5m0g^6qvQU6~*rs%PPip zN*4VOfU>Xdghk71`zS7vVY2~yU(><0PoUcW&Q~2uQVzsJjmC-;fPD^D#!1ScY|2Jl zR@J|EM5iuf99S#YtNV?pQnpmSZA$M!Bpc5Tl5JBB+>7mDS>Cm1q>W08Pk=?VSkTXe zS>xvLTik0c;q&T=S;0Nw{w}|(tZLmi)8Rda9v?l2quLHCYzQ0Y1TT1sjq@Zt`rxCd z+ywKJ4B!o1tMo%nYtBv2D^rS;{mX;v(?k1z03D{CmQI5DfG*59R`tt7G@y8|K7?~) z;<0Hhhklo|RHfJjM)L_c|8b%x33rd7O*@5fGLmzv~LoO92nF!<}T#T zE-Hf=@AUvcDfO4#DYWy@bRW^YSmUKx(JZ$+h4DX6F@;voejMa-l_OP+M)7GM#3c#| z-{q9P72R?_C0?%TPAdg~MpsCL0vcllI@9wtg5^LPHyo&lJdCEs@!b||YU>(@d@@4( zLs`X^ao#_l(w+E%VH=BY4P)-vc!b*j^n7{+z{CK`PWQ;)y_I#N?}5I10v&SQZ=?EL zovHDrv1-IjHl_M!z1sv@G!NVf#5>r$$!hOS`GE0Y?M^a~^8(UdAHZ@%z!??Zhc6Yl zY@FlMv*o#ZR-{YgMBfB_+36Hh6In|d=Bxh=pf2u@;yZ9J-CWRFWG6xUzLY5ywM5-z zv@LG^P0vSmaWFJTd2Kefm+adISD{}{VDtmZSz%g%b$06Y?%9$C5`Uo!+%`yYW{1z= z42&|v$BK2BIVY}UWLsSJxD(C$JvfCtOY!H24VL)%;4=N?2HfPMM6LCX*cV-`<~i*^ z3aRZHA_oXWwlP-{=`;OQ?Z0xynMWw-TP#Hf79m_+yWDWmy|0;l5 zeiURBzfKml!w|-Ihs^=-S_w`WNv zp_iDwRX$nUJvQD?Yvp4{V|&D|vme?T414P@79vt%kwCxtTqP#(Dj^JFZ>wGmFAx9+ z7DzqQ^iPI&B3xKqY)9`9t0wwNstJpI!8Zv%vKkRh2yzCj$;%r=ni1NsR4YjBUhR)g;dG3)!yR&Ij9ujSEcuLx~#Oqg3eaMAa{_mY_&*_&u z8)}A&wPDztuJvFZcWJoK2=_>P@E7a=vt>45Yg5L(tp0NopKbW4(u#cgy~Or~?Dg5Y z-|qG%sYkC7COM+u|q|B6>z^qt@Sc#UZ?P#X)Ks3X=clzh+0 zk>XZn4z2Z_?k((%Y=8F0B52_^Fm8QhfQ}A)GZfS~D-}hHSg6L)D;&A%Ew9wr*XN$G z(^<2Hdo<;}rUN213CehH6Aan#mmdQ=lWvOt6SwnIayPRu!a>nfk^2`VnF`#O1fYMn ztOj_$dZu$-iec}~3gI_H9sZw0VpDwKn9L0Zbq^T#&-?5V^<%-P2SJ%ZQ}f5Itn5Gd z#_**<*m>ISaWoo9D=iM+mxBZmHo9?TV`_x-Gd^g)5oM~PFmxDqVc104Qq;fqbaI^7 zhmutF?T{1i1nN{=&f<43Ubu>vU?}Ho2)$^H=vQ!YQ`jsUUFBBc?DnYwRCgf@c4;@IozWc-f&jU z=usW;7_4s2?}nb{7gde9#e#1-mdgVHMG?_K7sS8tf%~P|f}Pz|R*uQPsGFx;N&|*` zB}q~TIg`#-uLpLN9#iX)K9ct7u*viAc}=_65KNcQr=!|lLfvMgu0T}*CG8q@D30&I zT1!d1TGrY-KzlRA0A3LPNswlJ;+%|SEdRR$f=&n$7Ia8se00Jg*5G^ZfWOl^yJt#N zdLTI^5O2?=4Lfb$ObO`Yd3Ls0=t>-pXCZuhcy6VV+IYTjsfQc42LtC%O`{<}`+3H- zMd(4&f1u*cT$esPE)|SI18MqaL%WNH;T=S$Js{gZo8GTGF8>)3i$Y^9bIx=~$izIxzwS@x8USxu@pM@M&L<(z#ZdL+yH*TWy6ue2dEZ z59I{Zp!A~g0Zs8wJBK~LnsS}%UoGWRx!yl1;b|iujMpLVt@e;4OJ&o*PN;Ram&gWo zd;tM%pCh=9(^9HLL&k*hc_vFOknbqZuks=NK|6^VpfU*OwHJQ=fcBQmO1(15NzUEd zf0~lGHmsL3$6O1ZYjsQJ6&9@uT`Qlg8#apw3g18mnFplRPnd5Rs3*=OQrA>lbeCb_ zKelDdPi~e{Th_ju;`{T+J%R4G!m5GriPq+P|N&bdqb-oJ-XIX*eS0 z6AAVCSMCAIlqIqEQ42r~wMP#aLyl}0f(On<|K6A!P(ReGpoOY;LBE=g;m7t<(e*o@ zU!7vh_5wpX+#WuxqA9iq9J6<&g?Mo}+Njn%UXCgL3e<)zP6%!ySwtwh{yFlDIGgZ6 zu4YNl@OkwxBdGQl;`qraQjK=S-U1o|sQW-$|E<+noePAt3WoR6N7tG_lHlH{Q!!G` zz=4;_Rk%8b>>k~as0hq%#CW%nvL5|FQ^!IQ&BuNkG_R>Ib>#3xR*F(I`n~LKn?uKX z>bytg%B5>c$*6MQhvCmeGDEyaq3X|J?1KCzJ<5Wm+TwLgzldif?-f9yvd*|S$R1r0 z@23nSxU2_j&vvO>f7d7(n;97+ESDQ=&{VH8?!wN57ojfQA`Tw5j$Xrhv|-<#6rDA! zb@`qOt3=mg=7iLZ^u5gOStcZzH2VP2Rq;_AK00~F^Fo_dbI z6h5)JAG>r+s_T@thB!8B@eivXxSL^#=Kh}v@Gisaj0$(JM4(lY{D4ZTb&z9DklfHZ_csw^;hq-E+J(yKH~^Ll#mnzj`%(un>8}gAVSau2+!{V^LpFV}&)YrfXm>)!gU0T&HHp0Of95uP{9B-XCH2TRgeLD;hxXWCby8KmHE8U+ zCnBlxtv%l|Ex6~5$vdjyx5%%sz2L_h8u;P)`mY&HQco?PrN=#0D#1;^4zu^;OSdyE zMXlAUx*ZYy;#=*cxt2EznZqZZUheL&D7qVBn~--QX3qM_!=9K@kEA&Xf-KE?@Eqr7 znM~BD^)SOwZn&8dn3|+T-7ze_wZbN9(Sx$D&Z%fM2q6`7FTPyF`Q^!gqCqt@32>A12s{q7fNh^10BnlF~ zvsrT(q=I0+iY?GCNW$RSkpEoF|6AOJb^gq%lEx8+=8L?hp)D>eYqti&v{w4t-N_@P z7w~Kx-zB#557hMkI#4AYc2&7i9#IlHm{jj=X@B@|(5+4)+3%@e@JM0ZuWQPdVB^(k zvAAH1%!`D)$fLF9odjc5eg1GhY|>B}hrx@^ooTUDI}6vLTYzX+Nh3SZ322Xhggswn z1rbZOJ2kEp7pRh&@#r2&eXi%XNm>rg@K+K($KzDsm+tAc&!`h8KxXXnTLW9#F_urQ z3VuRGvK0wKfTrK6PHAMmlB;6t)K;gS=h^6);2C6#m_c=G5iamYceVF=NSu{Mubz;M zZsQ2?UTjhj^0NNiU>i^a6h}hLBtlmecdgcjGM;%)m}}4L>PV7+>}=O7=azk>(gJ5y z+pez4IaD|baBbV>eA2K9DEaU&<863)lge7f5DyJU_;oGIR}z4JTKjGvCK!;lCqDo} z=-qy*t@O-XE5iFkN22ecHhWP`>L|Ij0Hg4HS;P}(0h-`tBWnJ{WoheUx&$xdaMsZ! zsKN^X7kBQtnzyU;mkT>br5^$+T@f47si*X14|txYe~g^-6J0DlOrtwW@AEl41OI1p zU%Lm%=i``s=N)vNGCs-&Bv<(7y$lZfy5N9@C+9q$H-y5Veyo?t*iRah7nl57tu3nf>^GECG(&t z`$HC*>AX@Z5B`ibm?0`7$vc|wOU*iNJ~acwd>82KYP&-!%Y&SpLPK7vB(%2>2(!b zG<7#?MJ7{O=WT!NGkMAOtUgH9BW*^7VR8($AZq%;Fx&%mM2q)dhN_^|0a3p_$~puh z4!DB;mekWKeSn>Mv#Xd}*Fgu^*c@jOmoB|~l-cB8>g-mVkqhi(KH3ZglB5Vo>-^sD$E z6J`IOT{LzdSot0-?*ZYdT(rYKJ%!d$s{QS4f)ZWHY28^%24o)Q5m9~yPix*jH7{N+ zK&rHZPu66+)pClTg-?z=U&=oTSqzhR)0s8PZ(Wh||Ba?z1T0Si@%EkpR9x7s>_gyr zP2D)JI_(eKB0Y}WC|lmr%qfGhY8V8^USDS&@A9htsPc_Tj!wS>w7BBU@ONQBUY5nRWNveAqd^*ed#5TK{?dPKh%@Rn{0kq!(2e@RGn!q@}>Mvm$ z0@6jg77bT|*KY9Wnl&EY{`#m3Cad#<)LB`hJ%h%;J-VW=&s^|Gxx?6o%8trAp(t*;2zCM}5J)xyJv|)Buv3Fk#ueYHe-rfbppcvCSZ(3Rc z0n=r53_u+^l)LDAIWJI(!YQ8u#6To>&q6!RBJ=4OVsQa+=8m-MobnZGzv-yve{H@I zay>n*AyghzY!6`@k8a3X&FO|DP7RpK2whP!O7W09@DSi2tatRAdJ`M+mSv*Z6a1LX zYEXb)c@leX@VAR9lp6E3^4njs?d_Kn`8EWGLVSH|6)NR?FciO&I3V@Xiau}a0niAp zh+1$M)G`|yUlw9rR25Z|_jsD{D>p{!8SJw$ghc9QItWx6(Y-sY2B1lv*r!SEuT|w* zpsQs0%Xi#8cljY`#C+^nnE{)p<9jiI$N0R^~#sZ!+~Q`?>QQP0Ib0 z0PR1QU09vk*0}VyVt;>%6zJ=A7k?!Ups1{0csRA@9OUj;9k^t!8dZ8gd_lEkd%z

y!lG?#~hv$CZAtnvk*~LQCsj1X&M2AJ^s?W@R%Q)isova&-tKs#eW9he~5BXj|ZPoGE_DF-)#By56#?v7h z*Io5b&2QDuwvH&NSFVJ(7`ca= zo}UDM7SRv=l zk=%OyZJ84wmA@NKgN$Uwuau`lT^~cBDtoxdh?BbiW+-s`8Pvm^$Q!~^X80Zi{A0m2 zK)xfbbKBg2{PkDuR$}W~$eC^5u~%Bk_X3>|c| z=Zjx-pH<>Jn`T9S4+;zOEXDKKc0p{Q%@Z8>7yPhb}5*d=&>Z&eKe`(jZ0tBdFrppF8#6oW>t5`Y)A zG+tB!lUl|qeYOB|fQ`G-Xr`mc`j?h)mj%?e8rzpF>~um)H7~?n0m<6>>;`eKt%oC4 zLKP)_;C&XI6D|1RCB(Ijhf$&YUD@zTq5_+^Hy;8+gv=`&C9KBZe?BZ=&(1zy6?9Id zwIB(k=cHf2FU6zVH{h|)Fv28-Q{^djlUw0KxMs7Fe8=Qt%(?Ec3 z6`{LC`D*Od_&(EVrU}~!*KAUM2*E!N_ei8hEs;1{k;m_+l^=g2!8G<%N`G#oh|vq( z-*t5>8{38y!3uL2H`iQ`-XFfmx{)t59V&$M8Y{77M3R1Z9kbaDMC{`d$-k)s1DL1X zP|sZy6%CMV`9%!~Wd+V^?pnTNJPl08<0(jaG4KNj!l{cg4iurgsm9!k)nfJgsssD4 zwfOE-o!BfDq!7m!*6%uei{sNvMVBRa+IZxZC*P)|PfH({mtK`i&j!SlIwAgz?w>v4(qb$UsXwyXBpoSLJ~7<=?hd z+*_jg|1>-@eVyQM4Bda@@dE{H)cS=0Nl~l@1%k&5886u3OWZBdh%W?rd*yYZ{)Sz_ zgZf28{Apzs67#+vkE0v-wvU%2p#VEr_}+WTNA#OUzp9ST_1mYI!naC8 z4zSZCUGb3xIc9){_;Al(H)pE8OR!MFDZ`XRJpq@4?#JYmf59;KoCRI=x%wz+e@IW2 z+^MT_5YH#5>Fl24aEvRUS2z==qI=31iZ zM)<2+%|7}`M#x4zM|&{9kguh+#a}*pDP?}R_tY2$u^K$$O+%%*Y0cfMTFbV{;+=#` zoDnbt6y(@L^1Ash2QrVGI{c2iD-`MYVvx9T{fq@^msY{ERG06^l9uw!ButM}_&9~T z#{Y_xqKAx0%vOVTwRv_-PwJ~I{liZLT+lz474XNk+f=u}Dz}jS+}eUF=v8pAJmLe` zc*YKQ3$A-08BP!7*H414331cX+zY)$R9x`e4C$IAF-PXM(R$4h!bsp;)DFk0=rQ9G4b~Gc% zaFnwv`iz!$O?K6zR6zfIOLA=yX4e<)`Gi3NJpL?DL^nj5gzYt-ku+NJRLb-XgVo?; zyB9;S2Eb#-0DswSfBD(5)GIryb~41oYidBV_6Aj>G!j^*K78VJh=E8y5O!EtX!lwDcZ|K#fC-aT6Ip!#OfapYJx3 zevHobOwS``p6|8_+zc#4;Y(49>(s{IWYlr`_CLwNPoU)Uf6p~Da1tz`5#r8+AJo-k_2ta1=Y4|qcW8e*?l35QU@EJu z;B#Kc&DcM_v#kzV{7l$r`L#yhdGH#=s}v~SFE*YPzJ;eN384?NI~j&CKDo(PT}SYqjj>a{8&OQx6Y$hH~o{f-}J?dnjyzWI_i%(t!|HgF^duq_%x&kv(u58(F?b8%VT8-) znR$ULJsPw625nkqf^uGh{92*`Q^8ruVvu#4C$wdUza>p}l9Ol35xgc420C@SES1@U zo|yrJTsI}l$F}RKKc*4`0l*)#=wcD80>7PAxI=T8-IyS*-!F9E8pLHa3OB0;)|0@q zqkj0BWulHP((xjm)*_Dx$wiiaQ?-l@uEhWR70fwW@uo_PE*hohz&aHV00i~Y-WQ0t^C_mtide$7mv{)N$nOIDi zr@=EX@*KdtH(G*wiWB3s>SB+Y)R@^w8|Cv#3lQ0({3uoI7}Y-Y=Wq^ZK=UBBT4J#N z5PJbGTKmvpXCMMjQOHiPcX0HYA~Hf+a2=vP)-3DqW(~gGS2B|9=G4;14aZhh*Oj#+`2k!8SR04L`V{M1M9F zVnhBkmH2!q+qBv}AiX?KqaL9U@wuW}Y3z4=;eo+o2Y>nQPEuN01-ZN=q;w>|s>b5L zp@n#_AS8SUoUm!m(GaN!R!>eC>kOH{Mq0dt>%Bi-u-vZ+jF2mlKzr-U_dz6{mNJXi znkGKob%++cK|eG-D3XUZ&SVT5jmJx_+m3TOo<*M=d!?gQXdqav-H;y1`veqepZrb7 z!Ipnc%1LyI*vgUvGb?bfDu@{eMV;n##pNc~DfcAMzhE!8g?q3^aqlG%9wJmQdkOih z`&q5*#E;?gAs1H{Z@Q1F%r_bG2z(snX4u?f(W%V5QEJce1%ng0W$VQA11@zG$w-HL z@QjQhf4i>WA0$Dcz#mgJQ0Gn>@23a5Xsf#~ew&8VO3!b3Toht4Nk5%g8y`1zZVs~A zfT8a@Q2F+s<~naJ!nZQj&(2?PmW_!(`E>`wY_~rd=KGQ~33c16ik2+a`gr(-Ztkvr z0B4!Y>0#3y68*S}_t?;68;i_48^_D|bYW#Hp^S);U)(`a89IlZi@vB;IW@0D<))^5 zBPn3{B$QuLS;Z?wQlFH>Y~nB7$x?;Ldt5nW0m-_7llO=g%1w4}>h%e+$KB zl;i0VRWnhTGf;)xqE$a6MLwL2XEl5H0X^DrXkJL&uOA_{nwGJvA?6UqGL;uFGJf7N zHxf(Mm5p@LwJX>Eubg;$IRy;@5$f3xPER*ZFsqnW{^e*p!0g11262KS#(3_?kImw9iJ$|0$5 zgS>eGalt~fVm^~03*A|EEXUg62;ZlW^e;Q93HQSBd#@rQSpVNS?D47rOH4MrH*i(v zO2%|^bJdyJU?Xt8Y3G!`A8l$i?`Fua?_;wGz@UePJiJPyJna1@8oNXuZgY4!mh7d9 z#GR8krR)Kd1{Ew8{B4$3o_u*Y!&?(sLD?M6)2N#{GW3bmBQKi|<|-O-{KVsl-FDAxIrjNJ07?qPrZweOB*R;CQM-iC_CC zA>+!3+>*QlHFG-_&$wI4{^)jv$E#mo$POTNmY9~sSU5(5-s0<;Uj8MGAk~VTbML5Sf88m5X*+21Hr%E5lXwW=AtTUO| zY`p6bohdnYQrk=orGP=OL1osM7y=FRT6<@HJ4c1VV9n|o$56&@fGo}(4vx`TtNy!8 zIjB>O3c;K!U1hQWl;(<;Rul2=mfuse96gRJwVU~}GjJ&D`rL5l5LD|urH&Qr=X+g} zbP_DN6vi5#_qbJQGGxx8JV20tjvWuHpH(=c8hK-Sf&AKY^y+rdkDCRjTx3cSY1f@UgR8 zGPi6lCCQ#8=8>$=kA=|fzTT3)tVP)f9d*jZZxW$czk!YTt8b!@@dqs)O? z-@>yCvRSA5l`xqYI%qsjG&^{_f8Oq* zsrN8m=^Ir>tf8Wbv#+-~>|Xoxbzr=a7IZFDi=9%$6LZ5^g-qELIhQyI_w&(yB_YXc z>H#O!%n99H)vsoQtYo24yy$vTYP>}Jy;NA3F+IgGqbrs(hA$L9Md?}kK5+N83YDZ9 zesVPlE9%iSvRspRYO{I(XTPHzYjmR^y}e2eC$9UU+Y9fC&O2aphoymMh~E9tA*&;% zb5@o^`qrsID6f8ok@cawEa{Xp(a4xf3GQy#8bDuph_&AKu|&UeNVWrfm?K6<2)atm z=Y4L!L&s>mr(mAUITTP-f0N;wj5Pj1q`*`b5p(_A(;S-K5QRCOyInIdt1c=Yxi}Mr&xR`XUn8YJjS3rNLx6yt;~LqO!OLS?>T4GY!fb)Fdhs z&$4RoblzL@H1zNwGe@YQ`>c)q(?5T88$$>j>jr@`IrR*8CoX+u$)nx`KiNJJIwbz8 z^aJ1RGC%}NZFB4R$8UV;NI z6oF_Q{FZ)kwdb{z2j8qb_?!3wxrZLsdGrT9vO8AD)#yrqPBgh+06G*V@l7E}D=UdY z25}vb?5aZpO#h~}1NUX;q0ToLJOj9OUx3bOrN@GT8vfcH`7)dYrTego07&~epW3zO zwL9zWcULWUsx_a9YU`W*S{bXE1++qMH~`R--KUsm@lb;YRIt+9EgfD~fG*0O$0Npb z*yi$lYktRiTaWK_3Bg*jXKx=#FPwv@boSNZP1FAB@qQs+eG+=fQPGvSPgTk!*3&CEyzG; z1BHHnIY?@yAk!D4hI0YXuj}4LFJFvd<{{ex>u<36yJH= z*5A4llmNs}Ze!E=N%$tqa zhRb^^Rs+_Uo_FQTa~oQc}Q4UQXm5nl3M?gZ#E9U9U#uRw_X>=XgPfS_gp5J%+`DSB?|qa-_3*%i3OLgSdk^lymfbu3GnRBUBRDWMUjXh8?1;HT*j} z;b%SkzC9b0IdsGInf4ioGAgV7L;s?MpGJb2{h@}xglVTyig~*yFQLAoWxgKu)B}3kWq(L1` z)>dG)vF-xsYHEO2%kjLwxr_*#*E7BTh?Obh2NkH9cs?Xy3z<|c7IAA&u<8=}@R^%lsgw^+7)M3ZF^n9PdKUY=k>Nv2<{l^r8IdIhhC=t0X zqBGn#=0UR-hRz%sSpg~P5K`B^$%-k@(~qGo#yiwHwC_EDgxwpnQ6?ra>-EiPWV@d2vD?fO6}@*Zav7@*1Dqi|b6+Qjcyq?{>kIy2gz+w#*^| z%EVX)M|TEHY>_U)@Q$(E2Rpv=p*$kH?r9r-(Z^c9`v#Oug-i!r&GANPpeT$xTVXD7%HsdAzUEv|7~1Hao`K#}q!NLJKvSGKbz_`J>gLsT>(=|5PcMWIOm`eCIUE?~ zyk&qE3&W;Z^g}m%VtE|nRPk-*R2Gm_c{u1}@%;y~DG%BLxY8;#lOeTEj}JXfhLqjj zBVC$^GC~+tS8Uhuv_k`W#Js3mA|K>0b;tv%LbfqVCnNcF2%2O&p@)hcpteDx=p6yz9AgZ z93r_?zcn|Jm$F4>-9EF74oo{gq*|9 z2)R~K#R_tjC5&$88vDt5Q2-omN53};A3km4-ZsA!otSF%fM(A-#W#ceWN3zK63(wR0Hi} zw3wuupudZaRm+tvE5t&8X*RK&_HYl{l=(}X?(JMz!4lv~d?SoTHs%ARe^#@vptxqL zQb{5@8Q{{Qp+}%{>ef$ia(IE5l{oQ@Oz{CT#)23MF&H4a1=G*~fN4@`n2it07tgrQ zb|^HbO2^3I$^sa$D@lK}_4SRFEqQduI79+I}odh|k14MvT>5mN}Z$=HuDr!~8$qFz+=Tx7z{GL>MS14;yQe zL52`dXDZXVsM74aN1G#!kQn&xR%dk32?NEtyOHBJlSyY>6*_drqbZpel7bsk+ep1S z<9i!)PrvBYn1kS%>7;^UWbvUN*Q^LQ!R_k(zx6}?KjqifHoJ1JRnlq;ry*2gxs-MQAuYGGLp7kAf zNNm<|uMvSIXA#x5P77kwMPtsIWc1I>YzT!VS_Z#X}jDXc`T(A6nf~+JkIqj?FXTqO0r&D zet9I&NpK6(zKHlD*U)pEnCX43#G@*XdS{YRl|kFEt)T2xr2r;*Ot9`x{Y?8tfh8;> ziG!2NDa%+9E}M?iyDhH0lX2DPtYVc1u5{U0A`FWWtnwH~!{S0&yjuL9?|M`l;zy~$ zUDq%MVgYS+)!|ovyMg+|hMxQj%FaL6h9z?iZEyuSA%9cNv1z#kGkAHaPhEoN!h%fPq{wiZ$g3P6heRn`6;DoksIxTE2K|ECdw20*4{ z@6O=6MKL>6V4FnDUnByQz!#J{!S-0&L<-CErJa|nnfMd?=XUVmDvci{!)vmLXr@q! z$o#$icX@8+Yn`ATwU5YxBY1rv;rRM#mXtcmXyf`ew%S~D+%c^+fCK-;)AO!fm(Cqa zejG+gQrQ&;LM-05o+~w+in;z>6B4U?#}c%ck5~k#gn2%L{fEf{t6)F;&udul+UWzx zXzs*8n^b&i#0fs`+w{byd3TG5lidqa`(3io0A4MwzvCPA?l(|I;|-VcenGkUdPg7K zP@oznq{fpAEuo85$h0~=*7M(}Kk%!EE>?}zx--HB8e^X;BR#H}DM=FAHy4VA74Fhz z!Oav(-c#9e{XSz@5TsKnUSn29xj$Ev<{HkB;r3Hl_r!0<Tg&ue5ZEVnIDo z$uqeDwghn)WI+4DoWoc%MT@p2n`{1M0KGZAhNM+&x_zKr@z0e1ncl&zb-dLYB2mMUG}(5Zhgh4-!fHNd=_gb$)r_ zToJ3PUQ_6Xzo6xCim#=${ug+viGZ*F7?B9HUA?<=GBKh5Ov$&wGe1sgsk`FEps4x( z!N@~xKajKLgRHYS`;{AZ2Go(a$2zX&^n1JT-Sa}b_;*{`o;3aj$ElIly@bl%w`1Fyz~ z1d$HtPRHO%T|bOB*2IpJ9gwRjOk5tL(z;9Q0KWBAtT+dSzLglM|+l1$9G z?U%yuTUZw}t1D-*OE8*(0+*fn(-w+!$?kGt+Xyalnek0Z6uOB64s()>rwA`t% z1S+kxmq+abYF}m5aOA8%i;%owRRZkkEcRuKG(4;Jhp2S0#A-TD;{g=sM2q=XWy$tQ z;I$x)h9-;M|3^btmyuTKB8VKSu|g+J1oEb_(J#TnNl6DytBINwaVC4sbON}v*0U`Z_3DZh~qG16|| zjSTyFe)AHXi*(ATTsAA2s-t?VKpHz!VNlYoNc8*N*I|kM{MLN91Wn;)DVA`?D^Kg_ z(m9uS#(AM9ww3-*a&z5d!hiaB?OdA64CsqK25jz$6@aohcwhS71&69kSucTl_R_tt^d{N{0+&$oi?mC=-N1en7Au+ClQk& zSb%Rzk0uosG&&EhbS%s|u9p0WRL0~zs89M^IkB*^MdEmKl-Rvgt&Y&l`WCP-w_Wfq z*h>>CKhbcjPw1g&n*g8EQ_R|^-65??$p#fNCD2GKJ&&~D>EpHiT}$!$mXZaX4Gw_o5-mi4=G@)YwVYzbWf4$b5#7E(X9>;<<&6TCr@`Rto%r z4G9Lc+;&vWxXdbMv{#gE4A+og+ds?P$`qXxnyD~!OlB_?X4nn|Anwh8qviJMcSMor z1aRQ2xcn8rn04F?m3c=fn;Fv<_x%O_ z$7c2@^8kh);w)n7a@#JWhpWTE9NlPRV2x;KnmBrmsbRrO*-cxsqPWLSs`=NDlYmZb zo+P^*GqPinb{>z zIdL%KKi{^e>$q1_WoSYl%~_@Z`by*HIW;yD_b;o|W{}qF!@WAp74quy>00+yNX*dz zs$AEOgIDk%nD6v}^buV@O?64@Ex*ea>d1ALLEJpOXUuV~)><-oWdZJtvUA%D|6~#_ zD7O18YSo?&-tY>{(;LIc<(7R=(n^2Y+4D!R^%l{Y!_)1J&)Re7x&S_UYfg6p;rh}e zUpSqV?}@<#|Jg*JPT~+))xV^!g*%p{KEG6PX;f2uiZ# zT#qfV*R0-SM*Zo_)g0jt_Rbmy0l|Da-VR!Y8D*~kvm9alB^nz4z<6zV{fqApJQ0#;VB-b=3x|w0qvC;@}>&<>M-Bu zy(iz+(8*vEFEtc+pbWJ!WwXC$>%eMny{F428cXm5hk)7k zt!GT%)g6)E!3h|Zx?d#{Jov2L!$pD;G3`?~xhWLOs+`7=dRf&!dYWd!VM@nmeU5DR ziZ%|GsFU9H#F0Dl$OU(IpSuQ5aIi@T77`TX9r}$eo^7QDF8e9DDCJz7SiyCQg+L}Q zl{7Q|Ui+Sf?+`uW6I!V+hjp~+4=|$^WX+k_h4PHedZ#%$O+?Cm)6Si)*0I~H8Qh80 zN=F9L`>su!W1d5iI&5jI&xF)stcSnZ9&~=OxHM90Jih!|(h&4c3(V9*M~J%xNv`ezS|-RFLjx)Cqo=*E%-@Sa|>s_v)d( zWH#SM$ddsI{Gn43z{S5QBBCaqR9?C54)!sL?=mB_#I~gwg`_#@yH8%AKADF|C%xDs z6I#ut1p(em6f`h-?kdP-^jN}nHK}R4MZoRTnZT=4AU1U5jvCI0hJkuB6>dKLLIglp z)$uwCkUow`-5ZBkqGyuLwG8X3bR^8Tz1EJwJX(lAOv0J;;qMyW8sa8~F~^YUSo$yC z^6eEI{|*girg;y812(32X^?Fytg+t>#MY5RM#nyK7$3`z@>Km5@M7_C0@WlT^XdPP zCotHtc~1mgnI$(J9-h!y?EpzW&BU&F!0EMnlZku%nnCQx*{7)=yt*|E!G-(;Yt8Kn z7jl$59|QMc*Je1EE%(qOxHVXeGMG_LwwA&YweH;Mj$Gz1_OKv&_BbdTPL8(EHxrDu zz4rKEm*(FsT1R5Pq9RvIiV!1KjVqn^e!^{&JdWEs*$Ni_N{=8?d4`^1kI59rb0fx` z14>?NKh^e+%bW=^W^S)9u9aa9WxS%-K))aLibedAmU#}GyLz88#4hU~@v^%dnRNX2 zo&A9}J1e~|I>|vOy#V9ju(}vM(F8N26*_NDxVICFqL=1G^^f@Wf6!dESPIK!W_cC& z*K~u*b<|EE4>Pe{@CQv?dA1UJYfA_-q)#IpUEWz!RvfRfx$)@G5uXUMue?9x@^cQw zpT`${j?nox-w)|6(eEFj)?^Cb=AN??B=Q{xril%1y+jiC4c7=5-bx9UjPKC=ghi7Y zQ5^UCVLnSdhkH?;)_4~G?`Zo&Dil>A0S+;r>EH_Fy1;9EZcSc?4=BOl3yRl^yiZ&6 zW149-3ZG*rQJHla;?o*S42!qP--%{8BBYuq{AopyVtnU@C1HBq${2_d3DTi#9zz2` zJ0xEXlGf}{A;4Ac1ui_@gXLMW_n$48x7nOShIf3jRWVL7> z;mT#^z!M4?|(8$n35!Ou34_T+wJ(p@&iA;Ng z)R?4j;Cc$LZ0)Vilc4@?ECepw^p>;?kXgg;nWaUtklLrhe=Vgl^=VrD^t5863r$rJ z5k<1)*3N(hm(ctoe1;8(cNwy^>a#!ox1)J52Ef4JI$Y7Csi!<*$@Or5(tjB7a(bsuLl8Bt}UDK*@ zu|A=^Se2*-EM0n{w}|jPlODaYeqJZK_r|l+jssoi->K`xwV%4w9D4S!<(E^U`ct!? z9%XGOoDg-U^E3Xm0xvoI6^X5&DP5!UrN1-VX9wP=|X?Z)JxZ<|@9UWG-yemX;_8jB5VjhRSKbU%AKi+i zMOKlzf*Jw-&y#gZ3+cW^(_6q{{sj4r=imUrYbS_=Kwt%ViVa&ySNc3s;U`m1CzM?} zV`mbFD^U`DKVIK`j}Y{ef`bmFzMQq%>fl9h=TTZJFGS3Np#z*wrxBspG%SqQ(c}^D zWzyu=wbay+Q)B(rbU*laV?<3cWkY;t-2w8B^KZ!QHLmJAG|ePaTR_h_D z-AVwQgtx=bz}B55pY{ace+r499{9?Z5OT1;wg?lk5Gz2%zeV9?3suwPa%3qhPjT~x zFL6|5orHdraA6Sb;;uUI_BPb|bRbO&`0KT(bqs;B4k0GN&WlJ7e3Hsu%7g(gM^J8K z&#=Jp!aB#?fHo=&%$FTj>9AN7;5Vt4#)q)E-KfL-#P{(1)$DH|W{)#MQO9_r@`;O!qj~Q`#qP;sUHiTL12*$HYYH5Zw-Kpai3A1aF8&R3*ZUEgPlF zSKRE$zVNbFXr&lA+CbCMfSjEzhQdqTL%M#SS$EY;)3q-7<4zyJR;pC~Qc*Zsz$y1n zl-Ao&Df^*$Y|SrXE@MXQ<(fs{{S3Y73n=b0*<<-^Kh^S^PfTeBXWG+FvFD4Bj{kYTOZ%>Rvv@IkYJ#NNL*Nk zwOnu$F+LvLKX~pZ+X95aM5M@{e%D@aMFVO-mB3rgzLFn zJSXE(X(ZSx#bvhdYZ-ddT+4)<6Dg%W|Br+nn7+11LF%O3i>Yo~D@c$Jt`DXW8|QU) zb}xolh0s=sLr^Ih+ln+1b1#~#RA)jn7I?oL>n1Qsfm~Hyd(*LnZGwHV1^nR9Pr-D~ zklUL6)XA?qyNMrO0C-ey=ky5#U}|AikXU)I@e{(_#rY!^SoCW`UT$>9jyMfDTtzJU zt-OQzm~b@EO1{_t*pWiSX|^t+%>=MVlnz|wB?8lF@>FN)WnICV%{oVDTLA9<6?x91 zl!mNm@~;CE-hZ?U2;qh6GK9vtibpmqohsJhm(}?h`f`!fN)&Y%g<-E;WVrujm&oUR z$5Qys1mCCq%=^bRvFyFEh97Br(r|NlJX4WzRIRDl(~63U1FLg%(gXyLwC^Sg;OqEn ze}1Mk1wg%!*9N<$<;v58Y$}ij4B8SyE3WULHr^Vt6ynBNDn;3{EQDq)bR!YEJ;IZ1 zZaQEyCMnzT_wV0pdB&oUrMRM+ zeGX^Ne(TktBlheR-Be`!|LAC5~>2Sf(#?|nf44hhhk z_uhovv&7xD1c)$V{bKUCm2z$;iRpFm3!3b2!xajoeI5%){abl=TO|49mTj=Tpn9%qLd z5*1uY03zF5+WSbZJpsrEO;08odOC3F>vV`eAp1NRe6RxZbl3WMm6=7aVJg5`7;{tx zO1#gV785jQTJgV9y+3YMXOWfm>%!13Szy;dgl4_gbAI12N?)_@r{k1Z_pQ-NyN+BX*}D?>CcLP`)aZJJzC@?To1|)mQ7F~WLn4&u5-TN)WXq3j ze4xzZ=vSyQ=;i+Ju_w7H($H1#;H9lT*2_gr{ZdQ*yfi3==Oncbpgpv4KnHMg116jOJm^?|$7e=W{WoWdSvevF`coD;C;ReB1+bv*%Z-2(l`!ese8& zMDEk)AqSfIq?XdIp-5rG%X@|4p^ko;3g9}=uVJ~05{~IT-%`(!KN|FhV4^(YPXOr0 zgx^0zG`PZq4x2runcJ%Z8##V=CCSkBit-ohU*BP2ziLer428a3qf?OWj>MaM_wEK{ z_Y@+4DE%o4q}c5$g#;iHU^@3w#=lpvTwrKX` z12WCQzWYyvN*)YOhrTmudg3<`kZn_?dm(YVRS?Kz95yy)GMN%(UQO6X3);8w6_2b7=(+wyl!9Y$=#}$sy#Za4fFA z_-8P=WhO92oc}BvsEz$j`ZLi@j#eBdHXzm@0}oRDCMs+iSH^Mu$};OTp**o<$w|Xq z#yzR|Li2E%ug_ubz@Z21QAW%C8`g0yyI|uE&(<;K*=LyF8+8p|0bzK2Smiw1_n#bt z&Mk>t;hy(j(qu1ZyvI>zM1GIqXf8ey_npmAv-+SHc{q{f)$`5}x&$Azm-msdoFoCM z$55gdB7bDRCjh^zmyj(}_bikH?!||!O}-lfei_SxF5+V<#8lac_p13z8ZOW{HM<~Z zid63MP=q0^m4tpacB_G&Ord#$QSd3bPvBA+y@=K@y;yyZ#Ro78prMJzu=8K{To+Fx z9qr`o_Q^tzTpbpD4g#=^dQJ2xOv8cphj6;KSC<8W1XCbrr6)81)dWa&7#U~)aQBIT zL$wXLb`lHoNZ=m(7qfie2M!D)!*Pe^Glx3pvxb`X!UY9x!4NmJ4wi!}|BFF)I7Bor zThvSab+fnEJ}?H>y0)qMwr1UTK`ctd#pY#Ax9JRNDPje*6;ttKlen06hSwoTMg)&J^x;;ZlHY?&0>c5W{! z4%`+T*4YqW%65Z~+FhS>y|EqoiAjYRjCNY=$v%akP^U!gvDswdHTs0hwIU z%I2Aw36AK6lsecl$Mk6L7cjtB~^wh#N9Qw&4XQxL(_2qBw1dT8CCi~e-VnF{K#<7qH{&HXr2V4R+<0ewSS8- zB6-dbVA`h2Bje}*Jq=BlT6#hQy>WFf${?#}X`oF3NX+++{9D6Sl^3%$zsAOX1~~R3 z$A7-URb4glKJI`khN0993PWdWdE|)f+lbw0hS=WAO7D4jdA*$Zd7^2wtEZ;}qeb^r z@t*$=R37nhZ(kk}=aop1Eo6Higw?E)=pt)!GMBdfnGVt6dSvy_hEKic2*Y~m;c*Td zEnEz(*5s`wPoNT#rEN0PxamlN+nS5NjaBe?er~AAri}@DU25n3=<*H9SfU3qz@xph zXliHB0+W3xx!veI?{WfkUL`a+Yq}_WhoaEpJ~SjEp0Ip8-}JkrY`d0sMsN@LbB~J` zJDhhgVfvv)rG*Bu^RM&VH$A-99rR;rL-q1c3|1+rN(KKp{Iqq3m#5~8!};f$3%!D* zl%6b3DrQnT7fD`bzM;kT$r^S0$a%_;tQIS_>Q%y_yTZ2N_C!ncO{ z@9M@)>zasW(UOlQ+E$*nZ|mr6g*vZSYDl+IopmF{%jgLqYxa_ZLzi&x zo1ym*fYd+{>-jn$?8{Ze#jM~*rwmZ%<>klBzSBi04^!6)&NNJpQd|~D03}wIr`GEC zi{(R4DhV)2)%maUBy{Coc~^pL)Wg?oQjj4?lQ8SOD0MYnHPnq43&`@Z>#Fj3uSZT{&eSLVk{KfMHzs@c5k`;>V>;U#HCr(1mu__^i=C{al+6|Nh(sF;&{ua!3 zw@6^lTfRiX6+z>`G4=vl6!cmIKiQ>S1Acp1{WX%!DIW^;Z&fqj^!wlGGX8`{v`%Lu zI3BzA;qvj==NROi*Y(KUePhbwIcTAqEw4_tgs+VU5gT99NZ5|f&bRxoFO}RexuQM* zAhp<+sEVZJJU7xBC|BYUOOkya)I#%Fh3 zYOCEs?M>M^VK1d8upPOIs#MP3>78N9zZ~qeGtv!ImFmaj^e}I7PGt5n;4(b|ZB(3w zv@^8azD21R4BZ~j>Q*tbKZ-bhj2YTHDde0yj=5I@cumLL&EyE`R?BA@T?d2C1=BFI zdcWllcM-sCVW{=JZ-+lT)Xm+dpr@ogq)r-Dy;%QnCTPiUwol2rPx0{%3AXzy|7#@8 zqal~$kqd{VFHP^)W6>Rw#u>j60?eMfhMp`xNU5%jkJkH{@moUCpuj?EeqG7O z7}n_T%bG#?_opam;uLmJ3^IMRz!a^tWxF{F+E_;c^7PMVyb^|~NJ*$)o2C2I zYffoj1Ct}?y~TK4$>R(RER!;dcf>ePF(YGXtjo(Lo94@#&s`fvM32g*XccnK`N*9S zGiyW7oV=^EL$EHj?$MaJDMfgtm4b>zSR5>`N|6Q9D#6SQh6ESsj>#e-k2%y#} zD|y;bL;3=kHgsI9qmymD?S;;mG$)Hte-~n51#7jNami1pGBtF)KI}uwWfD5$wGGgr z@L~|GZP<~myQ$;fy%xw#E*HTBB|m}*`z)jYUQ|m@W^)NLrj9>B06}bT??NZcp)}bq zC`S{AiXj4QL-%j1I)(}iLI5zhpjY=CM29s14UDtJjy?y8P%gdO^gDvePbwI9cHLBe zZc7-=@hO!4-$0|7(oH+ypI|um27Y33=YF)8`@ECFcKtzn``UlAdxh$|t1aO#`l#~)-Z6mlf`x@|`O@IuvcTX@DSL?UY#`I0gD5Y*T+|a}~h*+t| zNUfPOsK8&WJ%rUGrq{HD&9(9irYM7JR1a(4HSuQ`3!l}?T!&YiO7fM)+}s4c*qQKW}SO>lenfaac!`ivrGxn93)9gnjTd!w~80IfK+fDh})4bb#S!usX*xyTtiZ>;I=YYL6 zw=M9sxw??ys|G$xevO2NL;}e}F}UG*|T~jW-Kc8(+o~$v_dBzCnjPzyc46fX{V1I++2A&TSB0QWlr# z!nq#nqKNQ74+yyr?saSU* zrDeL#adPh*S2$O@GBM~`s&Fz&^yo&|3%u(jP`uo266wJUGZMnzqp?hAf^p9rHd>9wu_MWCkt1r_ye!KCPCICRF-QJ zz}lxxkKA(W0up#SnnJ7WB%xECrpNsL5OQUI<_(Zw#5TU)e=B-8HBf7xuSOWucD{~|btV1>Pjz89#NJJW1X1$(e7I-jvD9u9M**j(%PekGy`tEQRx8JzY6g{m1o~Q9&7n6 z^yez{wYToMsjZQ<=B>ijV2$?045f4`&&llAg=(*_vc);hy3fDyEks_)h3YBY*Y*zE zbq9BEjMED}``Wg;GU6QG#{~%wiSC58A~=Os|Cv-p_KPtt$E;;0s~hPiymOCWK`M^cuSr23E9{=h|R&yf%nFadlzg#I88CXs_Whwg!x zST|@FQu9S0q%uk-a90UvlhCmh8~uNO&SPP6l-)r$s~w8g-LFW1Ng$V_Y?lOpPA2)J zZzipO1g?^bZ(96$=X;4KblFd-AdyoEFU}K5Y z?};UB>fz-{W&30Ul3{`=|2fk#)Y#k71Z}hPw#2d8D}e>*;alR++h{xfwlKDOcC^w`5sqcx0;bZ9VO-deAeTh1sZui?lGRZ_XcAF`DK|I2UMJVDw7UGc!td zoFqb)b&hAZw6&VpD>cX510l5}+1dMnAXZ9k_`ns8^0oqt5~nJUq;V(Whm$DE4A$Hl zxOgw#FyVDE;Q7?c1-z@nG4P)xv<1}+MuS6?FI?!X@(KafJQB1>K9V_)^a!3b5p44> zN+yECSOfz*EkPyFoN zpBlH2-Ts#&Rq@R(8qbG@A9#TKjrK6y51j7aSu^Afp59L-E-CvirQCnWEN+FgE1fk7 zdnU5;^Y-G_8O>me_%kYJlV0v_d+wjGo&57RNz`rVcIfKVXOSwOVijSw#Oj;07X1b& zX$wk^kP$_`=~ZGP5Pr{8Nz@y<6~*FWrEfrhB@T^M^@& z1MFXCDBbHyqZ=h z@=~TK6eq-tPSL=vsOf9qShRZ`xTtfk@O7+D%TXO8ZTT%R#?m`mKX6$e``Lntuw(_{ME z1;P(Ij@;naC11Zlf-QstHrM=jw|+Qk@Mp^XwC&%Za7T^SmA<&=>xx6wf%U3`4KPMh zSft=-Xb_Ybnx!DAZB!u3^HMo|dEhmTSXSfxHmu*}M#%^l>%4J&F!jIcasSwdmDhtr z*QpZw-!Pcz0z$gmXIe^;lhkgf1Ny3zlLwP;7Jtr1R3hqE>QvcX9UX5ezL&Zt&G=Dh zubDD8ms`2BvlECOEIWA94~0{>oPX7q%{Q!gEyx9({xnti)}VSmYgbHidmX;O4wi4% z;BR^Z_q~2&E8@4Cn9u3l@U?=P#_F5PW1tI|zrb5fwiFBg^#;(heKt{WopId8$JomZ zr&(n@im~a5De-v_EL+Ch;A}YTD#!aoDGI1~PSqq>N!)?6_QXoB6DzjnUql#`!GzdF z3;0pr5+lVNq*RTxMF#X(&;7hK8syVbla>4&5na*8;_+(S+cSPEn#o^~Iq~or6Y5mc z`i|5{iX+oDX~$&xu`aDHj5iSO%Wll9i-HRO%}F*nBcD9<)>%)D)7xLC5nRYs_SjMH zqs6%(oC-Z|R)9CVl}8ACsd2t}odEfrHUL6hb@x)h=IsLUQ+3H01=r}3|G_Bcc1bCn zk2UU3eCl$N4RC=id|*Q*-K)G0EGPKqA8u11HSdb*=smow66qsbh59g>tiq|UI=tAH z*Ld5+aXoC>M+9&oNNR8hLQ!42p$|=rvgxnFAE_hw>&~ViSM$XoU7ikM6!lDE)QN|1dLR za{gH5fZ^B+12c2!9uqGnpFH9f_T8NSqk6~fr1)w20m|9>?J+GUF^E=5#8uW>V*TD! z#2hB*KV7S;fl&?XwMU0iS$7MR8&!+~Q(2T_Wj6|p0<9Rwi_QZ2aXwD*Cv*ogT92eu zwYE@)`Dl%7Zp=(8U!`}6rV^o?7(F*Z=CD{z%+T1dNZ{u#-)(LF%HXR~r)%!7dIT?hvl@?!= zgFgSgcn#f9-B|p}Gt8D7p^jekq87{-%RUYPI!#>FqhOLCW^gfn9`o2L2mySJ&lLJH zLqZpSL;`GV7r#a-`#tf30EVX?>7~E+z&vdu!2UQOXcS_G)fmNG3xgYfOm5nn3D658 zfN8-^aS&Hhxd_#Mq6;%mbiRugg+a}6PG}hfiTxoriqURS6X5?_3q;6TL8M*=PDCc~#s1KfGD5P=r$64ZL_gLJ8Xu}k45kOuHXZ}p)oQ0S!addX zpJ|`6`{(m6m=DuRSR2H2HXnbe)ynfHVfqqE-q%rBd4W-5o;G%v-krf`T!~&r3r}BD zYFMZaDVUJy*8T1<9?x^D*R5V@L#Mo|o_~`oo_p>N&OdrbaL!QybH7*j3@bL{l7cWo z$OSp{QWKW0`6jixigQGHLG-QknKPx}w8`vVU%lxGSQT7I#r=xH0=vJi9=Zo&OV(Ht z{BU}^JZybHY6x~(t*TKfM5~Mhxx*g429|lrYE|q|h};$l{Q-o4_zW7)Us!rI>6=ca zXlX?qdU`Pn8sf|>W4!}{h;O}UN;wwiiotW;pu-vwz@!hD>LCE834jn%XyYr~(qWLgU#+!P z?sYJ)zbE+>v#%EtO8RH|-9P_#8F?Z}P{frrS(ehaXfQoQGe)7=5j!~=uqil8YK#!Kr5MwyHyw;_ERx z#cU57!nbywD_ds41@W$*XzxzNep}v9!1wR6YvGsq%Ip~xMSu7;3r9!4ny;;AQfPXD zm4QOt9wlFGB0+j{v9P93Rr=O3B-y8zBY)&R9fI**F=+;2gCTnw-)BLAy{7gBEmsf) zlmwgAdZVpaAS!Bs?P9+f*h;9XA^^SQ!!!ZJ|C_9 z#(l~y$3v*gFPDbpQsMA}YCSJmYIPZEl@xNPG$l8D+Egx(PuTe8jH$v-9s|noW@}@i z(aPv!YG!(<4fV0{cL*=auS;$!%S1vqut4SvuCdmFuN!-FwZ+`Z0!8b-o1dwA`TC~A zdtM-!i*`ziKtSEC4z1thd2jgZShRW6g03e$Z=@il!S8c9@2FcatN1Io63DbZSD4B( z(y}NNLabV@mn;_<6-9Y<4lllFHnG#q;A~{tybvut?`e>6$Iw1x1In(zb=qvz78%9g zok+X^$87C>MLd=6NFQA3j~&(|^U$l6daLE{!2BVu{SW=aU6jmojy$(fgt!jtsN)Ro zqm3DE)I0hl>I%QKuWR&SWnCIZ=}&l)X3K%&)S2)NTL(NDJXHInN5DV{n52e_!z=-I+V;syEKnZDW4Svi(bPuH9klfCtT&jL=H*Ej-p7f@YJ7B9GOC%z#-^iu@okg(3@yDm)opa$D`6f0 zOWSh9wzfyIfsubnt+o}De9B`AG-|0RpAYI$zZud+w_(*`#>Be4#cQQ|1sW_8E2>7X zr~7L4Nk8(yze*5e%o~OYKNpjF^MhxH6aT2Iy09lg+)d0ipKTTEb?ewU<+~eY7eD+s zdSe*BA<9e(qwluRcGTl+5-%J5h_!^B9T~2MzI706<{1d=IdV+=#HVsw-%i)0jUJ8L zGSKM@1v33Gg2OFpl;d z30zP>f9Ze#v8UH4keS4EItAkY-t{bq&hqi%+XO&ZWvO9`28<%XUN3xp4}`VVfX6|@ zNH~y_pVL9%*{3J{ud41!y^`(JPaaxl4B7bK^HuoFe~?{Eh+*bd1jKL=i)nNv`Ipr+lW_;{}1+Wv?1LMjhEI$2h1K`5GQc((lm& z&Dx{g>0FAuT>sqC8}B{8bBMtpu0TN1<_F671LU#V`dI)(=%AOQ)LNUVQl&W|!T(&F z=wTI5F4NtKyZDr{>!{(!lt<9FH+|GKE_T++y*~O}uq*+?co-LH_04N>z+cIpdvR8y ziDk#auZ1nx?{WjHU}Bca%4kF-{!*tbNQ-DGcwSHmD}?cTzFm34pJ(?eMUYbe^xSZxixbTU9MvGf$4PMcr?{JZ?ReMd7UxF*lfE}CdG~L7 zAPAo7yY~R=1axoNCUIvjY}0=)qT(N2xMT~&rVWtLiGyIzF6}2J*$3?U7MR)qPY(~h zpb=rny7j&4h?xikq3|>yFAxx!OaG?u|ET)%c&OL+eI*qNWjzULr?Q+1SwnRyS+dJc zQI?Q>8Dm#u>F7kZiLx(a-x*7c%9bz~3@L*##x{l-Gw+>s< zBvM5?EXViHmml6KTl{5qj9Qjpny2a|&4-~BRKUP&9sWe!jF-f#p)BG&>Vo$2vxzYn zv}-%#Cjl$_a^Ims%a%9^z0PGC^@%3i&x5%#%`;0f3#T`yM9;=eW}6@_sbI-q^=zyK zx8x&NEsZ0r+>)=~M|f72RAC!xw5Ou3SMe5IF~kLn#TU()PjS&~DGu+t2L6C$8j6wA zd(b-3x}TdDw@YHSP0${Hv+?{|zh6<$w~35D)}eH(qLtk(oV~q2+d8fK9_4y)wP(YP znxiLH3h7SnG|WCNZhRu0L}xk}MjQs|;Z;;XUb+t;Io4jhDcso^)swc~*p_WTSCgjW&VEPF^e*d_! zz==6bTMU}Ujj7qHL~dsw{~5JPzVdSv5%ePN*(0!(0$|g+DvOA@hXg$_MDp=;Cv+JZ zWlxinu$}o;@TC+A^J3A(uZm-!dnZv_RB6|LK=a}d_BaNTxb~Hdits3f$bkdbnJLh> z0cHRzFz{nj5ZWhS?c5X;eIp<5K>b^{MaB_}|G7F|esZnNb}wMNk7cq8cGIh(2N;ce z=I=u4jBUgYEvV5}Z8vl>NBcmIJ2L&w{JGiYHaBb@)T^V4h3@6s+x}px3+G0|bQtHS z2q%}_9eUI&h!VQ!Fxe?CKRe#EPb@auL_>V7A-_(ybIZQ<=0)RW!}1r7nyhxA>zkTi z3c3SoyBnoGFIU<@R{R4Pc>&*;@3BYuUW&6yRbLQz%pJ<9!$0S%Xn4~o9K$x2A(E#f zP6f(~KfkvfA=YHUvJSUQ>7JBkw|0FRnxuCP%nC+}U_I_@zq~M*j!-ygW_s)512OC` zmxt6Xs*mU|-JFiX8h>nyU%Z{#Cr6jSt_LV%k-Vv%0{d59HN5<fhCj181si!`}Y!)sl8<&5ne=5AUajrZsi=LL`lJ@bdeFepV_ zMLUw@E}7~))S6!FWo5CXH5$}_{4Yt$o^! zqRVAn8WlfhIODeaVHc+?g{F-| zB7Wt!$GqDp4JmcL8N^+$I#o*0`Ud4oTuH8!SdAZh` zCR@HzsAD&b+iGVHGzhgk?wrHArc0GBFZo4mMxI`Q$*bYB*b2H~*5CciZ&@EY@|n>v zgsy{3Pg?iLQBPvsGYolfO~!F)>})Fy=&|ihvr~~AYBF@A=V>^Q? zK|R@dKDXRChEDJ)tS1IJo?O3SGk!u?yz{|2Rr*|%@&g-s@)5m9{bYEy-c7S%2%tYp zgL3EMDW5^BXVNj_#u(XJZ{oKkNL3TCf-m&doON zFe~Ubq`hg zNjd=qla?;B0x2$@4xpmWQiVlor=O!fAnE0^8Z zEWG#f%pe@aqsB{!fxxH;*deb09MN~`Q*59+Wfu+&)qiFKA z^93PLS!={r0IZ`@;v!O$WzeT#I?yQ((q}H~Nm~E>nr!W4i5_Bz zi|_k8Qf4FKTFU+%kJrtqtW#BHgXdK`*BjM28wOA}xMe?`G9q)+YxW&?TBh{(edeU4LvzQ5Py1zJeUnsezj}-mB1xz0Dxc{UzJRH}1%QUMwsRK)4Mt~z6zR!Z0=&9q4$zmojMxHudt9(X7>F3T|0{s| zr2?fJ22|$_;mxBp#<(s5ocpTBXA1)*=WBq!z^h@D@KDKWa9!>(a{;|4Ku-yvdZ9BP z^mnMZSPGOuR_voAL_?HKXi1Kiz zVHY9ld<~fFz;X8!-9O`<6<3}Z3-vzQ!*k~_|4P8)AeYLd=N|MGi<6|wqo*G$*45l^Vjq=3FAJg@kY(`0 zPER$jx63NBe&Co!doCIdZ;PXK+eH4JGgK)3#KWC;K3`WDE&E;lm_NL5o3}qJS?6xl zlpOa58CS~jj@!4QufA+%pXy`tRu(b-fDh5azMp3KVujQ=W*4r}9m@Xq(@inFx@wIR zqAy2q@wyS(>I`sFcnBcg5eyJLxKara8$RRAxsz@f$heV_Tm|SrF8C9> z&D}}{V593(sbG(u0sTK5l;6T6OPdgWf-}uC9x%14!_B`s=Q1R{a4mfWtw8vFsR0-NSM7YuOD97P z=kD=i_()RxLwpobM#?H$;^i~@rwJ(?`Eo^0>lrl;_$t^N`prg+`msK~4tdYA$!IAb zYyL${mY|1nQ1FY-5yqcdKEFc5zo9r^N7VaDoYf>MS6lYp&ZGYHQiyQMyHRt(x^gb# zk?EAEiJ@s&ur|d3mb^LKqQf!OuhvUIy#>jwN~A`3%g(6D$P>C4e9aj&Ti-I6BTwE^xML*t(*zGM}&b)1_@+ z6Hr{C|3eW#y#k#WfeRH?TR|Cb&7fhmQuuml+4&z$n;EWNV$IA*{QDIW*E!X{TS2mv z$!+6ge<6N%*K$&8b`w~eqJL}77;uWp`lO7k&Z3`O0(f7thvkbe=k^NET)xFTf(A*m znvHDK=8;UiaH<@7qo*Rv^9b9wjj4`8~JWG0f6 z#7zpsa=Aro1aVx+=a`6AeXp%q5w_UqSwn9?<_ zW!EfUn8la<$H#rpcn!GMOpv#ZT{&HE2&GcSivhhg^zu-PZ%r$T2w@!fy zC5N39a#QbJzxOz|`pXbPn(J{P@cyZ=~ws{5Fiq310_ znMhHCH~jLIBZkmaa^WVbpNd3(KX7>?y>UwsD|q!_ZZJ$}cDZSAA0NnV zYz|G1ZhomkZBP|VDr^pku)H~B^tt?~^m@yBgOHESTxsf=zr(mUFg!=H@KNt$ z=O>fEzIrZ!8Pdq{x^V*dJ<@NFFNRdG7emtnhn^$xPqFHO+O=BsSGn6~eQ_SKmwoA| z4S)}a>vzErJH<`-DCgQ9GW;}vj@*KA#lSR~f!heDK$FZoISyig+n2q~uU9L|S}nuB zlu=;jH+JJ$a;8C&%e?CDjk*{0)_`xMGXs>zd+&G}bJaXS} zm=L#8-S-)#;|@H1xtOqs$oKqPZap_);bd(AYazl`HOl*AIQLpT@wOr;nEQH^kA|M` zAW~hP`}Ao$nM@$HP+%BkHC}U&alaxjKyd`F4o+DkJq*w%K30w3EqTcQd}9EthD*|c zKW<^5GYKkE=PamET0^5>2goQGt!PoUu)VX(8)E&7*`&LME@l|H)3w_C@f&BI5*i1| z@Z&K^NS_Ggt!m~^VJd6$vt-pm7*k|+cFQf20D-1F*{_G+6xbGzwF#HrLys+C;O}3* zK|&QZU?DKM*&NK&>RT}s2NZ`(RLpv1Lh#gk&qCk)%mTd6a98K6=Dj*+Zw0V?6!yVg zf?#61Z(mDBB#~hbIV2DIp8egA$ETRzDC1D_S<2V^+O-P0abJyPb#9vR4+7`Hy_IEC zxyYwTqG$cK2(S=b?B`5VYD_a7#f{N(f9@2%td}sxP4O&M%tmJuZSxwQ!p*&`1S{P* zzRM#?d;@O02Oq3@SW2jBF}d^x*P1I?A6f!#JAK5U z-OmrSMk2^J-2KH2&4WhHe^EQNtZ$Aj|6zMz)p0@e3}2_bItP%1Dyd*T3s5n$-x&i( z_vxXBXHRe}=M5aVe&}|9+DWnSgBDK72A;ALOV2x426rCN#VUfmro*0FA2xhS`)%hD zES9h^)N~C$k#p}&0*Wy%%yp~7e4EF0D@yKpE_{g<|2F660NxoVMh&ZZb}V# zm~_r&1iz9T&*W(3#w8>_5A`?anLKz%VPU)Vh0!g`rK6u+>o@r%c{qv)chGip)kwAo zlI6%qjcK5ZvDKF9vT-%+rRh^(hl@=^43}GuylZ-j;yR(ndNweChKa)jh)f6m!pKdg;`d zx~1wrMl}1;C@s9c1xK-|YK)C`?hATy6&C>_3;x4C9q_9hoy-X8R5f*{LnL3QWj76b zOfhpT4WK8VK-ed!T0sCwR$E=-)W7fc0mHFZ9L1}VH*NuJ@eGJ|FM_7#>jmUuuku=? zX2+YI@t|)%4Sf?SImUjSwgjoghm%cv=Jx#Uu?*yY`~qic!?=;?N5 zf5<$3_d?2@e*utcNkc*gDJZXaxZEDSsFpUF;X5VaY!qk9zp+P}ad;O~MM(3Zublp= zdMULOMUiCrFe!Q9h(bB~6>X6(viA0xeU>jhXiN1{w`eRnI(ubtTkkNgmUazI=}6JMosv8&yGcewrp>C+*V%7#9eBf=MV_fVHj8HG5Zd`$(&=Lr*jH zFEKT?OD{75vp0WtDj(C3-Pn=Ai!URETN3Z!(pD2|)Tnp12XvctHnu*?FG%{{ZV#HFv`~ zCd5_uvx0u61d@k#>L3u9{>D`SNqP#iD(IeE^6ulXMM0OMYue?23ZwYCb5etRG!` zO90YxB=pt_Iu`~xv~OF_?8B#*D)n07blLLx0zQzC)bT`E_ zgXT_dEv2ftVjvefnjT>f&i&Vsy+T)U5ZCW@4qhSC;N zAOGkSPIUYM$FP5R86B7OIZE(TJC#`8zV6x`%H{IZ>Rp$v{H4urzFTZB0d_-Z{>CD4VaWbOOR9Y!auIUe$0u8|8i@+wgRkS7F+j2nTJT}A3IXtEq^{Gi<8J33vs|KlAq2y0E29P?d`)YrJK z$_-nc$tIvqr0LJNQnx-@Kwt#9lf>^9-Qb#eDk5;MK6~A9hydrGGIC-J(J$69)Yj{z z`Q9r5Kt*ObAcp`c0k@U=1tWu&zVPgATi|`sue`#xJDY^^~k`0&1`oSB5wxyhKtzhhQwv-W4z z!TcTwws3b+HEn||N7a^Oq&G4tvI3jQnHnpFTP;%wQuCs*tF#uu`$96pCwO9Tg#BA) z#9+$@Ee2jz3IBW(^=%lVvQD|u1dW8fLZPkOKzMVbbom{J-$2}k{|sDkP+-G9$@DOe zFVn=c^`O8f;_S(#kFkrfguVe{b;+m91)v0*wi}k)(9f!j|CNfd`bA9Q_a@%ASAai_r z%RS)}o$j~1?N{u9fsnY>a=U#NG@Rcnrg~RpY7qkwqytLcu7zTgyYd!wL&Yu!PEW4W7~0VyaL66=4cq@x{pD=MQxVR%?INJ%dsy{Xo-a0RLSpM6A6 zsIzT1)UA)mFvf+TgW063hW}G}^6yc&?)bE^33m6&<1RaU19y&YAoO{BwV?eA*GYv_ z&euUV^Z)9Gn*ElCR9{BUuA|TKJ@Q>FUkVg#@!UK$Q(bsCfa6M{p^)mOd{IuQHX+hJ zB8;f&?0eY&uafN1#fd#U_Lvu-)-`801zs z8T~PzG#so6|4B$o`lsd_-hjN-`u>K@<%v^#KN8j-ECmmy8?CoFemUMi`l-|=q^NwF zIFh%WF?e~&qkVPw(P7u`;v$Nt#G}-!oaG~eKdloL>4Lm<;!XiKuA`Tp{&wrNA008)q78~qgZueic-C4G9klfA-zTZzppP(Usv}LphpCrCe6-{~z)H zU*GMKUqH(ve94{ACG1ul{#7pgLZ)9f*9CAwj%0IOXat|`%6cU2i11NJ+Ce& z_Eug{jO>C@R!!4YL~q%7Z1d?)+kFqPZ;tcEhTj`+Hp!2|Jmx?)fJ*A6TqV+-O|SBp_u z(&8+wJ+m!evisLtul>nCm!Q61s&_p|G}b9PdCG?4f>W_!$ju8+)!7b52ou&%vBA=X zPJ9n`rIM7;@k9Z)cENEsMe9HZ%(yma+&mG%Ky3u%8~wuRk0{ErbrMZXp&^%S-Q2>l zS{RmczhM7p;fMU{V(9&Q*UtSDl38%sQ%(_N>qsplaU)*N3%q7O$Z+en)f?2DXu@=! z-_|h}{oj|3=6tjC zE<1_E<^T)<|6|jat0U~&?)Os!{>^MFDJ_)d7sEy-Umi{RU|qfS#vX;EMl|`eH~V~B zJeS-n7I#|&Eas8UlHA5&_oV4PYz!%`?+ZkAOB_tYDXD7I@cptA$K z6F$3|0dvh$KvmwJgQ;5SM2h{w&chlUW);0!y=h5S6ITk69c&iHC)KLXr4yR&G)uT6 z5VVy##tEyB=-U}dLOOcBV>vfIZ6;|52FV>%0ckg6V%s#j}%c-O^U;-(KJ@s9oaa$0u2n$?*PqT;IT26Jcs*SJVS*~6O#px{%$C;0<|!j(Say+qML+1 z9=5n7GU4<4iCYEQ=*O8FiAIJ=DEu5NHmuNEtcmUpob3HQe~t+n!|2wg3KA6fGAEf- zgd+Pw(E&jwMN~_ZW~q~Iz$xP%I0J$2yX%Y-22OwZh}$3#RQ-(gmK$VPFlZ_VR*Es1 z%MVuQsQl{bjj|`6qjxJmr+F;3oGOL=2R>sFe$|2&-`Qhi$rQGr4Cd(%9&Ap0R!!n$ zF2L4np)J+$LvxG-{!yTgU|3CaWyXzQS#{~ z*5a(7Ltos2W`!%h?|jzXiRkI+*;UH2d|YctY)olbY__<#NixXVnp-uTXqn29a1iAh z3HB%q@wb9ENhwNg)|(zyojjWkPrTuRH?-a1vCvbSOZ~Z`L$3B$H$H4V)cdu3pxNnT zZ1@1*b6?3{r}*HMLcL%|mNfYFwD*OyTvyd4xw+?wV4owuK1$-o?4Ik zIz7HTBo@0IolSN9m|A~)JU!+&dKDA?2i6sDA^JEA%b##&QW$3-qnVoQkLqZOUB~7q zht-{jQ4|916|m-3UmBb|Wgfn2QRh|aMbik0^36CC=XL@EMHHi{@czcqe+W2=y2Jty zDk3#MI`mITFrB}aZjernHE+P&*M1c!n&yv}axJEYT$0902L^AZyENS~hiFm(8B5Ju z?LXU|871nI^F$Fj<@M7On9u$-W$uMsYKk@2mr-7zcr?Z z)Xc`+uFDPIC~hwab8?x>^l#z9J?dXAi1VW($6>(fF1c&oUjP0uNm8f?uubbNN=lrx zMQ90H>-hI;vj!zab?^RaVu&>{9)Z2qp4V)BxT&E1*d?QRyX#U{v9<4UO8+?Z8Nv%$ zIz8~Zy!xB!^|adOZzdak;(h4S1+8SDyac!&P}iL)hT;cW%G{=)SS$U-kR%v{9Emqk zBGSv;s@qyShi!caj(o8hxN1yx_@=#E#qaKn0L07OpsVb?PU2JhR@QF6`^Lq4Z_5+{ zQtL1a|KI^5o3-fW=s2{X1jipdB;9w1IY736~Qm?Beo_WD>^b>L zbmjR$Glm9uT#AeQIsI!thSO@NpNp}R-=y~4TKvvZFTizf|3J$#{&=qHph$5JZQ zdPo5SUv;*EnlaYQ{ts9ls}?R`fb{Ck%?Ym&xL;>$r{~!*@q?xBzb(Ec=?hBp?CxeVCI8xS=tU+lPfRAc`)ercp~nz|Ug1l!@~0E5rIW$9!_-(~lsC+Xfj9^Lh(tEWSgMu+T`E%^R6oR*v*XW`sHNkV#|(}4u_*G0r^_QXga=da z*$a!Vn3+LkZX)N8;Vp#Fexm8AcF!^cyfYWK&)UuD3nd7Z4Qvi~vP8MK3K123eY6XQ zrE}cy2xfO^!4yKh=(zr8;HJ))HCdi+o;l%h zq(YDFhpK1a@aEt9|D=wp5;skF|Qzp2YbxwOf88wx$Bk3vkMPZ) zF2dxuVm!<98>T!izK|*`Lv72vD1P<4a0_dH!+dMT!+<0(y}ifOU`_ydppf z5d9WYCU478z!H41I8zWpnqPO9EfcT05Fa}#oMT_UsTVpmcJ`38oK0Ko6%8@QsKySl zrVw&)fhoRI?Qf`56-#XKx9fXmtT$D#o{2jkN<9%f8)&u>E0=icoAS#@QuKtTDBdXW z!6Yu=^})V$*7b(EQw`S5%Ua5R6&U3vxXwU4L*M_v_zb3m#3)~G&mrm`4%b@$0JFp) z>bM`$_0a6j=lh!H1T{F=_3R%a{c)KGL{23p`0Cba9{W+l;8e6EKs&(4&$@o*#8^)u zPYy*az+_#sjf|o#Wu&RvINf$`+C^PRAi`r`nNeHato`~=vTL7SDQMZpW1n9R0HADh zA-REnXQV{_>$0U|9O%e|fsgKj@Q@@kni-&unQehEp0KgnD&pw1EXTncd8(TqC#{b> z`7irL_@1?p*#gaG4?)?GMKpGc9%2T}W_(X$vll#@vr~964xbERZ{7k262?8=@zLz9Q&U<22ECi8yHt!96iL-a@uNRR$}PX(p5e`Zgh>5RJ&&pRa$fK}vlV8yD#M_Ufob_a$;5f0a$r`Jc{2 z#lMC4zV{%jU@9E7LjKB4rKT)+SJ;=e7!INV3t`y$>x0W?ZhmVX(_zOQ&dtoM26^K_XK8If5|0@U~8}MvzXZtXky@i zpP~yju|n}_iE19`!sJX>`@SZ?Ij}#O1j9;?Md6MxW?Ho=d*dhxY_`an!_Kle5r*XQE3I<;|GepH3$Dw0kn4AD&ONUl?iO)BtG}gZUpt?_JH)`G>qJ`bjc5_zz9=bHc+Y8<^-U||4h*ZS>gI+5)C#D2ql3QNP zIn+-6WLW8oZwX!e05|Ua;^t68uT^KuDr-@23_JeQ+3OfwMD&AXeOkyqUgJcz-pKYH zRZiK%Gn@fO49ztVE?gS&2RisV1%=PrJm26#Zul(er`%S|U$1h= znpd2vLd?x~aSADz5<+prQh_Pf8ERPHioJJ^eEJ(++X036vO^GN9D!=)kRb(G-S;# z4L}6ydkSRRSBj&-X^q|J*)GhJCPr65@DuH!9}jmP2V?#d%#au7y`^P$!4usoYn|xIAK718Kppyt`rJB#uv_ zfBbUCiHj;kqsX5y$$`@F{a)pPd9=BP1OmDN?Zz=<0*x~2bf5;?BYk`e+;BS&_`2Oa z*LdQ0TOR+qkfYBIcdla3-gwnHJIn}-r4%7(qMU5=_PG(BJ`)=+KZk@EnyNjYHOLCS zs#XXcwUF=mFxFaSEp~zJyx)&t-ap@1eaAUWb}i{;cnZ(GD_BLasn2f?+h35ZYRg%l z)Bh0h&5C4jU!y1TrCQy!^XGe7)7qZl!$!+k9W{k|>>CgP@df4A@Gth~I?=Um2-|{z zH&M>Nr;lQ~9rPkE7^$l_77gUFBO%4Zt_!BZ;x>!Gd;437yZ{GVQQj9n4Ad-WS(gXT)#Q8tc8c;-@9rNcUJV3tj{DhF`@@*dii2ZAyLy4X)r<5 zbC4R{GyO(ZvkFF=Ln_(}wfsz-n+cs~3PL*2(f)7d)&PqiQ36--xx|2J3MFlPR6#qK z|0j@pL1HoQDeD)F=)V`yf4*ey`tHU5l{b5z5HhVW1v?aojd{%d}^QEFdqjM7! zk+y>NO(`~(`421xA(IPTIbr0}1up~f*BeSQfQE@rU)Qo6JtQxI{<&zD6<}Tm>+bFQ zY5#`Oaz(>-%Yjmm*yNPrlq@-dME?~Rcjd4fy7P%E2mVoly0SdcZ=ojTC)uuptKDM2 zUT||cpzCbSnfL_K>uRo>FRktO1u5xd$gVFuUESu%98h)ZLwfoY5BpzLdCA7;Ja9%~ zL1GTse%_98Yf^Wi$~@uKxGk+=Xj$3t%mL|$;Lmb?IQHZLXLOIpz1|vI)e;cZw+{mljq4Z)%`jEi^++p3C?7@m17FB(Ifu*^Pc1zO8R(^WtIXfhI^ z)uc_C)IM{_)w?+sQ3?(3lyo`!V7l0U|Ct3RfN8`Vw9DGo*H@D>89R71jT~g#MAJ!7Ub0ML35k zLma+^hl9*3)RvTk$qn6&CXK6`MS^azIaPp(}qlG^iMz)aNEmW|7b#Jx_{;yPzV~%b+%a@8tE@I=08wI^t(o$S6 zQiXLIXq1^z+x2U;Ybyt@qHYbQ$IQ<4rfWreoIl&nx8sKYl$oH&*Kxr7u9KUonnQ68 zzH8f|6|3D8@Lt**x?>0Bzc7`KpeN~b z;?J4A*k4+p9ch1o(8@2~7~8}AYYIdEw=4kOdN}nbx#dd8@=xtL80BX}?xKItp8{*# zN=~uwlmt0uv8NffVmka+1XnZcel^VdgMM>}?jQRWSbG$ETKQrHY2x_vJs)7>^+Oz! zs`nCN$K>SB2VQnrc5T}+Emi@1?sLQ1>i!S00Y-IGl-F66u5zwk1@4Wytb_4IlBY?x zGY&aovj~_X645l^M|{xh+1a1=iJ^~Y_B|{1hIej=!*vq;@lhTK{+)y$t(~)xr`R5P zUOI5>*K8sJ!E?(-lSf}cBSqK$rTUNDYsL;-5k7f_2j1)`*v8k|vgy#DZ4!P2uoDlz z;##d63wqYCosy^Bcs9Q^GKi zlSScrwWudBnYGeS(B?%*hS@f+jG*peuyzx4qw(7y9nlK zP?7;K~uXe-}`fF2|aH*uv5s0L6}vY0jgaR*Q* zV8I@-<9w+Ja*M~$D!x|g)kzaQk{6$sHu#>9#XYri<-gU_C`;5N_FodZ0CoM$%(_4U zBUHHI%I;$L@oSg1ytgnyt`Bz7qCE;!;gX-Fv-}!9*)2=)%TnxESvB6gKq9!2!!!Z& z0aboW%EG>6jse2ybu0dLN?%?^z6PPeGWTM$WRw^u5*WRR9n4w}xNzQ3oG+7GUX{?k zwW`pN8f^XH>b@kqi%u?XP{TGk-zkOyVbGZXls zo?e%775wXAPq`v~9Q%_|-t3Pf$F~bb;D=Aj&alcRo?-)zphDTXCPBrM1`0llgh5Jp z4oR^21T>W3u+^}BnO@v)wsdPE@7sQ!RN48>#y|2le@h`WdwHLwTLuO+Eg(gdu5$%U zSSZ%L54Xxt!hgdJs4)@Cqb&?%-IBJq-+-ekg^k(b`<*4NdO4{A*vmG}5|h7$7U3kX zVryIhog5DUD7@M#R?Gf{;WBmW!sF%l_?!sIlm(MgR4$jJa1Db{RwLWFE`1tUMuNny zNF-VMa@;qPpm6dBj`e_kU|dR&4t6C`ah8%NdrSJsC?^s#^#^1KHqaHqB{MyZ=Q6J- zIjCRvVGa@6gfd_%n8i3xI%=^(i?D4Scl&z6`5Wi|ne**42gyV7yI_2i(+lS60e~4( z5(eF|0MC?MR8jzhXoqeYld%QkkEbgRSt{plsQ!1Y$Z>e$;;lQI|0O|wJB;uh$a+kz z6zNp!*abUV&+oAw|6MQNfBnHSh38I->!{S;NA8^BK92G3^*$D8)HK1DjJZ8$FS}K< z1-H6vS^E%1Ja9C^evY;(Nk8=*D}ZyO2*k8gyr=j+V{VhZ6+%aa)7FL4?T`8McTFE0 zJZTUqJSwYY|C^>EcB^Nr%O)gX>G6DYlRe8#?A)l7imDZHB2ekFVo2Mz5El=^xj$5D z5%*eMvBf$3nBBhMXXC~ANQrFe&2qlyi7H~8>w&_#iR8(mpEQ4AFP~c~s*rsaqEnc%445vh4iXNpDufI~=j|T@@xHEgu*9xuJU~x zw!JAsuc(o`Q>Q@s@V5G^PNToeqY)_6)A|b2TePbhC{23$h5hL!;!liB$6f)YHQ+f) zSqCp&da+ZG$L#ridqf0QCQKX1ob9dg0G*L_eX{RIBy{QB$RNK_${G2D`%To6`Wxa|SDff!c| zzQm1=tdgPy!@X-fDoQaBn4$GG+D+MzR4H019WU?jkCRnodC}ZL&x4e!X|q&Z6{s-v z=%oc=$57NEOmf$+UDeF%t5UYpaDDP z44^b1p&c^N3|CFbfsFSp*{$o8oFg3vq3KWX7HYWPhVX+GE`2O4;6p;!l+I8y%|B9gcg=LW7t+8RFh|KR%2k&$4=puZjy3+84%-HW#G$ zcN{!a6}s1excPMQgh`TxLQ zL%2;*($~vGu@V++hjQq7_@4?3n3sumted>*U zik@1WH5OX|=kyKdFI%9;iPDEb4|s&5gPZ!C&f4a&plaHB31( zJi26cB8@Y6b6CsX%a}Jm7Y&cxoop3=`IKos<3UrBCNFqdK^+zxuK8hKQ?GB5sm@zT z3FcRkeXrLGPoPU)(Bbs{<{AdCl{vB`=enE;5d9hNWbk(V4)e7BvD5dRVBKX6%{Vr@ zE;(YOohFUpUTT`wCIKa)vmQuruP{2~njmXBwI6-9DvkmXm&v2QuxkvsH$lx>%t@78 zt(E?~U7yw*@StD*AzHXbb8o@zKDv+3Csyo;?sWx1s`DA~wlYE~DCfiQL+TgzW@Y~W zP~?MhjsW~0Nr$Gf@W|{BgcNIS_CS~ z+8$nYG;p3GPo+;Rg!#Yutg-DtyL_uHEsXeg`w5LzfBt|EZc!s_o3Q(=Pow`NstH{zoRJefZYSN%jTP6u`wH_(S&D>~PQZzKpi)I?VvqP^wu`or6h!}-Nk?7>n{ixa##FQ_x6V-`QJrIX zamz)d2sJOOkJP7Ky*<3-Vy;WauU>5UUJM_BOu>R=83$CX3X=u`E-GaMfxo6~ZqIaL zdbeantppDFV;F)@fyZ@m@zdsD?37E<+!w;4+Q(vOYTGsSy9lnNilNzxWyo8cuAd?! zeX5O+5FT>j=h;r3A|n*lf`Wj%_8laa>bBWNR$GSuLqJtd1@(`{qysSPIVGS3>f7eu zQ{<(0!0e0Bxt5J{RZ*mjO5(S3Bk zL7UZuTb6>&{g*c^2ZT^pk_*p7LJ~k{dC1fbo|kz#RS9N zX;?`xocmU^Xv#Rn2d5~Lg%oOD$V=9F<1I~{b4cF&NA9U8M?YTuNMT}=l#5vGk6N~w zb2Xiv!CRN#c14_wuwfD8%BKFR$f7tcS=5rO)&?SnYO8b|u@kBY!A|+;qqoUMHk!&v zGjo01Qs0_~j5uE@-n5V|dsjV9J>5XHSx3yeUjoo(hF84@!n8!5;@t!^W-8_K{pXAt zUvDa3cRmGkyGL=ck)K1YQg)Wj(BMSlvly zQxVXEB98w`h-}|(h-=7ja?CZ*5iILRV35yf@2I~TU595_d#68XY>lB z(H0EvsxKxOL~dK2bo=p`fBZx20Gek%f^3AEDo`a1?7MQ{Y6Q=LNnNA{@2!Rz?uIxb z!y9TKR(kd=U6n(V)dXqFgC`dV@tNq}IhCL#WN>%Xku9N5HX46uJdOP1Nm`R71tG$? z?EW8H?;TF{|Nj9W8k9oAC|M0E#$T&u5nAzhv%1W|l8OMr@93z`^B%JJ> z?HF*1thgGTcPExobFA^7U|-HT zN-P$=ae(ed~4VsVn7}lz>t{N(!}RkX|{u zfR*U!lKyv|`%e~X?N~Nvwe_CE_lzQ@YL6?yQf9CM2X72~_+PttMXP1q{oUreM}Duj z7vlT%_0Wl45o7MVCMACAL^esObSJ6`L4?1aaw63 zu({nSU83TkYC~)OM({|a=xU}Dc&4KGoNdMt7N3P!<;V+IJ_sr-PBIN9w3p3uJ|-#& zZ2GhdHz^rSO~L=N7K|7C0o$SXz8URvD{@lLo{hd^rJ$Yj2Rpu0zag3Zy>mAF zMr-j{zu%^daOpS52y9H(@LN>NfSMBFU3trhdy?6+Xyx-bHh51H!{g8=Fq~0E+U;@Q zq}D>}^5Z_Tb0<*_?&Mc&9zV7+4jF5rE~#w#1vTvuN`3?{ZMz!N5p~-`d1cLBplxQ@ zcaJ;fRrSsBIpUsS^m}5efx@|Kn<372}m&AWaMP zX7_uNW3oo<_rv1PAu~JI?x0iX@G#-Nn?d#V>(!^y<%QN$T(`7 zXmD3PAZ3E~R zdf$Y^8QAU2(^f=F)Qw${5TU)>iax7z`aV*-`3`C429#4FKc|2t3*#7cJR3Ag~f`b3c(1C>^-t|M`FDk_CA22TpVh&UWY&{Y z$)>#XJRA`FE&+6_r1(J3=?D;JFZq7^%beD|bO7eSqei}@ySExlR0z#&{h3kjKt1d{ z8+iY}+CF>JA>TRa1-%|W@U{LNEAX=4SESc8B-S@-Np?5NP%!1;<{64!;tl2a&;Wo3EIOy;g_ zan5w?8>C#M7KwGvk&!9HpxDy`{mO(#d|G?#eetRT zO9xI#qq?QCXQr}U77gdn@gJye-ywE>;l_dt(%65&&R@O6eRZ^4j_^&^T2OZ%PKTvmrey-XkE^97EvOa75=Ri`Ra#gXw%;4SRAoCy zY%r=$*$YNq6Lo$%whR{`+wJ)~amOsYc%M%1C^tA{GtcY)bo-@X_hwb8rlql&A;W&bA;AaH<8J%6_ivgn1WOgUT zhj>U0*MUFT*cuI`3bToHYBy7HY2@xUPp2(a(Ss` zeLHin_GS-^kBc;84UuxbISXf9Gd+3Jxq#6JJn3O@Y7&3`5x(*feS2!1J@_84%rlst zwGHEuu`cbexT2$VC$vh|SX;JNu+}lquy?1k!bx_)v!tB0$0LD=3w1Y2u&YKdwlkh= zP8LE6cfX(pLxesxdGnmNXgX~#`(EnZ$tmf^^G!**bq(cfw@|m)sK!-|K)a?>wb$7ISoEns`HmJk!vKkF~ znJjcqzQTa%{sT|*W@+XXkYYP=*t+STq5bc9S>q2a(TKL@7mleMvlZaAR6rx7b=ih5 zmD_#JrJVNt-(|R|oaSiZnNW205x~kiyn75b1TjrWO4}>gr^yKSR!{w3*!_0oFwFXZ zNETywG_Mq@;XtIPIqp zqRS+>r#qs$_{9H{E`z^tu8z>ryAi&kDrnbByrMBt5lYWovmBxWl3k zgD!}m2P#K$*S%UcPjOJj9YTV2lX=$D9w;~NtQ>c!e$V>nH=fIL*%y<;ns0HSVfe=z z>1V#;QBECtV?>**ETwL%?Hk(#*;-l?5iPp{a^Oq^W91@NF5|k25(c>s*59`t#){C3{o1h`M|sR@;RLVSW_%})bl)BJys$X-~JUsHt(=BWqch&+Mx zp`IY5-J)-*h1>q0HvTxomDztyzj~?KvaxJyx?RFw>)qveDV$~~gA*^XVB|d-ZT{ut z^e~O3r6cX;E{yBdaPSD}b*QYoN5r->ZPW$LMO{oOY<{$2WVC$`0uFDrA^ zMkkO zl`Gp`Uxsm2;g?*~iE^&ygR*X|&sXuU?Mlx;?lO7Hs@m~=qvmXU*xejaUplSwz=2&s zc-z6J>5i&wU^+O>;M=d0gZ>WAL8p~}llWR?*~UK5>qZdX)(ke0DL;-hh3P>Zs{C)2 zvgu<(!_~k8WBh-n;?PIUTr;tv3`dP3c1s5y!1nH5@>F7)Jw)eee9;L^jfc~coPH0d zQ=#)98ew27;#^NVxRSvJP28>b!ziE=u>M%m)_(V=#gl>~?f$FtTOavplHN8v`jy+A zu;;|>fwBYALAtE2f=!G28SYxP z7k99)d>Xgx=ii8DidjO1I2?o}jn6a=RNo;T17c(iOzUr)n9BdiKl*`zj=VIa;DQj_ zF-QM?v+^ype?H%?t>^5H(#j5Ak2dgABN6FHEtl>|Xc z=H=92!Z%dY`PC#3e*zWoKAo*|91vb26~@3J6eP&_9P9|P?6-T}uA*Aw9#=qb-qautcnLm2%E>yZuI+TYCB_G|Hv%3f*wCIH(@?)VGXyX4k$Sg zpkVrc{`jYR(2Xjp#736bRPooIX@b71QhZlc%RHp$w|IA9$vgdf{?mh@gq$b6%1ZOr z?{G$<;ELAZ$;H{dW#2Of;08)t*v)qFtoQ7GJ`Fe3`NOMHW_Og1ZCOkDay%}Lg7M(}%{dnbi-6Poi@=9KU724gwx=g7mSn@zwgr~Inj^J2YS~a9 zEKey-WTb)D2q)>MPQc8w^#}vGg??L1IkleLlSH77N6T(MLX=b{HuS@nd|rOUQHb z0;s!k(=iPD8eCb#WoD(THfj zN6OxRX|W&$YL4Re#P-&Arydr7g**gPhfV;|h|qjmJ$arMMJ@y>->mDNy1~`3?$kbB zT&+#5Lqarpt@fEE1MJ`P36Xb<9{042Ds7cHrLnuteIat#G<5&~=P>49lBASLkGu(e zq-A_$Ae=8Qi9))v^{WFd0wRwao&;F%;k+&cED9EgC9}fQ7yBto39EvY@#Lal| z0wZ6VQoRmgv&A;9GO{KqT7jKq(C*mw+RPYzqx-r&gZB~|@#;&}v3|98JBk7VmACA_ z)qZun#Y!5w|JPk`*XU(~)1`XH!tgl@)?78FG6BXX8)jU{o{w9hyI&AhQ8GhgkW)8% zF&%i0G{f&ZGZmSq*IL6JWYze%V@~$tS5`s%JtdP_UwCHA8MnnFQ+it4{NSprwWnip z_YgPNv1T$)8!H7yf2|CYX{kx!!Xrw?J!I-@uJiXHrg6e=;e7E zaQ|(z%zzRs&;XYBIqBe#idw4{F!L-lJnVNG4W+b2$5QEeG4ofGFolN-002ipdq3Ic zd4$trYV6VzXt+ICTONocM=&8#-w;qtw9Hi+0eNlzm!8?KXEFqwZlnzvw_%SJ29c2F z9ltTL z8xA9P33e#9+WGGeZt_nTeb{G$4h;Twx{={PjxC-%k)cC5@;iPD$C}>!1m<#a3J-@r zZJsK$q7<7bf@cvaJVC#9UC`#WuoJjS$XS*QQl-1NlHue_#YIA+D*{~^>8q`>QA<YpNI`hk8AJ+f9l8s#8@5n5XQ_7gWQIW>};HM*CFN#&XoC!*Wuw2X;_> zf2TaAif`pE?UF7XoX-wpI<2T`u)e+x?jKRYPpJzI=p)xLkSDK>{1-2 zg<^4NOR^smJUGVed@5}LjMx(+B*BPk1O@s;%0poH34p~`_Z}YK5k+$vrZ;ni)M5xW(6>3G3qZo;DZnCaRUM0wGo{(l6*c`a z(tg-I)!3{=->JI4z}tH5?E~oWc4ESqWp7BB6==u41`vS%t{XX(8H_>-8{kDf-kP5!< zs9KN(>k|331*Om<7_*fEV6 zh(7`hh*UOzfgZ)}_vC>nY9zTEMGN-B`l9KM>!0PH(Czz#0l1&gLJl6%o()vj)$hWAEaFYqhyIO2dI3fW~JT2|D z83X_bt<1wH-%>Y66$Th6jf1=On&d$;fS2r zJ|L<6{X;$77^L`bX-+$RGm_T7s@|JNFtpJp{snGxTSYzA5ANd)QTHcP174 z_CL`#=OR~WLZ0%!#{4Es*OPl0V8JK9 z&5~=y&i*=Ef1&R7@ugofwyACwYS&)?_u+3{sq*wS3q;>D?q!cjzmf}}DU6hYmryD8 z%su~du#(KI>^J4bjd!~2clj7&8lnE;vx>9`Zq{l|c{L3ir zllH$Llir_587!hL`%V(+Bpif_#p+GM>&&ISFMzZQOzy9?s#m@lUHp?*vFZzy~c z2nZM=m4fy@)UW>Z{lf2uS$OKJSFp9EHzB_M^%&vjJoXHr(X9={bVm7!q-fA5=L6*9 z#(ztb0Vkgu8AaIm?p;{|BvA5loU6T^zJ0gr|EdAmLMQ^0~z#u zwwv>OPvlo8PF>WNhlxSFBByQ7SPYjz4+uBG<2G&Ix9i;LHCJ{KzL$CL$!CPvN%}BQ z)4DxvjNfPMlDcLDOk5jYH?I>q_UlK>O4<}m)OXwc#{oYcos^gTnJ2}CDO+}J_^zc?VX$?r z>zF%2hg2rO!mDo4+Fyf{N@er{7y&lltAB8_&=F1wzszr3qFYpPhtp0 zP7`v*t0W7J>#}grhrIAvlMdBP}qBuyWbM zL9WM7d8div(m9&JyGI6%`8(%y?)waQQ2a#C)8~-uAdrg+lT`hu2(_L2RWgqJ9|;(T z4>QN}7m+=BuRTsl@mV4ZxS`z)YLgCyPDnZuG=oKZi*p%Sxp#{6Wvz!u{UgO!YdE3BvPI!> zJO^m}lc)@y?!i%H}UDUU~{+dmB&w^K5{hw-oy)=tgONToS zm=z(N-ozI&E}!Ldf<5?YH)Wq4PC?BL$sE<1n8Kg08a~$lMAAV{T z3fN#L;e`T6OeT;fol5JV^P`@{lM#j}?YRUlI5vpw0lJh3ay!L2zSn%|={5q+dCWB${ierw=@;Y-D?QrJ7MU7XY@*XKdlJh=k z&9sj`JL!g^c-UH{afbswp}wpVtO-ns`0kN1=_qD{A9AQVTk$GIOS8tKYBcC+nT}Ox zG|za9gG$*8lS)8X{Poy?dyGe&ZDFpT>5jD=+s1?X4~^U2;{xd&a;^)Va@&9qyM>fp zC{H9>1khX{5q$f1V`Y$9FKvm2gA$=5NmM;r9&O;@GLu&I)W2&N>IoXJz4PgZ)Xn#u zli=8+aWyBGJT?w{S=miYO=Cfdgkv>$D1n0jrAenzKR3v7eWYgZV zNSR&ODU=V&FtrEIwrphw_r)=^_{{9Qv~;V`uQj=92E5MCcxetjl1BosI65|u)zUq7s8_NV=gYY z>2^u&`C5tyqQI!YwefwTtJ3fWIS2F}hSKnB&%nit2{~RS z5XKL+dQEFKISDIF`yvduy0uQT^}~gO6JZnrI2MJFzL6v@MlZ(?x7Tc!zg?9=UgHus zjfO(#n0}GG1XzRtOeh>>$OAhlfqA)Jh?^3={mS=74p5)$YP%rb;~f)Yq6~tf?f^D zkmo8ts=d!XpXSWmM`A1~`A*B`Qerc><&UgnXhx*^%#dGZ`au=1_#bnx$ndRqtTXuu zo^{Z3%x?wI^V5o@3|SZIyJt#77R2|`#OP%HPnuN^qOZ1r)sCno{PYOoT# za(=3S@2PwKn3B@L{Jljl2TfrImyem}qP2mNfYA9jWj}sg1prLnY_qUzjYp9Yeq=6M z7!aA>~pZ@A?5BQ_>}rJup?R7uXD9x?`ZyRh9{tI zZOSc@n#8`)SEpaEn*s)%3vylM06gNCXwT>kC)GiAT)sEoWqUMAeRiv>Y*6~o)w|4^ z7ODjQnzYw>6GT$yu+F4{1KxC{hBP-KKbR$-&XTdnF*|eQMl9mI*{@1O!SOwmL&C(B z%Q$G}fvnaxEbgmo5CvI^JW$=VibiGkb zP>yJ>C?4%(f?Qj-u9o@HkKe7l(eLziZ&6G9jDsWR9mo0TDx4AU@|In$&EC(bs;IjW z()8ZC)!jGzK%Q0!HX5-Z7VV?IH5@@t13+%IO$1k>)@v$)4lDA<&m9VI27M!LTVJ-u z{NCoA4L$9!|9F{~_vY%Ge3|4Iz^alO6Io?#fMix8J07mV@;$|?>4Q1VWqm8U9`Bf($c=P2nuAGWd!uMalXireAir)-UCDEaENI>(TG~ zBY%#G@3DtCXJx{Z_tJ6L&(6rP%EnLku2idpy6w#kj?dSAX=||hdgs$mA%5mA-%j;$ z&YuFx_Vi|<1Nmj4%Q3(1|H(2A8sk1f`(CRw37K&`|0+E^;>esIv=_Uid{abvKS>U- zQ0Z#Gm63<}3375&U+8tZ__8f5n8!jrXvkPPJO+V@pMrgH9V^^xmD)HqxW1_74Te7wZYc?^}BTTMXn;x zj-R0ccRZaN^bv*dLZpMjx91khqQAv8y#mfuDI?FAaZKjU3M*V33L)0Jjn{g3hWCYw z>ji@rAz#+!B0t+3hTi#5JK=bS_)^lRA)|ri%1f5p5+ZDSf3SoT5Bg2HK{rjnY%8?Q zVVRR^FE2#%VMQMCm{j#5M6PU$ETDKuE7mC($Jvs1dwgtMS6J=zhkDI)(&98#Ph~lW zEz0Jy_LfljIEEr0DR8^r62q|LpWKn_oFsYoCl-qzNIBhF?>lGX$KuqLmT1fD^b4ds zaI%vAOxr=zu8F_#e)PEYLo;vPP2~>NYD;(D0{pRkJ_yKQ_9vjJ^J+nH41i(T=Tw&f7ABS}p7oZzl=e$78N% z*q4&VqM!e+PfoI%jb?CRi73RhsV@J4j{z+yUKPI~KdO-re7OS(<4&F_sdUK1zv#pJ zoY^V4?#uf)gL5Q`D_%yhDZ{_0=bF551F|`s7z$~+cLDNjKB%>I1XE&E$EGQP$eO+Q zP0TN2q*$_ayrMMWug}oVomRLW{}@OPHk(jOAzR@#-x4#|)w6m-TDJ@43th|mM_L=O z-NLUq-F5rk(;jg&5thmx3!TVj{?50|RSds9cIAe4b8+u(pAPItSb>@~^7{A2v~>B% z0yhJse$tj`>|Fd2>`Fk`b;Wy47JlGoZhV$yNh^9;SnrsZD$qg1j_#pV2;OHBPrm&N z6S`#pBX91rN9`glYj+8e4(tw3beMRYrs~bs$~90h7c4EYt^+-&ku+L#)X{i0LF7L! zgn1G*7yxiqB6j0Gk;9q%4W@PzW!e>xw8~xNoBeZNxhL--067InvOy_tTEp8J41G4@ zA9#4=Vzag&*8OsYN4lE4er|m}snCJvjb0tSaR!vGAK@TR`jkvCE=^JY`|u*M{Yu#; zjr^fFJ?NeNBn>eF<>mpHYS|c#*sT|~rX>P{HuE+^{GA>5;$BgzG1#cr{O~D{#l17e z)G4)Ce%b4EKk#Q|=Y?L^8~t)@{)nU7`%DY~9QmhCj@TSJ{Wnoj{7GS9C-!q)008_w zc)Hpdf8cAl>#cgra&W5h^B>9=drguAyV_c*cN#Y8%va6etZ<=$ zuPhmPEZm4mEcF5d;627oR_i|Rym(cnhrK)pScj}gkD3F_r5coBe}V}-;R{-_ z&~sp}hL_NK0mKkj54g@t(Cge-^H830=JgjpbGve)u55aLDQQ4o{qpx5*v|8l+k5?s zQ|xI~2$*RK!8uGHbKR|x7j7u4GA?<|cp6=uaZ=VU_s` z{KFsVJhAWn@4JOsZ(rk}Ly`N+WZ#E)Q%I{ra8$(QD>H0qaoy{Xo?svA3yO0y=yN|A zrQ>(F3H|uk06IO9iPq)R{+X+1-~G>?b;^U1=i$HmddR|Z8%!7kfc&jnK4F-s~(U`Db&DgUXW* zYhs4GP%NfJl7~O-5Au)quFJk2X$2z`6g69MyFQZzbYs)LRM|j>-*@zyp1VbBo1?1` zejGVev?FV{f33fUm3+{)3XpjDPk*rMrYqjSPkQoazBu3p(;GrXvQC3RHQl*9SzU=> zS`c|=f|A8oB9kaP8o~9CaYf{3v28L}y%2L87NXL2%76a0MuX1Gr zxqJ=h>oNe6F3EHNn`AX)T=&s@ww(N37pc!gtR{*HR6a;?WkS= zLUB=vZAoaIqdxy+md&+h+5uuRLz^hz4|3u|4X%hT);Kd{hv?SR~jqdPv-;b z-{KxB_U3E9P!XOdi5R==?PdE>&X4iSPRx$3o)Fxo*{Jl>gAp8w#sv)5Uo?e86M*6t zxoqTm=AU2ZY#1wrq}VrX9v>~SGztj~IR+@H>;#Jb{;C!J*3sh`9f57W6>SXOzdh8(wJj=$cIDRK(!Q%&!dF9!0`b~001V$ZL2>50XdZhKqCA`tmPT-v3(w*>1Fq=#3$v;i z5kpv%=5hf~&Pj`JoRNA*0644NKNTsCxj%GbTc~rpFXez{z#a1+UdZ-mA5@R~O-^S1 z@>M3^Wl|i`2Z2gW4X?_B!#dmr0hg5lf+tn=hrz%-Z1Hzoa^w2SwSMN(UQ`t4YE@hF z9?`#-WpCv^Z8B)^`ct)GjbO&=-7ulY6{jMr`j9RS1K3SnmzOH%r!B6r+J6ulDCiggMVm z>K}c)8k%Y4)&%-&|1CA&gLuTEQHj_B>~QiY(h^-U-5o@>-n|urx$f$M_UZ zeY9dlXte%S+^N%SZO)e>8sY; z3dNnSym>TrA+G}_Oxj)Fk7+HGv{0``_~sM=jW-ypK4!pIS3f1Y4p^C|iXTCB)?6^& z3rAknf)aI&5%U4ffgyt zcANbm%1_J~5%W`KYKb_2L0P8Nri>_Fq_-eWnB1G4Xk+2nh+b z0}noP=BPeKj0^4whsvyTC&*PS(N~>+>`H2mXDK#DHbL%4_Na%aC(VbMGoaO3YDo^b zzjJP9+!`AFI%Zd;?Wa9^!*^H8J~lFNot<8u&pnqIzPX|q=%^fSZbS#+DO%v;+^Q0M z(WvM3lm!*4A5hD?-249J%$^SiS%M+&q|C<7&d@-3N6I3%r64q$M`G-Un@aBA)@2wg|V?}T%ApV@Bk~Cccbe8zq4g#IRyN`ww#fn6P29YFQa3$-qWvJP|Y zu?5ZRf!cVep=;sI1Lv6A2$(7-0;7CCw>2`Hd$sCW@sC+(9Kw;NOkX~*r0b1kqZ!jt zBX>I@*RGU7#vjx)a?r!qWEePlv_?*Dhs?6o9EXla6VuRf*|g27zb#xnZ3&5(8gd?e zOQk1ukMy^j-xp>-A|XmAU!%{UQ~)EuXJiXRR=H5gVFZf&=4E;`DIv<^)5pml6{iHl zgG-+*tbQ6XdB*njM*9uVuvUYi;1Su?BE@2!y01);L)NA8lMY<-?Hx{I!(zVj7k_=V z(Nn%jSj%Pv336-YIjsTVd*pm?+}aHOE$Yq}AxAAp4>2j$SH-EgnCpKy?U2u2UCE?r ztf|q!bVqxZii{76bFZ{d>TVSqdZtdk8}Y!;as3V$wUK_%CDoWg3ZGE)N3?RBIObVUs$NW!lrbfw6gd3YSKGMKBw(*d8Xb4Yrwt24{RVCX^CN?9z&*7A?=0Np&-`Ollw7`w0Tb3ujkZQbwymxf9ef(_<`*z@ zO8QJSX>6l%(nV#I1s$v7J0&Dk$n^ssUT7FKsk-GzOap6!fxG5F32Y9w<{19I!!oB~ z)mbMKgrj}EvwsI62-O|(l?Gmw{rz_2az{3W5d%L8H3gc8}cBUhPEWb&;?<(~H=YZ98T<$Z2y_^&56(+@z zG2*dKKCqNJU6<&9u_}K)uv$Ln8a;Qf{`BR$*G!t&%onlotcaH^@%lRv#o00HDb2$T z6zW^J3?rZFTeT~LDqtQ@HXA z_kA&V9Ti(yVQ?Wy7jz-5wRO@dU-zs1K!HQ~mOQuFj_vq}>;(J%=}CBXrz-%F+jU4F znA$%4_u;tvwk~0Z$^X9pUJo?@064By>O2!6V;_S>uB~m2f=QrxLwYJ;i;cEPy9}h~ zhfFn5P%PVDJ-(E9{$*;>1wLENebZb7j3~VX=nW+&9*Iq0u-QzO6d5wgTCG9-s`RZJ zS5`2NYI&3R^;hRJ_W|lH)6OV^^P5)?AA2NuJrdEk#M8=FeXb@*`lqJK$y}k_+IH^`o8Ty|B0tBVD+AU$MnL=DvNxe+5qqfFH7nkA+e#Vo$RXj0P(}fp|0L2B;KK# zaJo;~KZbTyMPT&z_aJ!sm{+MQhjd0qna=|3<~VTI5%-$(`i6IQd!_W3#mA1%#rye& zrA`W#+|>K|!1^b?+B_GsrL|S7!}$y!R~qr-_{H0{f2-&6Tkr!JM?5ciuHJefSe2=G zC89S_-6%jrEA%0^d&N)|^%<}m_EeTt**PzxxMIm2(Z+~#CoSqTD|soNd>k#yEKs1< zpT16IuZT+j_V>t5RicV%y7?z`D@Lr%{lnOGvtCeb`q{{AVi3qNYOa|cr40x~ET(I< zhp!Z>vfEGC&eXQxD-~W|F}a(T%_^~9D4|B}CKB<2H=~?r?CT ziPv~zboNP8B2&iWyAY5XAkmv2C8a{Lr#@?3-5wfqd{y}z}d-v4RZ@Pwhq&;zW z;wDdeS`kX?!kv4iMA43?#)L>ne^`h}zT}&2R~v~hh2sykJ?kQnM9-$BFrK()4UdDe zyk)M-6!5in6CuaACN^$M0j&JUa_7nsU?~GVhd_&Un&LA!h z+3Dp1#xuB+i~gC^moc8NK*hIL1RZ7=eblVUHF@)s>AlI-+YdR{$6&Ir*v^mH#|HP(b*4)EtLz6r~4!E$x zGCsz(@tvb*=~<*_@s<*Bgc^vXvNbPEe*rSg*+dsb~SA4+rm5$}%~ zp^{qbAZNywpp*j9c5t}Y2?(P!H6XDn{%_i-O`>ntM zKPyU{H4$WCa109O_pmtrG*8I92Aaf2&t_t1k36uo#sI)|dv3l&ekje?hHg;XLG^f8 zTQ-<{uEuRC*7Srhb({aU3}VOOpng=cKH`@8jtAR|Nj?tu-^RY)X1N?&=$-}Lu~Nmz}+VQ z7`)q0NEla&XHUo6SP9I0+RVuiX$)Khen0n>vKxDp^=c+FLZ^Qz=;oE5YjaVSamQB8 z8t>LtBZ`s9vd$__hA=%7-`ZoV*>5t1nQBr`6pm^0_f-DST<08cGCv1B<;k6(HT}gr zX?}d<;U(vc$_UQIDovANnOE?(_4&%0n;JRSf->fxhc~Zfc8jpaMB7TJKF$5$bm|fQ zVF%$(cOlB<;_y=(p#>ky7Q*hn-gnohcSfLAv)`jwjmebKfj{g}_{K-KoWcLJ*uLdV z>TSi*Dh6oT!=FrjsQI^Yw8!hQbxm@ETvNkC==UzwU)suQoA+=BX}~}m;wa`Xp1P4E zDlXoZAaVQlRjdL|ZHonj_k1zjr9cl19I8pKv!S$!y7{^A@U1>5E^59oE=nuiq{fs> zyt~6mk<33ojYkX7xwO$I8};%>#1J50aBJ$w>s2$s3t)qK!=q7a+zI5wQi)dWCZlUI zQ@WuazqdM0J?Hv+Jn}wP-O_G1adSiV0@g@*b-UVQ;Llbev!E+1sn6(VS<&b9P4#w( z^2^_fMk>GNtgpB#SDyx$i;O1e2KA^ZQLC9i2!mKOhl#SuS2X(Cc;q<5VGcNT&BG3F zhgxa9irmQB?)P+QaX!a~o{VGIu_F4sO#^#P+~z`h?z}1KH_AYm_JRsI z+oOcxi!GFE+7J4|k##Ec?eJXRGVnu|fIchY5+k*(Y0uJ1r+yLggBihcHF}r6O_oKo z`dy)&Y;nYx`qOdJ{XMBCm4?huqYYHK&-06(0lM)AEC1-6!J*(QWA-SorfBgzMbv~e zi#ygpVedX6yE0r=@@i_yYlSPCW`izdLZQJ{N+$E`aonY=r+CozY_1bA0Q8YvE-N9EiXCwU=s{STXsXc8WGnV@# z`4jH;ZlhK91mLA*M}XKIsyleo&+$^)jlk+eN+nUMm2Fl8#nlkoTRbl_nf~d{_7zM) zS+`E1{Q46t$&eyjxH|C(_uWLs^|kn?m?zc^ABpFGzO-RCnaN%ER!lh>pa1&2rLC&U zg@EDSUM{_$QbZfpug|WiTIabxXmv+cya?}0^ePZv==_n)Wp~xrO7(AZjPr*h*!X!0 zQ+sb2xpB?#+W8_`4~&*@yGmL2Fh7U)%mu@?bWEw2<+DU(=O_T+W{aIqq7Bts9(|Q} zE~dNufYB_X4+kHo@6bcgG)3>ne102uKPM+2IKA`*?t3>_+k)6+XqFv|o*iGy2g=-L zQqh>%&Kx)ChzFPuUaNuN&Y>o3>ldHm^a##uPuwh~R=iJjiFmqV13CwhdDQa^V-;sG zGU<=tQy*m9T~ggr|Bm6~jAaXN&{C?BFgFE5Ju{CJxgV9h1Bq2sBhO4SQezyC$lT~V z#=ThiO4QV8`R9=Ism91iYc&G3cIDWT`=mRs>4n@)b+4{wy<1C3{WL+-DSst~Vu89D?lpVURB7pF8@No@uP&}2FR&OyYjQB`Y zj$dfb-ZSUo|JFO9pCx??^`mc-(ok4zcHTdesFh8XmII!{A44KbyZki{EG^~S|C`i5 z=-i@JjM65(zpaI1xWKW&nB1(%@(7a`1BgxM8#%`HBIP?|qLIacQ(WRNCN9=C%txzD z^`{=IIqQKPy8+rvqP?$Q*#(KE>C$Jd7j5QRBeuA(6(+L~wM;_^s*SO!Lo z)K=Pzucy=<^z9l&^>5w=l$@=Dz>>G`|4wrFlz;O@&$GGc)!grs4jT7s$8+!Aj=a$- zx^r%<;?>oQ*c^3S!W>I^aFXAM@syISi@g+R{CPa9lu6f?7*nl#Y7y+3HCuRDcSu>{ zE^qXVop9XGXVRZqM5-Fh0MixG5W9ktv}MUksf$i4{``Y;t~+*BRBb~Nx1laSsUlL` zAwS8RoO8+@`8OuC9YYv*Od`8mm-&qgwy|A`H|%g5w_el|&e^W9wox&5W7ULAU{OBx zTfJdd%KR1s4X;U@2K*^7hdy~go9ZRH?l&|60vP@us=hs*=|B2^B}tS@?zb+Y+$rQ< zxs;Gp?w2LxI&zuIFe;LSA|$!zlDS_t*G&o`%c*3-5pQ^z1g^09dJ36glRrRncvQ$_f?Kl=C5F zzV(f#Bd5CTzoe&R?l{zqceCVG1%0xSLXQk?=8+NSJR09%WECt0N2O)FJSxM3ZADNP z@D*?_ceI!FJB)Y2k~E1YE&i}DuKaEjxO^<1ZA6@uM~b8-I?VeRo_2fKg8xu`-0e${ zZPc6wm&6I8v!Bz4Sx8>!n;KJE(ci=7%3AZSziLB8kDq*b*qcPvz0RheM69QsVc~gk zMnC+ZspYT>Ol? zm%wW3`cS(mdN~;4^yw+LnB}1}Y}ghsPc#nENATFr;Fx;i)!^>f0-vYrFwCL*yt2K4 z2|!>FO4~!R8Dt!zi}u^J5auvH&EvE)FWcI!a79WPy~v_Flwb7N0GDk2{z-|tdtSTD z?o|yxAg6^n0}fe0_*~#l>a1r3Zz7ksJcAg)VLD1)V*oR$cxEQeGXWDs0Vgv2wkcDC z^IaTWQt%9VUv8M;-u)WpQr-Z8>^@Ihw>iyi`)MExd6iGMRdH3`zYoVX;{C;5Dd zDEogt!eyO2lw|L$rk8t7F7(IhWuwS+t}iY#b>;9^{9W8D|)9DLKshL-An zu|>+?vyRPHjHL|NPtSYjdiRzVFS)0eX;~dE;W}XNKkLGG54qhG<+w#VuCGvTPn|K(D`S$w1O&fQ#5@W)ZIi{KbTgaf5@*kODRjn12f9X=LIwzsfXwKKD^VKt9Ki>{TDe^&nKNydg@{FKIcHwrbSTG09eFSQ;Yp%~!y2#InQ zufi*PGq)Y;PHimxmR<%XEC86XDs}*nH!g3Ymy;-jm9MMKm@T8aQ+Kv!wcasiw8Lz7 z?-^NC2yqZZzxb@Sf5j8_F!A`wt_Ix5@KXmea`y2j@Ya|&pQ>_hTi54R46D*hO#@Et~0plX5^ml?Bn znm31^1#E);7U9DfbV)R0oFmKv=%oQR2VpCna7nuZfHxf9#Ky&S+6IhXVBrwnS2YZy zXa3;a?%iyxrRLjuB}_-M`FK+ieaN-mJDscB#U6ONS%;Fx9bnV$BU4&=A?6!fVaoK) zM5?IN$0c#6Tc?k_xpT`qri@0h<~9mwFCLyo#a4P4T#m?6)UeO9J2O@oi8l=oCn#UG z--)rFvlX@b2A-hf=U<$O*xY8Tt66!fGa>h!$c(v#8RGQo?~pcYDWdglwT&Nw-^-1S zPeRtY7q~n@RdiFM?G6!agK_L`a>PMX;cJuXbl=INl(H05hCJB`RYI!^mOGm5GQND) zBx%)sRlqgk-Wr7%Z77k=$Nd{y+EM%)a|$h!6! zhyGni(wKV(%|7^l&iCMbxE7keAGBZwonW3fCI%ey1I8T_sRb8I52bQo*_fQf5J=S7 zm(qhMq8e4x2eLpMTsyaSM~0i$s(e=g_tTD~zZ=WKDIATArhVja2u@{eW4(%*eZp|I+Il%%DaIV-2X281VpT7`4kDj^-2xV*B@ROTGuH?_NLLS1Q%_ z+lb&XOFZi-ZgR`Xp#)QOpf}aszH_hGZ<>QoL)OX{`AjRWh@KFE2`{g-VkeeZ6l`aOcddsG6GeR@{<*Ys#;w5E8%RXlOmI?vE0v_SBD5S3Y2f z*stRKiG9wezgdoe>2m2N=P+7OlEGD3hV$0 zCvlMg->=fc`$T9P6JKB7)n+D+ciD~u0|N?$|M+n1&)Rg@U1c_w#QUI(;IBY$^fP&J zbyZn7DMUp|FGJSuT}-dK*zeEcPLoTh+rSg|jUBQYlR)x`wR(ZS-B{bOxmdzj;x?q< zMgk86V!|7f%I6vjeNW6c-GUGDuB_nU@S|C*1`Ri#9y%qf4nFQtO5&G3mnoxDhEunE zO-Rb?!m^PXA{n-F*KAUr2*>(xUX6uHom9~IRdSW!s-{rcddYL8I zyXCZ*8e>vIMofm^Z{BO3H!SC&KGRg#@1JAmR_YpZiZ#tE`aZGWGe&P9EEL7 zu@AYYjYiBD)5iqNhQQ`%OSrEOU`SjZV-PL8SBc;7L@h#4nt=PrNyuYkNU0H?B%T`aAtwsyK{BR z2pILnE#w|v%%0PpS*sAEu+ZolwcV_SHU@zLYj*n6wfuDtc5 zcLdoTYj6NlZ)@n33$tPAF5U+o>OM)i&mmdF0vnp-?tIr z5;n+%m1qTC1klF}v5AHBR5M4-KT%k5Xc$7CTM6u)YU0XkA8>F@Y_6={=8FHENr7pU zXm@RB)*3Aj_;{MQSR2sR@7csm@amr62eB(?lx3ahX1*;<3O%wn^art@`xkdefc`Sq zm+P~==?!$_Z?2mjdfT*?zAXA~k9{W>n4`}@KDR<7moGGZe<75suwCH)z3ww1F=TwY z@V!Ez2eo3=d-g@zrz)o?Kc)DWd}Da}h^VFWRjGWtWXOc+pWNJbwn0*dXy2sEb$Z}R z2uiM=A#+N4e(AVNF)LfMc>5Y#6%NU~kobfdVP_7F<(KS`sR->`f?V^&s0;U04Cva`zgM|qF9@7!=;mv#ftcp7@-sppT_=P(H#HFw79u}Vn z*y}f%5P-(|9IniHaKK>AM z0%8il^Ey#LJtk>S`)*c8F7X1WEp{a$A(W(4>+0HC(aV>60ofm@+^$0IYB)UUx=pKk zZcG*VeI0D!9fQ5X$-?3!=rJn()oEN$@79X{1?A%6Ukv=~U%;{4pkwc>>ngUh&~NWN z?F~)O^|rF*an9SVIOk70HH4BX9t?TwRszO43&^ItU;cW;r#|N%<*xYrMhVCEFoEo% z&#}7$O^SK8h0I+<*7G{N;D8@H3U0j2*{u%=8oR+!5!V^7baeU`vU2GCfSGG}Y%nA# zt0DvH$@)0PZomo2+(dROSfcc#2*1q>AY1~;eTUICvatl-O@X}=;^CWpwdfyHHGnGq z@v8TYI3YE$swz6iGizg;i@$eQ6#KpV61aPr!VgS|p&|=9wqFH0(S2%{UUXMZ28TV6 zxf8Yv&Xm;rdURkdiCcbO*5sZz&)x-KtVu?+Hu|~iZ4scliLxV>D&r{Xd2PN-GjwJ& zM~bi0g?L;4J*oA>Cry7REbDA6A3L_kL43M0MuAfbK`v2dX!>@l?*c(10ByMy7TFC!s(R?})340B}PZ@p~N!;qn?2Y$rRc_b3kSmAlR*aV4 zOotLK82I*fZO@B4ZMCTEy%$9K`g7P;!Edr#^ZgIm)k&rfLo({n=dW#sM}#CF#8^bs zK#MSH5z#G6%O_wbcM}q*`A_|+15TF79pAsl0uRgdNL?t+iRGPTT2~|+&+fr$VT^zF zXa2?nsQg37av+5er91sPDIW2ra)}ai7UU7MY*F(whxMtZS*%?D0Wjjy^S(PqV!rvs zBGZmD&R^Sbl*ny`w{Pa|zm5TNYH=_=ghnJKPmNMg#dJZ z{j7XDWaH#<7qpEnoZfU&F0E0dLa8)h72aDuQm>AaylWWsxG?b2j`cEr)&-|VmvFl=lW3i!U;B3q+8_nD_ZOF!N!5U<%(UU!U#odVOZTG=$x zyqqVwJCxAZm6l++QoR7H(O33+f240^$Vwxi{lA5NvK8}v?>>ej3fM&iM$V*npHG0) zURSk7cKk(#PLn=MT~0u}sgOwcF+A=Vx#`6|nAy?oA}S?od6+SMPUse z7!RUCpdx;D*Imqm@bxE@lRKQ!`2<+Pfg#&hJ)xf#;Bf_u;s%J;0jE~^3+!d91ehduwznxvp#VKh;FWBUw(UnQQ=Ru(_M_ia?nh=u zZ(jc*vt1lw<25O4@Zdvc=edTLf`W>QM@=aislQqV~Fq4g}FM zYO|FSUnbVTOBmA^f?l)fTIu)ggK{jUCJSJ{CJp=|V7!RQcSclyqm8T^@)w6lc=A?} z!RQD#1Rx2JY2RkEdoOrKJvm=f6JJQfri}OWwsG2x2IV=G_(58Gt#6k-uU0n?y_WEl zQlxRCfftOi!z!*z*;0sv!rsBMmsfDsfNL~77LFZZq~TuvWZ>dK%7zQU zM@><)`y)xKT9UAIu=!hj5gT)~-yNgRs&TC>Qag&CjVX(2Qa?OLtX{-(opq!}kih}~ zMYtmPW)Vc$95>>UMtqx^nYge}J*8&Gp~v)On(L{3dassL`nmTD(KmymzXH z&j@J=2a$!{>Ow^ZH<<@5QRbHo zUp0Cff*$@%P41nsvv-^)00VL0#d~`HG=2jBxhk;rLP0IK4KQlkE2cnyxYnw+%tqQ? znk1e#RGI8H8+X+FRD4%9U`f13w@hzM0v|lzLewic>E+RsRsSG9m->?J)(L&*i}|!U z2h-^&-DM+EXI#dV4MsDjM_xfSSxnDGV&okgyaA`uigr~sK>C2PpPSn3@1g0*a%nk@VKs?H4- zx48{uy&dtFDeVEagMjfqAF-;#eEv$0aSGNa?i95UeYj6Pw!Wa`Wp$|k9P$pes`UBJ z7y<%R=Zr!dDnoS{m;>`r!vGo8zRLF8I^*XNY!<@+KZBOf`$y@xUeL$CLcOwQuo~QeIk(A_%x9t z-jui~huc;MqFZ`A41)4CAFpJsExePu9rmj7hi{cz`Fow`ivTon_C(25&j4#>WQnj1 zkH6%zJ|NkYeqLAs&Y~am1P%FdTB)sTxr2htJ+mWy#4Y?{%0k*ssp5Lx&2Ml(Voj%t zcY$iS;?JL=qlY(3q(?QkLXG1sk?ikolgg(jR)aKtm+&vA>7Trpj4L{QpXc#t#8nfv544ppLwaT-EQeEKW(p8E0}IE| z;b4Aqy~@M&VZFaxykS}=es2yG&jv-*Z_;z9zg^NEN48?W>YkWe#na}4Cn%qJXaTd% z5I?EVxIU|ur9qo5s`>+K-Iz}2d%dQHV(Zl<=a3L7s4!Ln`zgq=@08iqZwnvY|(9t#A7+tll!QxTb+MK*b;&d}4HF zi%D4|1Ty*4%@y0M@8^x zGA-LEmw_NnZ|vJ?F!eJMkvR+m6*0LtCIb+L5wqP$LufbV?8ti4Xm^{4S!fL(=wfVX zIBRG#M8F+wHiYQ=zqaws{9hi81N~M=JG`SugVXNT;p5N@wgV5Hnp2K)7&whWXsj2j zaFiO3yFL!R+%w;P^u8Ls(oY|;g3D&ygjNrwsA!$1cT$AkEDSor?t)Vc({TXQ>R8(V z;XQ9AQYqZ;+(Qnt0i_hIZ2@i*mnqRf_9O9;!)G68@NtG2e?{sXQ&W@{xlP}`Gkv3t z_y?hP)e1+|#KhHVrKg#^jp!+FpH1W~;Hrd=Gp7=AJan)9C$IXHr-vilp0P zr0})u{iAzQ;TbraiM$@tfvZIs%hWD+-H&N$SXL_b zludP9IYe}9i!<#i0$@|7j1rw$$Y2mncVXaqf2=Nv#91QFPp!nmQ(a%27=$<%<7BHN zFR{4HqL-93vT8{PVi`A%vY4lRQYY$o#Nz0q&GG!Tfcq}8TtMG-v8t>9@r?faa9QijoL`0b%9+%`Yb%G1>xT30=;1kQ2QRne z{c6f?q^c!Nzj&kl)tM@(V`>8qT|M0|N7e2@z=v|vKR`y21PAWq2C@is^{WN}OTILc zCk4*TRP~=7d?&>k?tV6S+mY*znG0){)!U;Lw<#5nFAuG*T&-^pCVFpqXSaDrJ(>(H zk@$M+%ISJxjAZwhk4u4wA&B`DUhn1mI<;FnfSSYicV_c~fFY~T`dpvF`(4wC{SREqtyT7A zWUj_XWgFRX1uh)%w%Ec$gCl0GfqCMBRTm-Io&^aP6zfWL`4WV>+B7!$(rUdC;vHgM zs#*vaEU76Sep1$X;%zN)$D(yo;oTX_IW0l!SEnOjk6!UF6!X-U@w8IxSyn!5B*>f+ z`Jf`z4K9xJvK=krj71_|Rh*s6(u`w!Aa_bu7;JRShwR5xeHjA^GqdwGXW^9nQ#Q%= zx?ahI6LFmYJ-_*s&=$!vw{|No8&`5%;w32yX9QM{X34x+rK_Dj8azypJXLwh#yy|! z7&Mr=W`3Oo9p}j-5SPveE!#YA&YZIR+mS)tE~)6-FcUYi7;PI;8s8j;yBvmG`G7#n zimo%6ve<@=9?wCNXTg+XsOZcrG4BQ%kl4Pe^zBVsshDs7SfG1TFoydO13KEWB3c>~ z-KE?HaY;lw5s?U`SM^Tx3@h0^D$4#v1{EK0-HZ@uu;&H*V2%B0XySF>s}`5k6o>Fi zLIM`?=AAv=w=k<3lilH!Q?)XpQiR|vT8Kj9XQIWS{M0>(v7@5y&(z|-EAKT5dZA&w zQF-mk_4l=KVRHQ8T@Eshr-czKeMDVLd7C0EjY>`?%~J?u zk?D8FQYEvsuEY=IJckU^{!#TYz+i)x?OGyM+U>hJ{uwwNfus^eG<0>epmy@;C@U zy`dMm7dTM%wb6LRB4ibOC4`G|qO_^r)yOB&k=%si7aqT(`O%Kc;x1M${Te@{4EJNR ztBkN7Nvz@~y9F(|c804KyU*q<)v*nglS3c-rMJ{{8k&j_$hH?>G(09BB}Y#B9(Xv7EXf<3eAfev;fMHN z=ePL)0Axo((d5reov2sueJ?+vlnUQXUREiepgmR^#2nF;Y!8e)>y(YQ{XKu_z!S)O zHv4pOoI{+`6=4MrK{TzRMhULy%+wfA;gGt#I*!2#CCRC)^Mn8SikCI!baVC~_9P1A*BhlG@5jAIu`@)}_v*jndL0oAa-8nZx0qzBp-UdF^Ff zi}rmE`fw`1##DiC2l5#zUCeeofBw7zu8&|>#b8)rhTSl zK3*FwvybV^TwJuxMET9EMJrbEmhlr$7cx%{Y`SDQi z7*Y&4a%}VV3*7~}dQ1I{X-)D$vRTdbpkYsWf6T?;!Uiz=SVZ!`7dAZ&EY!#Ow3f1T z`~wXg`2n;EQ<%TcUqXn6LJ>43A_iTe7Ry4Ww9!58o% zL{)1{03ZLEjuoBdf}_2izN`Y4iG~*|Jp~khey6)^CNJ0MeECSf z!`{j%`Z#H~#GhJ2>857#_v!UxXX_g#eUHxGW*Cd?B`ijL%*?Z~2+ z^XPd}YKy?GUn;FdHtWw(fL=CA{l5Vw{nz2Z>y>y*UcqL`x66F)=(%<*0C3v?wqy{} z%7Ze|YDw9Hor6C=&jqji^*91x7?ddGiENJ{gMxff0`4BFP5$PE65lJHp72t3M@gh4 zdZF#4Je#z8_xr20$C^!Nw9XHLemy(B=tiT9^QnFA`ioH`AN@fjtB;ytTbr=%IJ=Q& zU)Ir08Q6IGFUL^Nj;)R2NEwuQD6Yp6Wxu-f3KLWBo`Cj5)Kw0NAr4j7sTPJ>lRkq< z-kz?ui#;X>KoLPFT4x=ToyZ?W&buDTgYQkc|DzCjG_%{W_ZwpN!=1xw#3->A2`IChIR z73U*hn^i4Elm&QeihZs1(j`_`nw-30VPo0F5>wYc2Xq=L8s({i1(5P9tDEo&ipx{~ zrbCUa4O>AHZ5;38xWmdw_wSTm66QWOFOGhIdra=!(Y(?k(k=#w?3Ic)dPD9<)xQXm z7aOtTm{OwhM-I_Q&C#19rlv@AXeIUIMC)HjmC*KKrTV3zuP_C-5j(2jsDCNCzUjhD zsjejl7yI}KRyoRO>3Z1mD-0p{$K!;+UaF=s@$?6aQCX9b=WZMUy+zoTngQ<y$$j;q(#eNvM&GFnR$cYR>pK6QZ-lk^0azqqFg_!n!XVX(fx>x!VcDCrvw{q_b! zYD3DL2b;H%pQ$wp)$Rb}Y=cOub>(w=4bXnND1t9h`b6CPu$Mm7A>j3kzg2#QscP7I zPe89MkyX6-@$x06?%j6st)IHD#^e4fBs3Uj_Tn2xZ7Xz$2BB?2phD|A^Jt{}6H_kS z`3OYlby+v(!(&CObleSa4a zRva@^m$7-*IOEB4fAY1lzcx9oowkRc4qaXiQ33GiBAS_W-g4)~XT3J*e!pRwSyuI< zCHKoxI*gSDyQ*-X*e9_r#=~DZIE%F9?ZY&_TYfUp@_wETm9P#)muBdpOmc0jh__&X z@9r1e+{W$4=dHYFPhrMBO7iuTsot3(&2r~prELcv^I;)@l#th(JjDlmS0 zZFbq;#m0Rl%-khtkD3+^o$sH>QQsKS-)i9zNldXrp8y3MRUXC`vT+GqV zQVV5W5*!aA(lt1dLucL2^{sS{4k8f%uUAZ4@*_e;WUPzuq`|Su&02%ER5F-qBJ%j~ z9|<3(_qw6RA_l9$cQNB;ecm1b(fq?}#s{61_AvRTI8m^WJ};*2t?2Q>(6BgSNF^OnB0P(9;xE%KUP%Y7gG zFawdY@-I2!u8Uht1c1+W7*EiojuY#q?-;V!`?sTU371mpsdvRk}rGH ziCG|#ix1aap53oFh(=(BOb)m`%Req4_b8`M5%!e!b7Nd^vhJFa)CX`z?h00uU4f$b z_w3=bOlJkJuHjz9IBgg0jE{UPksxn<*1tQkBywrlJnWsLPeMcTl0?+T-&QIO54PsY z&C-_d)Gw2U@OCO`>YVxT#2TGJvbqIZj&PV`^&s!0uFNy*K^AbG56o|&P^OgZQs)qj z2Vhg^?xZ&DU%SX?-U!qFAC$XJj}9Kxu*rE{Z8&a`!t0W1tiBy1=D0NcEo4gpkLh`F7uCxruV$H(o1CP|bo@HT^9h^J#22@7L2 z_jmu+MysE>g;kvCuiLM4js{Wwx|2UekIGiz^jf8Uym)G0uO{n3lA9B)C@d!-)R+y(Nq4WX0$yd!=98bNJYXMK55Q2D~2r+#k9X zUk4^2VC=Z#Kh&-qUHj)Lmw2&-Vxe-n)cKWjt1FJNF#7RZh($bwL%>UJ9+D_aMe+*@;><+Dtgk2;Nkou26y!j!j8TLe?!wW;*lI( zTeL0BrNOEnD3cnnja}olkkN3Z>Kj{LO`ygM!B0GOWi4VKu<~vOCRkh1LswslVkKhA zzz^~Xh3KABHSVDn-qI&Jk0dJD{=7D_+Nv_OgC_=`iy=K4YT1;D_+l!0RJu^a3Yv;P&bCqJ0B0?L+Oz>(QIQQ|DqMbGoN%Y39A<6$QsD^5;Gl)kH_`bpV;H}0KQf$n147vU~H~wNd|2H<>>q`O}ZP$n|SxK9ZEV7cwqq<8k=pM2Qz$hQgvo3nB$f| z?B21=n46gS@Uo$JwW5Qao#vwKPbA~i_zre%y#Ab39rz=6S0`Rg1&s=cdBbgJ)!&Z zXgeWwK{Jz;T!ma|fQ*=F5WpoyiG&nLm&MYmH40uv-9GRz-8(a79?wgMJq65mfmd_Z z{*a^@ntkMgTF7QO-n(7M=kK5E{}AFBFcn|#Z2#z%g@6Vt(_Pwp#|8Mq^0fOVDSW-p zUGFT(x!IFl#%^1j8x;{BRL=NopR;B%dp1GP0Nas#{lM@Gz_2lm-6h8({K*}3X}(>2 zlf+k{cAZ2BQU7;Q+>*)4J?$f_I!BM z+wg&#@2Muqo6E}^2+sqGmkj)B!*#Gqs$LkU&@pKC&R7L($s#y(qUY3-<$Z#6X^x(; z2QA0KiT+4^7RS(z;(0D4!sZfU@tRGudf z2V)nq)3wvA-wp@wq2=M^#~XPmQ(@Fu#tq@kA;KBJr?WJCVmT&06nY!J!^8OOfiE`! zd?EJOKH;RO=(al+2g1!C1@&4qy?mq&?~D)rkRFFpbX!OjjvWkrpH9-sDrOf_)3{)A z0A_>TI-w4DJ#1BYdRi+_lxvo*zB}3g1c&_4?J_ul;=#$040&fMQsrz(404e2Uk%BlZh03{-&5-(do==_ zK24gF61=!ziS={dev>#FTM#YIdX`kzEZfW=HK|HLkTkY z^GUV9G^`iGmx~8a*VAm@C&e<>JuNqXvjFOIxpEH$f-Z@yx*LC>=dbo z@oGfjq-0nDXWb2r1pQ*-wx3GVNZq2vV!QcliQSbVurBJOD+aYrCu*O|VT#Chhr0cGGI?yG z(z>=wRV>bwUe-ACHP-u~=f@Ay@i=3^NZyZIq%!2LR!@&{jOBZ-25+6noYc*aBojYX zpsO8nK;e0{%rTVe9T~?#=YiXp^Fz4bc4cK7G*bl`Gm&1gFZLP+x7+U(v>TFg0CSml z`n+JPLk(Qn4-EhvLL&{T@xqe}Y6UY#*)7*sg?MJ9N__2np+$MJRXbAHOYkMhS4tlK z!Z$#(I!((=75l{Ae9b+L$9}u<2uY;C1Bc(|Pz>)9D1n#F{eOpQxXYJN&ZvVr1|J0O zW1NX~r#%%Stq*AdtNl9Tz`jo(+m|_N+TAPju_VJLUS#0y*CTjXW6DV#BGkHQv0tLu zEIW=vq+zA5`_m6*-wmtV9nu60(_tu~+=@)ALNLm@{$(Q~(d!t3I~k>~5h_HaQfJ-X zhM5*TI(d;KCZzvT?q|$Y`>b?MnH!F**q2MR4fTpp$zBPnM_op}4Ab}`A#+(J+0(ag zv0+u{mj|xgD;aOy=U|=#ksW9PxHNOvoVD2Hvg%QCFp!8MBYn$ro~b9yJ%gRW=!+$# z4y!z%Qm@cX0CBVYY8RE#-5>p1i2*+$ka%P1yHIF3%EptyK}p)2tfjV75?xDz2N^~o zTzko#_YfH7QOSNW7*M1h0NDh!hfz4Dl;h|(w|-;RP>z5Mf0t<#PP&P^dIV%)gr;26 z==mP{VlWrca!p(Wlnej`ZKCs#&J~kcXF$=i5$Xp1-2Oe10{vx)cDCW4Bz@bIzeg9w z0v!iDHBv=!%p_+eXa-;@YMdWJj{3@UDUA!#1HYWBUEHpo!X2qNP%2gxWW9|qd|3MT z5!p7@9K!ucvtiu==eS)|yiR+dfWE5gSw^94(SIOciwx81+lh4$KI3$+F^Mfwo8#7K zK<6q&SVFsV_*4VNnleH9$QZTJYi!Gr7|D9hQ}bo1InF!O@M)RvRdmwKIf{~H z$QbIWK2MPOY&Orcc!`|ScsB!-mkE!E&Ks$}BGNuyWKH{E3f#^(Il;xAQOG}}6}x3& zX<6GKjLARQo+Px3|}HL=P*Up`PEx{zZryn196c zJH$^!dC9g9!e1m5CxCi9cvuX5e8@?ugSE)oYU02a(!_J5w}@WSoqK&7+ErM3is0LK zEOSungGE|(il){{Dx-Nl3>vqWmcp8;mxAVuw%ucP8beS zriPn(MUc0i(#~p>>jCx0lNIwO;;2|a-aULt z^^Qd&n*WfH*t=>Ybg5YSt=u@V-(%l8SYws?aYTn#T6*`Ee?+9-ls@KZxOU7|$D3Wm z=TFxU7%HIV>E^{F(2|xz-EGGo;fY(&?52^Q#>K^D4dg|B@N8}DNN=|Y!aIKc$syCy z;0pQFLiGgEKG3;7TK&738c!RPa6I`T%?|rT*?}9CN>a0$p_PBI$?qIEaI)Kdg7|I> zW5%D9ki0{fWD4IAIj$;4*J5qRVxYA0fuyyyWz3KT?}O^=VRZ-T;DnvbR~glL!hhPD#1$t6}L2_0KHWmGG9^Ff!1hn3Je5PXh7Zi)l2-6q- zb>RvcZJ&h$%p&4cbkCTZMfh7rGldbCotn)>vTa-COA5UqeVa=6U!;%Y6H-um&Xs8Nn@4US?89qg>gAlJ%rEI-pr>l+7NMdBbC2-`HyK>Q&g z$?gu-SYHpmYQe*oqh@2z_3Mk4N?Uc_?yc%OOkTTk=nGal-DN&5EqN&ScwdD%B25ck zr(4BN_BpNa$8KqIM0w<*gj%Rp2JeU&qfK!^{x|Tvwm1&&ST6GTFx3&1AIP+Y2@G5shCvZ63 znhtI!zLC``Dud;257R?Z(@k)321;r|ZSqOukCqxv`l2yEOBMWzkLdY6eF)>VqBkA^ zRR!x6Eq0x#QMHib6UQ#gvvmcxIo%RI?ob;-k9k(?SX8Xw(p}qG$7H27jPPIqnI{2y zjriDvl@AaH`^1XIeo{cbpJ4e|??_;=vxE@Sh?1F-G#K`pW(LSDErESa-+}E1$p@UC ze_Ili*6y^q`}?o+a>~LF!!^~?D{$1JRdwvvA{?qo&}i$3TQKl}VD zWO)z4{?GCDiKX7no71HYOA<^S{}e(X^V{{e>Y$u&y29|EzLYmNfSX6(>#4-rw~(91 z?U-Jc9GkY#2+~`e>@*VZ|Am|!W=f06VlvMN!)S5^9B)l@IlH~e#X0n~5c%@pNxXN- zV$(}BHRFZGH@4xZj%)dLSM4$}UL3YaHFH->Ka7l|=M=F)8dre+5bMi9zSqqqWR4Lh z3L4_v^=;SJ6kg}xduveN7E?YxFU=LDJVm#^eml&nJw7r-sZ`U$J==k__d(s$4kcH= zMfcd>ARax-tPj4OX$_T$bsrxgG%exG9P^%>LLeaxVFSVj|IMNp$s4;eULaT)^~Zrw z!F(tGWY`AvMMqZd1<5s6d-!!3n?|6sM=aM36gUN4;@SO0nGm8)$OTe%p!xPHgqC1F zA+Mi)|NeOTBb=YddyyzpNW(??rLQeE*HUE>a6H}1-|gP2hJ&dPMexnS`lq7I745 zT?(a#=0U(6?hprqp{suav73RA+#502SJx_AMHqjZnG!F6N|E9xKgwa{)Dc z3=DG-w}clr7 zv0qzH?;BZX+X9~FM0^~OHq5!Adn0$iO*eOWeIbU_Fq`}^Lk3Sr2`0|%vTe<#+Fnfj zO-}K(w6o#{H4g=>v^4(8U@Y%eyW#F%;%f@)B3srz%Gra;4DBNPqxp-I4%NQ*?cD^G zhxFElX$dKt$Hl>zvu#a(rQW*o@@@;NXTII(^B9u-a`3{yia$&gzsnvq^CCQ) zPO2>Xyl+P95tiej z%4Z*7l069sO=8{K9n?IjF^@9+vS@(!`&ck8DI*QJ_9Rg|QOQlRps7;nvpa24&*bs+ z?8;dWnhF`#RAMOP@x~KZo2(%-bF`fp2^{?z)G!)Nx@`YhHuYDOY&2%XZ{#w>as7;; z4LWd7!9i*09_u*<-NSk#amZ^A;{g5SysiRzQgY(XE>Em+1CSO#fHfC?Z@$_=oU!>6vfQ@eQHXnif7b%59|W%W!M!h{7!++lTL0aLxr{ zh`$v>adnU@gTF)LiYC({>y}guQ*c~h{{b`kwJNgwg(9z$k7u6YKsLI%a_^2`M9|6@ zr6t^V^1kvU5^`b5ecTfHamy|I5Nj3HPab&=bK$pYmUkB#8Hm*0{FyddS>us=uGja4UF@=*286JaS)_=TqWi24kH-)`G zH5JqhvCyQ?8+U0?@Y?@&r|iAq|N9cy+JE;LHyg`eH%p!oE{qjR-D$8MKr|gfI2Bq5 z&P-^%GUSo~N*{m%h*S5pHS?!>Gu*f|&7vZEDqq9J?eggaBG{MOwy~fz{Y8re) zeUAAwQ_MrZ+SqqrAfVFHZKy=ZT?TqU@^x{FXjbROQI!~f`SE~aQGBNcN+!Hm+Rr1h zvWqfmUYWj_v9>KMc;Vv$SGn5F>Ha9qv%3Y%x3S`uivj{KPFxM+a9R3L2P9_uHRwAvQc!^5aPsg8pW|Nco(~8d7tpa`|<-YQ5 z@9BwBv|NScjylBG@*D7bJnkW5;9aX3D^pxNuO1UCZ_w99SvpT#CK0De<{CdTGEald zv6aU!y0aZL*y=6)e{_8bJk)*H{*)Wh9jlz!Y-U1!Lvbf#UF6h8WzG!r&$x-cw@q9N zR1t{l?!(2M1eF;&Lv$B*eCY|s@901Mx%LJBgtz+YXS?E&U5dLm{ZWBaSCxp#N{@^V zYZCtV1pNKyg=mWt6Wb>M`LYmEJeMx7O%c_s+WTyJ>I!fK#`#15p_h%CDbDYvxw2Tg=`Ox;9b_eRA1tSU5pM6dp1OR79yO1?P!SQ{z+P zHFF=q2 z&%eysdWnXH##w_uNuvK8^ZtkSp6#$15D-dQB^91NUYdLR8@(6E2nb*XRFbbq28=&i zBsHi$H0C^JgQV$oRdN~i=oup`l@{m*uMxYEb^vVwzGQ0r_y3-qVkS{)G;#a*%ZM?BH`*azUHdy@5*m#vDH zYXH4@@;B{+x_~g@PYu3N3DA$el!&E>+STRdXGyq-r3cFAT?m_HPC%0bVEZRB@p?k+ z-5`#AN`!4xvxv=IpMwFmS^b8bJJl! z2Sr7;5k+I4BVffUWRp|n(#MFq2SYb9N)fPCL3h+|@%jOE5SLd=!`aKLlXCE`!D&G# zKd%^}pkB_t$2ekF!G7by+5Kj-QQ}T|TU*;x+NX6;C6C$GkUG@=`jOgMPv7wiT?Mm+`0$VP`|Rz#!$wr&rE_ zj9drKN>Xa1ugh@`)d``=}tQG8{GtNxFHfKGCRkH9>^jOZO_-obJHT&(o+kt7YO1Mn8;F66w?HCJa^b8Z~<(jE?}y zFK4(12izpX{?OUsE)Nd|HZ64H8zQRm+t+XgERb;{2v>W+4(O!*pQkNVN2Itec>K*l z%LU-A=C>Ke5#PQYWn?&b>2jzkPc{&?F;s31ETf#+VYdO)X`Bm&VIB6dOluo6=4ybB z$95?@s{uLrN)616OJ=OOBvS8)%{)0~+q*Z`wAu2i;;XWa5bcoz9HWN7`Pau~GV@cc10-|B2_C;L~~C{Rye;VNtiuTT5AKrzSh|g;(NZKxMW4sB38vRAj6X ziT>6-2D{PlC)wzd-k;Kz27WUmw16T|4#CZx-u{~T!aomv8%ve@EIn}nLa-(_!b$bV zKqFalkl(Hdd+MM&>zM<7eB>NkN!0+~LGdBqE2PWib5eP)d_E_zvauRCnMQ&ZujG1! z2c+}WmNq+APriuiyE#aSFV}_t_LDsM)k^1e=5;5EYqceAyAUYfSlL~>n_Ysst?-=S zze_Hh3oN-;7({6BvUnHg>FCAT+;u(wG~k(~fQ*>8ak<(`x`Vj=n>5JPgDvu>2F*{Ox&)-B926EOi&R>X}l zV)^)2<11I%u!2Y1qTH$lH9oyzD%~=X{q)K9bBF>4rU^x72Iwx=SA1)AVi_BLaL?FX zqx5%cnLDqNw|mXtGgi9}mn(}Q&Z{nP_-WCtJYQb*19&k|88rG58n^Vb`BI9_u``uJfR2#KU$(RNhThE8`6y-S|H~W#T~XHX%p# z+mSCOL>HDZ{ZFn$%@kRYUgIgLsbIycALy(11<}Pbx#aC*Gv$x=@6*t+Xy`Q}s&u5YpRt(&M z0bJF`ai&MO#H-$x=8J@!J&NS9{#PJn!V9ywx2=yR;m+~1MtL8e4+(@nK|Dt<&uces z59XaW<7QRbKAHH#6kHr=xzjq;Qc;?^vV36x=z9YAkJf-?xW|yR2*15R7&t3+z|nIa zs)|G&27RVwwU4DL>`jMz;BtHsfm2(#lC@=ie=z1hr=`r<;ZkN`?@^!w?BE)yyU;ac$$Qt!)Vkz3ISd2gsMXInOaLMNR6wVrgnEo{HfjoM0KO z%<_}7(aT)gDmI4w#cQX^F1BJKV#HRt!ez9E9CpX5`&uYt)`t_U0{W*++3GesR#Mm{~Y__ zciP52vYlZFMQTU-d|r9EIjG72&1SxD27YeHhgi?d0lW#*S8YZT>G}^8aN+~W0OI(RKNG}H3g!BlW zt`@!4uOQuVs@nL9EGt9IqXin)Q930P(72^eow%dURX>E}Y(8;axV^_G?o@@6x>ATH zyN*E#K4NLVF;F>TxN&qs!NbQyHvZr`T|T%@zlfe~@yq@{pm8K$ZmmIuD`anGb9sHH zT~fBGmBGEC3d$U^9u77_KTz7&{&pBA`w!D$6b|I(9GM`MsA?3lYTsvso&vM z=!AHV?-ZQiERFhn)Y|F%b^#JPz8f7|Qlv;1?Myn&=-CzH!@2>^iZF(lHM={qd@vYA z2{1KEXJ(1jGq~-Da_(E5xTcrK!cSaNDJeZGgHhuZM^N|o#b;_XK+jV12(+6J}zEf9E z9aHy;|7CY?l3aG0vUcuBXq^U4;cG++%LM8kLFRQxIP!2Jj8K0cgtx$MXn&o`f??*8 z7hEzN(`3Kyi@p3wo8&ugYQstx?9y}n@dnOvTk&n~82af= z5$c|TVzva6QyrgTLtk*C?-C*A*3<>syHk9+*V8ahuDrPfWVe0pQ`4o{_^u^)z_)LH z9dloF#R&M&{2Io*&wiv@>jrNc5_H*rqJyW11vUVJeo4BUL7!IVoc@0gKxTix_FW*8 zMSiT`6i61-`Y%Io+j$^<;^%Lm@$D?{+y4- zECvx9fIAWQt$Q287NFn)MA3k!jfdw@zR0L6$_g3}A)Dwc?VPaJ zsC8~kb)M|H22L|A0X=x&TEAyQcr$rd$S5#s8Mf{l|Hg^^?b=mtH$#koVgob;vOqXd zNLS^vxb9iLXBy5VItU4$#vkg3gx>$tk6wa@3E1HWXFh0#(NYl3hWc*TdrS-__~YAs zo|#?BOx>+grUzp+&A(4Z*ks#{VDV7K(22EtiGt!rCi;pD)5 z@0}z0;a3XUN|r8dK?rZGR!u`lK&|{$U-cmIM_cboxkFSS9%dk`@_{1@@2Jh;<3*#e z!_R&C=wT8^=n;cs$&;Qag@0|@*qE3@*^_?`4&Zs)4$f^MV6qF3Sf5vtsgc5}16FF! zTepjP5feQs_Tw=eQB}jD*cnwNVc!^NR{uGU$-|6dpfKNA{!GR>7SYfBQI#oNtn|c< z(y-@+RGs^0qCK@afvrm%XG*i2xzH9_RlOipHoAO%-+e<~YD3W_!kA!^ma@3(o5#2# z&@;e%g*9*xVI9Amcvot2&xBcQa>w1cb$HTVnyGCI)2=X*d53@g8BK`>v{BT4mB3=T z=E^9R;`C9=t3}~(Na41JUSj_8^w)5#;mogU8=zoO`%s!oT1H0T{|klubNp_^sED?~ zj&Fu0@>f6x$?-9dTOIy_647V(Q*D`SKM7pN+%|9#o^%Gco_h93DY*Arwr$PeuurMc z)a|LQ7QQVWIR$Lqcw+EIb&RYZ(5amG$T$TMD|>EtKXnyR+UO~I5%bz2`+RBK$Hz%4 z5j{fRPq*E!S-+5%@`JXZ&{Mjva^K*zQVeu+f=P3D~5K?I6kURbgMVT zb})HhF$6p~iTe4wj;)kO67acFcQGof^J}Q6EVX7#{bNO0F@J*l#^f4z=~VMi^ZmDh z;6bIvn#yUwKg;}e*Iy$oWX}b->BWxr4eEmDfx?ff$m-f4%b?P6t@8%cGEZk+V$G9%w#WM@1&<8;T}o5Hrrok+KA8-V1EsubG#-Ddz9KcMrH#*#81bPz2rC91XDanb-UV6CXmc2r} z0T_@#4QJP3;=4@;X||C!H-5J7gRZiK>g5GV%UlIjhyq!hBCRwoc4|3kyVT=ulAo0R z(h^pzWy{U@(+*#thjJvBoJC|3OS~x&^b~=}1#cnF)22KEt>RTJ%;=Ja`uzK*r-3cCS0Kkk@}s*X6_01=_wP zE4u`EOZm`(2k~hbh8txoK%Z{x=mFJu*Kgel&594g$su9?&4N-qqhEiDKQiqx*UX&q z^_i?-FQh5sPOT$xryjX(_^FgI=~FaSOPb8ai`OCn3f9LqF9YCR-~7De_Fl%$M{T7d zadTq;@Ri;L7gZ#Eu6G;Xfu|uiE8N+WY;^R;-o#xOYX7NcEPcvk@auE^=b4|E>88g_ z_TM*ypZ^AW3h8ruar24Y?SIWZw(ncC`J<|w%K2D8tc3Dz+g^>Z0pw0?U<%R4*D!LklwUs~atuAZM6;sSYpA3-)lO=Bf`%B8bR-`>HuIKma>+7^!!s8Im zh*VJsigU73Wq^oyyB{>vs7lZP8ZsmcbeLtcH2ePrn5~Dj*8I8%0DSxr`^o=v)!ws= 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

?b{U zY(T5!rVQCwJut^67C?j{Iu+La$OdMQ7nv!9{k6nXEF5GJI8J8aKc#Vdfq~fZqWE@> zIp+UZ8c(?pvVef=f)VbJBl4XOIKl%>oQznj@(j+VPkFWfMR=sfzxH%|;)w=f9SpRJ z#o=al#;k(?*&$J8rr_2C3?b$ZesB;1@{+0pMC&52z`A20dt63*=i#=wqhHAOTZ$Cv z-vwMw%|CK`dz-!-@AEhOFX5J>Gb;kM%Lz&(UKhamN&gPI&I1pa1^{X>SXBc6Z!_Ms zR&N2zEaEf(z`WZudE!|ZwVwn4oeS6N%QX7yimvX6)KoHjFe5%(i=yo#EQUC(#>UP> z7g(QVZd%3h_Xn}{5$la!zmWQ(CFMx)1s~gJBeXU(0p4Zw%hz20W1eaC45SqM{)m_3_@05D#_f1lXFk{v~E$8}gb^(9qPfrg6!Aa1>UW zeeeylyko*>z9P?jPc4od%*^xU4OiZP9i8Isiet44FxW3l$~&avYI#^Z#AK)*$Dw`V zQ}8UY9*8Ibz2~h<)V)n=!RQak%hJ%QRW+-6PY`~aWu{`t{bojI)E~w*){2M2xCY(j zgN<>;!->IRlr8J%c$kz&oWsGadLw4SQNA)af*)a*RSW6$l`~6k|{j)T9;Q^Zx*jG3WaWOi7%boeu(@KVJ zJ!5bgY)TO2r_LWp*$KET1&GvYSgsG&S1Ca>niUi z>|>jP+k8^u(^yAnC#t!1Jf;q?oAJEGss%_#bIGQb4W(|Y|M`_3f-58BesyQD{vdKu z(Q2dedB$H&#=9?^%ydx^ijztC2f5Xo@W(OsujnWngzb$W_M46|&wmF{-oj;oxC6n4 zFYP!I&(sLA(08Fk-IhxJ%fP|TdJ)_^=b;aF70wn$zgvk z5)H+*fYgM2Gnw;4}( zi9ok1yvLXPcen1l`~CM~cCw9`pwRJHhcg zZ)rT`__T7wS7>SV*6L$1$yrtZOESSFK@qD{)#l#kR8`E_#=k2swPyxd2OLGd(sRpv5=_4va$I zp!JMrw2EKFK$|Dgx2k2=q~;$Z;jDf`py~w@`L;DsJDJlW!mMdKcKrB?U*t4H@ChA1 zDy~5D{7PmB2af9jMhXHrgJKpiZU&GAIk5LCge{z6lX3t+dHo<}K>Nydc5Dv{|FC9d z(ql0T-+pCH69B-quU7`vtlnkT{n!u2h)HhW%}~M4q}g_60}gLKMk%|Y#|0R_)Sjk= z@{b0YQUv{k0)Yb^^H?o#N~|bDc1<9$Kh`>1XmD?)t)R!0TH(t$lGie6xvR%J=?K~5;Tn~;5j{uu1KG)Qxt-N%72H+&7-u_ zGxaQsz6@EL`FO`B5Ot(zgLF-|nRn>P?)-nlcUM8Fh{y+e)pa#@G6?t(*7_r{APuY; zRMsf`F4N^m=`OHWxTpfix3i5l!0W1^H7}3Q@colBegUSd(5lL4D|7=(C zf%7xe5#P;$3glcZo3-X2g^t?#&(M&Bobg*f&Z;aV$;FEUOonW#K#I#vh=G#kNVZJu zcRHR2&#KforFKS-hoGXm<3N`iHMn9^dAP&`X)4mLsT#oUCvmgz$&8jX4aw#tqd11^Cc_aph zhNytZ6T!$WKdSmc@Q}e&u+W{`Wc6FKc^Rh75gm))-!L)8Ij;H}*VSF6uKwS`UP5=p zT|PwZeim3?*=%TqQ9scb%G`DfLCgnH{%uadky>$X5Io4Iz013rG%H=hP^RtQ;rZ?; z<8>=6DEvB`Lc{~ZMQS=af3q@|g%?5|l7dib2@X1aKS7=He5Xn$6o2sx?|T~0ckfdS zaLB^jItNRm5apt~jNG6yp9xsgq);|4wf?x2?NK2p;K0_uyWEkOi%9&5C%7Rn0~2N1 z--gmQ9``iojf1YdmepC0W7C2BT%a5>FMf!F<2bn&cC@>KI+TOi0YJ>!??n_`(#gdd zc@7O-%GoL@fPKMN!jj^D7Dj%0_~<_Y=x7l-(RJi=u>FEE;wioLp$A@I^vSksiml;@ zJHZCV0ke2nN06mO6N6{{~#}gxQvgqO$W>bnnYda_r2e5CH8pj@;U9?Tf(5 zm(xrYWBgLv{Zt~1rK;Ejso%q433FW0skQ~}fSp&%WI!CR9>(eu@a{z(llZ;;F0zKt zUc~y7b&=B}#z>m3!)4?3uY6q%7r2BN2dEJK!mbYJx=0=0T31K(%R^i&vJ+I=h-4&i z+s_K{$O*2e32bi-q1iGMC^O?7T;lNF{0&!7E=DR6J0pSjRzC_Lm-+>s&CX=VL{CpP z!RSh+*EDCjHa{0%L;a@sd7#<|%z|hapFf?a>Z6{t|9}qaM7(2rKp)}rfxwKr-zYFk z*&ys%p1eyT6F0tVoy7=|sHM`BfD!_`_(0T^#7{A_v;TQ8HE(}~_`Mp*Fw2o9Dr7|J zjMBm<@CGEH{%!{BN(4c+co`qemjnBn@A3A0^K^eq< z$A5Ol)EB|NJtY}3Tb~{pY?=Vh>*b2q=RG@(Fb?Y_|Cdgil5omu{Ipea@i~X*Lkcy8 zM)rpSQ(r52R(c~r%4jRlNUSh+ob0g;s|ydT{vGufyh)Zse$ zdOhR8U&tS|H@9&}{N_oXXr+T()x5zV!rcuw-(J+qPJx*Kn~8gbU8gbYL%H_-j>&fj z+%`YD05x}0G1{TEnfMea2udw0+rIPfOMp-RtL=8tLGIZhsmnmpN2Z+;X zjq&)9K);YV;E93zr2w5}|l3%|2Ezt=XERrlk;fx(c%!AcO4 zf-nb%)4>lu@9Lo^30=eSJEXg{6vR7gYgrzeY4g~vK6rq&o4eTIqk_Jcz8g&F(x}7o zNngU|I8{@n?c|k{#{;%*uLbyciGIcRJ2o+r(A(!|iamawgkpflv*X6LHq4F`V7T zc1+&o;&h*!-h)pZCJ30$ih!;xfOegBSLX&jwsx$q9+P8}c*K4>8=&~orNrKYvICib zGx7!d>-~Gx;~ip#sNIOS{PA?Kt+yaKqH{1hI*e=nE&x2YfP?al!*%anPX8f<034iR z!SB-XIoNh_-&%wM3fWXp)wiSQSmN&SXJ}V(1LS%hHadpdpIQj=Fq7Nv=Em4XqY_Lh zuYr8}-?T9asPn>44lnS$)wP};y6Br={4w#3B}R8ekvKh}r$`zMDOV`bPFBCi=*W6Su5 z5CjBhFVRp?yMNQ>`_|%mU*f^7GmM01LfAyccLaPrW51YP4ae!pPIq+8(;q>c0T;Ni zsOh}<_6=)GPfkVp{jPD8N6P+8CIdemyV0uEN0vT|pp1|IJ(oPmQ8r!>>}fXv?Zsf8 zArR$emvC#@%*ko*_HJe#AGoR%W-&=t-sFeSro}OkhtLG+fYd{Y$9!iN(ouRD>sPJc zMgGMx^jYiHOPAc`uw4)Cz{I=kFcY}s`LOZ5^3i0>2>0^AbIh^>a-nQ$Xm-xy&4-U_ ze=D5nJ|tb9WR!;=_Bml$s;8}ydx#m7@J?-T8)JjK{rx+9_!t|S zo=Q0=kp@iq6DmOw@h+o_is0B0haE7#ZwdQw z{G9|KzD`bk1V&AUL(JY{b3*gWlQ{jg3E`ejRA(IC^8JGsXhugjx`W2g!xh$ugW}JG z{D!tb+Pl{B!YkW0Q}7xAPUp2bo7J_>0KQHkJ}few3rMQ|xiI(HWnmwyd8YM%oo5@k zTaD=d$o7ITRNr&tOBHyb*#$W)w{S+$o8Zl>kB7>Evc4hlfOT=2ai#JN3ApbYF1)>_ z3PB~we6rKzT#c=AwM&SGch-sg5Jle25JyKM{1`;umpx!i>B^H9E$gtiL(`R+icoyA1E{xx^Noh(`lFzFDmeA7dd2ej+u zlStQt!L>MA!dScQQjlzpO3Ip=4*v#OQWOPF$A!(?LFlr_b~pbtK7e*wQr^nXI00{} ziEd^c3S(2&`+m1%W(9SRA4w1+IJU=Z#@cMO^quhRd{O$c*l23+0O>FM-$EhY|1&Wc zKev{*6-&S5^_<|<%4zgNXwteej?jS|+Zi+8FtxSC;hHKyu(vu{<_Le=UG1yOD7EPkmAZ#~QB zC(%1E zNS7`w+uqh6w2+MxP$$h)KAD_XCa!r$ZK;dwP?3zV?r3IaM)4=#5TmoRGtE7R+$R4k zsWvlB*=cDH=jP^i+zky4cU%+|gMIz{R?zLe(G8fv!9gRR#{Jy=E)9%;fWvZBZEkk9 zqP=~YI}S8&-*HC=F;?I*HZ-zI_>%19@r_pc=zyW^#OnKuAD6^p?i%OQrP7^%`Ev+& zbUl3N(+u$tzYov;D+7(F@8BzH1tbaqzJ3Jn=`-lTpoTGnFC!BCuIcb;$AodgL}_@R z8RT{4skP0Ju`h3)&z|GtitJN1@rkHxW1r>tF^su8c=&y%MdY#re5)Oy%(Rvv74Nk5 z%=~fEgaS8_;hP~hrAlLt;2i(M%dj(T9zLLlMU8{Psh6CgM-Bog3RbWa5zG}){WkUosM*j$f%>E!@=ClOi5ci{=

9Y2b(SeMgY_b=Y^sn5Q<`5p7hZz3lre?N*cuADpj+ilpVZ<`vCitw?4 z&6!5^XL(U08|9VJ3+d;s`PI<}IRDqi94(3G*a{{1pmcN|z9*5yk(j&qaf(=80tkmm zdU#%W(<^8BId31gTcy5NTWjki2|u57pUgLP<})$T?~C7er7lPBVU# z+Mh?{3W2IoCH4`|;)L_h(40)HuL9mSyD3-*L_>O%7Nl#Ose{GYVn@c|s&mp^12J!k#9rcq0of{F@r zu-P>KY7DZC@~BH`%x9&MOJ1w zBYobd{=wwj_wS1bevwNb$UJ2Hy6(!)T@GFT^hdn~CV)$%fbjrrIVyB^Opu>nO&=Xt zCRTg?!i6U)Drv7?od7n04Gy2wajev`-r^#ki zQ6i(0m1O?3@cW3p-bniuu`&F^be7n#;Tx@$CTqaVXb2A8@%bhG& zD0A34q_XqQ{Pyr!hBl57zWJ;lG4eNd{ja|cC;9}FqEOfg*stRsO1*%H{W4s1-=OEd zp{V0EoQ%O^@Z%&1W)4jBwnrbqjo@H8S^flwq|)ctGSSa-r>Cd+Bbm8#YHGgf#Gtrh zFWn@MGQ?jEJ2%kU(o!_kvHeI=(&xlrmr;y5e_npR+{KF*tvSuj&F!Zf&N zJllU76|?^=;2#Qi_O3lMY>VWA^!FxkcNF(zDJx|m;}BWcmHC}mZDh(IgQ!P~AxI84 zo;b_N7}d@C3jO}kXX1P%{5}+GdR~<>%Gxh1=qVf3OR-cfO8(xD((p z8AHD744PYf`o=fE=w6K&(uP-wnI|6jEn&=VUy|VtIhVjoh|12*mEkjL?*_?K`&PEh z;m_*K9OhqMpL2+cIM+gB!u&9&sHle&$=e2uF4)c{<3mbc>{UuiC!kvp`}`*PCBp}$ zG7GALJZl9;V*w1pqZ4(;3C@u;@zMPA{%p6FEe>hVCm2%_Nw$RO zNH^~@L;eY;RC*PoK_VEFSQ@pnfvK;81Z3(WyYwU zDqJysV z2)&5gQ{2uzLm(1%>V!WoF?CZX1v9ea(Y6g#`}ydelBOnJqFobEUg0w(e8Ul6 z=M7#T2o_biJBX#IkQQKb{Z4mTXY=t~LeXn*e*D6lheU=IH%**1zde;FGAzQ3k4qT& zk`@NlCW59P)XcfcTZlv_6?p9?YJ20mRM|Gu7AqH*{QtyGnzOQjG@gdf$p5fn((Xdy z((sAnA8J$-`c;$`wJ2cZAvpj^g*Ro%v>=!LCq z<(W1ZvyAob>NvK;8kve_^w1isuvTNGP(sXVpqFd*GZ!3s+l;v+eBpXfxBx`JO`^*zXT{c>`1RK9E_g-JA9st+dUg zHvkcHh6>Vc*a8C#^pY4O_PxmAqWsuZL*_6NcdGL5>R$%UDbM@l4H^>fys7@ORIE1X za#J#z5FqFrk15@;^wAHa8+4bO&$?Q@b3569n2e#HDM%lq4P`QjY|9(^I9#Kr=t{b z?YtTM;)P82XC*AIF0Y=~Klw+nJNmWJL(sknma()X$84GWybQFMZJ>%=VCbUx8w~UHMfVxZv z7+r|5PHIL3DZTlRMH4SI`wmO9jk4_W$|o!Mq|uY{OJ`4AF8r?fD!Fi6MpykKW`)R* zGqRqPrLE<~2*xNnHZ(otl~B0Tizgak1?R4xzQJ{~G=kx7yca)Q|J5dPGj?(z!F9FA zyr4Ftj(Z44h1zw;j#^Ikyz1+`1p8~-g;v2R|6IzvvCH}QK?$heT_MbX^F*s>g#v*Q z_WnH7DGxi_hQPo0iuTw_Wjxoc^2@^1$q0;$J5J_R+RmScy+>YqQ;<^MLIO?ILKJtg zLyj}!LHHMu0?(Is2*xOtDRn>BZ8{Ix0AOHL1g=OKooHElka5Xj*S+cjtKS_y7R{1e z)rdI?xz>cclE2WKgAOZwA#T%TNw$r&q0g zH7l5p_b3u#{MZT#+(M}C)djr{L5rMe@STt!Rs&ja`T&`?-?z4v7mG^q##ZSF^WVFt zNu}eIjGtn}-(Pd?a#hjLxM5dNSlCT!&F3*M^8A}8gmCOJgV_M#R6DHnuFt&ln)==z zoeDJ#+ZkeGXJ;}ZB5mZ8@@Z(3UzqJk3U=jX=~bGWa*A`{+vk37U6 z%)!wSwOE>M`?9M2M1tZy#CS&6_>Zm|^!#!#&t!6X(Lrr)Hv?o7hwMkT8LQt{0{Z(I z;-CFOxIe8{VK`kMgCef9#Nm`Vik?%RpY^{q(|8mqy%esd*l`Xw{W>@I!ufoj;hQ*y zpt#cvm~u3d1MR{Q)3-$B41oV(Ir^1I;`lN;aFEqta%WT;s6=gG1>`S!5CuvjEcvqd zyRm$H0tIE#X6N4w(5)&7oW3pQJ~%Ydvs$649wA-wqWCr+i*$7UO69hLZTJ&b<0-w% z?z5qg;=nu`QqWuD0lTTzCwuh#vXr^$Vct!IiB0nH?Me88y}$^7J=JbzGRL_g)a zDm)jEFg-8{lDn6#b&r&8Xlm5xw3ryJ%pf_cEFg%#e7NHvmszF5(6Z0B=uM2UOJM<^1&x)aig1W_1^;zWqYhB-KK;{iN{_~f>e_0Hg?t{H|c{%>X z%-yQCh751k;1d#0!pN>Rv3&PMWcXOJ{{CRqW&jRnWmDi1`=eM?wyf3ypyI$7nzC9Q zL*l6DvFL?nclO)Z6zLEbdJvRu1}c0EZa)<~piaoX@J9Jhslm|H`1{qh{en`VB_+#+ z#l;+%yy`zp$~0tfZ8Z0CbNBcVDyvtdbGPwvZ~)KWU8LooaUplQgmaoLTCn$hR{xeX zpRcW15JwCmnh46o)xE(yjcwE}+oc~Yhc&&uO7~bX7Z7`|&Y#CJ#`TliUYRoA#W)QgXUKLT$MV_$>GLmc z<)?}2`(3y-p`jW^X*vz8>J>yv+;ab^jQPqEyVaT&L9`ih(4B?dE{ncI#8=Z5zId8d znW)jv`mzpwry}B&L?q@r5-*j z+=AXV2wtU8?O-+3<#EW;5%2K<1{33Q(W0k++9YtiZ270Wcz!-YCNNmq*{h&T8hPz> zE0gqjeWFjFF8pI{)c~V<`_E|Z;+m0?aDMZ^^SOpo>eARN5AHk%z)-7Xei-w{xK{XU z`|F6Zl@G;Uts0Ii#a?Z^S4-OuCS0v>Ro@BAk>_v>b!!X(cFb>cN6ciC3pMJ+Xor_7 z>IkcWp5q{9>}+rAd2Nv?6j(=9!*EXKsrtC1RoRd&pk4shRKZts1=kDK5y7lahl&%+ zh_43G&y&jqniSzAXB`CSC_xbB6dSvJXzih$gIrg(cVMO(oqbVh*K$2_ff&B`l9J=; zk8;mmyqPIkR?kpIj7!0eD!&QtrM}?v+|Q)U7wo(#N}!>}RW5*d<7{<#ZoV~iEyDbL6WWNjtNi;% ztI9dbai^I8K|t6t%S0tjPwQ=~HII64cUTJAr97tnK$`tx{@J~x+<}-23ujN|?M}U{ z*5Ts{+ONuZGIU#-IB;w?)-oY)s@S{Z+JZ zsrYHIb4+ubY|U?Nhkn@`?!u$!7UoI8d)3f(zH^{AUHUYPdg9uf;uJrGViq0jOyD^t zb9{Y4f02DEh?gWn9{n}4aDht|zkGM?EMAI=xu*SKxd)Cqx3*zbtaC4C|Bf|X(M!Yc zj<;@3m>VSA#(Hm!<&E0n^wX^(p4Nrt7S@)I-bLiwpdgbjAOV5&c zpOwf_tV~8O>n|>AH@POlnJQCH8%n3oQ}?S+j(*yQ9B$V5PPymkR)?2*=ufJ1Pq&n| zZ@Q-L8$zq`a(q0d{X`x*xmlET-1K>9KTMg*>sg^Hk!yUl&Z(}_cw>j*b?@@0mO135 zMPkiX>Cn0VY*su{dwRihy{*yOk=nv>(!}z7lkc|UR`Nl<>foj4-a71g<9>;61s6aU zkJ@Q3)b&iga-EuvbR8!?wF+Z@nC5ISoi4kX-k>Vrdmw_^?G#5B0EAgxl)~QI45*7P z$BxNAWfwwFMx%dE_syyK#9Ab$Br8h_?2KirVHT3@!N|D8Y$zT@&3J=4dF(AA16$LX zVtC!!Uy##bNzGf3ozcQikPMdZbvu*Ji^mA-*{TUs? zH=ICMAqy(#rQOP$G1Um;RzW39FFumqz9r<<#QdA)v@4Ioa%+@_Nm6xw@HS= z5AP&G_(nFGn6MfJwuK*arK*PX$R}SfEe9#PM75AIyY54$t?^&4fp7ppRm^U`CrEaUytePtRNNGiRSjaWM;ITvIo%6<;hpnzk zE2^+tysT9h%VMY#GgkK>`q{(bbx(Ls^YBuAEIKh#pDEUfyC57)s$7Ec4`wzv;(K#} zxKV*v{5Yfeq|bTd6%I|*{Epg2oxFX-TAu$!7Ct_{+hhe>spVG|h~$QB$6&Nq{Mwg_ zwLN!-x1z3qHvjJ@hx}K~7xmZ_swXh(_5oPdFhIWSk{Ru?K2OFRg$(*wq^mVE!3wq} zk2YR-J-Mo%_@gK$loR&37m99o;@*K$@V3u49gon!IO_90=xBYh;(EWCRw+GlHoMU5 z#84n|tN<~@cbmnqR4IkD)x|I<#=VmQ--sh)mKp=Jt6Lcrpj-92lSr*gE(&}`M&@wY z*eoRkpof!vk*iwsy3<^WogJ)X%A*X?DcG#I-oh+Y37T-D;`ieR=(p zA;MGv|DwG3NDU7O%+boq6z_QwQc$;C>>d^y9*wKm?N+lT%I|&ueSu(?`tS1Y3td4;$>=$99ttmO05$&tOARhLphYu0l_?W< zxQL6lPLA+ThZ_c6*gd_1s1^AaNXL7Y*L`oyMzivlZ0ecma)0AMPD078@#nmx{+$eA-`%@J2(yfE~H6jZJ4M zL($0ZeS3#gGdS&&890bR?G|DdeGF%t+q6t$^H)q>Zd%fg5c}=E5fA!sBJyUx`s!h) zP0jkG9&-K55cbvA>r~1g72HiKXVYe?j4VUvF2IM$qfg?JMZgHDP;-`vR^UWq7nq8!)aN?+&opV>xk9q~1aZ1l;8oaWHqw((9 ze=2i+naL@V!mhQsjXTk|==Suv$4BxdtKLNy=8@_%tMFM|R;(x%lBxDQv2*L1)KNixDrV zO&Ip0a{8kfU;Slw-))I{$&ucUr+b@!)jwHa0cg#gPcHra=3PXa-n)s8pvKamH?bK- znABQDGDdK~e87bmTOAlbCW%C~tyAV!mW*4s9`cAaXDUi~?+R6)io}&+!oQ(12 zw?@2|cA)DW#r>6trgt7|<`u@A@xb;*RBXSC9@R$CoB;~qdxh{9>3q(<Xgc$y>9vfrhJScw07Z0gsW320q7%aa8z{KJ49yM^5A7m*wO$ByoejT$BxEpjA)avW*C*hxY-3v`>PesOXBEJ_pJy!JS};U}xeMDf-U zYc3UNkjb`dkV>Y>+t`h)dqXs~E7Q+le}UQaW91fcAr1Owu^SwA1AHoKJHQGLCMkx_dFTlY}%9f({NY;Z#) ze3^dT^?23NJ1O15^gqYp$Md=?Kkom+qyQ)_na(YV;SGRT&G3MVLtP!6+{yuQqnkGa z?|_B&1yja%VUnLO`BMVopN~ZWh}Xb|3qMn!KDx-Vex~}Tp~Eag;`H57Q<@Duq1uaJVUEN z7!yDw%N~MD-Mf!R0ei8$$9Fq!OSk`JzZWF91yB?e5d>~VGxhmaPg3tqynTDEI5f~d z;WSN{{1R&mwEmJiRkgM7`Idb(K=Z3HZQ8eWH>1X&8}??WNkC^3S0c%sV^gy|;$LD2 zWn%N3tQ`_Nh_HDL<=3T#MPPLvZENKha7*OtnPm@+m(gCXd!26%P`Mq;RMNQ{9uPAI z!kv>nrl+gBrt&P`@5fqgjWqGqFq?sVr|zd(C$Ud{LI&J@4t*VNlRuk)7I6k=@+yw_jSU6+=mKaSOEdc z@96nEn*ROI8Xkt#7o&npVu-@kvKsWS%%f(3H36D)0zU*SaBd!SBfs7jiTZveHv9>v zTBhm=Y|Ep796O?+(K!EnJ5u=hjw1EB3M%Z)Yglv3Y7NwWc-1+o#rilnFj>9Ax;XJm zDQwr}Ej`u68O&p9_Qt1VJZ2GIE_u?5)O|r7-V*kipIYr|&CoAtCbtiAp*|Q^Yxyb65mOZ(4no{)$U6bRW&G^yZU6guJUM4sekZr|3N)GlcEawv}xGH z)jnj~Yl*>e!b>6j{0j+3iiQO>vfCux#>xskjE;~HZ3G5tV>2m&lmn|r8~wDinfrf{ zx-W-_cL2W_O?H6sjJ#<0FQWVR3;6Ljy9}~i3BW?f#>R>Y3a&%UUvSGp+cac(EuFC> z^B%N5`JLVj;HKej+xz%^my=vu+#H0W@txU)mk2%|ujA%8MT$8UDxmFW*yZvYY)ltBCB4%ixpHGH{ofCcZO%O)n+CcObd=rF{hBt&@`zRF7VB z7Mi94$8&s6_~ZZ~nrmw@-zq9U89#nk>J--2lVTEb1& zS7KP*LsZMt2R~}Aqp5RjgL0pV0c#z@^2b3yRwM0DZu?#ht4lkPT-o}%D*p>czXfcc zMAf@itrl)~esx%;#qLpx{68W*u(xV*`!ewiI61o9?ZbLAl^114}lG#~bBW`;%i1T{n`atM;Q?;Dx29xJ>Zenuq`zpF%~hCJzN# zzk$Mf)Y;YBS_$*T&J^U`2!cw<2La;M@j1mqee!R->pL#n z!G%TK`?8aqJ6D4+1Keyn5D^0X^U?Rl%kNr+UEbjEzFcD&C4vk}6#Mz5!!Ebp1+I=i zw=Y{Unn!&ELs_{`+_0rl=Ofc@1N2-yw7B@)u7=}E<1PN>oZ_x>n&*A?o}X(BKw)Jd zcDaF(xwFyai27#6Z+8G6Lw#kiSxfQKinZ#Kz0WqDnhWPN$=S;mLqR1(Mee=+DO|FM ztaChdUy`Y8zUWqRn_?l2@!JOOber&6H8AOJ=j^OD;XgkoR=;I$1|!~X)Z?`V-k2Ge z_YSzqEjK2PCjQ>V{_BbUA7B4zT<8NqkqLU1QCeV(F%~W`SJ2kBpcOa;Ke9X87^}a$ zI33elLDrpaH_YGIux_pySgni)OGvbJbmE#y%F1+$WSdvP%?f>*OOEK3{|IfB2DK(d6JNkTe%q4Wd z&#WsANR9#yNsWF8^Yll8ebTc`!^Cgboc!LN+=Eb39=W@t=6!aw1@A;ma{Gy1!qE0`=cOPQZ$Cly^BMEk^Y+U+eR#n->yXS>5tPltH)ET1}|87%71a9$(PousB}|(6d_k68L9?K z>O<07 zw9nrf7VtaCN%OPvZ~MX9I1*F$;czU{gjqgj$KA+td<}N#Bh|TK6!>avm}c-?%?Y+h zRpd6($pvd5SB@KrKI7}k4Yk?$Zm^7sZ=fA3qpybTMP0ZmRjZVuVCVKns0G9G_KM25 z_~aRfZ(u^*9EZA0zg$Y(?CcA_YaepFlV*sx1cW1kq?4&xV&o3K(NCeT0LFgkuOr4Nx4CROz~Tj8>D5HtYT3{dA%1b2L9D2IL4skk7En{ZN5y73Jb

_J95%ZLU7jr9xaV_hnVH+fjpdl+2pegtzmIf7PRT z)9Q~d6=WhqqeVOCD=g>7Y6I(looT9LXW{4>_kX#{#F@pc$F$?~h7ZZ@CoSovqgwtxF>p!9TK?ML@!B-Ngoj)or1<=Y&IgZA zLG0HFepqAC9_$zNA@K(XX`2;h-S8_sCSD0=Gpu&75vVZPsh(y1V&xPK&l^@+hf_!K z4>p=Y!5smJwKrK4W|Zw~d3){CS);gad{KG5R%E?Yd1lis_WF&3ov<_r;S^#WYSxVdDG=|+h-;XeARtP$ zW^6aWW?VYHU{4sh1cyl*YaZfmL<9iDxTQWUtS09XHTP#G+mGGAk0 z1qRaeDvMlttSK4H52Nf<)n4($wbvhZl^ec#)0W&GJ?(9>vRYqVB4|cSy)~5RB}5PE znAoOh*QAp79pErxt1c~ZkDE_|x}E=Sp# zP|0IY)yn)1zxBE=H>}v+HvpK##h$-zaUN%#mt+1&q0e@mJX-Mcp}uUo`hd@Orv}P) z$J*%%1jV}VH)itUrDvqO=bxxF2%+fjL+B1B&Lv+V2;qPsV6i3BW&wV^R5cP68hs$#g4$Jb!E#DEMy$KaIF804%{_s~w zEYLV_01uvVAfQarzGS*+l;aY+EP+6LM@;DMFUj*E0Ya z`#j?ea~yMpZtK}ZNy0NhnqQAsa~Q7OzAW~j5sf}P*neJe1d>hxaSE?q*$pyHM9*9{ zDzBO&LZIgUm_Ja+qxwCYv%&3W+?<|JhnmfJ#@AgZgJdA>h%qe#hg4Ge5+n?EmqG{SZ`P5ktbR1gP3X_&XGqxP)=Vt#)= zNT?+-fL*F0xc{9z4fz~PXFX$~i_qpLp33i@>M6W8m6;6;YLt4xs>rPbsno=H`bc`- zO9yY}>%)GA*)4qK>>&*O-2;#en^qhDk!MMhnc8k|~jW_)y zj0DeUXTjggHlMYah``r%ukDs0x1*G%Z@RZvy5F9lt4+J?MACroUbS@nxgPuwwJ?=) zP$}p0{OB87|IpQZl@WysE6;FrFILz({Zac8SnHvWmY$!V-;CeN%cEMRi;_2|NIVF) z%RNi@_UqQf|H~Kq*Mp-w`W?T*@G1?PysJ@*8^utVg86G0YO!JdybRSAm<@`;R}~iR z*cgytUA3#wO{r1Lfav@OWaK&R$Wk)wbZ`r+Bg=05WP27dT*XeRyD}XAES&=lwM7Rax(yaRJrsFEHDd1lrq(7sWKclF zv$2RG-Jj8k_Tb{IP|a6SD_>RTUa}b!Hh}kiqjHk8bAvRVJ(l-fz;>yRz1u)GS5eRB9D;loZRKAYn3{BJJR4wR;IT-p_G}y6P*wbv$z< zwO=9k*P=0F!FLOskBL8c?hkBH=AP6!aj^B*N_VfUTKA?dSU8WR-fe>2h~@fv3JHj2 z5msld-*tCrg=^fp@C)#K&k;Lkvfu`hd=7+fewW3UA8PA_oLbOoZs4%v@j_knLQ*}_ zxj<7TRc}q~z|>DU>*^-A@8e@_KSGBhaDzCYVtln{r8q)lg zt$@y+*XE;9=X6(Hjh7wVwAAplLikN2K1rmSX*El>DDqMib zec8Qka-9Ie)9UB1;8$Vmwlo(fytmdawISQqea08|3dK7AK=CGL$cmN7memC=E3?=j z2>c$*lYH4?ApFZ)8E{$&lPcF`(-$Q5e|l}whykITtf!in4^ffD`S^e$;O&}ALzg2+ znVVisiUVRzoE9x=pg{iXya}1;czSrqb{g0$p0`C-*}D4 zneWK-{7E$)%XHeIYflH+#`L&Zo@^y6C@yMn4nsQyHX;X{^mrOJ8|IEUeSy)L!US}s z%c5!b&>I8d%`|jJN$^bf6XU_ZO_P2~z1A2lP?YYU zyxa%{g6`r^pVZIMniRq&g*`TW09xGlgQwIM(<>+>TI~olm!WT`CKHJ&tfp9TP2z)+ z?$0~9wg%`-IR!T$+Z&~f*Rok@aCfxL{CIPDI`fHBO{i}gbRS2@D)nHxE#(j+%y{T( zDg7g@p+(=<`B5cn=WnFe!37-& zN}}&k;=|MAO^204h!{8FE`o=VprmKzR6RR|`tCnCIrqbWVC!xQig@(@2arZTy8o#v z01Sh&&$$|VghcLnH=(BIFM#-U;fd?YcMX3u>?X~nc_lP!1i}*zUf|GBVZFJ2;&xXP zTlH#<(al4zpjXSYRT|Uc2#dCE&HKIeFAH4~Gk8K6Z9p2RQ2yx=HhgAW`|;+gz1Lr_ zfEed)?B=yr&UJOV9LKHH{UGLDeDkGHvdV3zFlQKjY5ppP+@@dq1qP&v0{`k!ygW@s z{N76-pH*SKzlKz6%GwZKu}5vp=BthwR5CJd>W$A7rJsAf5k5#9z}l>+u&hLk6-yNgO)QN`4csF?ytB(dKS(S>q2P_qP5 z8{pJS(}D9GHVd0KMQ%o1BFDjem+zW1t>(Qs7?D`>t1k!5x3{`S7;O5rKeAoO2erda z=RZ)HjkEbg;;(nk-x^iBY<$#WS}U8{s;)VZDB<2He$f1lwW|-NpkV?Z8@M=&kydvzThJwT4^7+1~D;90i*LA`P126MH)lOKu?$7S!iQtQNag2Jt)$ z{TfNBTZ4qfiAT}bbyvWtN<48gF)}Q$l08XEPv#J~syt6dt*mRAI`YQ27qd%JEGSAg z@*q3ZOg$-4jr246@ zY@6R$b)WYSX!6*tdrQJ5Af2b5*J%=qSE{t$U-g{#L^$|f>{l+kgoVK^NUM;CFyE=& zsmwrMiQQMk1zGnl%5M;2A05u8T?d61NnaE9|Fr<~*SY!|;sf6*&|itomGiH$&&tw) z1RR>b&y%A_jUZypen=jm{Ij;-{}k(P!wGLAc?;geRK=#|>mKjBYX?o`vz$Jb4G)5; ziJq6h6Y+t>-=5qf~d^=gKP31dT_qjDjX8Fy0+uf_vFHoQH~4eRGt`cSaml-;L=Ms zv}hs4P%$t=s4dTU-6Usz^NVkFhTQIra`=^Mw1TjNPxF9GvcX~OSah@-MmTa2K%!D6 zp1C~@LQ$r31bnPQDN~`&t($`1wr#Yg5aw%g4MwE%%cdbCT!3>)epE^EXBEQOAmA0^0 z+O0WpUrjevH+UzXpQ`!s(K!KO{iBZnL5n3_z$c4)(4dbn{B# zuM%tjFwDx*0_8FXe{7fw7*aEJQ=dF$583Bw3@~@oPqdKg*|+W+N(klD1%hr{T758s z(wNLT*68)2>0#}I=ZW5ZAUu1(t2l#H1KaHIdJWb3+@tXr*m|Ga`IQ+ty^lt8&0F<1 zUHO0^Spg7*d&qByciRMqf#+Bt>y~W?);RkJInk10A6_6^%-%Unxyw}dl}ha_*H<2b zxS~XPwm^L4X!^+LW>v=}W#UHC^kFtm`iNecS7eV80V%?NC+n8*%dEeUm+l{i&v^>! z`-_1P7x2Lw^h0Sk3d_wGvuhW;L$TKXJbDG4V7w84x)@2yf`AQ)o5L z33Twe{Pwp%IrTl3ujh7t^E2bx;D*sB)g9`W(9oF(jiiNsn0755)8atv-f@Ye>Fli- zuCGVTkV9S1Z)MrEp=7-gVJ9qs;ZLuOgH%T?H7tr}6S#CrAf!7GC}osd#1 z1rYmqd-yziCIZj-oBhxXB86Pds_85g570CF@|;n-|_4QSPDln4|9_aB~bv#Zt8;Ql&h zFrjZQ)oQF^n@Lfvq5NmhOlqr1&c5e1wiRPD#%4#%PaR<>b^UN%dXOBwxxCruM_!2Y zr=;vjWVzQmLm)9oFrVGC8cyqRk%nxP|Mugx@Q^`J2I z1Memec;x%{_yBM)^_0zLk+}xx-ZNV5J%1HdO|Dtko4IE|w5*~2$>>qKUaN_z$Y*vM zQtg#PvZ+=p=UEE>P4l)+RX^f%YH^Q!@wQHG?uruW`maJgPSmgmX0#icO8Og{3*Oxp zsaT5C!;KEL)!XFgtPp`C4N~rvx{E6T;-N&~{MAMfvGc--LKdmNLBToHA3D9^Dh-Lc z4|g*-`FViXkm`$=d@h{&>~vECRzRu(?sqs#51L3JLHL|2q?*pPr)cEi>bnxPfX2B$ zQ9zc!Ce~>D>mJl!EtUTf&>uWJ|G5{AAKa!Wze2M8r93$B94vKB+D#gmo+$}vkw%a3 z4CGrAG3t8$xl#Nd@a8LUIpK?Ps$XXmJKJsFm$})X#soGv=3{G`)k5(U;as4hG=K4y z)0D=C78JVft)FH^`!!hIg*U(O9L{x{mrqCyQ3V|??}JC^>Q%YgW$;6zb#t<(7Dhbp z83|5nzFUl?3BAr|RZ}Lu;QFP?vh+sSFi0geL|&1WvHR4<@(e!-JPYf2@0-2-K?^BnQU(U$2SzQAx-0h>iOd}&Y{ztjq{v_Rm@Jbu0(!T&ev9diW}CuEMSotB88xqPnc3Lc0r31(W0MSmorX8F7bI1*E_zE>H*GmK8TGmg z!WhWzk1)0Z^FVi>sH$aB%UEwmx*rO(C^ooM=rh3fOtLgK+!SG#vYZAlAHgds#mRKD z$8Z_30$$z3V+!SXN$uq{rE6iTI@1g^_VpL%q1BG_ z8Yfd{it?SeNpND9S67Qae-?C-Oni2BtidrTg#q~qcT&yE7azi~0^+~_T+aX1%i6Y) z+4DZq1u!#KF3_o<#m{JJEjJXbgiru@eLVIQIC^6g)>MdbV5tGXXT#Z>Y+xdZu*Kc< z2&X}tg0OgkPm$DE)k}d}Dk+rcZf`kkcKN1QaMaOPGp%f_%S7?TJ(&u@lEr9N{QH=YcM(%gG*sI_@}?<@Ay4P|Ud_w?Qdo88{GOA!RXCAR~2z5xhkO9DoBIJfi+ ztxx`XKms?Nvvjt3l6M4yOE|VY^<8-HN`eq`L|yUS7?!Q~>6?#>0B}7C;sJ&gc(}qZ zk6q57?q`7z$UE7A?nAQswCDO)4*p*faLL;b?pk5pQnxNKZJIJgPZY*?E)KbP03Z;K zMoY1i@ciKL9r5cJR=^Jd?e`vqm-O01m!LVUZo;3`<7gFF7l zt4LJIskJ+fZ~x(>1Z4h3+vU)6oWQ-tE+a9DhqS8NZj4prisR?Klb7|tD`G34LYsTK znhOlB;w&Al!I9vMG)~+bCF{Zr7~M5mojk|TsuIr8jBj_M!C5sXY+k$0<7rU$yyWN= z;azw)=z)67v^c_)AHg;EUvTH3)Z@=A-Zx`?F@Hza5MdID8`AEK{Xv$-<}C7Qxdq3N z>?j6gZ)^V4d#3YXfB&K9wlPD6@8yFaO8FzhXOAL4FlMl5dDBT%4TQ5@4)Snc6ci_Y zD3z^DGrD@`>teU7{kQ#t!z~^AG1yR{+puk4-xbV9b(KCB1uu1Vbu}@p07VF5j7`Td z2+tL&g8#88LO&_#fAE!rEJsFV_M6HRFtBE>1gJRro!S`+96;$+f3lLimCmJu29cXz ztYwCbnpukwcd83Qx@3ley6GQ-%~;r`Cfp7JweB>W1J4U4)}umi2T__kiM((qtLW{~ z;r0QU`e(RqS?dY_t>m{&Si(fX0lHm!kch`f@SGYCB3+c91q3qIdnimf&jVR7;XYL> z;wL$o@W3(#mAqEdBp)=F%u}ZCnjfY#0j=10n`9(%rO{(I5G}A1_x~pJy!i&*$OmMK zxXI3>$9tN2^<1}+p<5Ce-?u!NZmO>wc%#cD?+6BsAD(W^f(t+Xa7sY4_bc4qUbm0Z z1qArzIhegGJ#PYQD<3>rFknpB1Y&PZE2~`1t@ZEHwJ0g7szaz-aX+ z$o|IJZLA(ipLp_TM(@dR$%I5L>%&2@FDSvY^X&Icr51Rhqjt$(8Zn6v?M@Z^(Tq_m1gWY zBW`^vUiU0M;O!XK=AdnU>^gwwLZ`WUmq$Cf#HFE|r5Y+G-dEj&q!vC$Cc`BF7b+uM za&QNL0a=|>vwcI{p8qWy!EEWqFU?=h4N_|zeSK!AK(%Z-xDZ>r93#r=ll#lPTi2o5 zA#sW76<|hsa$aDp#WHX*%@@zl2b_$}tx+?e{kaLKV_6E0NX{oTL?M8TH+}>cHKusT z!5--vtC&g@SAPeSkd%LApN!QRac6lT>D(GOab&T48~OCJ$jcqY6MK}z$FGBxs#?M{ zgzJITB=@VXK!Ss0zLjJXJQle_B$dBQR4zNj5<>*(7IHIBm&#P15eJS-I<~g9Gvb|8 zMgYdeZ~EZamzmZQm`_x@BCFfK?+H>H4-MVy{^!{^?1?1#(X@ zcf`s~u$4G@v)YvI2L%|F16ASLfT`QK!2Stg#~)eGe~wGi0}Bh}ynhR}5;z*OgrrgY zHvK6A@gTT<$dA1GBl3<=R#6eF$dWx^*%2sn;ONt^6r(gs@po#HGHV+G^0XV&s@CdY z8Del6*Qix>l%23za9o^?(DKTt=+8t552=i`RM(8va!)x-1=qmCOV%(j>S$@r*)MlW zejInbk78TX-KI-i0u-a^6Xm{}pT&i9!tPBd+!qD|Q-hU&V`JR3ylCEZ6JA6V+B@m^ zWN59W{}94^Ccs9P2-^mFf--ErvE5Hd)nNN}dr}C(G7QfwT0DoI8Jl z4njx^xPwh%k3!BDJOw4A>*|HQcXPN%r9f59{Hw+(Hb-pUxFH221seQ_nFdV-HQ2iR z5tuYAl>;kd3C+qEkGr0SP9faTD*4euqCQVdb~Ag((@z zkQnCN->4+y2im#+i2j?q{Jva?V3qVlDJWLjU*X-Ma>}&q=2O6=RGQ^lNP-urWe+@1`*ZUpd)+J_5D2YN#|;r z-r(xZ;ZUxb+zxa7BbXOc*U?=Fp-=gO=8A_K#A|79&96N2Zsif#a~#Oqo!wR}ywjBD zJuG;qqoY1TNJJ}0xn0pe003U?h@`c@KA8Fu5rEbf&t6RSF7>~^EZ@%!wO(trF&#?V z@Xmd+>0mR{DIy|r6A)oFV-Rc6j{t&cDeFE9T)J>?PyGoI60ug zt>*DJ{3DoW%jqlzfn@_QD?m&C9ut_$!dTv3g|x@d3(KFrF)1QQBV=WB7uMc*j{9cd zTy8gkTAbhBqXU#@OS`^o%h11C)OVDBL1rFw_aJn>G98H7s~?WYB*9zHk_{OL3RiR- zTASRnlwTBg>d)26Q`WfGdHbc*PJ(#_fQ1-0k$_4oLGpHdUYjHkA$mGJcbg-P7m$=$ zXLn3o7uHd&&gr_`*UF+RO!lyP0F6nl`BA~isSE;XA{CJ!y;0X^^WaY@g;Ml4w)zwZ z`_MH84m&d&DKLebm@XIi&Tj_t3w9gOJZ`3A0K+yJe9Ld-U0LcvkQ^M+drJgC+9O^( zB_#k=6v*GjKzBZ`EWH?Cl$00?!Rqnu_JWsBbAUT*KdqBXk1Qh#Zpm=Rdm&0WjAzfF zV4)3BP#Rj$1g2}iLkLEXl$W4Lhf!&Ml8hWGGVNnYnBU(55P`#`f z1vbM5M^3!fGYQm$NE+@NtvdUEQgUzo@W?7xHAK;bDAO5expaZ?gbonKfrR=706Iln zmmjzrPXOSsO*r`|1VH-aRH7fBioM31Si?I2?QqJ<$_mJW0TrNTWO&|KD65cG7bxUv z4QK~|d3iV0>q_1Obi~@is^?lIt^S=_vqx!0w4$D6K!}{BE?BbAW7~Jlh9QB9AHxb5 zYrb1rL0@Fwn^d=VTmh3M;kt*I6Wh#DB|SNl`ZB^94K&?0*u zffbyE6eb0tD1LBo6F+5w3ZkYOXKVMa#3W|Z%50-XL-QE}_3;7{&G)UPVqKp2p4b-! zEigSzC62&ppBlQM9XE!{gcT5dm*->THM&UZMfng_+4q)Ma+~5`&#pu6=Qr{BeM-P% zb`?%l%mK4;3Z`iyM)|7AQJ>})NL7lQok*h;z9dPXZ=vGHMq`fkjd^S;tX0{^TyztX z1u4t3v!xv!-$h1Zs%4^^kk?)H1R;^%hh(_uBmdNu1&jh2HTa_yEIntQh2Rsg1ddWM z-a&5_`wJfqH@lU4xpI0f_+kn0!wx`lHClVKBKGTimCh>Ru&0#|WJfbyV;f&owDU0zv= zzij+)Au)?bpbIPNT+{%i@QiqBYgFTv*ds+-jkLUv;i?nPz1wy{UTV@Yz!V4ybJE}@ zidjrSbF=Vk69*3-jBKBk9$%+pbFor_Ub7~&H|G5O==Ha{AP}NC0T7$?y z_flFrpY+c7#-@CxOVOm$R{2LTGZEy;gW(ji(;!btQ_^m#lF6xK(CMcmuip8_0xnK; z@uGqNA52Lw*Ce>#f630%0Zg*r5@lmDzXy7rk*S}dr!O8odSzU8d{?fIW9uj3j0iBY z)ZwqE1Gw)Q0u?Qc(Xm3Br4R^Zu_Lkc9pzaT_?fTMl9N60cpP9~eNh!%=R=E^3WB#5{UH!kel>&NXHYnU6A!S% zYE;l%qHw1EB;Mnah3r8n$4EiEZXYTvSBQOxx+4{vCDfp5!gmx&awBbF+YNcOc3$t-{W4 z6z7|Z0A)-<08a@0g}jbPNgX8+Cq1{@KmHkbUb7?7Un84-$NuTiVq60WB3U?7W=F`k zJ~+E#Cs(9OPZ1WE-Nd~*$4APUmb`lRG@_Q=tmcq&ug?>V0L zH!w{9@7GLkGPqjVKFR(0V3tyGn_jl*BBB*cc&2mmrf36*5)`-% z{QJN=H{36Ml`4S|kQwDLsDGG{{!Ak4hCU9YFgiUw{gavx9o>CsR+U1w*xs9YsfABK zta&#BYPSKTG%K@sr-01MxjMr5RIt0dUsJ7_Cr-GJ$^bh;zy$A9v0G#1j2z6B4Un~8o#W*xCB!T8R zRkJ%VBXP_A_274JHD55%oY#a6i7LAn)NM)d5$lV>!>#?V77c4@^WM${{~P4*-!B|j z&OoDwP!Q%u@a$&+wCZN%hiFI6(&t2gu7%ZKU1aJFqMTw06uPq~anZ2&`e}wfatGRz#(ugoibBt z<=ooCsV)^O*yxWs8y()h3D`2koJTR*QqZR)*K^qNSAP8aIPf+<4?P!$fSH6tN^uWxHI9W* z9-h9@lQGxou^=G)dk|pR_b-({qa}6=MvpcJyxQGlJ9S%=v263(F?pypX`p%yCs+35 zxlO}Qgpl`P!PNA0TvzORzKaPir0Kc{k_7K=C=rV#=bsvwfYxQSA80_>C%1LYhEmfe zLo|T}(6P8}pn(DLe{)$K@G+wtwKI=HViMDBa)Mhiz9@{Fl(Q=mVNw?E_p)t9RpehR z7*>zvTppW$ev0p5z`H+j%S0$GKt5looigpv5DsqW0y%RHPXm zKwK(yX%G^&A#E{swcUS$H24z=vOKo>Is->A8y0@);Ham#j*9-?XhVH3)(xnbnOcv?&_4PTJJ$VBO450Os z9cYp}r|TzGuS4zCETm0m>v`0_}CrJ!Il^bm!wxORVyG zf?6!dVH)t>ndl5;xGMyTW0Gb8caLE0RmYn=@$C;3>n06)+-(ZMbX7M%%j_Q)HeH5Q z8u~>{i*FmNhRlw0$9HDBOb{^<7?#ox76K6tA8JYm@YB0Ks;TN(T0NbFp#c?t!FH0t z>KB|6%UoWI7A+uV4={aaoG)Vrm6@jy>$h(F(^af#zZ2c zJ+y?q)3CCsJl!#YS{C#0%Lku<0g|wzccN;#h7%YDo52*r~Saf<{&R@ymA_0 z&n#48U;lpi>%m=U6T+}fd95o=|OmA&j#iT3V~lb!6gbf!|)w0zKZUe zAInWr9cg3be`KYaub9$!`Tz0t)&Ws(UElYB7$9QMNGJ$MN_UA0C@I~bG($+YAQI9b zIW*EWbhk>wAT>jGr?m95N3U}~_j4WJ_xFeAocZr;*Is+YcYW5r1dx($EU^~-_}=IF z`h^Kg39_489Sp3j0XeSk6qtZ-|M)PeSk)WLBLla^5c2LbgVp9IZ2eEfuZW7?@c?G{ zA%u+$_!<1e&WEYYW=844*r&=U7eX9=Fb_MBlkq?ezm34WtNN`h%l$&qMFTFoId$2` z48`z@dwPPfZ9O0g%|8uWT@)T3vtqeTVXgdNgN>49+} zr#$_3@0rWIPNwLJ=~(Jbz(0g>Im83ZIp}}L8HUmdI5WEubM(5h1sjpi`0p@Vt%hcQ@a-8 zXkTK{{D~zTQGW->aVsk;eLPCBA4`voeD?P7z(eum^7f4Dz19t-3uYbiHk)@C+vkvF zqAKi977AhLiR@lgu;!=0lcm(NsOl!_Q`E6b0L|c7Q%!Zs<;T$i6pRiBjVpF^=(lxp zViC!=x2LDvuL1C!wztZ^9l^^#_TNdV5Y#%glR8LaJ^tq7n(?_k@kx)%e2AUIUXp0B z7Md~rt2w&M^N7~=&IR6sE3MbUUiizk5;bJq0B|AyVSH_nfY9H$QBQ-#p(@8iWh5(6 z*_~_C8gQj6Z#_Pm8DFe&BavUgZOgD1CWOFi0J#4di?1t~bu2K!m#5}y6-&OT{U2pT zjQYI~Q;!e{yecWq+u|(oDsTK-ZV_Lt8D(T88S^R)EP27$>*w&)s*5+qW{*_RhDS06v=KzCuH&8_K6hZJ4w(3J6X8J z`#`$JeeIhgn?^WWSQJN#At8`Xkx!;P5YU}6t@#DSKU|2Qr_X-EGPq)ItZr%8|dHw@oLC<%1>;lCA>()H3 z0{X}^B3HgH^dG(UA%kR`p+O&KA@K)0^0_Cg4rA}9ldz&Qy< z?YW;Cef;ht^8dDZ+A@Hf9P$Z*@tk}K;y&EX-!QV?0Gex@llre(Al}^3m!dC-e(-^U z0TjsSs>#Wg$!OI?1Iw?4spfA9R^_b{ALY!PtBpN1f|oY>9%3}VBV;h8U@%XTv4P^~ z)2KQO&U4{Ket zm`YFN6WBD>8r>R+dJ}U>*(q<)17PyWP-c*_1&im|kDKO4PB`1;`fFI{-Q3IT`>RG| zh_er=n&bYgsXenKnMz%4-!}~~39GE9;m;*vLtneJJ_lvC*bSi|QoDpc!#i?+3%mab zg~0hae*|>3Kz-QT+z1Cqv!HAo-|H7@p6dRd@2f&VOpK!_Brtr2bo(qil36>`nOdrt z{VbE!KfE5K72~}+x@je9+POBddParqw@uN+alr*#`v3F|T3n=9!W@FbszP_}EjJUH zfWH&jCiUEyb}H3bNs-(yP&gqZn5)0F(2dcSf^<*m#{H7F%ibfRpjUk6?OlywBr7Y6 zgN51E*SB2~)=hQ=l-E%Gt|>ptn4yX4Q4dlJdZ`pZ-*t(?0)W*{I^5(2>*SNng;BY) zjDp>{x3xq~bmb}=PuC9sP2aal2GlRM&zicGUu$N}BD4iy;l*i+JY(tozBM+d<>loo z%^$A3GVF;{j7E1N8U`kXq@6zO_S%sVJxVd3zXg|Ch6eM+2C5j*>c# zqXy>rxT{Z=gbR81hS|BASDOKYlR_j6nEaSN^k^JQh z6Pas>VoWU3%*5%jsSEwsHE;TlU4LW~;=4fSXw3_-j{t{~*{1V8x5)hM?D>JP2z6)$_D`_GX{D>Jz( zVD!tdNk0YRHATvq#kDmLpd-7d=dTRGqy4Z^QQqpSh>?MiBcBF9fL+sMBT%ht8q`g; zc<5VT)NGE2Z)MbbTcB{A9p5T?8xfNqyQi$7dKsqeoJWy-;3;o~_;qB6&+2q=^``yU zeyOf--!!SRlC(b>sS3t6OsD&=4Rcp%j#oUJ6+w&|*Ch%-51*eKwnp2W!cZ1Dy1aI@ zyy;)KfC}pR?Xi}CfSyEdz>IGUB3310;gR~QWb!}rb?Co>hO!wIixyi*2Nzu&dhf=j zQK?L^-0JLUFPhG`ViXQ-@H?J`S?l@YYc;##a1_0 z3u|txfMeLzXNMxbL%VSbPuHDU9P!?>X%x_{yITVi`-90B#1&icW`})s(Ne@Os{Sum zxB->XYauF_lHzD5G!zfL

N2hm2c;Apc=w|BGYnQ(SvC81^GKBkK@QxlBV9sE$wM zUDHd@9K$r=pdZN6_kFB zNlQ#>DI}qXhdDy<-Kt zXpdYj+ZX|30#!v?!RfDwe{JjJN`mRl-``I^si$98s23sOW+(S#kQ6{vD>NzxR4N-k zn60a&Kd>ziEAMG->s-uqzT@k=|HkGY&&xCjy6pcMlI27@@Dky-6Lzn@drUyL(MV5s z#x#7E^l-B<7j<~vzCRcGF|Rq!JQ8~8;R)EVWmTtV`n|1x;6T?b}4!lpCZ;Tc-3?@=me8h?_{J%L5X@Gn)Dl3HB!IATEJYZx4<)N_=%{& znvEU{(wU&Z&hREhr_K>5kjEmk6D?vj1u@0Jl#n960@#~~M{fB8AU|E-oMJS*ua=W7 zq;QN=@`O1JnN)2KBA&G$OugVdfE_(~ZSz#_>oSb3%x+l@8JVD~MptV(iyIws_-NP( znBd9Ivpy);kl^GKS(At-__MJ3fg0ckDo4fA#CGR~bBm78K$QS_H+0BlIOe-eY;0t7 zahNEHA{!SMhEZfY;dm2N=-}XBtjap&9*oN+5Nv^b04Ch+bH`JEC5v~H{8gs*e-Blx zZ9VswN=7k1B-jwJRzsS!8MdiZ8HRzw%zt^o>Q50rOtPl<3DbvRxvH|{KXC!asl&S+ z%Glfjou>y6?aq9$>F#4ut~=*<*mlfIg+UVE1<3;h-R~6@0Fl;yB<%YcLpT5n2_|Pu z#`a0EPd6rNIA>3HM&{i4lX}#?NAfmf7^S^ae^T!%7G{|v6!xMVTL?3OaIV z{NLr&PF(T#u`x|r4m*T4VQerFK&Mlq+)7aH5emo7Z;X2WwY~*yu;BuKUa1Z#kra9K zE)tOKqlqUN5DqQ*R``avIshs#IhRXFHrY2+6d?71^-cool`Tb-$IQbFYz{g@E zk$Dge;)N@7fcftdGw3SpgF91&S5gaNRc{@fd-V~Dh}U#C-bKCAUIp(|uMe1^mm(Ct z$o|RgdJmg1XoJQl288r>%PBF%@_<%DMn=YE+!IH68Vo9rR+Z9Is%h7cx0nxNUGxOC z#|uP-kMfcyYo|4>6+IU;L_L(SIF}Zm9?z%($v1f|279R|I1u$=MJx)AK%qlC+=WNtb0`sqM z_AfxPwbX;-aS|$;Q(1EwE?}pjERv-VFvMGB_P9C`FNr^&`b`s%`GJi}LT{Sv`kEA2 ziK$Aido>0I=hCzo_Rm}VW&b5Ob`tlqbu;w&lNtf3t?SH(>&hA$3}E>W=(VAzPn@+r z6gUadK}cYJjET(g>mheiKuin7vtSal?%@H5b(2M%Z4-#=dZ07P0g+$AX(IaYFmOj_ z?WGDW))RSx}oC`hZBq8CAUvBD1`pdNX$vL2|!&o?;3=Q6c{AkD> z%o=|x(xPA7E<%^sA1@ZEx#*xcM z)%VsN1e{uw8=U%OWS`u(yxz@Ucr3hkAdZYHtw>jL%UjAq#LXVw5yG^meWIhT!9h)to!pVBA76s!T()dlA)j1e;k^GgUr;~dfK>$wdF@8?f z@YCL7>!Y-C9I7tjyn4<(gsF3zJ(XFo+lVS9fa{kZU1~u&oBX`A5P09tcDr$W#cfXG zESnY*ov;D@yadCrzh$~4rTx=Z_g|4L|1bi*?rwHO&}dd!C4o6cvnR5T!Sh%0HYmDj zlO3bEgOmJg=UW%@^o35xJfZF#iBv`P4{T2%AJZ5{j4Dzmkf$>w04`#yxh_strmIMC z+fSKl?Q_cf<3%Akf5rm4*!GBv>xdnFb$|wppy4Sfs5|}eX)7=jU;jEJz^HDLF+VQj z+x4TPk?h>umw>n#&ugt4=FxME^wHTA&}Oo?9(wHWnm-s%Wx8LnJ|(F)U^jl4pPQ+3 z4rAamkbqq*J|i~W8nUt7|4GT;My4)Dsg@Ch;CoR>@qSYX>=)5>?mf9U9a`Fsa;fv! zZ%xt!QXir|7|GFTZ=8DGF-U}?8ziH#Wqu%ocL#jVlc#OO`kHF>`aK6Y+R^ayeAKLA zTR@vCkQ5yJa!#gr5cg|6ex@#s4L)K};a%+k3K2Y36FJY@t-Dz;PN8TPvAown%0r~VE3GtwC{R;TX{f=2h;e}Z6Rmz|cQN>@ zJNg^?GGKSw3QFb%CDuFob*s@|n|YHwxr+ zOV*WONvImNQn$oS;cmO5Et)2Z52?vH6PcH8wvm7W2z}swdeW^dLRVu7wIfvpQ!sP< zk@jl2B4>g!Hnp6=uV_EP@0 zsZ^?E}IiIgLwNKw}60(K*vj< z)QT6F_~6d2MCO%$qPrwqru;2jraMbLLL|Ri3S%E^3}n<{-DZV1zYTTH(|~rtJk2mL zZ;A=tHhx?0PF#Gs%5Exfre(=En(b3mXyd> zEr89VXCx{3#&T+u+7EsS`5#=kFKq-2r0_n*mmycnIZNCC<4HG0vQX2_%owtgcAtO{u{W}OAwoLs-Ye^UR` zJ3c;s?$AacNfR)dflfr;UJ3>>)^>wy2Jg^L`@g?yxBpBK__04VL@)DM8_vh3xdM40 zVb7+qzylCFE$OS=L~IW=tbW&P&2b#iSa2ma<_yp+moHf5VktmRm(cOyJ$quR11AUW z!tw+98kS! z`(s~^2OIMe(O(H!1gD$3`ob+@W zt0tg9*dt10Mb|;szpk)5>~C&)&*SSzV*dAeg8%lqc=%gS*m37R57?aLG?@v8F)<)s zRY9zdggSS+37E0GB07BJY6iJ{(nk2Z%;k>D$n$OkvGmP5d7O{Y&m_zvdt&T@GW(k* z@iiEBWZq3b`>$h{9|<;O81Q9yTEGj5dR!x6`c5|80Z;hOjj|^gBs*W)PfC1CKA58C z48p;j0gS^Te!|LW;%AC%0uMvP=QHh)Ck&vdbG?zr^Q(w_Q>mKM*&zck%vZjmP1Q=V ziTuwrShG*Qu=TO{f7@BwWGPZxkH0Bjb*0!n43;+%W(64fhUU;i>7_{mQFro*~ip~Hmh zIR19}mCf>1quqt?x8=c5>Iqoy2!9?JC~dzqR|eX?O(SAJ^4`HWUSAaF&^Gy(1BU#M z(nU>2rw~9D1M~rLW$qI}TOf;<5iv6{Bv|ND)Xfuf8tF3SGVM91KaUw0^;nU*;x$u+ z`e!3p>7qvng>^(EpY(jr%kh_VCE1+vgRmh^mubf_fz|TWJ)X+J+ypM0L^_*Dsn9Z) zh|Nd4j894{24XBaKT8GNqgC{&hVVhA?Vdfw{PqZs_TD3mv&M_W+hTLnFCa%6TM7FI zybqObHI_lCgn_E}j_NEJ;!ICB)dn!_W zf^?Z{y@wmW5jDfKVY5ljF}xIl1P z?$T>f6hh7lz}=&w7?k`9a&fM5TiQz1P4hF>qXLw+S}b*DFA{wtNsBi4Gw5}v(vS3h zWR&fk!LJCotR2lSY<_D8bls|@IxM@wINrj?m|bg7#xfB~o26a<&?IH*Ztm{f#!nX) zX?oCa){ZOlYK22!mrTB~qQ>snk+W0LA-O=-<*#9$hC}2V+I!#X4s%l!S2gYC42!-R z08NMk@|`Mcj+(}g*a~<S zXv*gj5D+{W7+iS2V7%=+H0nYPo!pwNL$AiZeOx`gJY0~g@^wb;pw?eB`kL*jNejD4 z$Q<;3ii*pRqKaJOmTeq`b-vMj-rEk<*P=k4lYpCEI`4bGeA7v{ zdo55ONw#-ZCwI^pa;$=ys+_Mqola40w&tJz`XurjrCnoVm8fIeLr1F?GYM$>%NL0U zekF!&vo(9`I_s4dqo29O`q}LK^NZ@9LcGdMLNlKgwiZkLP4LDqp=>6`GsvL$z;omxYsyR+gf;izo+pVxN z@U4At`E)gm{YWyaM`A4)>}7AvTkDMHX=tg~>gAobFNCQ`ij69K^>~enAMlc=l($;0 zq@Z`(ce;K4Dxqpy=`G1#w~w9Nq{mFo#jaKu^fCf*Bvff|c^9?a&*t!%@zoVZ!>3JZ zG2O0NfUZr=ek(RfDkobs#=S4p@gRqD!l?~tok*v$2I-`;bal0H=r!?Vg(V(noy|?C=Y#k+*%-;Wo{<9V=`?-Vevd7PR z1iadJO+Hn&&k0ach4(s0A=D8Lx*;lG%uOu(gnXyZhOi#4mJ(_`fBJ$+`1s_;Y7)^DsF%jM>iJcm%qVjlYO(nu^+I#)gK;4{iqX;CqDYob1xp2VYYW}#Hmlplx{ePzcamkn;$2pbj0EcGe}5!ee=_!~K0tv6LFl?ZCY?2e1o~n#xTQ}c zzv#d5iz3BXj+q%VYQUO@YD;KY1dd>l3Y2*( zSQaj7+v&5UqD49ED!st+**JTt+T^h121W=ZP|Vc;8n)bkkqg_q7(!)u2G8-R^O3GU z16p6{*ur;(U|u;}E(zc;BZ1pYAXm&0%6TW>^wVm#@e?#;dO4bx6cRh`#WAm}r#wAg zt^!_6$yjEv7}n(=UMN>4hVyc3V{0&>kis>0F;x?fTxp2%WZ7(eMb*jrd9=}ohtMp( zX%tq}rbDAi8b>U}72Bpml65F5UIE7F{T@~8;V;M?)0LQFws^J*cW^u0=!fgnX|EHp zXk@LKW8TBif5fr;0|sSdC;&T||NXI5{;AR7V1W~a1e42VV-Tc&vfCIRrBZ6>OE=S% z_AO}JJG~WqE$(KE2rl^~oQij9ub{E(@zWK{yR+?82dUjH1Uu^jLo!`< z(vA<_J(+8%9T~jk2n#FObSSQ*@jGGJ0gRa*X7%#tglVsmaGY*y>@6i&=QTBnLd9Uy zCT6O=k2W8rZV1afh)eM5W11#`*TwA_jRzD{o(%I?x*KPANuPJ_nS=q*=G$*KUj_+4 zY5c>S@=Gn&!nt+BB<K*`Tn{=I3sB)Z${_8iqbsW?!z7-9}FhP|E!K>kz(>HwQnb zLeTBH<_<%i#xD-PAc4BBW$P94kA2jn*FMS+fGe1%mgCo7JI=Xcdd@xnvBr9wE-#uEYg^c`4~k(DdgfNb>N!Htzk0UdTVQoP z1hUWou*3f7TPe}Z)%XRQ2=bWJysYp^46Ym_5qFHFEQWNqbVJ^b>%!4HN|KuqA3hj9 zPA0WJ{jy`Iug1>SS4R6bKXaN+j8Re?AtEd~Gt6j`0zYyIq)zN{^i!V5J@2wG8RGLCcVA5%?nsR%`ljzpe$I7*Qtpg)VdE=(wh`w|Ef2S=U34U61DCf z8sts%;;^1-aAX&R3l~zr9qh7XV-^`UZ2E@>Qx0tQru@1v$)wBH>AL54YnwcN`LvBi z=Pz}y@7lrUd%>+*Fz)pw&bQKR04mJL#wCuST4_e*DJi|8zondznrGM15`zbQU_0T{ z`@B{J5D1?eETl+Lw#3x$YN~ogtIWHoBwS`DB8=sc>6NN->J=4(=2w2?Z!F?$j4|sR z4~e(p)&ucnVx(irE|D(6=C+4=_7KAka=wf2zg36^sleENO>{agryS(4ePExaLVaHBu{i;{h^{d+yw(qIP zDizA$_6X8 z&Tc+=MfnY!1Lx4R+RrLt^xyq|f5A@xd=HpJArh&IUS|h#9Gb|fj6{h=$UDvN{FGZk zZIN$`4n)Gt>s#?hR1En0P#i@c=#S6p@UH~kJBzey2$&A5VVJsS(xZX1u#-FDBZ+>| z!fxLokEWrM`r#8HYr|uGSEuU5HJM^PZONpk#)CtIkoWY2kGR#Vhu%?MF$Hj68X$$? zGTCxR)fQ@1F-Ax|TKDHDdLMZ9qMV3Z+s4G>AyE+14AqIerw3x)*kyOS!fjsPA1a%} zmAV$FNU^^5I!)l+Be%F1?3DZj^3N)BHb|^{WH-UJ(SeT12V0713>Ijr#9Y;0Pj*>? zE$p=Q##h9$(L=mmvn6tqBH#S?p`ibp#O=@X<+73Nw)GNJb|7B;2XJEzOs_fBo~K3_ z%i2Fysy-hH&S5)@fKb0ZYsZ?M(*VxG6HabtTt9re%aj9f1rpB6R_ciNq@}O8dP(m; zl(*5z^~Jwp9@3gLe`g*CaxHRd#kIIOQu)`Aes3uTjR!6U1R*#0*$B>kdbF(uo?LS% z)V~0xsRCbBVjQ+9Rr}T8!}%n5gcZXmhSr)A$!;DX8skDX3t}Dp+8yg+4KFdGZFBcz_o3+?z!$iypW z4ntW1s%62i4)Ix|{p7FRwwgctd&9GVzN0PV{#D>X)EMKKhlAr|hJd{oD`<4=E~uCe zNe%P$Rkd*xeWX7ov!}mCAp0(+qXJJbVj{1e7O`%fI~HI4G4yB71kfhYMHL`PZ%$D| zm0blMu(yn0LWoy;3zUuq6SPctYUO$_KiCIjgZi>Yh#etlhp%o^m1cqDqCJ$Nn+#NA z6GUE&Pm55WCGRENe6i__Br7WOTkQPiV)vE*deh)k?h8(TN@BvNP3Cmbx#1O{3t%^8 zkb)vB&9WIIP>DJj<^(J+8$gfKo)ghK`O7+#y!mCiCpV z3j(R(9?_J=K|jI&{uW{+(Le4E2p>(*KpikR(hs*pP5mLghn=rlk510*%WH zFFPyKeJ#zq`+4m8v>(U9-Ef;+t0CK?<#QWmm;~LFG4{u0rzsX?< zor$NG%9^Yn4Use-jB_E zP6jvA5mVrgHNFktg6X<(szG%#;F>wj^GlJ>sqdZx9{%S9{bFS`J*P;JI<;%}v0eU6 z6Qgg@&y8lcbUZ68EF7!0Ne4;E)*1Xw4cOMi{?v%VQGER5ZD%S6{*bbTxy(Do=7C+s zBJ+C6V_~k>J=~mIV{fzDb)>(`g|?IcilxCMmZasnD8(z})*0?0L=88+lab-|$G(9Y zM5EDRiNf7Yt=u1Vn}`t(gQSwHkmg?`Gvv4slT2y?#lg8u4d}f9A6>Ui?iv_-#jCUd zub*xRY5H*u|Bklx0yP)m>!A*{NG>_14X@I7*2?s5CuPXE2eB>(N?b(X`9_hI z$xCjOU_a*UJ0II4u0?`nn(9v)e|>@KfZ~Sc`z=QB@=7vVFpnp?R%7h_+d9CuuzhE6 zTObd%akC@t0q??3_6FdY_{x7*06e8ttuvHiy8 z>P4pac2D&+NFoKNf1ib|@JM^l0ee(S+jWC$5QGeydLHd^LHM?$r~)&Oy?#H?;p#N0 zJ(JDMWAw&3df_woZpKuNW1}mQR^Rflypz!|DcjW{gR`J*bu`5Uc+&F?-$}%J^!R#I zY3g)Eo8x;wDK2=4f(FqKmpzkeq$SuI$-=?{caV-rO0U_QywV2{-jSLKFZIGmnZtW5 z&;MpHkl;W2eZ7S2w;PUdK$P3KpEbypdd-W|#5?iu#XL^bBZ5*;*j=1Zd!1V%gfP~< z88-G7+u^r&5$UYZUFPP|s`C&aL14uk3nMH8=v}74ze&saciy7g9k+X5w72+r`Tciy z(dEj%<2bR>iPFTQj5YxZRul21z<^EMLPtS(I!}ueB%BEbfR4R;uuB>p^ZJZ7JcBC` zgf?)>j4Uil%#OAl@l{i<(n(24d2l#osqxrCc$v!DOi5akJUa)5uEyfd(8+VBqA!Rs zxgVnTVxv)A9I!!$wB#?ndt--(cXg@_9g(%ix0JYYg>n$@QfzaXXDAhn;jt&_>><9Z z2=+HEJ!CcKLUdRq=%72P23a>3zYZt{08}Du^~PY;^RIKwkTx`w5Ztol(FV6FO)V|o z2&k16^5kIsD$wO3i^)MEM7P24_IA!U5enrP+|6BAWf_?}s_E0OrnW!PC!*ep%{q&d z+|Rz!ox$4b8_t|Hj(r&)v@&IFgpcw^%3DA3+MZ{ytw(PuXFi(h2nv6t*u`cXFz#v>eX@do3+G;=QKxTg#va1NX1{4))Y^IbuI96QXnxI}P)E;P!-hJA8Tl2Q%+GVaE z_Bf5GrqQ)8&ORX;-sniW)!qCez;fBo@NVq2G=?}E>)_bg{O$7DhW(*5_gRdUzvOeH zi~2ns2U=*Le~SeXd*wQxuRPBTas}Ucx;Q3yrMh^92)>jdgMy*QwMY!h7;LM(M|%Fh zi%kg5$8rx%JWYSP5bP4@ed}jkc6R1HSx>2`ySBA1e=Vv8M$X8x z;#GPsDYx>yQ&%=wna5D}MXp{xaqkcK`6eULFkz;^vXCF(?&;C4MHsZ6Ew&Rbvchk)-8wSR3$qO=6K z==6tk5p$w7IPgM@J2P*|EdRh@=#@>ivTUK!c`l7?W5gUevfh z7alfVS%}i})UjIPmU+!KqvH9U2uH7)H&5N255vevzyGOumw-|?k@A8jPS*8Knz&QupN!1Miq`g zEXeZ7;UUOIf{32j6^@n|fOr?!!L3O8)xvM>z64e4v1h`b2QR*?h1FhT<#_9Hf`(eAU;L|v!KqAf6Nz(QX=A~5A2DR@#l?H!!w~~G$|h!USY^BJHLzN4 z^%18fvXJ(9m%j;wehHskUV+$t02ioP<9G!0-XYB+f`bahv$ZE{`4wQ?jzzcLS^c3d z9rK2aXAH0@C6jLV&4#0?eDrP`lW%ABRnzb5DBXX8(G#%92F7vUkoI1{5f=gzxcE!Z21KI$?CMSK+&oSCIsyLzQ2VL0 z>|Uoh=~I^_iNJnCow`w#*iF=J1h*92>;LfleR6jX!!LCdL$Bl1o&}v6k)s%_hG?`l zd$__%(M6$~EPIhFmD#-RdYen)I(<{0qI{EW))(f~rPeZ#QFvasR8&;dAPWUNLx@;( zfv|t&)oq&l77etVTp}Jxe|hY(`!A^=;WsJK8EMCT@Kln?_V)I=uH|OiPkS8EZr`R) zw5Xo|gN?4gX8lz(G;+%TY**y+xQ~e(?l%ZS%C@&kixZ4&-TKM*2Xf~T7c=Wmq_ zT!sjW%j<^{a4q(THLCGs;)@MK7jy;Ni>~zZfiRZsDemwM zx8h?_wz&NVAQ^?3#c^vzW)B$_uCA{3A8wA9A)S+ImcGD7!P91gkm*D{N_hTOY)44M ziDG-pp)eQzSSfAXE$3Og{2?e63sLIWBD5D3J%=0%cle-U7%6HsLla!j9EJ&hhhXn_ zg7i%JtQrr#VL}!V3VWt|$3q6_`}6OQ&GApcYqukYMT<)lz0^YZ*UU$AQ&WXT^?K)$ z#=WHNL#?wM&STxlLzlf^aF7|P&JmjN$oZ_(%sa$>HDusA zSY*fMUS{0)(#nd*dTK+n%)AA6mdkE+ZckGKy763ybSSDw?r^^j85IlOGId{OBKehE z)Rb*uo$0EDivO!-$qoK7YU&+&xeMl46&idbH1A*8Eg~ov2#VA*|wveV^D+788dN%@_0lh-|kBdUj zLPH>iuecjt-KM_+2~PQB$GlaH-#J5t4>8;#PA9{^_-!lsWJQt}nAVNKXZrjo>02Vw z%Q4A#bTK)(|y#}yV z5AF;N*r`?{8}^{h`1cbvroo?qxr??3+n=YHD6d#p)N zOYBIYVC{K`O6OV`Fq3^yf2>9J!g%%U zmY)zn9#nCl^h$->>i@PC%WL8f-KB#DeXDZ){QT-~(My~C34|Ofw;GOgD=I5-fSv$$ zD+dFD{}CGkip@~16m)X-#_xGE{}b*LmckpZ!={*N%qJi*{An-s{kM zj*_1J?uEzKp0asf38K$YpHYa|6sZN=TIA5V8vfPnBex8FGadvnWu&IJu2}{}u~Gn( z1620M_F&-gpzk1xRkyoK6JK5`KuE;%;P4hrYR|F*d=|%3_U-t%zr)PROH^%spH}K_ zs$R{VfH8r^-Q-`1?4*p{y-n5=&Mo&;)S`?F1D;c#Eb%rm@ETLqZr(gu#;4x)?PU-&lr6u^or%HHU4 z5Z>_`ato(W&L!u;0c-kgd;lE9*Q;DY8pBigFXbNv?#18RjuzqJ5WNoJvjYtTwSxuF zojKf|E&}gqjYdW4Q}WNz?zhgSHarjShtU(rFW87_a4l)#&5Mz5Y%_^P|7oCcwY(;3I&4{C*o!ia;cSe#UbtY%el-#{{}Rd!2etxChS(0Q z&yjKeW@gpvPGu5kyb5`DNGF?z4AvO^=qs1VmnQ6X*daUEqj~jQmrxMDi}0Nb1$AYi zGv)Q-!$V0O=&K}Dvl7-i!IHp24aN3j^Y${NZx?2*?XdgeE0gLc>+`xMvZX>RYOGvx zMaT`aVYow|@KGWjmB@ysQ_Gk%x7(IBcrx1FU0irq{Cf}xn1*ebTr~KHSn>*RD^fZh z2g8hy=qw7;(w>^EgBlo^K)R;#tPB5SN0Lt z94a5G*zzUTeOCz664e-_SIZj#)fpp~(#uh6Pp&LeX1CPg=g8%r?#Z}t9{GsGA}u`1 z!O*I(YNU?alz4BVW~MfuJ9Q$c59`Vl!WCF2oM-Q$ewIl+`r>)9?5QfWC9+jiw?m|& z?1KgAxK+0Gc4;X0fH=%<^79Fw9b7mu)dl?D{p({B_{TO(v7$hnqwZ@ALOubl{}=|z zGw#MOwA_$|uy^iar(bM94QH4vh)K~$_|_F#+W-6Le9MCH#&BAZxyK+_4(L8^iN7Ft zXCl~+ZC5|CM$W5wvbkV`1yOJwSU!6;dA-M3iV7bq8VH@#W6DHqrp!^bfC!scRmBUH zwN&w4qr+!Z*3ezk9Hc5Ctk~mCWsZ2)BqB&5r%JxOia)C>+_Xo5s@157w7-~xb@|fJ zPh6^IvR(%=F*mr5z<9+L%p|!#ltNH@-Q0Y=d}&S^oJZ2Wi#=nUMNS1V@~^cm5<&Y+ zEO0o19nROHfd6D9Mz0YMK|^*PKp-3q`5GW+IHsE;Zy$e)ZMhy~CO@QV5^)W0SUuTr zWavMIouJ3|INuB~H)K*LcT+P#csxaoGkb$x?mC&(c2#8i%>?5dh{6w0clUS2W$RJz z;8BG@Ot!uTJ+Tu;H3Bjn;2w2q&O0m>NiPrOj;KUm*MXjVDXGVMd_VN9pxWg|>trAw zG;!2fh|*)_v(c`r#AV;tu*4g6NSS=fb4W|RD$9?dmqr>;i*Y1aSoZ8-f4`%b8#S4l ztHd7)5o687>>OA|=0aO6);W5<{VgWxVEH3bpneS^G6mE91o4n6@W3qC38L-cHQ&aR z!cB;Rw&JZ^49Ia&H|3A1Plc8qm6`@NF9M6(-!$`Q!|vn}J}n}*XNR{MI~ZmPjsN5h}lyKoclNRs=!wK7lekk1^JRs8f-c$tkTND zm@UcAA#k|Td0<;V>HGMPS)&@9SOx55~1cINbwlLniK<2JOIoyU`0Pd--7;vPc7e0P_YTsvG>68i>DtPnsRj!~{V{OH|aIF-(YW&OgTC$Wi`S(dYd36$C%RM;RS98vWNZv8a?qk`}BM9xHy7sHpQS57Ay#hYXPIui1L$|&Jmws`h{ z^@o;>urTFxd$O!6%^ljshxbr++jf1z^t_-C!UbAOff_0r%@tX^_aZ6!roA|Dw?>n; za~e3o^Kcwklb0e?+cHn@M!mKgWL(H_`$Pr2sHQ*NSahngUdGU$x*Dme#ZtN~+(kjT z!}dH_8+OS6CK8A#4mmz{D589Ge{d}Qb>;8^SI3**l?g`E7}NlvQsOOG zwFTmKk^*)x0r2ic;(Q}E%NUY)Fgd3X;cUb66r^pc|Em#+*W(mpN1?PVOd(@DHLh^C zA-l5de!@&kA-G~EmcBvuO(I&-qITJk2h+EBz=#07^rdr-D?!ozPV>m`doyYq9TRO- z=yHC58lIX;uAoyU8k&PuFd8e-Mb{w{ZLvScubk-?L1n+gj_3m|YN@tDR%T6{YW=E} zRif6e#4-<>t_cliPklPAj^{Bl^sI{DD64#>22WhptTJfF6F%P`ggYof*X=hCctPpG zkz*P8tZ>*62|8;3`n)FoL(jnHH17E!W$)w^rYWWI?Ab?zvb1!N?V`o|WP4CJM~7st zgSHSco0Zb*<-BjvfhsO9=9 zQ;+MP94}Hj-@-7ur9@}ORiMlF?0`EO?jZWB<@V6<@Sc8@|larN5o}!(G5u8)1|YNp9~q@xP|F21t7r_3k=-xg4Su!$w7Hhmhs9mBy)mN z_O4=6;OigDDy?kErvY0+w;wazu15N}of@?VJxSg8xwB;`1xpyq!~vKAA7DhC&-EPx+Ez^Ip_NYdER>;-bSe*%?yzWcOD z5UJcj{!gTG2TBdZZ?{C2J#J{mFFr}u=NKQ|tWESN`Yn7=txI|*A~`q^5pOqGSUY#I z4Rj`{sYZxx*f!1m_%Bz)tO^j0yqqyMJfCeQ_z#WiV*h-3qe{DB0i+^tF3} zsTPVZxlma?QiZH9`-ijmC&N(dI+07#3YFR&?^ZGvppMgqX2~(ePD9Nei8!xvsHhlH zU0HJ7yu6sc%r_kp&as@dq*Ae*ya{A>=4I7iwY+s z%y;I>poSGT^Ad|+K_!J-kzPI-2q0E(758|=D_(i590L3WLM~P~73c-WPb@0h=78_X zrJhNozyFVJka6$ldREDuQlWUNpXMsLYCtJa|6-tc5GxYqVd>UXD2!+%W*zWq?l<;k zRC^D&6qm!X*o9aMY#a&tb-nAo6QbOg*Mo=Rq}UBU(xGk2xF;Sojfjm4`5o4xDy^qk zd8JKIc|t2nCAl}QDX;C;8#rp~WS>O$EJS2w(S;0wHg9@_z<6qTKaP|%YHUnkZ5kh_d%kNoR>cr%Q*=Eggu=Oo3>le;_q8GgB9o)=T-ELcxAjB*A_j zgMRK9V={_2kwiiIJ5tqzit{vH*10Dq7r1R`(`7Nyd-n!NYPg}F^eyge`IJk1IY0Z= zG{;rhK@13uni{WcP%KNIra@>;)7jz^ov?=PC4ov z_4`;5UpirJ#sizI7V#eS>tcb+RUb%lC05ZRek(i2=jM$+sZj!ZbsKOvyntJYvAaAq zO`$^-ZyNiMnA++|ObJV>4jB!mS!@D!)I?M2>&@NHu zffP`td-S6fznvZ(bXbN%EayE*wL2#A=Ckdnn-H8y1&oD{SL5yq`P%31g!8ki(_nh_ z+kjl5mQv|xb3A)Ej4so{lC8ldOrw3mmShU1E?2dEP7{i#F7LA$cc%9 zG9&R;39@kGxRhz1s1QG}O8f>yVS*z;elUYvjNP=}C5g4^Qjjs<`i_L!e>pxg+69K! z>CM(*8xhQ^4sJ8BxBAl6-Qu9&l z73AuZ;h8ZO!@y&ai91r)AT{h2m2zeGmyHWoH#UZlg<6ZoI|Ty1UEYjz6)KBx?)Ex2 z!ixDWH4wh5tld+HaStsy^9W7PtP;)I7OhY@e|;oY3q@$N5JfO6%i z=337!W1xiIN7S%?5vyr*J91*Kcmsbmf)_FBfht>pfkSLQ;}kVbJI|0X35 z|409nfi8x%=)+_bMxxbpb3xxL(dGwgt~H4J9?dN&PCY@ghbG_(!~PFp?;TDB|NoC4 z5{Xg?p=hBHW$#rq%(6Gxjuno592vP~71_rM$vj5LF3NUnC!36eV`LoTSijfNec$iT z=kxA+{mvgn*Hu^7%WFKJkM&eW^(H;}$hA6wTsK)OfbD+%1xeA?}^C$+*b=Scjqy7_!)F>T^1vH#-~IoZ+I40@^}Q##*$~ zS0)(If--}TF8X%N(22H=U7Uw|B-k~zmAnQ?-;?17T&|3yiu|i*i=NFL;Zo9{qUz&JdTM;VSIF$ zEHfdkgrD*uf7zUsfU$U@!t^fqix@TKlBh^Z$mvu{l+9rt>ShI>zn$}k|MZY)x1R-a zByi?EpxP(mv(YaNd{tkNOJ!0-2fdyVJjFrY|1<0;8NDN%4i`G}$fY5yfK6~f%yEoD zu+sMxBuyu4Zg(!0{Kq98eyLZdLE>OoL?}6rw3+;Ml zg{g8@JC0;NMq@rb7j~0J>hnVjysrk~p!ko z=TVG2F?Z}F*kQHUywru+0BKjxh~9*%>VFvrbN8t&h3;- zYQ~5yNKQz{bbd6qeFuvged+YD%Y6JVZ+*S93*W~mz_!zf1uu1Cl&OVPV>#{XALUTP zFROhCIzZ4jC*1#eP?9j8FxRBSo;a#q3)Mx^uB z8&lPG*|b~5YmFdkfAKXo^To9Re=^%w_j!qBhR4-Zd3niL6Qp_Qv^9^_|kDArrp|A7RC_>D3j9#NhJKLV46of!`4 zoRBYpS+lPx*%a+72mAYPLLj{sLV^^2&&~bY5|0+Ss-~<|ZP3cNz|q$tSR~M>00rGJ zZP@@LZ=K5<%2guV{5J}p66~4q>(+)uqQ8;g$$Wnrj?(07BMuzExH8b|NOba8+6nF; z;cV79<5)&MBzH3WvnEH~mb=)_6co0!ay08R$_}}uvEV}3e1!=Rto}eE#Ai`>@}2?! z>pkI8hP!pR(!p=f?-3P}Q%)k#v6(i$0;I7LNkN8?b zG#lXNFseQFalCO@?n1{4U~<7!ozcRE9#dhK%cAR=sLRaOwB1mNy!63>sv*LP;S@Rk zt@O@)P+?A(ARO=M6z!W_`+w-_e$T9#Nhc2Gq5b+`S5(0k#2*u(liU!|@FIv`TFF}X z|L%ojHR*4OUeU0Wt7S^QGvVLX+nCX@ack40#Ob*V1;FtEAYCzaof^Z;b*(-6vU=W= z+Qzf&%pEz#5=d%J@x!wE8y~4uoep@y`D! zUV5o$L6VBr|B?3m?WGnLG6x>SI^Lbh7bTcqRC)gj2W;Ow^_RoJQ~e3}Pfa|L;c=|A z&Y|xr97uTGOj|O&V!COx`skXny52dWrkvA4R(}{}hsXpbAe+N)j9gwqX$CkmDnK-bfL=UrhNn9LhzI$+0K* zWAyHN$7-n*Xobfa^M(ce<-!AGE1wE@KS(~+PnM_O2Yb<$I}%vNKzP|3!Q$rR-Q7fF zDlBvA>23q?GCd%Y#kLi+C6#V%3H?9#(%~7vTzeQv0o$qfc`{`z#SOXTn&T-gJJZ5W z2g2f83Yt7OG{x=p1`}yHRIK;5mYqwTjcQ-dwu~`{Sioz(?eb1;bFwC+diQkX6v7-Y zFRDpQu|#QxrA*)>gd8WZdcvX_x%yyru+%6|rCO^?#(bj_1)V5VPgXm#A#U7b`i$h> zQfVQ~C{>m9U?yA8<{99)ra9<@=C-yO0NwIGZoq%PtdymA3ck6jN({ASa%4v?<>nPW zlj1jZJags@*jTks={rA?Ol}h@>M|SMeV?ovAkwQ2mvQTO)R(5Yqxe7D z4zNS=uk(#FC^`A3lq^RR{62*6t&pO-dL~O<9TpaTeN=n7N`tT*gFJnr zaBO~5F{A$0loo7%`RG};iw}jLT+|S?_`uTe=!cc0X7%vWmlvB(KxW22*_=tNPirk( zS;$UxljPw|)Xd9O%XuxUYAk@81}KvSN&R}yKH+w5>a&D0b{17@Y~789NdUwUns|+# zbwpSd@MG%ZP)Ol?na{2)R)!5w-9p&C5lI=e!3V@<8(l6pRn?&HH1mX-w>K!0mPB1( zz{m(t&O>tj$j8lp=HiS0mYZdRJr_OVQH>wekLdzsE(hR@l1waw_`uy3eJU8X9!#@1 z89Wh3&B1@oB7qeBqBl^X;8e<4PAc8~HdAiU#%TOc#_`@XnrtTF1?J-A)FM9qL1m|R zyW4T%`n6p*9A6p>2hyHsT`M18bhj@Kv}5{fPAP#u#VX-kOPb&Zvvx8ymg+qOD5btS zF&kz#w8bCYU~}O<2f{5XO+D`zzF5W=!ccXDT)>aT9(aqod{m4R&b{R7x@`#`1yW&+ z7>ak1aYEXKiK*0I+CJbtDu$6~?vJ?d1g3acL|)**@PbBXGZ=%?@^spH=@#1@P5sry ztePy7k^Qp$a<6D1Z0^#pgH?oaBe7|+om&zGB`d05Pti-nrwvHH{@sB8`9fx;=MI2m zAT4IZDDS2u%Ex*6`5JXZ?G?~<%J$hnL<%-0>IwrYYE?N77go(#!GvB+d|Jr|KY%f0 zp8%s{8`emcLz~J9x>Uv$UM~sIuh)p@=a0uFrPcUttSY@|f|-5Eb4kAE&{RbkaoY2A z^~(>-z=Ks&+O5fvM4$}WA}Cw+|Qsxl0R8Y??)L)SB%J?|*)ehain7Ox*KbHIo{ z92oa}(y~&mT9cd6=#k8$FQtvWW7qAJ;Kx_r1-FwoAZ_aqF+?oe#+-yZy8vla4$;<9 z=-?GFc(jH9lS-aH>CEd3Xz!Ko`tuGP%}~4r7Vk~7l1The(+G$3?&H}EFc&Z<-E>E} z0s;x*;zx$D>p)&VJsOO2{nfatj;V(mhB$5O!Xrzf`d&T-HYnhYU1#v{>w9qi1={p) z-TK|zF7-d+i;Bo67Sd`q+I?EnvUSlq0U#D4R8z2dr-1>kAtTIp-B8L4g#jc7Z^Y$O1aC z{P(ltkIZgaN|;AbHY|}`6}H8FuxafDEG96Pfoa>8mAV3@3dH_!{;YeTA=H*rSw`2_ za01D1mB?hvUO8C1-z1Bde=g_d$g*uW&Zz>0A-(QjStL0e* z_p4@=xo5^G;39Bfp+bQ2*sXdh7M9pF0f&4Ej7l`NLgxgRI{c|ugeb87Ek53B)2@3D z6qMn_;Tw^AT~0zos+a^~LOqx-ffv<_IdRK1#7Sq_{UGP;#P02(V0}R*QZ_cABG5Z8 z0bXR%_jiSq;`{5s@(`A0>@y$ji6y3GS51(ICroB%$tr@Uxb;_w`Ml1srIeKrm^-*Q zcPR8DKlc$RZW}^gntvHnv0C~xLOCRGvNM6t$|te7a+J{#FQvL3FZ%SRN>H+kY!c+E zaYCh!WsAn*p~)FkANELQ&cnnG_!E?Ja|g4u4RLx_q^~c8EXgjF;&NdsX33|=#Fvvx zriY9}J@L_+Oux8N4a%Jh1FOL1Ri2c+fjju9l0NaSGV~N(w zV9b5B`$rA0Su-fp(GBCZ-lbAfh(o}pG${Z=fS4#g2HzfI;svi&bn1R4QR*}sO5E#} zmAark|3ImgD?8}j&<1cYuBvP{Min8wujWKAhG6VP#$O-r#HzVJm+2vUh%pEi!Il!8 zqvGjrU}AR*n-tZMrgF_YCx>j%=blzy!92yDeQ924)xgB;lVa{S4rV{L5m`u~&nibE z{GaTKP$$TlsRDnH$T;Z4X3bIYIv0%8BkF;iFdNK=Kcj0FF`_Niixa9wBs35xScY48MIY?}WQ2N$SD*5o8abcm|F9Ag`Dm?#hlQ4Vu`F5bLa23J)@NF0 zR=<{>e8&jH5;o2j2@%-GcQHdE0G1prIr&Z(4eQ!x6!gOVqMQxj(n9z3Kb{2&P4s0v z235<3Oh~OQ(vK5IIrilb=fg^E^IXSvLpKkyjoS4XIM3Xg-?k`NSPB0qP0zep3?%;2 zpBD{)5J|WGVS_I3IB34dz1j~XQ;nqWv(_G{8|0CEA$wOh$oHd)3VQE958FRq-Oo5) zW(guos&=jpfY*6|F`p(*$|epVU4Kc*!Q4fxH11^yr;z)}S+P2(?-?S*ET_y9KqVSW z4upRA+@^LEXVa5!qFo7ZO8#l#UBQ5Ij1K2xnFl}{0KNwdA=9c#Y`TNF)GJ;cl$I8&F zWt*+gE_Dmtt&i)k5+6G=YXKAI;tHdLlhbUCSAn6BzQedD_R7Idvoy;f{>QK)FXpu- zc;4ebztRhSOvC%f8@KWyMinesnoPW35e2J{lR-cQ7Y&z@6WlYW8=(q4c>D$7WaoO@UDvU~HB|AldOpC~a&FyY%^aa^q^{Pu6e zg-4z9BvO_TO;x6ihAM;M} zE!<>rbVI^Dj6l6dv3&Ko=%Su@S*}59<*t9e>5K5b(G!pnv8sWwy>UN2ROy^|b5&XE zRaxQfQI|-!<>6U9ZKJ2@;)hG2>%05leB0$*{k3`3v*NDjKRfMz{(iUWH<^0+FUpB3 zAesem%Jgiz@!|UCSD6-OzC5%)fpgr194eDBxsF#^*D1MqPw+KokvRwARC)2ml^ZP3fPGl;#m;;7+)as`&%@M1T8J)nG@0 zknRTa2kYw_-gjzO_k-3y8-(J;p_-BPn;)FqexirF-B}UxW0D7jWpTWzc|)*)0I4~= z2eLVLv@U4eR(xeYDP-Hnw=-%87_JBKIGQVBnLs{`P=Eer_3c6%w6M=VNExgi_x*xW zY9WjYP2`^&B>CXu&G&^HXyRz3_kVp33QTYkpd}Sbxn^^J7t{cJrj31|&*H7UO%FQD zi(-34-u%;R&;8**v;Z(lpmUk&YD`P@0VMU!6R*qhL%ktF-#6%tmuxOW zlfC;sy_PSyvB;6vGjw_=WpH{?Y#@&4qfQHr?d*9VlwGI?jYGQ?j38zLw=OT9Lyx0C zuION`eIX1M&Gl&|Dr=p(gkBu}Ba+89Sdx^`^=hN@a z2Z|+AxlncqS}-|a7LYe$Cgc3z^=NXAP==w`tZi~!>`v>ns(tsdBwcnB+pFq?@%mkl zI?dZ|a02oy9hQKT%Lh62EAL`sY?j&Lss|;H_BU+p?O_Ggb(N1(NTn*0oJtD3M;kxz z7~fN!m91yb<%-!4Gi87^hKJTyPn|zYO>R;xtZq5H2T~{4uwziF{0g z$nTR{$ThI|?wEqvg+|k+Yd^o!vJ+Pfc*a$L*ge|zRm}LsQ1)5KY0cndHK}_fGU2|& zBm>)Dy6afatKd(}va%+CB0}6#S}wJP@_S%2KrHN+L)q@no=N}~%Gn-nm&>PtTcSjw zUPU0?%{ybwJnahPBCUV+8|As3NAM2fDIEkiw7RaY=U^hpuC)Q-nj*-PglLq242#KNNwaUHNRxLWXvs##~m{ za8{`%eG7|+ds?kx@|1gU>i~dxX)*axBQY8hEEws-Y3ape_t9oBT02O}8=DG1pL-Tb z(%BUL+2Dfz!)NCAcsLmn{wWjij%xFzwUfn$3(`KblRwWMu5w!PPMIvP* zIwx?FdqA}LvLlHBhnv{6RW-kx*amn01&D%v?meN-A`-|YV=DSPJ^mj)8tPap|VS%Nc1vfY-2`(*wN}z)Hvu&^6 z!ww{2c;0qFEegcXuk_vMTG8s)rT988;<1@Y#3^vAwMw7I_Ammi>&U~6b#^OiL8at( zc&L)ecxW|E4bBCx&RKQ{&oi8WP9N^AVn!V6m-VzYC3O)y9I4S5EG^_JB7Orwm@vwe zQu%)XXupvkwFl2+E!aP|kD~JP`*rRfMF46`)}r$4BmSd7kdznW6Jd6xi5y(xA4KVu z+ej`}0Xtax_8Yz%lb7@UW?lqj<>>1P;IgAW#xQtefuG})MBB-YDi$ZBiDmTRFumeA z?wI>?J3TX(6}A4hXlXDusguk3AAv@U_SJWWxRp| zQD_W>LY;Ad`wC;^e@7L{z^g)@X+Qe;DTp6@bXcA7oh%yMj#8CDax3A505@W<6{Uw< zEYWy0w4at$LVD`wv3EKpj)QTA=tg|koqrrzl|&iF|5ZgZmeT!#!WW?3QM*l=~EHFV~GY5gGtIfQ10NnLotkcee zV4>%7ZEjmbt!g>ssabL+#7wp9RKwKyQSj{_&7u(No5Uz0sAJ9eTH974v*{G5sqeZe?R4WS zeAH<6aTwrdY@S6NZ6h#a?h|*~nY_+Gfk`osUg#n)n-@n*dhKTk18XtJVIl3Ur&=yC*oqRpelykV7@%lAFyK#r#13r!_CfhkeX9YO8`KdpImMKPur`@l!7XNl`O1WA(z}7bIhqnIkqruR8Pkl z`|3gm^IHkEJVrtfj|imr%F0s2( z1F-1LkgM1L7+L*V`J3n#5Hzv{WP;2z*^@CRk!B!e?bzBMHKl8crLSa&NWzGvQ2)h0 zr4}SquHa8xGBCLMmTp1CgK<(jw>A+{KX?8O&q%E6U0X|QUCTabqR9<)TgjHu5+O2( z8ML)zHW7HL0#=v0^tQjzs#*`^YOe3TI$NEapi$_;l)Vu1X);sJ1^5l5J>7y2!AzY_ zJ?x7@cL%*{9o3qWhvHkTPp{wG!d8IB53PT{{5^E*0IT z)e)_8*WHFOW3Wfhy1BBg20uAbg^zMEs7I~+FpFGhy9!w!Lv+`=BPA+{O10BnVl*&y>Ny!(C=JeOAw%c0uzkB+r#M|zsPVs@ko@G7# zr%c_c<6dv&vV6mXHK~{c@(SC^RFegt&;~TP=O0wt72)%dn3`?%(k5PN_`#;n=f#m zF~rfqaHUK3t7)^;{ZI6F^sYl>e4nJMTfKT|MHT(WmNp|)CF%Dzx#g8ZuCt(A1k_9< zGWS5i0&>s%X`%o`-Fi1;_E@*M&S3Jf=fURQ8lB=e{o)Q^SG$BXcrTmDsL|`rn?}mPe)vmz^`@X<%ZDL+XaZdDvMsgg677Pn-@S# zj@i0U5+U))9i*sc(8_M|UfFcTwWkh+QjRYjs_lYm27eqZ{F+fq801b^*4rLMpAZ{_ zJ-Nhuhaz%87_};nn+TA9U+%lUq=Mgk82!>OwRV?O^BuWCJu!`y_+p?uBv$mQi)@Oe z4qK(2c>osNLYw31I#=+vj+|e^>Az7>up%vIj%$#aa3K7e2XaGr7nX~Qi|%v~`9Y#x zBuGlI25h^ng9C_c^$-T>4wGZgWS_l^3OtT)jCCS(6s4QEqBZ_#+ccA;HjBY#MK4c6ArZ)0^}S~%+*oWJsu+kcE(Q&=v5SNjZSrWeM;F%aWhL_v01YQ5X72?Qz}=v z?i$nhrH$&FwHaDS3%{y{3B&vPXiXA?8$4G)gOolNfpb{l|DdAoC92CP6tdb%J?i^ z_+udHaVikRn56DG9vOmr9jq?v!qF5QG(KSuaG)LL zuf{=5+=bg%jN&RBtXQe%g^qmuaE_Y>?$Zi3R3+)s2u zi#KZ$159?vlbW+BqAh$KKB8pT+Ne>gW2}=t)DW}WUH{OisRu(KDhs|hG7+0piC6#R zHe^Rv^_f3zKhoH~4`qaa>bx||B1x@VPaQ`|SElP8UM6qsO3{+c#~dwS#(URRj7;qk z6p=?ec(>Z}isi7nmC7r7_?bNK=xu2?FgT_nZ5$5;#VwuzvBt2tvf@^d{ET1Im@rbW zeOHGQB6G;^VjF4o3m4umf%N<%iBu_1p$r!8Y9!HKlVRT!#z+cD`;pbq+RLokcZv$~ zs*CJimVc&_Oy2{VD5el5+Bg4(1^CjW(^p#hw<)PbQ`!;bN1`wN>p>OqaNjkpUki(c zhMvDAI1HTO1z)We46IW5_l!P5wnh0PIxbBNseZl!Lgu|L4XMeh=QQ_8ca5^%z8`+l ze+gn@7ZVeQ>*Xp0p8u&J2G{xS^nAg&Q@+cdp)+^FO7=M6ONrBaCuS;gq{{;!0(qUL9nLP0bBf7OmF!r{zGQxB z+#~!Rcx&Z)$K4JOmJiTf=dY=8GIU4{%q$rLVl#K!bLP$$93SnRwcM3X5wXdz>~T^l zOVTeok2F-%o(}^i7h+n-tR)~Q3gV>= zgV4wl0QeT}`K}{X@bbZU!SoV8DVIK`gJo93?A+IB3hMGUGM&o&1~|7R@%O?sSLldq zMtn{wSP;4)riF?=1i}Vr*^&?Y!WpbV$`$kgSXuDWN7y3`RnXf zE0G{SCpu9zdnFqoRqF7u`G2q8@9)0Mub&2A$ry>a&cAU^2Q&`^GWltnJdO4?^ldr4HW8%Bd7s}^OLj5pa%%u$IZlC(++MPTn0YTVNG zx?y)?V_Hezt3p-(u$nE^ex^Sq(-`S<_3Aq zhHKFUWS6gKf(#zx{T;e>?*dbJ=PVc2*0=HD=Fwh{<$>482vfNFBxs*HorQ#XSESM+ zqH7y=2;0i~u%2E~J-%#oS@{cs`9UZ634mAL)1WY`)0aSV^oz#U(Gj4bTNF6h*Y9zQ zN<-xZH|z_<+&&rF2cKP>{OZ2BH7-2xV(BBn%bPAlAqw_VMy4-&06NsBnJA8UImc`L z>>w-w%)AHWWW-nhpXx5ln}SS#_GA8&wSN5=qY2nE3;?+h3JqkB0l-3c>+#L|#12A* zG?Sf__T9j|^w>`24F}R;rH^!`ah0(wT9)x|`05|#ZX(ZeZ^R*e9bYx>3ca?KsYO>T z!p6nHGGEnQ3F3~-O`dMNXthCa{V0r4AH3qt(`XO2FXtf6oVDlOEYIz#;RD~9-dZpN z22!{Emwtscb6#ajV7*250di;4l_IHEWFkAb>ORy~NkAYrndP6qpbH@Kl0X?x*xXAB zzVb}SZ1Ls+p0!q0wdT{q32)oHaVa0UkniJ)klv)>C^bI;|?MeR>& z6MC1mZ2V%wt| zg77BlIdaIG8w<^t&9X}o)qgq;e?zpy6G2b3H1@n@xo&YRuLC)Vbh$Gdr5zhvfAjKB zjo@y~So#n7E_jenkb@+wd!SnKwPTXOYxPxec_L2EU6Tz^iuOvw;n*LL*&$ zU)3J2c*Rii336(;8=D_$SoQv?gXazLtuow?R`rH&MSIB^1Z=7Vs`_Y0>|}EB-DeLY zF84?u^hw{oe71T}FE&eGYUio^_48#{#BPZ@;LNSEtEyDyo%EB=vWjI64xU&P87|zq z5j?QiAHT=`*OJ}MMl``IdIXi~CD$dn0li~f{3LY`g(z$M5?kf193a^)|Me$ymu%DE<^ zK)=tFUx~0-$?+V4(J*3gd(+R>=_{ayHy5g6$=wy)Kv1&_@{luw5s=01E4vKV-#zp2 zOQ=+|-Zl_qJiuX4n0(#Rl0o=9kQEd3?ZpNdH18E9$k%;{46fawyvN>nmnz2OGDDc+1Z$D*pDO3U9rpP;AirBeV zyZR~#wk$8>aj2|D?SAr-IAGCv546h1fmawFE=Yt^r|{xIhD`UopeK znIOj|H;*@};j>TG{}btn^j4FQv)m_k&S6Jhso7T>TkWvtEJ3}sH3ZyRlz_RrXPIa~ zLw_@7cei6#8b@+T$##^O-aVcD{=FRQ?ze1%t^skkrS9Y8KVsmUKcgTgJ3HBucx3(i zdc-_taap?5L}()y14>k^gHfL2{%$aS;K(P#!KhkiV==p>?SjGVi*eKjz0fTUrWV41 z>eeQ1D0wet(m;I52Ui?NtgjxrUik#dP&hu%IL3aYF<15GPcN&9k=BB|JQtW zpC0UUNDs0ieg+H|FAIXKUL~({x{E?C4#_OfVyi4x{$yK_aqv7&{^SiLrtE>L)lMU2 zNQ4SFLqJ_nI&@m3*_77EydjMAWFnnuirL?&P5+d{wv&%f;tFY{xu_W&Ot82;9yRls zS7Yj%U&If}=67wK!CKHfEcJCs!^1GrtuPf&oeeZKzf^ihtH+ z4j+9#tIF=Xf#D~+h_kG#T0nqfG}f29?8w^l)y(kqx1S}`FR&9&TC6 zu0lNet%~W-h2_x2@$fF&!T!g364@$NZGoW&V-p9-L=}*adErsH#CHDS5>W5g?`0!; z+BUSPAa^AezM3t#EPj=e{xSWh3-hY*KMrUMi!4y>_YV)}_L$eD?$!Z_yi}u+iz)iV zmz_|N<|$xebKxyyLIJ;lM3A>ZVKEYGoq%2bcVdX#xi=F^Urfq0-`_^h-RZ`0jtnVB z0#l1?xf2pj4`F`|bngqj_gj0Vg>~+{)}I6EjV3+=!0=O8*xS=&{KtmjXG76j{{n+FnD1fCgL)k&9@b5u4|;K z;G4}=vKV+$W(l?BD>llnvQ3|h0fx{Uv0hWR&wB0g5M2`v1)~Czl}E{pQ@{l#Iy$+>o%S*^g1)Z%%wJmqC+WHYO@` z<*z2~_?WNEw&q{dGC$RCSO z;9NL9g^pmfeK@nQt-vy9eE@p`V2khErHr17W{K}keED|nMMN{uYGaLl&+DOc!u(`v z2a}FG$B&16s8HWZoJ0nKbkt~Nm#tPk$H%e&xDHH)caue*2bMls?ee=hUlUDFB=iQf z_6Drzu#)<%4fsrb*ZXW8GVWuhJy;dY;`Qk0p#I2RIYq*=g{ghIXD^9ip}BtOLwvH@ zHK6N&&ov1G3pl&70DIli2dcPvB1bIWjS6%rJhQ!p`vO2ymtXOqwoeDV$YSWXZ@L~! z%7xs0?-|u$yKSCg?uVPEh~!F3!uWPwHi!>@OEam#ZAiM}x zY2b7%G&P=<+$s&mho~NK6d1XrA4l~DGulbA!~b}j{hKq(QVBH3_*q_rF4$R2K!tUO z*h6|su&`ZNwO4ooOwjQjVoML4sYIIR{|UpC5c!vZQ6kTUDq9xU}9<-3a_H*8iN0>wd-}&% z|M)}$h|TstSN)SLnnXtGv7@!5#yQiOnfeZu z@n17vOJ@o~j?oLTBQPE744 zb-q2bq&}b(@;R7QHJvhk&OJTHJg{m z)?S8cfTzm(*RD@1qzH<8er_DF{sm~Xiv0Dr=GF;A&oGNdDB<0d zq&GFK(Hwb&g(`sDJutjFv@}!-qN%}y6ia=Be_K(ZNX4RV73h%T_T3Si8LSos%Zn1} zhjmgUkP^kzhe;z}<$h9B|H-ip`De6%6?oYdBr}&rCJxJqxt}BuwGL~MGayn+_}HCZ z!$zs8%a*(0siE~H{0z`Ul|Ft;a-0M%-X)zlY4EDYRCF(bk}Yq|3nVnJ`1a!rj$p%- zSIQ{NUBK#}WE+?iEA0^sM;*Ji*x4g37EZ7pe)L_R4(@m1lrT4RbVSu{m7x}Z_Cb~W z(wVIO)q#g7gY^3St!d88taqVtf{XR=`3oS{GguR`^y40Uf0`8ojI#0jJ8|{+C9}+}nfJ zJ@14I(e7LLH+7V|F7!^o$M<>AWYO{&Pl1xn93;|`mm&{FM}AYhk6 zbVU^@0=%>Hmuwv*`~U2l{heH9Wieg@l+R(Q;wzu;Ckfd<16EFjMvy5$2||!BmUTFl zXx!%}ZHHWyPI!}kcEpbIf9~;vvnSA=c|C}#5T}O{Z+%t(qbS169&eXe+SM;69#-L( z6!_}*Tnl&*P7;_0Ema)-&ve*(P2)BrZT<>R&upTE0wVhbw{E!sSC@W)72?2ku!)?cxfrO-J-@;0CmDJhVE=fD_0F~Ug!ZmK=kP0 z8tc(h;R&D8!p}cX8#=eLCGuNEG=%~gQPn`Gz+?yZXs%k7)96vF^Yjey4OMlpMn4wO z6`EseU)|=Y2r&WLZNeml^hd-21_3g}&52VlDz*N*?eUxVEl?(wp2!%YznHj$z~KO& zn8s1nipb1|p05CO2-)+#bF1&CM-{m_m=fl0;lYog8*dj;Jre-fL204t|xtNb2p? zO$G>i)NMg^eDR3%GLstM3_cM1_CgX1U;}~m4YO6bPrJ0a0~f$iRi5R4*Ku{X z15g+L_j8e-@K2UB(Os87{+KB7Ua8x22!F+k*li~&MJ}E`gR%icRM4$jQ>{fah>Yo7 z(+5||hc2AfWy=nzAtEDwE>h4<{nouJYY)~dgQv;3C&%(Hl=6xsb!C*0Ivg^pP|yEW z3ZSE<6OB`-j~7AURfi}%w-RRg>mtZFzt`}Y#4iLp%z1j0HTGj%f zI=Z+*QOB2nLF2h1F`VTi2+W0D<32C7(0Qy{aIp8Z>j|1fmZDLtIU!Mp`@`1O)=W9@ z;dn}<%uLYr_xFDU{VI5~!DIj}b@gasq1uTQRj+)0xU6abFB})~P^up)kKEYQJw<>M zFLH6SY$Yqssr+K(Vq^Eo<5oxf8S(%yeCErktl+RSbQ(fGnip`Q|93Ho-2>!aA@5H>h=P~0uxtV?^#2}DoB2vfE9-!hMdGvByLcrAS z_mrnrHKF?84TEATQM2c<6O6R7T%P(M$*;WBxMGCzx1TdG`iwe=$;zb{q=c+LwdBY3 zOH4e1OW`XwC;0{;Z=&>QpRm#jJOZ(NY!`F;gQeCfTHoXXvU>ux>bqV2+HAty@5HJLE|ihQ z+yD~?V_7U4L0(-5mdIQS9sb(0m3H=MVWJh-40rUo&${=cS-@qk;~I|J%GYNjNO3dT z%6*}re7cPT%!3*$Ub${qX?s|-*eTdmQGZ|$Ft!7A75{jis`={&jnex8ZEbC@G>O4> zt=ij*KxYuM@3CAWs@Er=&JWF-VQi}Uj+#A6!CKV7GaoRY-Mv{O<@2~OgkVP=?Q3HXf2d0*LxS8s5tPjH0%bCZ#v;Jq~=jeQC`%rM?kF}Y) zuu^{P91MhZiIK?EJpom(4yby~5xqa)x;jo6E^`O(bOg@Tlr~GHTyFwH}&UXZJ}P7471Q` zH{e!-rgZu8w6J%OQ+gOMd`B2jEYF-el)f?!46KHd2gJ*rIRAh^Y;B@uT?7FJ9z*WK z1ifda{1K;7Uy5J&6|yqZ<+S2Y?peCDcYBVoy4K8!z`(A8H;!CCnvwgiR1p4;M89+B z@sNx(nS3Cp29QD3!PdwCfLPvoOunS}1Tq1~z$^UQcq()6k>EG4F6>FjE%x(RgLA}# zkb0I=k=)aBjCp7-59AE}eM1Ls)F`VlcY09CwlG!?pf9C7r*pWQz;7^cEG0Cfy~=l8 zGn5`TDqXEejF{`pA7j$95^DHBDfFy2U>T^(;zYG34x3YZB9Epg21fe$&67n_q(KyX z)pGfW0GzOyKe16YC-1dbRy%ySbza#uwmBoxpjZxrvWarP1l0nPW1^wAZZnHF2LxYd z6jW8|ZI8L9f=)s(p8#%%gL>)Fe5~7wHsLm=#+ai4P8c-fg(AGO>voE{G_G)V5^8Pp zxT03a>JNUhv56pP+S%HfHJMrtv!~Y{$Vr3wDn#+X0BdRKV4`1ThM^>ZaD#WgImz96 z0cj)VXj+T5K^9iEUIPiEyR}+;8jfIQpH=7JuCCHK-TqzSz3WfQ+_&aiLL8Q*M{^pR zs|V)nnpL)}r^1-v1j3wYP;|nJZICBmeH@lKqhgyPsbM041L>;&{h6WvZNen6v9|73 zSCz~HZb|2{6NaNov_EeZ3bzYM_WiL)A~) z(B!jRdE2_DFFHkjGryE%AvY}1iXA%3BZ(w~sXT)V{W#4XsYI_BletR*AD*n1v;xVvUg`<@xPF3@@XjFWEHv+}PbmL7{; zNBdo+gUSV*m~51^G2#$`NTD`J{3wC0TUrO>?sh8>gV2q)onlIQ6x7w^BtT2(!l*t?qElF zWPovnwuEY@Mz3#%NN7C)|#){g~4a zJaM_nm!ARPrNmDga37@TJ!Y3d<4%vF<-nil1CV!9<((lm)JFv2IlT7<=vLh8+;x@# zzBltg2ZZ-UnXx5P%2qEld?%ES@F+`Rrsn78yNwTLjoFV3!Kj%tR>|Mkn9rq*@+We z=&fm0tuEz`Mc_O}hgd1m%z{+Xbwu}so9r$y+#$L_^s64us2-Z@V4lI>eq1@)rLWzeyH=TOsr;*-*T}91%r9A3 z+@LovNY>N`xqt&?@6I&ZVPUzaPa5JnpsXvQ);Bx6eZ-2EG}g@``7Q56axh+=gRR7g zY$sa`_FWUqA(i>cQC9SoRaUAcC6XK=12iggR>}%YQcq5WvF!9puUnv>1r=JNo5d{_;$$&8ykL zU*{m}n$ya-PaDQ7bZCSBd%%wV_W^t7^pWz$OpWvaS_DbFBXxf;IwreaXH>GJy~t+^ z4wvB3Y}|m;_W@wMe|!3>A|tnlT~SvLHS@x${1blrFIE->tLCbhH#bu4TJ)?MD#1QG zlt0=-9IfWVfM*97Zg#z3s!+bmHt%&s%yJKx$`v*FbDrzqi9tjZ(mS%X0L~Mo9Ix3L zh8~a9$j!-lQ6PxIHg+xb(xiCZVU!ZO3`RB~_GT4rjU?1#Eba%}CQRnBxh#HyIVQas zTe8ty_bxpe`b~yuF6Mm81;Kfo@7w5qYVy=R&zAW8z)0m5J!1}@g14Sp^h%a>?uHKwWKYNUs&rTtiZ3#0^c58<)n`cE( zo)6t4*S`Ya)d7aaFcYw;UV@!SF9nP}fT?=GLj(i=oaq4KS9v7YC2COd`Wzv)R>)LV zs_Iye#QD7)-CP`_#vqiB9-VS#<0fUfnlpH#<+{-=GJV0rmpWz7&PUSos&SQ(+vOC< z7ikqJU)&$k6RpX$U}L#N?W_=W<_)b|D|?dUdg|)r80Fc)pe~S+UDUdJip)EM90wNV&=dSne^p6-ZHcO*?)ecfUeuj8p zs>kV$0NQZ={vAr|CvwSk_yWi8Q^BlKADbmM=OcEy>JzRx!G?5qEeulE3oI%~Rze~8Eu6eT%iQIIH6av0P_mYg#PG6a<@IioHp zNKi5oMnMUZa}JV5f*>L}DLKQCVFu>AgZn)1vd{iMs;t_w%G%-1z1^ozpFX|V>B#KO zcDFiPBo;jYH>EVgeNLJVw_#+lVqZM+#@?tHqqIT#xl4n^OwX1A_P6FxNpTG~Aee!_ zldl=;<^jU-mer}l6p#khA^01CH5%F^z2C@S6d8`k3(v&ZLex=m(nKsXWjvuq?xk&hiPUq& zCH5X+I()9uTG z4>xBZ^NmeZf7L8@6l?8px*7>B0B&2LoK98_y+=cf)C;4BQAE8e7Cv5Vt2|w9u`tH!4^+c0G}S({}QY)tw-16=9Wl4y<>@6-`G#A4 z`@@bb5rn-16N2W*(Bf~LikVN3RdeaZzb>OW6z<6xK2((KmPWtu99NEbnNE?@IWcKg zXJe!TzpO=`Us zNuH5TC^f|&A5G$9^zu#Un3xEOy?4M4Z%H+2bc-jb?-J}YQ{0VIKqGBe|M4+)bI+f7 zvTiz6Tth^6mT-4*X{~*QaDL^7%Wz+F%2zZ9j;C&R6pp+W?_=V?i)PW%4v759$ zaCS#0_V`|x-N{y$x3QUj%i^&@@)FSxpM>e^_8i%O>v6Dkqi%dIvh2&3Q)erO#*?)` zyy9ui%G;Xx1f5ZuAsP`dIjb3P`0c@QQfHch|g~89iN(X%iHQIYU(c%KV)ao zr^{dN6kgAf-eS(oM3QnVpBX79dB3DBS<+w~``*H+{m+$p23v4r^+s!aENn8$!e8AU zL}+eS_)ZjOh;P=;K5Y+l&My4n_Ri$f@|Zg!;P6^9xw&g%!;b~eUghpEK>BC+W5DW9 z83wIVKwoq6@GMuXUpn76l^0MgevF=zvKU;e6HKPR%t*z!ZIR;wHu>Hi-Uz_KMpl5^nFvH`?&87_@gyX4m=afAut)MWU zAo(In^~;^4f#`GxMMUkPR7LCfpeYr_c&(9!;cM4bw6D)z{fe`?GutQ2 ztsdjlhnFqm3EFDvNzL~3s2NB}3W#AT+}u3VtCt(395BZL3N|*l`xfwHbmVl+xI0_^3;p2o>Zj7v270blpO!C$I8$pFq(k79DmRIArSw`^p>#P_xkX`s%W!r zJu*HS&CzxN!f=VLYk4L$o1-&4_|5(^?77(a(LJ`{i?eC*;0ece78Z7 zg?z4QP5Iy|4Gm52&=AZf)yq2BxsA?wr3hvHtRr-G*G%?eK*J%bXfB^iJy4s!hb00c;HkT#{`)P?5+Htbq_tU@UWi{qG9bV+KcU-p{_3fMZ zdJ{a-!_>P<@Jyr2*U`!DF*n1Xp2i8%ryd6RFXJcZVgRpY>KOO!^A|NuCohKUo6x+} z%ca&4C9f8mPh~5#HP%3M#Rc3!nN%2#}aQXfoG<*hOm*?K6ry1FYo4v9J2%s!~B_?w}j zVn%FSOG{{Rf^^ZSmgbB}2ly_kq7FR;*WDx7p(s9q)QxC_g)AL4mw2Rn3St{5Z z(+=KnAcxLn7EBGa;kK{iJlpx>*2D>5GQgx2W|M+B+@H6_$yV3YU{>O>_Bfz8rB;sT z3)ipTj3ty^PxC67O6gP!;lZtzFD%g=Udd&9wfr>xMucz#+*ji#qq^*YC3YHqjPL~m z7XEjk<@?W>XdMGEqtn1o*oD!1;hj9i;U7)e%%i=uIP5qQ6(EA+ldP$pmir)7aPT>J z(Ye=FEj%RF>Y2BS`fe=~`P=f3zF+;K?_?I&-W*-Z&{61mdC3+ikkP;oM(Fsk04M3c z8I5dN8^^Fj=^N_OT&`VBaYp;_hlh521mk}+!5#Bv>Azh+#SR2cQJqY^LSCC>7F=@Y zHxv$TStY|}cIS_F{iZ@VriwfHUQ(%vvs(A0 zjyp~kN4X}ujr5`$=T_0-ELh3vV5jb@v58v}m{NJ?<4!fkZLbAkg=n8Ux*P@eo3>wfD#y#oWZJ@VWs5$d=jc#C0y-<_D)^Rg}H18o?( zMEa(`@l@CocO*C==q;g>xGrow(3co2OyTEtQ|9bopuun{5D&+e}IqB&vocX(ly1MVybC2_eR|b&05<+~EnD3m56pRiNS8iAd26S&9 zd7Erjo6PU<4_7P--c)x3yjt5w-JB_)_p~mps6e^6wQNO%?)fbgOs}|1QbUZDV~i@4 zSCD<9mDK_!r#)&o`FXWXB7{^hoonW#L)@ZO z6fjh_@hEkx5#lbdW)F7;gFKoQm4cACK-HO{gP=k(GPC(e9BbxAr3NpP`L@K?Y(c1g z-CXj%gLOSO$*Me00};)Tyj&Vb%9gQ!gr_VEi~|K{`-l zXXa+#b`xSMDnmORyz$24cH1Lh)y%N&iIPfnc2H_+$Acj9{Sjmii0Nroqf|u>%?LEk zzg(8t*V3o7d_6oh_1*axq)eUYv3qOIRMs8qmSd%R%00iO3e%G;T~`h3bpX32(akK! z0(e1!An5l-Jg~KWInQ0yEwv+`iP8<&3lhhEJ|JlPs?`1EfWY!#i!bQ3(dU0d1<_-o zqTuSr>x~AnXBXM*dqZQL_uO)dpIEIX89$wRv_5>9Vs$klibO_GLWb2kE+g#`Ijvh4 zQ7n78+ny!i$Vjz9w?y^zl+fU=`U1u9JU`^{7qo%Chh00bE6FV9$8T>&5-M`JmU;}h z6Ucu+GsIClv}$?=kg=d0>5kq_nV$v(Fuz9}s-eoqn@8v^i(AR5ffbiIOq^S3 zimW>0G6$L5zH6dW_iAlEH?>E(nE5E!l$Lq4%-QFG6HiAkV^~6-GmkSfRFu=QmzJ;z z7Zh(4CZdnU>SrW^qS>o!_T(e0UiyDY&|*qgrR1yh!|Zx)d?KF4)0V#LK9etj>z**! ztI;lgG5We{NXB3TmtOrkq2{Cj2gDgMk)Iu98=}L#6NdJ1WVJp8UmCws{)*FR)EPrX zbPo1vQinMK`8_wkJ2&q+>$-b2f3cmKDDanH#*H7ot&G~pVk`Y8mLX4V$*`Fi+;;sP zRj(sGar7PIz%nP->pJ&_b+||S8G7TB5xSChbFs)JQeWE?+rYg59xKr||02 z6(NZDx_*}=T<0SwuGT~w%#KCdDkdUWpt}W#NHfL&_uh{EE)?#%xLq#)dq>DSoBPeSm2?m#;GqpDV=FQUZGK*+Oc8040xtnGIL-y zw)5QUJky%pMK{ozL?7=pz#rLC*hwcQuN#$@waHL8hWLotcTP;4;6~fXpW-z2cJ0{8 z%Ej*q5?&V4`r{Ztyj#x2yn)>)w;!c#04jqiy!5i%vZ|y+IJikufj@Mw9(N7BjN68P%7IjoqS`*0F zmpfL9SI1=%{f4e3*ulQvco>$hpBq-)8cxu-0mwlrIr(>E&YCb!;>LyTLorhr9D|OW z+Fi%(1^NvZm?6&-VDQic+^En`p6IRl)_20!TN|*0J<}083R z?`h>Juk(GxOlkxD{WSPW2555No^6NV2kfhMUWuHk-JMyhE80PGOn*`+M-?!8-t_k} zQ=U7{A1bL3-gR}CSMS+K#$4BnY4uW853AsnVg1OhG%dHiz1>oO|3V>)y|oIxIFw&g zBlT_Z6I1-1S&iSg;Si ze|d8Q)awb%<%es?ERILW&AA1|NT%$o-}}=wmNKDUr;B+n7|gu@ettnusqu& z#JF1yo?Or{F->$CD$jqv^SdrL$)-~4j4QWNn|U~jEjL$MhBX8C&i^r{9t;~vdhc4r zAxZ5S>>e0OZke>pzDXFiY2VVbaX?vIF;r8quCO}+`G^mlZt9w`1K-N-y$Ii*B`lh5A0K{OFjJgfo6DA}55IxuB~c^_1E`shSV03t&%Uz3 zdkkhrOZrH+h&Mu3O2DFoe4nv>d8n>JcbGz}_DM0^E_WEO_5`b1EWq~4B2XNz?7q`8 zGNSK+AGcI)Khtk{$S-ltB>*=`3_Xiaa^Xt$;TnrOt3jK8`b!M`Wxa$)^!FdjcMt^Y zfmoIy(mH0P+PnG8*a8H!wAo#dP?XP9<0*G`DsPL&-nYAWRpt9(eESKt6(66nm9zQg zNV(KhL~8pOX81%oit0W4Oj>3Y5fazEX7`GrH5R*?RI`$t5HLYB3^sj+^20rO8$P@J zJYozgY`(S3h2Onif?tyFFeJ`YkFeo0p}~%*DmU7l`%R z%h`r_4rCIfJRBIETP_U?)Up-Y0sY&sQ!QI1@9VSi>?j*bxq;r^*L7f+HXJ-8n>(l| zxncztznj2x?&pequ|s8k*>&r=`+QzKX01eyIGj*{*CR;7w~i=kXCSlSAA<+2tS1pJ z(78Y$AvZH~K63O-;JX2*!*6z@WmW8J^$xmrhNFzDiB_RJB#R>hv8Ab5f1{1W#fRBH zA==@gv60hMK6B}jsOoBjvh_RV{L3|VDfYqvP>i@zL_%99LUzGBdW0+PP zPgpEZ=!f_0a%i1FvEQd@^sVB>#%izk>7nAJlAcZ3{x48cy7XBv`^J{x`pQzs>iE<4 z&^*AJ;<5t@6>k{Vr}nB$)(j#_oO_dXDD#@x;aBOh>yVj{qC?67YA!oC=s zgbM?zolE`w{dgq9G1o;`|0*50yX<4<;Vb~_$RmGRhOo)4c1x{?1(98DGv}3fq{b_q z%eQ7b%-m30z|NpkZe?R^XKNqH;jte6PDfjpxg_Izi{SH~&^}rwS`KDlqgc^Ez}8+r zRO_`>xP~jrDz0VAP+5UL;6F$iDt)LOrq^C1Vt=^p`EyZu+WvAKPlW8;P*!d(DHN=4 zw#6Sb2;FW!b#UgMc)mDLa}K;ea_Fu2At@K(+x;jxAZL-Dc!+h~TW+rx@7Pi%Y*Jn~o@qeEiG<}FYWOhu3SKPHGfTTmO@*w~o(?eq^j z6@!U}y-|tfI$!qVT`iaM)ikh39!U#&qN3 ztq7+M`@q^)nE{1#S4G9z&0LqMrV2CaaDH(SB_(CTz-KuXw$T%3Iv-CvZJjnv&;UO4 z_*4BsvH8-B>-Kpkr~luJ_a#L}GdlI+&WA-#eX7SgiybEQ(gXcW#zJJK`BtI%Kf^va z&vgpyy59tilOQ?A`?!faC*}aRcpsf-2t3`Q$#L=0WngUjX@`u}M*MJ8mNfl*ny92vjo2j0UJrqhkT<_|8^sulP8&USpeh@4w z=FJ$;Yt7{n(S+BDDhc;UP)9kHvr(^6A@_ez2IBTlPqMpx)yfFZ#@LTnivZOyRl5A+ zaQ871k)>oLr4)&Jum%GUTC9F(J=ps8tf9B24{(%yHcWc># zQ}6ktwm*9sL$OhowoAAQGjTlSjWXUS4yB4qrgoIZ z^-Q<0hYNajeCOms>v{&hz6uZI^=N2-im`A1L2r9v&4%%J?;<)^PN+4L;BExh*5$kS zpx4|EI+SjMd7&qKQrSs!63=HC82UYw?C1|=6pS)22Kjnr{&(m8zhmFe-;FP=B!eza z{h6#Q9Pmc9*RFv@TnCura=@U1upbg|+%&16uHN&Od2b;-vP}wb6P1e@NmEhEd?8@m zCM(x>?bf~ao;utyW_t2?b#lrKw_}C?3%JlGU>j|V@#NvaNlYvNS+|5$EnO45chRg> zpz^WAW7OW1#7K(=DoHvr=1Ab&YTdkfH;PRs4{TZljWr2cCJ*1*1@K0iLO5E}5Rosx zeJciQhqxJS83ImGZ2xJ6nUC@C-n|FYJJj)PUIHa&9eI? z=r~#!4{G1C>H`?#UKDAt*&O1HdpG3v;L=isuScbv!#n80HtN ztBXF4^yS@MnP3w0U#z}`t8;X8jC2HMoaj}@0I6;`4qLg=L^d6x4Qk@3BMkQ%fI8G> zn32IL7bM06>@{M)@E!D8rkK_}6;9Y%fY-lh-n++~u2@Wg*M{Kf8Vip(;rkE1zXintz%*J5j@Xx;x% z3XPTe23Vy_yn(L^K(r~pVN@hko^K!V|GYF-KGdcPlqv(mOrteK(KP>w`J&0ZU?ALl zduXx|J#o7e*033wGFHUa!fd|s@#DvcNZ(CiL9eCn``_UCp&X+l=Z=QDySw)X!DNL& zowr-EQxgdq6jC=M;K%!T@YR(nBb8yDD(vC+`m6&!d@x*f2TLAN0PNvfQamQ;09Xja z<6{jkpv#{&UH8`a#bG8RUjh}g1X1Ss%o*&+w{Mhq*nV>TEP$!t@R892fx(C4iB(a4 zGTnoIiGp5Jfqp=_O)VoMBWUt%(N!fy^cwvIiJN^FLcA0h>3XC#vkWR14g~EF!?F@@ za$O(z{`;fb++af+>9fz?qp5Hf1W+#i{U=l%P~|=CV7h-NTV>XdV^l#j~{ zLU~;Hj?)cLCN-<(AOOaGI<@>cSy-xix3xSU6hj%A+Jbez_x^Y{0KyjNE|q+8qE@H; zJ??BYlh?$i-99Unt{!k7aj+capRLoekF;s$PvZjz?3c6x`((w^(o(1e z*n*(%3p|%2k=Pjy=K({TR-Z}CM1q*(z1Q!w{x> z@1E=rm+QzN-{Of6+Ef-wsXrMFjC(-z{U}0etWJ#$J#+4 zMaiZh`&;|thlVk6F_zCZKMFP2mI!TCDs-D&RA7{V&oQHG6HMV8*VPm0`)t0;8)i6u z&Ln_Q9g<$>F+sAB|J}~GktG{PAtwlgkUu02fIa@i`Yzb=g_ld-C6mu2XbWc3;ryQh z)mPC|&)CDmZfzyxAv=|Yw<3s!G16D?eELsE0RDJLt|tXdY^u~;sUdG%h-Kl6<$0V! zHqFWFX^&NRB#k6Pku>gW7#h;7`{SNlOk7fE`><|w$;W*?V%`EQs2X-JWS=hp0?ZT9 z%Knmm13kabaOV^d>4t&%uUTS|EM6~D;FI~;KXYZk*fD$(kd8;^qGx}*At4~5OCwnH z_{Xb?nUcjG;^Vn!XY5pTT3(rm5|3;-Z!Fe| zGlq)GB}B(cB}D;T=B$tCs&a9eueZ|}1`@0C{`b^T%(?kpA5aL>@gnw;ud|ZxvzFp+ zw)OJpS>HKN084s2eb}V&^2yqrh$>dl{Vpx#y@zS5_+8%0%6Efbzby{M9RAw!Cn&Hd z>X^`tetT+;s3smd+;m5-2My@qL=tYMJQ?pl3A&T;;;?3(?W| zM1qFnw&00-MPe{nARbDqP6J{s3fX;g~qIPW#xZ#EGV9%$u51 z&;kmM`wx%NWYI^eK^VrQxUcaIuVpeG6h&1p>&XK>&)fZSw;fAkuen+yGa%P-MKa_R;Dsiaw>M@6>$S>6~}qSY_#YKc8CL)*$q<*%)0#O}%n*qVhH zuXaQCA&+n3(FmR=C?56p_I~9!&Qfn&A+Or)`|7IsU&fo>4Hs*5hbxvt(K z8~BKiZFMWn&J)Iy%+Va`A=TA}=!%?UK;9lbt(>n|o~l|}S?LOX!M=*;w|p0s%|gLX zf{c&*_rXCF7_%r|eRbzTNTRS!8<t(x<|EOC*Y z4=~3{&&#_J9QyVvp>TG!GnqqMm~Um-NSN_QoM?(7PD@pSu-xCI7wduYNb^uS-8#9=?1dWxRJ_RVm#d>Pj)& ztK8$T55tolP+LXQM)3(Z4QJaVd(Uw;4GJ?WvF=AHz>lgtzf?Jf_4qAKqn{b$IJVY_ z#swLbl$54pZA1X6SA8|rV@9)`WNFbJ#prR{_h@b%6-suGZWu4q6R+e?$wOX3PE>R#c*0Ma=w4;4?IbEAlPDW zIpV)t)Bc_FiJ;jSFVcDnV3qByynMdvj+*OE%~Di^)wO7 z0Fk%hW;*#p`I5;4*}iVSznn~ub2@53=@;NEd{-yy8TZDwMp?;-cyzS1C>3s)P+xA? zE0|J0-tHGHiWIaT?=nPF;11WFcXU}^;`UY@+agv+5o1Xk&)Z76QjLUvl&v*Wz{l2i z4}E1g$&B_CCbwb{s`2Ya!7Na`EntLDo~OZ0_3XBRE~-pJVh z*w~%K9q%1E6lCRw6>n}rfuA~%(TnzL>|)2XTBm_b^BsK8Uw{*kTxc){Dr#h8GzHA? z+9$5(m6eJ3NEwusmEl!bA|fJ1PCuJgHlGraO@Tt7J0BFutPNN6w6NS=Ko&E$->2VjJwjsb&#vktjHIKNyjSvJTHkcA;~2FWwrPI4Eue~z+0SwV z=Kwcyb$7ool*uN;`VAql;m_HXD!Jhy0ETwIuJ3;P_N_~H*Ubew$1kQ3{rOAXwg}GR z-svL??ZF8GT(Zqg!iE8D6Xf}8HM4QXyF7p9!WG{dexn4_QawX)XXDuKTZM;*J8!mg z*fe)S=HfG#Q5`g&xp|E)H17XPpeWK`@Or=aIr$lgu}naA3uK!jpp_b#b{J!tsQd!- zEv!JdU3?czjUub32lXKI?@fG_CFBC13la^s<$) zr5atNOtr+?VgJbf>Z!9`T=7ya5mWG_R-n^}Re9P+6 zxL_R{4d7QUG3ns(w7dIG%kG+A@2ruCWWc7Zfo_^X&mEW#orS3H_-p1)tP#pxCAq6K zl19UWSRR=(@EHI2U~-I-dJyQq?Q%d=UIbmS?$W4B8Xna=F4={3H5~5NM#dG(!4_G7 zOy0@l{}LR{4x;>5c#jXKm0z=W=RmrLDhz346v)f)(G)3L4&hGg!~+ zk4Z8^Q8*kS_m2a{85{Nwn0ovAsED;6kGp*%Qws+j`!R99#m?daVZ*%=B(|^zMSk5R zk8P`7JAX0N%E(^#z4p<1yo}FoOLvMCW>woT)+^Oj5;8A5Bt2TQztC3uZ+8fW(kr;f z1kBp$oIhFCDc(bTi@H(P0Ea_9885kJ<1!mNVV*B4; zC$}{KmTo_Lo!Y+PC9cDAau6)ngkS|bHPBwomYZIvZnj6)5H-yfAj^6Ao}>S7fxTJp z-?K%?+;=XoTlQml5vNL_u%#YA32&IrbjGc(g%Gn;@-vUuN&t0Jn!zIu^@w1J=WPJ- z(kRobN9fm9b6rUB*e^MZ)z>+D$8jzugm72!Oa~~}0DgNjqDTN?t_3(`R8+k&L5?Mi z{__mt@$U$Rw?B&5ZzPtz2t0QLN7xD^yjGur>4ryDXABNHN$(Q~z!U?u{(*OU#U|+O zPY`d~b~tI82T6Oo!3PheiE{Iu!Ky?}c+>(*+AN-||ej7HP)@Hu#CkA0LEzQbR6={#xH+WVMv-hUa;2i6;um#^wogn!ySiOv`QjgX zu5QZo(+e~_CAR_~r?;eps4dW=iQXHy{McK{0Ww!fcy;!vuGMH*qVV*_JQH1kfxEkV zU%sh&+q9HN%e0icWpn4q=qRFRZFga1B@76QY@}5sO(}JC)n(LGRqa)hWMwkck}iQc zq@o7y4$KA4PNLvlj+8{4V%2+}xaxMo0NMuJ(RzALPR^ukKh+*in8@6hoBZXvy=+Fs zhUc`ysaW$%^dq3=d(dgt9qd(BUfyN^!d@L1%?QV9{_$#fa85I!_qhCt8Z{K3#zmDs zXzduPE|(ZEt>7LJt2o;_SjJDn14bD-H)HoS^QwI&wIdPxF~8EjLNlE*ViQY50JVNI zh7!^6y2Tq~7ofK2rKOPJ0fG6iFf|}1`}9>QQ%h6qgEiew9L zNdv(z+$(W1gzY^!6s(kA=;jzyxb@QPas-Z8TRI8Nii35)`MNF>$T`RA>+M$AgSamE zD(d^6e|#|MndRYEMRjFU5wCcx*X>;xvgs&&2tem+cm!XvZF|Gy=s$9yhBUsHU`}rP z@L|YS4;=0z#XK{yuB&fK1Aqf~{^^DLu}=*k@-iS(7(RW*k&$TnX=6NT6xEL`x`~kj zosFOOU z^+rwMkYezO^oQC~Hfyy-_*>rLib(0rKgcaRzJmkB(zl=b9k@e@oWba_=K7^wVS3%>lJ;VMb5uq`o;%(Kwcct&DhsobRKHv z3Xn$fIHd%@#kza!wSP6oNDu!R_>Oz*kh{Y$<9oOgf#~K%9X9S**biBy`EF z^%(q%gnm?Yu-MKo^BA1g)WN(@zVxuF=?JMQLY95Gi_Tv}1}zS!w0@~)KM9%Az* zD_pmifdooBFeIF#bv_>C@7Yp9T|0J6>JJ~5CHj2}crXeGOFS32P@ztV6@B<^1`3@2 zD4-%6aN#be_^Gd+oPq$1nF$2rIehkb6H2TmrIx5ock+Wrk4T{f_4_}0S_>g@gC$xq z>(PI}3;{}fhoP#oJ0!TThg#)a;|pz=Y@FFesQ@On&B(~H!+>p! zaFralL240L^+W+l(F~9#y#!W3#UNDmQ>B;Y@lvkw%gVHg=l#VE|Oj5BZn50@1q~$Kkm@KzWy^8%GqAn-J7e-g(Dt{ zs!%|8(pONq)uuJxQpRpGqYBPCxDgTw`nD3!$Th<7Luod5Iy}qSN^cCqCy#e4 z(lauyLvLSM%s^qTiofZc&e+xnxnOVi|k^xN50n!}#V+lx9*Fam2Y@IfW*jQ~QQ zZR@S`!(D|J^1e>4AsakRo&Az5v<2$s=(U20(r5MTKkeg9-3?8AR?7&WAa9-sBIueX ztW zgCfe+(@KA`*teN%J!u87fJ% zkkggN2s_r(*~n=FKdK+7xi7!|!-gw&EGYzA{P)|K z`Vv%k_zA9rc!^Zt$tUoJI#_8(ciTwN?l20@5d&Air>0Pz6uuC^e|Z+??HG=pqteg= zL35;GUOEpfwY+XUB8R8u2{7NkB-20w26bOh)cKu6ZI>g;z6_o^LOJxWPiwcc)L#U` zy!nQavGFv1)-aYw{~SdAJHaF9ZqonepNg~F93)q5#izRxfc9qvO$eoH2E4PZT0OWm zbMR9e3}Zy|#H=L%OZ@0$k4|oHitkF@oj!YOt9$50*Co0^uW3_VuZKD@d$^qq(Dz53 z;Qr{9zU;mXB50-H$(+q&zsJ*#lbxqNZQ#_2LNk2`|8=i4FgR4Q^3`rMWo2iRK!32w z7l0R<>I{E(krqnV{jbug$jaWuuJWOqU8R?*T>am*mSNU)G!5Nm@$iA38)Wa&G>%PP zd9gw;=0#UY?hYQG@i^?Ls*jIPLnQwf>H1tYCg~3uAEkjPZ)em7+|1!u#mD%2Jl-{o zXO`f5LK>b~PN#DcO!g$$#~6X$7YLmE6xX<=qraofOwP$9vjoKGAC3b=-_R-};T< zZRNMcC;*!m+>G`0^-Igjb};)hF{Y-5^wL%Xr6UA)?t?K+7%;6|tWfvz^1`ds0e+*7 z$k}S1BAd7Q*AxDZKVf3yJ9nD<^2*CaA3b_>xQaack^KhOmAJ9%x9$^no);Utc=?{^ z&%gq@Ks0-Uc-2%N6ejYotl+18d^2`S2%mtZqLIs^o;aqg^6Ma3R%3#Lf3Do{3Eg6 zJ2`9WYLHlq3CxB;uV)g?ZKYOAN_3Iia}5W2^e^#xWxwYL=>$UuyY z**e_7O9^~-Q1sTguhK7YSy)&ZZp1WOUtrWIcb;wouzdxjTn#-j7k8-9rGF5sq9$~# zmQNVgd@AJrl^!@t0Tlwf06^fdMzKq`1P3^Ni!AncHj2vrYHDDf0grHiZ0aSmUL2Mj zB zOJe4!-{e0pRfTdycO|fxO@NnVt%KqM17CZOijkHjK2nE(ydtlxtW4bu zl1s^x2gK8UX9PB9F7puwqB%*5|3P1vB%@)in)qu3W+BffD>uzU=F;?>VRmmZVP*$E z-P?;Lu7an_zUZo>v(2mlu8~Y-U~$Lh6^lpqR^Rn>w?}ONR4=;U91Q#Xdh7C85M-J* z_Dz=aSWeylzd$5s^q=lDouA0B^8iCvDpgh0VRt-814DQ}@$0lT&dC)JbrP7@Dm~5n zxSUkXs{em5?IlCZmnVv{CD(e4nZcDeqy!gc+RzMvZQ40t)tNi{nM?XE<_-K3Mi)KqSTg z`&k2RBX#p%*gW#}0+495pNqxYPxgWq`JHS73{_z7hr)|-XTC!lG5;IWDOM}P=JUZI zD`U!mT~yC+P+Lut#X!}Mq9(xr|0{`ShGv#|hAu#y`2g^Xl+_XEbIeZxp!WD}=8KP; z#aApq{mrSmhwI+}MtHY~Vwl{Ae&WG;Md1GN%$$8!@2P`6NjyI!5`oYba&jVfd#eX1 z=rO0}^LUyW)(%W#rhs-t&&25y5k4=$xZ98Iz%a#|H*cUoU(m;EWchcD++2W{^I6G0 zud+#@ik|2H4f?@#@We6%>r{+&t3vxb)Lk8#>11MQ7X%d5^bizyUrS%Cq{qI!VV>fB zImq%B-C-k~{HlASHhAf+?O?t?21NPRcUuv^L7se@^G2lewv<5#`%^=~fp9GxOP~d~ z7>zEQV1`*7g83^7=!4r$9?J$46Guu-4R@cJBp3H?O!+`LMu)Njv0-uizv zjS|mu`7F%!_ttwYw0+#>`&(lg+Rm?NEShNg42&zT^coYmp?-tNBS+f(a6Ll!2{>{~ z3oEaSf-Xv`@3oGe*V>I$3ZB!FdiWT~9Bx470W&>SGo2KKJxJswMvA|E2^)4xI6@8b zZ17ii!{3o}D4T#?Z0TU*5eErZ4RC%bskk!>ZacUK71*bJK|z_Zz~6rY9D56xt7^$< za4}AH-zn1sKo92K?ytJ**hdkNIzHJ1?1wzEzyGK|(>J)!@)+bO`ELZ~o+?Pq!TECV z1N(zgW3H4*&}XD^>+UlW&Ll}U{1m+eD3~~tecD-!F9$#Sdn+4Mwj6lv?A_O3@hOZO z)SZz4;mDtT zHZz9+1r**7#J%A<<}$QkW+q?YW8BRV7z*mT5Q{|+1eX}*wyW%cI8pG`CXfqr@`Rw* zzE(fE(~8%r5@1x41g~YNA}@j5$-Z^tz=hB6%V5GkU*c8(C~g`#i}6>MRIyQ?QbY6& z=$*6zuKPJ#seRTi50q}E^&5{qqzsVNPyoo6y`HVQ!?Wkb3ffEii)Opuk&QAhE7?9m zfRT_AR>%I^6OefWJL1q%8maUlm3eyJfsE`PPav2)mH>DQD2efVQ#kXh4NEX%<)DKJ z`$IAXSeFzJjrY<_>q?5STC3(QR`h_KVsv zPu4{XgaRbds$l?9sOWpUQsvtN!B^kf2@q96FqX$eh199%0tw^)v}3m)oQtc(08|YG z<>@7PBA(?Y0>juXys-s@DRAONvM9xwEl5_#4w_xE1GnKFHc$$ByKns^d3?Ug1DrOY zY(1L@>vg@jR-i$I6+}4|hZY|<(59|*;&l~rw8-tJP~ed+j1oz9+&!r`bUI-FRsMwn z%S+kd+mtIJv^PaX?}AuTTF&hG1jF{;j`z=U0w?oLGIRFUqNt~aP~#K4HB1{%e#Z&P zy_|1MvzkOC&k^M(4C*V;P%(c&6S2Pa(wxU%HDEK63PlQ$BR4xb5`aW`WvuAPaMXQm zOybA1^0(|PUY>CD%EoZtxs@%T?Qe;@VSKn6t@L4PAIz0N7#=uheTDfxR$ybs7cusa z+T|JzFpSC)pk_4*0RrKO#4GvuK*7a8dZ#Ra05-l!W<5K31|sGo`SAvf+}%4$8J*hZ zpGYueE_nRTzMx~}r92NG-cU{c6+EMdKP;u;OC@q*=CYN&Hhn05akF-U`Bp4FL|96m`&k&8UJxlT-fix5C; z&G-wzJ^&QeyF#E$fGG!azs&_q8+66Y|L0>c^V)yIp^I$6h)#D0aqSPED+iw}rzPH= zheF>8xbo5sUJj0#orydM$F;Z9sP({uA1@OGPUScz^erFGBxCUnBg~NNqubwgA}0Y% zc)R+_F#6oe-q$-6R)mv>B|jDrX&*mwLqFtb&IOW%otkb4e0I{i45I`N8iLfcd?ql2 zBFNO_LEzdDK{KS5VAK|BzDBZQR5}&qx#Qu`dCsH%2Hl&BORukcnPmo`@8Wmgk$aOR zaGR_-GEM9I7j2In{SKtHAmVro)m}Rs6aCsSA0PJ~q2(qYrXQWtnSOtyk-!Sb^&HQw zxQqd@PMQ|rn!znMN#Rd?rr0)E<+Op_Idy=5Cj=H%0{P^+ZwLQ4VIl>n9|skJua_&B z2x{gaSyLMMTLWEi1V1)?qUJ|gZ_qht31g(XP{7~@;FeF#<7gir`YES8sUrO+W$`EIKA4w%+2l;di0vC zlo$$iZ0j-v%`sX~-hpZ!tTcGgh#FMS2|+(Pe7}^HWHRoEmv*+*?ExQgq-!->1l-vf zpy1aGcjo6zHvvbC8Vq9HoO^WY-2Q^X;(cHY7-|%zp{?y0*AJwJE~J89;Dg$!k3(Q= z;=cmMuFXG0X0w7$%I3f3P=5SSYTM4%Sy~*$ULCHoA}VZ;4+xuyi2U;aU*h%IbcaPn zeY;24fv=4KeL1{bUH@G@iF&&K>7C`PDQY{T^BxTtQ&(5lZJhMWh>&*eOde&1=MK;*vY%o~4 z@rnS0H}C+{PlscvCYV}_VNi?`rwN4dW>y@;W7khtcTnreDuK4dV^X#7yLWc{udBYH z1{#y9CjF`@{r02xz;Bfp5_rtUYOW~t(=T43lfoe!WTuE49-=6x4hr@pp@3VvvXH@I3kKsP12$1r?D!s?U{h7 zKg#JqOyqJA?qY!L6OGT_Zk=O>N+P|onyd(T<1+#CSHaxmscAI4V}rqM_tJ z5R*>Z64uIr3S(C%dQSjdQx}R*|De@lFla1i=wN!xC>L}sa zYG{8W*v1~u(1Yz6ZLEo_eu`Gz<6GA1;cgT%^waHIb1IP_x`N;6peF!G5+$B@hs&dw z!8!&V?<7aT?o+8X@aGAVX-mYLwYNp#@eC=5ub<=1a!tH=A=q#fR21A7Z*6P=lBy?m zVh6pyF0?w~Q@jUkr4rA-2jac!EM)>8ZNz@pwU!`Z17F8|2)br=zjxWBQs;P8F7m!F zkF?rj`93DF)XSHa*~JbEq#*l-%N2IRlt9kSuz^+(RblWTutKqDvdmf?1u)gt)yen+ z_k`Wi`Go0+NU)@x7-G!At{cO&PkjT8pMIgRkl^587%5m_6$!AM;HA&N6;{%c!0IH7 z0Fc(eX36^y6xzvbZ|&Bj?Tf@*Mic2Sb0`TTriVO6$4I_u=B zo@)V+MsUQ`<%@_Qb?~ExGXmatyEhPDz?sKSv~32lYS)G1z7V|9Heees0@1_K-OrEF zd#Lta1=tMlN0^F4irE<9$7DQTnUvQa#ss}q+F@BM_S%};UsMkmaYRz7OG;c=f*$0O3lp?45iq639F zR262MD9>DO?%sKljMXT{77Bq)sh}@%jh0t3W6&Ni;uN*Zfs4e>qMHzNZJrvy(~Vzt zcY^?fAIpC^iJcQW+VujOH_*Teg#rm*GbqeQ=DAESV_)r0G@Xp#{P(~Dr%v{9rhf+F zB#SII0yBJDmmVb2K@%5M@>npY06{<@jJHT%x)FvSPIQzn^r`9D9~ecyHIM}GPDhf7 zgIA(z&>o;8u&_ext9HvG3eFGgRuH*TUO(SCe-0ZVz>I@|g(91tRM`sk6v-R+c02U( zPvy9+$1e!ASN3QTxb-1vgm(8p0uO>yChGC`CePGMp2Yw9! zZ+4)kl`A&+!|50^5x0~i?%rc5jvQ7j%UE8@Ew!-8@DA7gJG z4`utskKfg!jnX0%(JqnYDZ5OYN`<0qVXVnA_I(>IQdG*Gy@WhjLe?;rkR?JHTh?q@ z$Jocr{LVf4JRW_&zu)V3{ZTVx?t8BLy3Tdh_c=%LpVUX{Q5hmCNi@QG8k(6wr%ljj z+f7={yS4=f-&}*7JB&Fk*U;7RM09lm)BornEbQ0d*?V|-$5ikGj}%a!FWf&Anaugh zj1heFRff}FJoKF?=^0BZB4HLDw5%yEkr;e_t}l0|vTO4qn(VenB*=C5@qy}&*55O1 zz4A;(mYug%M2~<1Q%LWy)PL&7Zhqg1R+`ubbY*KNm}lbvN9TRAwTCD6-wi}x#5yxV zBS-B9UYb-~yIfjSCr25bp9&>#g9HDy+7l769wX!sTOstV@=D*2e}V}k6-ljG4B+Nk z3ms$Uzbl1v3%d-*oL5j7&di3ycE@X7yPZktWU%KIMA;Qnl6)niZM1I$1y8pNZ?dOQ z2y_Yq?CGJT;jY5nb%m1~;eJ$9`g!yH>diT;I3Kbc2)XdmfXU1Qs=qve{vMKC8sY({ zfrOUUxW_DXwAvhXHX_BR!H)M8bDU(Gn4?FLcRDsh zTSRlp75ego-+|;-)NOIPNV~*V{yj`xZ~7R^Apw@?>XZ{SKCBh_?6d8Nq@Nv&C*KNC;-#!SCZh$;Wr$n3#CcvBJr(`ML9Ai5FZ-P!5`Jdz_|!4}L+M5P)#G#Vj$EsKXehZ2P$NH%u12o1R4(4U?B#?4O;es-QRhX|GS=j^jk|y7%p$}eb4j1 z9vwc8;Y7Xuz#;&J2s702wAQ?JtG(%!VU;cTv}QeR-9!$j+w%nyJ!cPx!B~IG>Pxi7q5A2f4h)1De{|EJXG8{Y$>H4(}O=-Q1(82m`X&m`l(qIqhbns)>(of&r0RE1D)SNdYc zMxiN*)M~YK^g_hWL5WWkU3zaA#-t*4F$csT1@tX|>GLg)+KocBIaTY zyY^4!NriVv%9vKUycMxn9lbcZmF^@304>~5A`!Ej zS90QsS?ThP02v{Y&}SJ4|<&T>u$ zSY$hA!J#k|PYTY8)Qusp8M0Qo-&b=^yh1s|E+wt<%lf$lPWLC7MlV<=PZs0Lhsi^vGH_ zYD=OV2bqH#ly1QVvhe-=VXI-54}BOA!+z8k53z(z>$3qyqk(EIr>&;^t_^P1=fAwn z$*_zLOI&z$`?;a+M&v>W)I)P7dt3Ph1O({jSV1LF=*{Hoxh{W7@ukLYUe}sN z=P^J3$EH~~MLPn_pH}S@Fd)6yLh2+IO3G2_$NdPBb05OQ91KR&u(YXANDepRosR$T z;iN9PP2y9ddO>xTFj8g^o5wMDaMi+cE(ZZwG>Y6^z!A9KW9QHHr||*znik{}$@i;+ zDp2*I;T2&m(fgI^!kPZ(*0NB3-(j4qYe(kD;B;OgbWDTt!@{5RzC^19ZrR{AF`~|aR%+zC@C+3Y@ zCPr>LUAY{P(*XTabADFy^c`jMkrRCX)t!Ea7#OPXleZ};UmjWmt{5D4CA@j@hVDRN~y>B-{Do2#@o~S)T8rdLTdE9)$-%Y~uDK6!sfypg5mLqA&{}h}&q83~p z1XW(#F5@EYr(fAXm5SGvQ=h^#G}-&(&ygL*o;_p0su@Msyt~Px z^6;ExG~mtPEbG;NSw(HjX~$lohc@-(_dMi66~S#Pvm4ZP9B!h98vvK4{EYc8F%HQs z{f#a(4FchHD{y()`Ut{4P*yyW7e|9VXk2xW)71y*!*0o1S;ur|{!!nuC@s9{#FcxW zlw@vOM&{qKl%@R~ckX^MmTo$-6AB0EkUpiJN;jYD9Qrk0Hyyn#{_yZyLE*<}wcD(O zW4Es+J=>l2UDVu6@oH5E@2{Gg@!EPFkPs+z$A&sXtb}qii(^=brw{7P&pLl^P1|dK zjSGyK4i^ch2+W`g=f2fe^Kaf?aY$eLY;|;{HL*@A$nKl3Gt8O*>}Y-t&U-(a$X5^e zorAZ=-YAedYvgu(-)X)$HaFkrUwI2EP|Sn=F^59b8s38M4HRhM^2YcpeNZWp>xQHz z3_*OQJSm#6Xbp}!fXgJ7ip<}wXsPI4MJE44CI&wUM1jVALT?0J2&f*NeTf4Z^M<&ot=?Vrp8Q9{ExUC{h$$Al~KW3Ot=;tg4_ zEJgPmA|3$wL5kz09z|J(^W4X-y%?xHYY334b#mDNQ14twB|Kr(vAKCQ?Hw~B$vIWo z)ic)}>#>Reyk3QFhTYW$ z^rSDBRe?>dxASqG{F7++0O+3F?e)_)Q~-h%U=*kdMJ5wUi4`=6%h;?#83ktiQ*N&+ zo}$Wo3e&Gn6WC3Y>bi`$Q{5;6}80||ydd*RIR7t8o$QfD<^FlYz&$=hZ z(++h{qo~!75;4ok12HIjD=^jVXAx47K zr66X$8rEYcRy*4M7uPaet`ZB8~~GzIodom?p$h% zLyxV}4)zz!M`~~qV6}T5)jU9eJTMX4;NZ~8cA-T-C~xw3KG7V*@v+@$jMxXN>RvQQ zun%<%0H(s?O3@V?D6mk8N>>OXKZQ;hpusvze-O;e9tFJ}4d4uXiOB5A%!OtI8DnU@ zIlzAWJv;t`3y@`xU;SYrATqGGN1#0@CfrBl#m&Yn97Z|LN@9X22B( z;J{Va8k_NRR_|f(;a3blKqMfZP_x=%y9jbLVW||#KBCnEp>9RmYX}tGrp|buw<61f z+NKIYr%=x!-!QjN+es3kRAX7vd|eB+N{Rr?x;PgB|&_9!= zsQcbBuRp|MP3lk2^h5hi6}D+kW!mK+BHC=>r~;q9ALD%WuKDameP=zq}8pfMw%GfLJV-O2~s?DE+|i7V+WyxcG1rgoB<8DwI}Eg$AnK zx)tFu4h|oPje?yMfc+p#c<#c8a)R9JI$fSjW;RGv;eVwndqw}cJ&M(sR;c@tCN;N( z#Fysm;68mnv?AQf^SBmo*X}s^n>QTi2vS~(^Js<*sJ02 zrdeoIOv4ozfe)mHzT{8BV`%#J_M?wqyiG_D-}+KULP=`uu+2aXbY_Xpm1ICNMB1h$ zSOfnG-DNoJJ+kpR7;*fwbO&QdL-qXFa*TWv&XzdmF?Gvv&hB2fcYBV zyjjru-Q;3lRbqJ#oor5O6~OF~$+q%)VNe~EMEWPQ{^?<-vL}v*C8r^Omly#aD6Jk#1iT_lL0wgb zztfp^t8bK8$S*GIj7Jk~;}%RXP=_@a<2c^J6BXrVcogcLhy87AR{J($mrrff+zrmw zB;hBEi&AJimA7x9>=@_B=0Ou%+c^)8|C#^@^6=kMlG^&orE4`e2QzfA=TK~e_l8=o zP)_8n7`g@|zxNJ#S280a;x}&G_;fGM|8Y&)ywPY!(W&u|P9Cz>U1%sQcP_fC0um!Mx+F+Zr}Oq^8xO@0AIENIgnX_$T{Q4 z6a6Gg(-XPluDsvE*5!pgpnc&MdG6d%^*P*o_lm8rWxQQj4%CZTw)7X=y|>|XffWXxum1*6hxDqAbsS7rpSM7WmM%huUhax>pY=v$3=rS>zW zPo3?DzZSdS5pYe}2#w(*-g>v7ck0Vx@(7)Xx5<%0W6mpM2<4&)QycJ;8`gM{0Qy@B z`5fm$4NemkM#sk;J(WECi0;VD;vRH!UxhxUI3`RS-dIx@}Z5IlXy%}oXn^$#+C!h+tdcLf7;E%N59 zrg4uT+@sqf;?M&@VXr8Tnul)%pId@E5{O1UB0|A%4BGPK{r9d=#=#I>RK)9|tjab4 z^cDF4dr{r}_^JMkp)7z3PrY}=p$GFB-_s=XyIqQQVx=Xkwk4Kv1P~q#P0dY6BsQzJ zpViL<)ywswWp~I%AJ6d)JrmS+#e(ZBDwDf^R!`2C=uLh$hc_!D7K?5Ma?k5;FR~DO z;W1_`{{!?M?lKBF|2ph{yu7?b&UI1Q2>CGUUUL-o^-Tg9jXNUEBO>731{6rT2;Pg| zCI(-x^~nfx(CyCW?h5V0!+S)jJH3y|Bf@|6^{otCLL`*nHeZ9hE;#D4nQyrj2zme0 z5wc#!A$*ko%&xK{p*j2E9^IVdK4#b4o2-PE*wKlujN6Pl&eoPV3yTL(tsOxW%hT4c zYeHp;h_FhFvs9h>bhyICWN_(t4abx0cP~Y)E%An|mj8_6VFvrCdj*xi-)Cj*LiQbF zHaZ-47PMFd(fDg&o?t43s5DG}yTEoCy-!vDT_cpG*kmuP&I%4IJOfuq`lRX>?om@y zTZ^1e`f8uj{b9#9_*eupb(XjwiP=>qv?=BQ|DaE=ohv$tF%?8JJfiA8p{d}iT%FJ# z0ro{kab!s+adGj|fdODdZ&D_MRT#o7rmeI(_C_|dcO%TRd%pT~#vxYWEA6Wu4WTaY zlK>>R_n^(M5HA%_5cZ?oAWS%GTTVRn)?_3{+!HiQL*smIDAxG6`vRO_Zz{K zotM!&uZ&DaO?ycWaS4gCb}Ir@T=!CQ5z(Zf>@$qDZ*5ftu72SryMjNivIFHo+{$~D zD=#jz``ISr{+!oG^b71Q)zz1C9mljZZrx%=zP=HZivk6T!D@>74XKhFV&}yar+SB9 zATsc6$)b~Vl_I6SQQ$T&V^<&cSY8J2_x?~&zzx&8=pP>+k03Y|{p1X!`K=z~Q3vEz zCpEYyDj}$2zTY^A?E-_ixJ^>a=g|YRGM0(+y@Qv8Mr$zr!*=d=x4K)Cq^9M@!`P1M* z^QKa%A}MHB7H)6XFPWTI*CRStyla}`1dUKO8|k5V#%Lk`L!=e!btw+E_R5Ba>+msB zQG+*WI5}wGxq7Q_Gnn2q#A{5ahg@U$s0Ciapbp6Xc8=GE7QCO)6Pd52^ygwcj^Mv6 zs4?@Mz@UOYkiYV4Kljx?YYg2sM>ibt+P(~1lIu5I)cP7^KzyRguBa^En1!-V$s%{z zr~$Jn5oEp5D1FA4Z~VE(p&fBE;nnp*h7`Svx2nQ<`d z@-l{Q9`lXoM2(u-G%Zd|9^E5vo@lo!HD)@ZJiAZq>8qNr&hTnw=w+|}eX+(yfn8BG3_-`gyoIt1D5a$PVU z^{ID{a;XpHv&+UG70)ZsLv==L*}(Ol!~v9hu8iLT1M#}5`_+A#8~)&xDI|P5Dunz# z>2K%D+jfchz6L5gJvWuN$8L2nSL7B$cq99a?i#U1t6ooTIEdcYL?}r*xgVIO zBq#rc#0s$&Rr}p$7$jZ`yDDc~UW_DcJJE*^_@=AU-hfJFJ=?z03bQ}jrJN?fL_~tS z=+rJN5Bi+v#N1kh$~gL`=PGsy0=qy4ly|G#UQ!OsNP#y<;FU97Bv8MEbFMro|L?Bx z#M3Z{2bw%}!*i*R7iBc^0USk_y))q|uZ8En*hEnecb4_{>n&7ov0k^}NE$nDn);c0 zJN)^o(9<~JPsWc5R@oKBacKQZ*5!@q=Iv*C8+Towv5=7t#&2&NvyI{?63S6DYqzHT zB%di|5Vf?a?vdkz)qi*imTg{O32w%ucQ+1QIkPNPjJJ>e{U+IT_iJ~d&jsjVy_(8C zt5es2&awb~_LVWce3-i<1(=QEiOUfv_s50{OD{M+Dvi9(&=WuJ8wa#iY(FKEA$z*W z9dmq;yqd&L<9~1Z{0C08-4JO)6{@x{IpJq1h)XBff_I^I*@vE9#kd8boJNrXhk4_ti?AS)eRvBSB2xvQXtTObJJeA3oJ*weoZDieZx= zlIJYDiuPJUHr(dva1&9zQLq;DO-vG-kSyT-4F|_%pxYy8R1oXhfDBze=KlT0QN}%$ zZIWZwR<||Vcm~TiEdPx5aqgmQMT0-e0N-W=%_7&dx3|aXWpIv))%Px7AKh~u-8AwBzbYou^iu`8r-!x8)2RQnXfnZ;M* z1CT`qVqd8NI#86MEvn~AjIOgBaT`g=DYcel$z+?*De##nAS*e~d{-|6{Q&VYjbE#= zjNWvw^1CImtCanGh0RM|l$8#@kqXc5!iK&3@99IlXj|ZqB6hGpJ_LQ2_oo=Iu%$sr|_h0{da}x9dWZ zAc8bi`|1ByL{g65`A6+g1ergZ>6H=ORwPO?TB&5xSW>gS;E9bSe6li@&(7^$NKuaK zPL>y8C1yb53fCgg!Vs^%J?e)Tj~-!m*Dy)aQFB^La(8^jb5zKP6#o5IFola{Ju5aM zX7zG=GW=V+ouRE8@A}(cKDuny@|$yqvsbYA3wGwni_O;(4`})_BGv8+Yg7XZ)CB^* zEF5M8Qj~;Z@cc1koY~0gn~tM<`1Kv!&ejd*)IfT8zxvDIq~7y3VP7g?DMuqkeCT;sVn6vSNDgy_?6LY^y_`j zp&2Mw;UjMYH3S>PzGOPnNYY5`kzJ8>SiZ~GP`288w31$$_&NBMgYO&kJ?Y!Pt+spp ze|A$w#Tjy%HB~!r2l%2i{*tijWRa1P(ck{bytTcZ8M$UA;2h6_T7QuqrWP^&q?-3h zm%7;kRraCU*fZfTmEQ)8FOxj%(66kFufmMAOozCU&;9%N-*4=70ARyG!GL*iV+LkC zLj6-BUDc#Qk{pgr9+iE=Z$xKu(;dPi6xv_@aZ;B_XX|mTSalZio~=ImFGk14p7J2q z0y&C(YkXDE$8$gGr{~W_tW3x6p`QzCJHMm-;Q_`fzC8J`uCg)9xDU57J9rg#oH4;y zDCuP9c)!od;b1R6;GH7Y+Sh8S@_w z^uxDBFe-OPZ1$^GP@bR=Jamui@;-?=?B2#J_N9jN|K>_Py}_X6`)ia(ZrHFvzhR3{ z*(AWFgpt!X*bA%u4ETM}Gh@Htqdw^M|$mS{qbW<<_B3Vlt@+b30#LN#L zrYBud*E=}+e`^S@lu4Wtq4}N3#r$W~j2Y%ot zvaY{>u~S0gkDq6l>3sH(SpPK#3d>qAO!dDnD(W;3YlbH~+Z7TTS_bBSOG}G0=jCQ2 z=&QgMo}HPwb8*|cuLhi21q3A0DhW|LCLOhG!kA!nN1>W$ssNFT5_ktC1d&4b=)lnC9BjXS4F?@anf#aF^540sN7RJML$u>U9x+#F4%yEp5K(Q|T=us{CLrjR<@t`7=a zZ8bRd)6VEv6_76Q5$(p>vg*!NgCn-Ar$--rYVVj{EGs2``=3&nUNjwHYDRyfGPJa! zf=YXBx_jjZ;MEWV4k~#!^<1HWWGesyF4Sk*1JnH)3V1%HvSwZA;!`)F%)-CUtKeFQ z0YiopstsC_S{&S!YDm-@l3Y8+P@$Ee9^qfL+i4YT6pu25BL9yf4;lNhlciO!Q|~ti zKqiVc6eju_4?ZhvnEltoz^a1e-&dam@i1xCO?>UkNA;CCNvl_E2G$HWJ#aePKRMkfdv&)~NJaaA>3@IWKNbuM5@Yfrr@pgJhEogrpIGF>sHmu~ zKA>;wt|J=Ew?bv5vmC(Fu0aCVU(4+4_|<;|9is^1G(L59MPbw?_agPq=d+(yi}2aL z%ELN&bcViWsB9nny6Wz&KCc8sEk;g$(n}SB<>I``zOH%^DxfuMB5iPP(JkI3tUMA_ z%3!(AeYJ?bSRx>_NtmFqGwpB-uPQO7PsD7|)k>$=vfolQA1s5Rvn;J`EiD@pduGSl zU$JIx*1mI^=v;=s=0isxgjlkCKAKnA%s7_IUe@7*tvgR z3a`Uum_t=swdKgrGZbGO26`;_sr>iZ+3ZR+AbW<|1lJMsHy`x_=CckNyrnyRc@1iE zX|UWsLsmBIoyX3jTz)E6hI}4A7Rl4Hc5kA5W^EmXMuxD)Q9F!FSDr=n$9 zRLqvok_mOPnE$sZ1<^pD4ROpm59`*5#7v#lV8o{MiaxC+W{G-e#c-6Wu|&K{Iu-U? z3*R>>d&yJ#kM8MT8FE;}JGC^~Ah)@(QAKhKs2{J)xwyFWjg1f2HYWRTDDM4d*DJL* zsC#1IoMuP9>D8DDdRDA$T~5NJ>vCjOw@LO2E;-7la`j>_#|jgN0KV9BlEtSC6@2-Q zz<@=}b(n&FEXTO|<!>r!G5 zw~EzipU-1Ie_MC85dCeYy#~8h#JsFW1dSb~rlh59K*F3hGxFRPo^Li3c!*NM*OVm* z4r9red4D*)8JlBW$9Onk9Z{EC_8|7hg&)LULKV1Le;fV2EKyI*eO%SEu^#YA@pNyl zG<{6oH7aD)Q~i!^tJ*`v^!6BF@;B*xbYHLqkH-!_Lw?A>2Sxp$uzi#+>-rd);Dv2c zfuGwig`;$XRKOnCTI}s7i&eD2+m*5GL+WM&bwlI|s(7Gc09e`a$Pi~m9cM;(>`=O7 z2%|Vz7>3_kRt`++f6CK$8sgjTp!GCF@=iWe+D|flmS8G#W;O3zjuYx_2A}LJX4Hfa zL9Ob1h@rQ&Qo%`fU{?(~xdCOlgnB>2Q2F$y)T&_brYtnt6s9fpjEAL-?iAmqjV|uW z_<9OeI<0+C(?akE=)?9nL&JlS38~{fK>0}bl8QA-F9WxD&Z{-CU9eR0I%FlYtiIbf zYG08XRH-9;68*D?f7nxISZv8`*yyj`n(Ko=ntTlHgPrzi`u1H6l6dSOBqgvbQVHsp zlriX>t&)7dMDK(4!;s*+s%zgK?U(=dWeeP+czVjlK}y}QcJ;KE4bAl9AjyT`5WVyc z0<_|-SLP6Si1N--Gz3h6y)ha#u=a%FxbOgeaV#&Z!8l@8;L+y$#L$oeH&le$iTxRk zl|fxqdYj=a%0s%uRu8}ZcR=-cg@&GbPCD$m#(uvU*{8BBE^h5n}gJI0F&Wbq@A4=*p!wqsZ>D{$=GEU?wQv1(UYnEHKra?%@VVGRerAr$1ZZg&U%r|dPrH_6AI`0S07AEMkPdknCQdQ z0@&4_$w?7=+_J;!VJf=iGcz;R;g+2RyYn0Soi-IO9b;A&V1!x8apKR?yaRxJqQo!X zRP$8^=Fsij8|EtVmTq;$l-sgnH6bmtV^u8(8tZ@<@A)Fp#iAoEDY;R?vSXvi{Fkn! zFYiBmU{ktMfJJ$q02)DrXsUN$zjxxUWpVz(=Y<%PV*DI=rh;}z5qIV}YKA^hU+)o0 zD7(|P?4*HD7-L!W4O1b$QWL#ufl_Hd+U?dmXhz@8?nGolgoKr&BM#i4kIdxL8(ra= z?$IlT^K8-~1fedUaCe@!?Dpugez?*%{2?+;ILF)-J>M&1h|y*ztZ{0}77c{!xDLmM zN^k1S_oTYEfVJVFp`)|+6g&;as&GbasM=JQ88K1T{Oigxh=n0z zJ==a~>Eoef@rsU>-+)&ljAPRMS2K|a5QF(APeGox*<7yx(SUaYan$qUD zNm8}+TmE$;f1wD9;;J_YBeM4I6Ls4&NN(q&9yO4huTarc0ELTtpb^JxOKb+Hb2$r9 zT?{BpDNzVp#EtB$Vm3sN4aDI?&2C|fguQ&jTP$mLqR;oUS5lff{(6ZjAC6a; z5KCEgkdb@_tVZdMpf%tEZ}Jeh@w=c3zNlZJe~~ z>4rBe<12w7WL|J8Wdk61^LMQqq{YRJ(bFm zlFOvV?#sHKN|$!${asMU2w*%>uS!E%X5AtQUa)_r#e_b|HUW03}+p`+a4aL zkkf!}srCyJ&|oM!ho6qcolF&G)(;(dLa&030H>y;vsiIxV6?(K+{ZK=FUmm0{qJ`s zHKAhN*A1XLT={6P)pI)$Q+UO(zF5`AOw!otErutRK(wXJc3z;Z&nXMf*f zt}G`Dk4x0%+g$Xf1Qw`TgdpMleM4+l)ox+s{!6Hn{QuflQF&;rolW$U0C-;loE1gw zw;*QWo3YDY95UZiat*1dl=wk`=^Od%FSujz&pmiI{esn;KJhGyZ?iwTUJtbe1@D0e zz+2u96z>M7zmRwhWQ0bg_!lV2r;nqP((C+W<0;jtJhD?h-87&LWwIhK6)GBR3Uune z!<+w%Wf8Ksy`v+NvJE&@iZ}C=)|*L5WRWZqBq-oJQ@itGO3cFW26Ur_8YsNVMYuin ziK3o7Dj^)zN+Y|kMZ`C*p(}E zKhbuCqyM_NhQ>ao^}mvSH(G5wD1wDSRAq($E+=eD4R1He_LF1l@w@{Iu72jfq?`t zt`Y(`Dlf~Y_mXqherw>RGM;4bViSQ`fBpuE?G}UdSYJ)RJL5ac#$>d#3~kuAejuX)NLa)U<8T5`XCu1 zvjHn(J<^=gKz{wW#|WhrKtVr(GIPC}qOlgxv4Itzun~<**c;F5Z=?@cRanhs)#InF zU9|N_GA@Y+g|}VOrYjSct~)Q1X2qxZCOjJ*(XHvE4(V>B*MA&q*LLqcDvP&SI-M(S z61escwB7HJ7P=qv&=ZRnoXVx9kJgx45va3CdkM;#K`q9Wu?z_AX8UCQo)c8W54U%*p#pF75x+I2 zez#%uv>~%=f7%wWR$r#KHP#t79xRSv=bfiH2kA>!_M=9yNXiYwZN6k4N0{-OP_LZg z*E^Py#`Jl^z7)UE$4eyO0IX$zs@uN z<0MBD8zW@DGPh|4pfB<@_yo~y17FGaTh}yMj`&qR@<$PicY4LjyR(49A&3sYpMLyV zO#*3jzPb@z0N6}V20q2`64QJ9bSFB!{<*Rf%AP8t(!7<4XImC*c~0K6WR92ZP8EF1 zQ&vSA4n-H7_XnlYV0?L+2U#mKcQRFWF>?rWKMf&Eg=jr@Z%bea{6AdwjEI5iuKQI0 z_mg%-JtvTAh&qoz#ST;J80@bJz1U+!N`C~5@!}dQQqh{z9MuOj^KP(>a1pFe9y?35 zqNjRj9e)@^ZTPXxb3St$tu>RCW_bz)Z+Nle7oGMmReg+kl2n;D|LOYtOj|e^kqH2Wo?Lz#tp5?&B znA$H_dx4Dsos!DTiZ$d|1K%-)GJq{BOaI#ebHWJ9$xJ^2zCxqX%HC&yy=v|3Jb%er zljVRPVhtKbV&pja2o&9oPI&gx7*^h7%Dl$M zjfBD}-CXCMw_1OFBjfqd=`20`!5^gI=~tV~4tydN20y@yfE(CY)ke0)6GX@RNPYXjK*wD^E&j z7jwPbuAN`xWNs_fn~qx0P;bu4s9TTvuZmypSXYI*Qhxn1)4*bR3)g#TCNTvB%O^FB zq{(@(i4#b=^F%tm34VtKRcpS+a4OA?U|r}vBL&3{s{1rk_Bry1Fo`9NeaH&_7VX*B z%6hKjm%snA&!0SmU7B+1Sul}f;g1xI@#io5{5zBNe-kNgWe9_?JYXau+*`}0rly9{ zp-jf+Lsx1LYpto?{wxPmsZ;dE!Eqn@0@qw=1U=?CN^Tzsb5rk>OBqeQB662 zYP5;h2fxr!f6;J4@{sVk&qt0MrF4rm9a~prW+zXwF_n54cnP`3YT&nCQxEsMkb6{RTW>l4SE6`EOGyQIKjSu;{9n!-0_C946Qbi&Z;HTr*t zR};e*hag%(-gT;^VRsL$xi7+=Y|;3EvdIbj)Tg(cGs+mY8z9|~z{@2@=GX!bIj5dL zy^Mry8SZ(4_BsMVQO#uv<=GBUl=Sn6d(n_YG*v{FuJ`O{-4<%=eJFqr&%t41IY6K^ zQhK~MkWbi1Nn1)T>506hr5-R1|09WM8Fh^y$5gH--;jJK7`qEgY?kkGA1G_+>!Qy; zo;1PaZKEv?CiK94W2=aA^y~BWJhHm;jT?G~D|z?&42Ewu2$fIqSC!-nKk3~}+?=!5!c1)^;14YMxcQ@tA?ep!P@5*?!5K~99K0>3slMKJ&`wOz zuvT?$)-M5|?N}nWRK5nDU z7FJ%6PwE?q@f6`a1@>IA3u(IB)A<*P($ktKw-xLD%X7XuFnxaJBI~&-TVu%^Db5Z} z(vanMR*D|@O*gT3#JHQWw;VC^`yc{<*@&T9Z=?MxDfo(a7QsyC%<=FR(yH#G^Q*la z8S$7bf_-dvLYe1jwD55}TlAieG|R4&7}bZN!7){ppA{+pwJ8x~IRb$oH-46XF!Ro= z*p*O+;q`N;{%Z7L<+1s)A*JMYpRca6ZKtAlOB2nkT}kV0p`)B4(?|a}Sx;OHjuP{8 zDWa&d;E~p`qn$12yV`$!{y|z+8WCE zNnGmEc$-`teT1Gx9imraJoA+3MEVT<_9B6XZG;znLm6~pn}!Ru|AhTn==~Uiq3Fr>4jHcbFd5?6TFwjp1Xu5UYtY@B_WtZB6cVBG$ zvcEr5jaH{=M_Y|K;PN`0RQIPV)TNDaDh{d&R1l{Z+C-;_3v;UN%I!|iW6qgz1XfSr z+v@xLF}fa@!aCw$d;G|qHYXQL+}In((kW0(%hk_L;lh<8a&7-^tWCSctX>wkJ@w~yaylKS?H zsp%N~R4x6fAieOPticc3BYcWf!mL%tff;&u<&h;BqJ3>!kr}aJdhhqykqJC`wy(%9 zn_Sm5H%DKhQqvmfBb4v-cK<~94Mhs9Kc0$({otFR2S%Q2a_i4~YL0Q)w%Pz(c6q`9 zJfnFhKV?Wwb$_;P-f$+4lDungO?`TYvhE$>LZLZT%M%LQGnX{_sbO!n-NIpqJAbx^ zRK88I%2&rRZ82vmi%2b$9aLbh{;nH3#s|qgu144eT9a5%rG>y&O@RS5N_6!c-=>Op zKjPX*X2h3xxvuf_kY4+&Y;1PnlxOfr+iWN%apY}Q%v5H7QH|ZPE-M+!l!^X9m5&?x z9fAk1b-w#h2#YsZ-=8Pu;nXiSOz$}ngL5N{<@&?A;xC|dhX>kt2;>qmj`xi2D@x3^ zi}|V5BUwS^!kQ0W?~HC=bZ8Paqmbviy696f9;2dodJXkkYToc3Gm4qC;}~}hS$T;{ zq?2(#co?iueNIP$qbEwso{8`)F|EpD;Ss5?veOa_iFN6+^z-neWL%vqy`C6KFuv24 zYAUst;27CvzCw=hN-tZ8L6Wi8Rnv3bb#M_GA{t48EzKV$+C0O3gM%rS$4;vYtZ%;2 zA@m^<-|lHX*kCVY<00rOo?6`bW+Wm6B@UMlKl-vKavf{44ZLu;H-g=OX_0Ykw`r?0 z3Fx)%%jTb;9~yg)qbM-9^mi#VIZ2sPOr`y^%}tMeQ=}FV={L>K6ID*5m)fYskNf}kl9$0soYV#Fnu(aVBA0Zf}zm1J>#XvMtpRgKM(vJ`;n>KOHID&{y|r zxNzGu^=&DFljh11e!7pACfQewMw&1FIp+`1f<%_)|P+GE;ggWv5(ZMtr1 zrDhGbd00uO*-}2#YxMy78ArVn;CM3il7ZqF&xa%&dRY1$`$;u3iZNCcA@e~M8Q5Qc zN@`;Ut?Mf6a711FaA(esrnaI8Idz%PHW#tIZXHg+A_JL(7#btQ0w&s2h=Q2(rH42?#i zbY(|IQ-SdO?SAwRR^rs#t(qu`?SnvtgpO2M&%B~xGOo+`KMw^oj#7P7-pL=7Y&WfT zr=~%(RTH;}UZlSnJF0W8JukHKA1cFXzq0(xj(SK47ZCoS#@T$Yu zo}%jA^ggz8<^r6dyoMce?ahj|(OIRp`yGe#+w0q~X ztol&kdS=o}t~%^%fZ*B|f?@=i`@g52!t1bmx>;O(k>{?Xc_T&bb4oTr!S_qnk9wQA zSHKbGzB}Twa80zfaT^@oShQ@3v-0>?jZU%+E^SQrdtIii?n1sJk77(xlyXW+D8-7R zTT?H#H?hN{$-OsN7MSta*Y}#%=<{n;@mTmqH_YURtb6IeHDakb6 ze=5VX6^V6;!xXcR*=^aty1M?~E_BnS;9Y3_qHT5Y?ihkkmRqCaGm)nX?X9^$$vN+4 zrLhwe1Oi<#W@Zj^!(R*+ks77kwEP^@x&M#*{{NkjV*@>+Q@9=Pf@v#VsFu^B6rn~b zZKs=)Ggsb=2Wx_MuwOgytc2fUv*QED%}OOoC69;dM}KtOwSIR~=f|7dMoH5=*3Tar zNknja9w;cjiC(=tCMMcZB^MEN;biMFXSNt)%1gxvht~Mn#PzyffAR79-NKQA2JP$m zX&%Sp9OeiY?Y{UH$=hWQ?%jnQ@&B^qr$7a3=B){H1I$aVCOX7%eCKv)~Eg_GY|4)%D`s+r31ra!@91 ze#pI$=6SKQim)`-{u6(z^9X$^#={cc*1#u(iI{fM6mObZ0+OQ z`u4VnqN3JXDj=X(5EbGR>Z4MAy+J^X6#**=f*{EAcj(Z!iFXEJBzo4xn!*)wO)Nf{?m zb?H$xZJhs#a5axHlLNnh3?Y4D#20+W+D4rsLP%j08AAq33tLo{NRK5rc~>C_DN&XS zFV?MFe^A{5=f7bYr`hZ+WkHyRe$Jl7cg} zn(uuIv8y}`J7=Fhr^;E7dH052aCHbN$IDSL|HdwL3qjq2qd1{gg0UImCrCtA-FRX4 z5|&2BN+rB>(fp=elmz=T3g9q^hu{z9*^$KI3HGukqzPe%l_G>CLVyle*~7Rc_!gEY z!g5yYH{gmSmPgkq^A!zLF8{PTI<}fi3u@sG5YE7*iS;0!jd;v~{syE=Xc2OU8r{&G z5gr}g1f~kd_mvPW*ItYD;2Ij(3a7_=pJVoqk8t_2jMzwo2}}KRTaPy0BfIUJnzh8* z^s4Rk9D*-?t$(iCEs(h!fM3mZ&yHB5=yZ&064!b;aZVno%HZUN6XN3-52frV_d==a z6k1#Up2a3e&yvi&YGi@o4PSnUw4QwuB^WreqX$>UVXw#2vz|`L&;2&$XvEdntZNA9 ztWQrN<-#ttOwD@CP&&QvQJ^ToWu6)|87I^{6}Ldh;tp2;e|Y}t)mIbL&MFw{+Sg)6{DYozt|2Wgj4MsBW6djCm~61T@?uYBHQz8Ork9hjxHwz% z*ogP(0gJ?2lDL!w*4MrE?au}Wxv`SLA=FWoDx!cly?e2zZ?5QZ&WYZQ9jvG-x7KXG zE3h25RV|JJWD5S9y@*eBQVSF<@I806Q@=zaF!KJ&+q~pFJHkH~*H8wRweY<~NF)`| zJ@}{0x`YcimQzKE$ZC1K;0ls=vi&7!DM9T@_Z2RT3T2BENW*4&xS z|8BIAS-$%MGWpb4!piY=$2}JCmhe-pDX{mYtPQ3Q&~^*|L2Hh|E%<&__zg`Uct25 zjd71?shV1$G+ey`ZD{wRRe%KJlaLU2Us6Qxr6a<#@rSNv5EbpBI(6Yrb^KF-N~Gem zBo7oQ{imdBe$|89Agfpne$J&6qvc4XTqLNd<8+ER!&l#ZzXBg+1`v$ECb!0<$-sC- zsM-0i9G7p(+No_cg=OL{_W(l*jb+W!7dLw4ly+$E$aAcv#Wr2 zTMLbCi&ACgN;)n^A+qL%$I(vi`)G~(3v*`|7pD2%{QZ8tvM;^&$Z??-a)!Me$H1f1 zO5oY9FY|PwJIWD30XGbEhO26!T*RP;cmzM)Y0+G|SVgf3KY!#!2~vurp9EwY)Ly=w z%C7*;lm+z$0dkcoveDnQAoYbW5$Q`r>5hv~JNZ>U%JXX%$?d6ohwn>iIp&m8EhCg;O(t*>KJ^U9?U**<>%Y&BYJYxZvT=x+SF zj<(f`aMyBg{)_J~@Lr$v^?RKk`-m2K9tbN*d|YK2B_uQ5F2t!**tRHHT_g?krIBpl zh*@Z36A^YGHxE>~60uoqiHHm1?zyR37z7|~L_!iSKJJy^T)er0vUZ{E3os%1ScG)D z_k&Df5_NoQV;B;F0I84uha(S!tU@_}@p*YA5=rA;=Sh5I3{~~8rZ#83LA%h0T-*c6 z_NxBXZgaEMGHDQgzlG`Hd9lYihMv3jeL-W;xOs7OiQ_{g?WDvnrfzQX;?Uds3CDX& zefVB$Ev|=3l^Y!#)FR&yXGcNhv`KNT$@ULAid?6+BT5x%hT@nFwS{_&f&2-HiG)j)uKS$Bj?{1wq^OQK+JqthLOQeVtzbmV^;n%hxNY2 z79v=Lv@PNWHX)W-L10V@2PG&JOOYNv%|o5s z3L+M)->D{u(D8=8WqNK)HC!gd<_NyNi(*R3O?5i*jd&d=&ny)=qfy_`D7dt9ohFQ5r312zmQ2~s2b;q z7aDA=tg|vDb69{k=XH3jG@lQYu<19q`(3QQ@=TDBCpp)U{b4v;RvY(<5zj7tI<2m1 z+GEKJMa27=#r86_wP|=gzcS?M(NYziYt&%pBs&#c_K8{g>1*t$W92^Em1(D8_Ie-I z6=gYR86Z6o7MAu9Z+q8H#&IieIgrHer8ik>Gp5wXK zjyzrzd$rsBJo-_0csGGb>~+4#E$UafbQg#4T26!oE_QJ#aoJ4INjnT@yhzKdE{kc8 zDDdvikEpVZu)tSuh=>^!0zvpYH*I81 zb@le@~;H?vjz0ytN1n8wDQ>cGL!&|v{qRQ{Z_A*YXH zz6Z7)coZSvXs2hxYdkDYP66(u8qWYppV9-e-BPHgOdIxic=YLAWH!@&qyO-afAk&|v{aBaAkN!SQEBgKq<^`wck5e!~ngRC98^3po8b;#6NZVIc(B zij~Te(Zzw4bYR$y2l@1cFTO_gPkrHw3)oW~HW(T@EWlyC?>=o1hqnjBe;qJ>zL7j| z`$3x4H2O|G?IBGdzpn=V`GXOeh(FRLv92LY?*N}jvhP6q?)1(6K$kuuc4`CItX^=w zeB_Sv5m`Rj3g+i}!zbIYr#f&jG;~-%5QfG(fW}L(l4cT#sH5~v@tYjcm4n}YD~CxC z1a_B;#hRtJ;Z4)Tne{a_HIpHx3Y+NYu=%qpq}YTK>mwZk!(;z9iq%YXSU6-i*sdmK z>6@cm-iyw5fZdS1HyTmNf*HuB(Wg7W)&75>pY+X+2o0_IY~J=SdmI0~Ms#`n%}s|V zK6gr6K6yNZ%rKmJEN`N5EXA+b)^Eq-&k~sIsQQsMY%L6%=KNy}eP`;2S~K@>$aL|| z6M?x@Us!qp{f1hD`4DX%$8|6;^({$8X@!NsPA=-h1*i3-n=&IUyY;;K%S*0t8mH%v$sh<+USb|CY)0h6J1GhGcqrpoMn zewlCQK~ws+qbsv(4|qb*nN!W9SFG>Waoha5X}~qcK5Gb?>~`(pa`Z#mw*!;W9|CgR zSq8wc`em(M{@Yp5l<<^$d3fL{?d!upq1fWWV)E(En@3Uq=0Ux9N zZ}NPuo|ms|Uo!zpapf^q0$)Ww`Mg@6oRz*Ky#oz4@bc|Esqx3}11BfzEWt2Dwr(@2 z7YZ}pIG=hj_nh?dC-{t7g(WzFMN?TvO4V&Tk2E@6C^N$^6R+QPzjh0P3TB{H_N`;x zH#X&UgiIPNam2MNzi;Z5DzOsS5un7aC>3n!&AI1P0p4K89+7kKhP-P9@Pi&8M?tpu z#p&KXBr3(P8g&5uc4)rp08UF~*zc@Ewmkvasbh|g$@&&oK*1MKYSJ6tJz@3-jmd7) zpg6E99NeXN_RMp15`v)fTx80jiGVcYdiwfKbt%G2E%YGYY^6Y;OIARJ>I-vFn$jCE zy=4#@Od}mc0>YHEAt?Toa?N}akq9wzjsTE27|NBOhQfn%kiMd>j+$ z?dl4^6pz6k2UpBGGz11F6o;VrYY9b-;!#$ncQS%09z%qoiM)g6X7!h%S;>_Bymhnq zR#QJ*m|U@M-VX18DpztAL^Jv)?MxQ=A^vpcM{B-`opEA?B_&ywWUa6=3A)}c5Aw7& zYsmcS;j=>OlTK%B(m6m1e=P~|$1H<9&KXah(Ae0xR>W^=8r?n&ocgq?5NPAqcl2pL zpuc{egDnqf$@Z_O?JPR7CV8zW)YEl;=j)12wPP-Ie><@0`>{G%>VnhaQ@G_3?pJp zs(%i5t%ypc8bXV15Vv(W~FG}`P^+Bb6o%STeH6**-#i|9Jus=h4A_Fv=Oz`-H_?P zBd99xV3rV^bOEdQM4OCP!R}DNRAAklRd@cev9a;%+6>pc{pM1(z7^qS>MK6NCsej% z<1_vqrRqP)9|w`Q+RNp;^chegD+x1{ArUDFiaqnMcxIjT8jz_zULD0fO0;*vO`bHnDCAz9|`#n5S2pK;Gds&z{2nT z{tuv(pRQ0D;KDpVd&x1d_Tg1jTAF#k4d$prjbr4I2SrV#8TZ=^TUg+$y!|DS^6aLP zu)SV^p`rPVM`;MJ7f!$5*VpX@dekTe__gnR5aShiDNNyvBah+j3Ji*u} z>!>c_a1yssoAAhnazmb*T=gIY{Y$EjYlxin{FS(oM6srRnOaN=YOQH4b34@%ht|6+Z6>lPiMkBIfzN&*Yi&y-NWG#~;}eoiudKH|CVrfYs@XXi9% z&t|~N>0psaujTD$AXZQ&XWoAg2#Mf~+JEZ`84+uC?gB@8jVF7(|H*{rHgmA7G-^<; zEJJCyp)Rp9d!e_#R*=Y3CP^?U`ahZ8`|^R6Bc2tWv8xOJ!OJq%uqRm z4K^nqf&$aH2cdWe4VBm~tHI90$aoM5jEo17z?No*ka!38&;3DLPOv?%14Q%JGivC# z<&7AoE)D^LjkAMLVB_o{62Id(%S7snC!j}OoZT#SCdJRbIP?x}WZ7wbW-su3>fEtD zE4VP$BFmPE-fz5V*zKGT$1-%!8S8W1vr{XE74$}Q>9(;piDFIF!iDv!7Kcs09|0d4 zL5RT^jcV|~*l~a?Fv+6@0+T#i9DYa1GeBY53mF?awLoB6f))o%0uS&9CV{m;U=mo1 z115n77z3N9X@S6|q*@#>6=HxfFcm@z1g1g^5Co<|Xo0{~2rUkOZ54u?A38;2IXO>Hzs; z1gr%D<5evVm{=NM3`_!RfxreEEe_;&Zd@JT-DGrb$_*X^ZNQH0d$!TG96J3UFU62M literal 1233949 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∈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≷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

IVrnh+_ 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

=ZP)yfuUmxCQxZiTQAb1t7c*t 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 wqXzs(sR5HFXh!bO$o)wu@zv@7wcMW Date: Thu, 23 Nov 2023 20:05:33 +0530 Subject: [PATCH 089/146] fix: return none instead of err when payment method data is not found for bank debit during listing (#2967) --- crates/router/src/core/payment_methods/cards.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 85a0ca5f2441..044e270a7ea9 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -2459,8 +2459,7 @@ async fn get_bank_account_connector_details( })) } }, - None => Err(errors::ApiErrorResponse::InternalServerError.into()) - .attach_printable("Unable to fetch payment method data"), + None => Ok(None), } } From 3322103f5c9b7c2a5b663980246c6ca36b8dc63e Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Fri, 24 Nov 2023 12:33:30 +0530 Subject: [PATCH 090/146] feat(router): add `connector_transaction_id` in error_response from connector flows (#2972) --- crates/data_models/src/payments/payment_attempt.rs | 1 + crates/diesel_models/src/payment_attempt.rs | 3 +++ crates/router/src/connector/aci.rs | 1 + crates/router/src/connector/adyen.rs | 7 +++++++ crates/router/src/connector/adyen/transformers.rs | 5 +++++ crates/router/src/connector/airwallex.rs | 1 + crates/router/src/connector/authorizedotnet.rs | 3 +++ .../src/connector/authorizedotnet/transformers.rs | 4 ++++ crates/router/src/connector/bambora.rs | 1 + crates/router/src/connector/bankofamerica.rs | 1 + .../src/connector/bankofamerica/transformers.rs | 3 +++ crates/router/src/connector/bitpay.rs | 1 + crates/router/src/connector/bluesnap.rs | 3 +++ crates/router/src/connector/boku.rs | 2 ++ crates/router/src/connector/braintree.rs | 2 ++ .../braintree/braintree_graphql_transformers.rs | 1 + crates/router/src/connector/cashtocode.rs | 1 + .../router/src/connector/cashtocode/transformers.rs | 1 + crates/router/src/connector/checkout.rs | 1 + .../router/src/connector/checkout/transformers.rs | 2 ++ crates/router/src/connector/coinbase.rs | 1 + crates/router/src/connector/cryptopay.rs | 1 + crates/router/src/connector/cybersource.rs | 1 + .../src/connector/cybersource/transformers.rs | 1 + crates/router/src/connector/dlocal.rs | 1 + crates/router/src/connector/dummyconnector.rs | 1 + crates/router/src/connector/fiserv.rs | 2 ++ crates/router/src/connector/forte.rs | 1 + crates/router/src/connector/globalpay.rs | 2 ++ crates/router/src/connector/globepay.rs | 1 + .../router/src/connector/globepay/transformers.rs | 1 + crates/router/src/connector/gocardless.rs | 1 + crates/router/src/connector/helcim.rs | 1 + crates/router/src/connector/iatapay.rs | 2 ++ crates/router/src/connector/klarna.rs | 1 + crates/router/src/connector/mollie.rs | 1 + crates/router/src/connector/multisafepay.rs | 1 + .../src/connector/multisafepay/transformers.rs | 3 +++ crates/router/src/connector/nexinets.rs | 1 + crates/router/src/connector/nmi/transformers.rs | 1 + crates/router/src/connector/noon.rs | 1 + crates/router/src/connector/noon/transformers.rs | 1 + crates/router/src/connector/nuvei/transformers.rs | 1 + crates/router/src/connector/opayo.rs | 1 + crates/router/src/connector/opennode.rs | 1 + crates/router/src/connector/payeezy.rs | 1 + crates/router/src/connector/payme.rs | 1 + crates/router/src/connector/payme/transformers.rs | 2 ++ crates/router/src/connector/paypal.rs | 3 +++ crates/router/src/connector/payu.rs | 2 ++ crates/router/src/connector/powertranz.rs | 1 + .../router/src/connector/powertranz/transformers.rs | 2 ++ crates/router/src/connector/prophetpay.rs | 1 + .../router/src/connector/prophetpay/transformers.rs | 5 +++++ crates/router/src/connector/rapyd.rs | 1 + crates/router/src/connector/rapyd/transformers.rs | 2 ++ crates/router/src/connector/shift4.rs | 1 + crates/router/src/connector/square.rs | 1 + crates/router/src/connector/stax.rs | 1 + crates/router/src/connector/stripe.rs | 13 +++++++++++++ crates/router/src/connector/stripe/transformers.rs | 7 +++++++ crates/router/src/connector/trustpay.rs | 3 +++ .../router/src/connector/trustpay/transformers.rs | 8 ++++++++ crates/router/src/connector/tsys/transformers.rs | 1 + crates/router/src/connector/volt.rs | 1 + crates/router/src/connector/wise.rs | 4 ++++ crates/router/src/connector/worldpay.rs | 1 + crates/router/src/connector/zen.rs | 1 + crates/router/src/core/payments/access_token.rs | 1 + .../core/payments/operations/payment_response.rs | 1 + crates/router/src/core/payments/retry.rs | 1 + crates/router/src/services/api.rs | 3 +++ crates/router/src/types.rs | 3 +++ crates/router/src/types/api.rs | 1 + crates/router/src/utils.rs | 1 + crates/router/src/workflows/payment_sync.rs | 1 + crates/storage_impl/src/payments/payment_attempt.rs | 4 ++++ 77 files changed, 153 insertions(+) diff --git a/crates/data_models/src/payments/payment_attempt.rs b/crates/data_models/src/payments/payment_attempt.rs index 80ae283be85b..b866237745fb 100644 --- a/crates/data_models/src/payments/payment_attempt.rs +++ b/crates/data_models/src/payments/payment_attempt.rs @@ -326,6 +326,7 @@ pub enum PaymentAttemptUpdate { updated_by: String, unified_code: Option>, unified_message: Option>, + connector_transaction_id: Option, }, CaptureUpdate { amount_to_capture: Option, diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index 82ab9a1c02e1..42af827f522b 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -243,6 +243,7 @@ pub enum PaymentAttemptUpdate { updated_by: String, unified_code: Option>, unified_message: Option>, + connector_transaction_id: Option, }, CaptureUpdate { amount_to_capture: Option, @@ -543,6 +544,7 @@ impl From for PaymentAttemptUpdateInternal { updated_by, unified_code, unified_message, + connector_transaction_id, } => Self { connector, status: Some(status), @@ -556,6 +558,7 @@ impl From for PaymentAttemptUpdateInternal { tax_amount, unified_code, unified_message, + connector_transaction_id, ..Default::default() }, PaymentAttemptUpdate::StatusUpdate { status, updated_by } => Self { diff --git a/crates/router/src/connector/aci.rs b/crates/router/src/connector/aci.rs index f51c91f441df..f6384bf0a5c5 100644 --- a/crates/router/src/connector/aci.rs +++ b/crates/router/src/connector/aci.rs @@ -79,6 +79,7 @@ impl ConnectorCommon for Aci { .join("; ") }), attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/adyen.rs b/crates/router/src/connector/adyen.rs index 676f15d2f564..e101b796b8d4 100644 --- a/crates/router/src/connector/adyen.rs +++ b/crates/router/src/connector/adyen.rs @@ -74,6 +74,7 @@ impl ConnectorCommon for Adyen { message: response.message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -256,6 +257,7 @@ impl message: response.message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -375,6 +377,7 @@ impl message: response.message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -546,6 +549,7 @@ impl message: response.message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } @@ -716,6 +720,7 @@ impl message: response.message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -920,6 +925,7 @@ impl message: response.message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -1439,6 +1445,7 @@ impl services::ConnectorIntegration { @@ -929,6 +931,7 @@ fn get_error_response( reason: Some(message.to_string()), status_code, attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/authorizedotnet/transformers.rs b/crates/router/src/connector/authorizedotnet/transformers.rs index 884504154e8f..2c8a63a53e5c 100644 --- a/crates/router/src/connector/authorizedotnet/transformers.rs +++ b/crates/router/src/connector/authorizedotnet/transformers.rs @@ -574,6 +574,7 @@ impl reason: None, status_code: item.http_code, attempt_status: None, + connector_transaction_id: None, }) }); let metadata = transaction_response @@ -649,6 +650,7 @@ impl reason: None, status_code: item.http_code, attempt_status: None, + connector_transaction_id: None, }) }); let metadata = transaction_response @@ -792,6 +794,7 @@ impl TryFrom types::Error reason: None, status_code, attempt_status: None, + connector_transaction_id: None, } } diff --git a/crates/router/src/connector/bambora.rs b/crates/router/src/connector/bambora.rs index ff6fdcb46769..19849763ed8e 100644 --- a/crates/router/src/connector/bambora.rs +++ b/crates/router/src/connector/bambora.rs @@ -96,6 +96,7 @@ impl ConnectorCommon for Bambora { message: response.message, reason: Some(serde_json::to_string(&response.details).unwrap_or_default()), attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/bankofamerica.rs b/crates/router/src/connector/bankofamerica.rs index b6e19fa0d296..a01ea72338c5 100644 --- a/crates/router/src/connector/bankofamerica.rs +++ b/crates/router/src/connector/bankofamerica.rs @@ -233,6 +233,7 @@ impl ConnectorCommon for Bankofamerica { message, reason: Some(connector_reason), attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index 8af7cfd6c45e..70db9a6d8797 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -540,6 +540,7 @@ impl reason: error_response.error_information.reason, status_code: item.http_code, attempt_status: None, + connector_transaction_id: None, }), ..item.data }), @@ -596,6 +597,7 @@ impl reason: error_response.error_information.reason, status_code: item.http_code, attempt_status: None, + connector_transaction_id: None, }), ..item.data }), @@ -652,6 +654,7 @@ impl reason: error_response.error_information.reason, status_code: item.http_code, attempt_status: None, + connector_transaction_id: None, }), ..item.data }), diff --git a/crates/router/src/connector/bitpay.rs b/crates/router/src/connector/bitpay.rs index 856d0a9ec9d7..b6bbaafc4a38 100644 --- a/crates/router/src/connector/bitpay.rs +++ b/crates/router/src/connector/bitpay.rs @@ -121,6 +121,7 @@ impl ConnectorCommon for Bitpay { message: response.error, reason: response.message, attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/bluesnap.rs b/crates/router/src/connector/bluesnap.rs index d1aa1fa25ee6..0bc56d4e9955 100644 --- a/crates/router/src/connector/bluesnap.rs +++ b/crates/router/src/connector/bluesnap.rs @@ -127,6 +127,7 @@ impl ConnectorCommon for Bluesnap { .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), reason: Some(reason), attempt_status: None, + connector_transaction_id: None, } } bluesnap::BluesnapErrors::Auth(error_res) => ErrorResponse { @@ -135,6 +136,7 @@ impl ConnectorCommon for Bluesnap { message: error_res.error_name.clone().unwrap_or(error_res.error_code), reason: Some(error_res.error_description), attempt_status: None, + connector_transaction_id: None, }, bluesnap::BluesnapErrors::General(error_response) => { let (error_res, attempt_status) = if res.status_code == 403 @@ -156,6 +158,7 @@ impl ConnectorCommon for Bluesnap { message: error_response, reason: Some(error_res), attempt_status, + connector_transaction_id: None, } } }; diff --git a/crates/router/src/connector/boku.rs b/crates/router/src/connector/boku.rs index 87e8fd0eb96a..a2ae9d628134 100644 --- a/crates/router/src/connector/boku.rs +++ b/crates/router/src/connector/boku.rs @@ -131,6 +131,7 @@ impl ConnectorCommon for Boku { message: response.message, reason: response.reason, attempt_status: None, + connector_transaction_id: None, }), Err(_) => get_xml_deserialized(res), } @@ -668,6 +669,7 @@ fn get_xml_deserialized(res: Response) -> CustomResult Ok(ErrorResponse { @@ -141,6 +142,7 @@ impl ConnectorCommon for Braintree { message: consts::NO_ERROR_MESSAGE.to_string(), reason: Some(response.errors), attempt_status: None, + connector_transaction_id: None, }), Err(error_msg) => { logger::error!(deserialization_error =? error_msg); diff --git a/crates/router/src/connector/braintree/braintree_graphql_transformers.rs b/crates/router/src/connector/braintree/braintree_graphql_transformers.rs index bf51973237c5..5069a9fe38d2 100644 --- a/crates/router/src/connector/braintree/braintree_graphql_transformers.rs +++ b/crates/router/src/connector/braintree/braintree_graphql_transformers.rs @@ -317,6 +317,7 @@ fn get_error_response( reason: error_reason, status_code: http_code, attempt_status: None, + connector_transaction_id: None, }) } diff --git a/crates/router/src/connector/cashtocode.rs b/crates/router/src/connector/cashtocode.rs index a8d7d6d80504..6749f4189340 100644 --- a/crates/router/src/connector/cashtocode.rs +++ b/crates/router/src/connector/cashtocode.rs @@ -120,6 +120,7 @@ impl ConnectorCommon for Cashtocode { message: response.error_description, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/cashtocode/transformers.rs b/crates/router/src/connector/cashtocode/transformers.rs index 42e47c077e8c..cfca998e06c3 100644 --- a/crates/router/src/connector/cashtocode/transformers.rs +++ b/crates/router/src/connector/cashtocode/transformers.rs @@ -218,6 +218,7 @@ impl message: error_data.error_description, reason: None, attempt_status: None, + connector_transaction_id: None, }), ), CashtocodePaymentsResponse::CashtoCodeData(response_data) => { diff --git a/crates/router/src/connector/checkout.rs b/crates/router/src/connector/checkout.rs index ca2556544f90..312a91196de7 100644 --- a/crates/router/src/connector/checkout.rs +++ b/crates/router/src/connector/checkout.rs @@ -132,6 +132,7 @@ impl ConnectorCommon for Checkout { .map(|errors| errors.join(" & ")) .or(response.error_type), attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/checkout/transformers.rs b/crates/router/src/connector/checkout/transformers.rs index 6ad040da2842..90e65c8b0474 100644 --- a/crates/router/src/connector/checkout/transformers.rs +++ b/crates/router/src/connector/checkout/transformers.rs @@ -577,6 +577,7 @@ impl TryFrom> .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), reason: item.response.response_summary, attempt_status: None, + connector_transaction_id: None, }) } else { None @@ -625,6 +626,7 @@ impl TryFrom> .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), reason: item.response.response_summary, attempt_status: None, + connector_transaction_id: None, }) } else { None diff --git a/crates/router/src/connector/coinbase.rs b/crates/router/src/connector/coinbase.rs index 9c0a06a52c90..b294a4474f69 100644 --- a/crates/router/src/connector/coinbase.rs +++ b/crates/router/src/connector/coinbase.rs @@ -109,6 +109,7 @@ impl ConnectorCommon for Coinbase { message: response.error.message, reason: response.error.code, attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/cryptopay.rs b/crates/router/src/connector/cryptopay.rs index 417a36145b92..2af40a298ce0 100644 --- a/crates/router/src/connector/cryptopay.rs +++ b/crates/router/src/connector/cryptopay.rs @@ -168,6 +168,7 @@ impl ConnectorCommon for Cryptopay { message: response.error.message, reason: response.error.reason, attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/cybersource.rs b/crates/router/src/connector/cybersource.rs index ce283b12b798..1868611184f9 100644 --- a/crates/router/src/connector/cybersource.rs +++ b/crates/router/src/connector/cybersource.rs @@ -137,6 +137,7 @@ impl ConnectorCommon for Cybersource { message, reason: Some(connector_reason), attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 0e81b6b59dff..33b8fa56d00e 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -552,6 +552,7 @@ impl reason: Some(error.reason), status_code: item.http_code, attempt_status: None, + connector_transaction_id: None, }), _ => Ok(types::PaymentsResponseData::TransactionResponse { resource_id: types::ResponseId::ConnectorTransactionId( diff --git a/crates/router/src/connector/dlocal.rs b/crates/router/src/connector/dlocal.rs index 4ae3a292fdae..28ae058286f0 100644 --- a/crates/router/src/connector/dlocal.rs +++ b/crates/router/src/connector/dlocal.rs @@ -136,6 +136,7 @@ impl ConnectorCommon for Dlocal { message: response.message, reason: response.param, attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/dummyconnector.rs b/crates/router/src/connector/dummyconnector.rs index 9edcd957ff09..961ef005f2f3 100644 --- a/crates/router/src/connector/dummyconnector.rs +++ b/crates/router/src/connector/dummyconnector.rs @@ -112,6 +112,7 @@ impl ConnectorCommon for DummyConnector { message: response.error.message, reason: response.error.reason, attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/fiserv.rs b/crates/router/src/connector/fiserv.rs index 2bdb7177d941..28b6d932760d 100644 --- a/crates/router/src/connector/fiserv.rs +++ b/crates/router/src/connector/fiserv.rs @@ -152,6 +152,7 @@ impl ConnectorCommon for Fiserv { reason: first_error.field.to_owned(), status_code: res.status_code, attempt_status: None, + connector_transaction_id: None, }) }) .unwrap_or(types::ErrorResponse { @@ -160,6 +161,7 @@ impl ConnectorCommon for Fiserv { reason: None, status_code: res.status_code, attempt_status: None, + connector_transaction_id: None, })) } } diff --git a/crates/router/src/connector/forte.rs b/crates/router/src/connector/forte.rs index 3aa7cee32878..948db00c936f 100644 --- a/crates/router/src/connector/forte.rs +++ b/crates/router/src/connector/forte.rs @@ -131,6 +131,7 @@ impl ConnectorCommon for Forte { message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/globalpay.rs b/crates/router/src/connector/globalpay.rs index 26494d349b88..39452e53df17 100644 --- a/crates/router/src/connector/globalpay.rs +++ b/crates/router/src/connector/globalpay.rs @@ -105,6 +105,7 @@ impl ConnectorCommon for Globalpay { message: response.detailed_error_description, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -319,6 +320,7 @@ impl ConnectorIntegration reason: Some(error_response.error_info), status_code: item.http_code, attempt_status: None, + connector_transaction_id: None, }), ..item.data }), @@ -810,6 +811,7 @@ impl TryFrom for types::ErrorResponse { reason: None, status_code: http_code, attempt_status: None, + connector_transaction_id: None, } } } diff --git a/crates/router/src/connector/noon.rs b/crates/router/src/connector/noon.rs index b6ed231e5b50..457928642554 100644 --- a/crates/router/src/connector/noon.rs +++ b/crates/router/src/connector/noon.rs @@ -137,6 +137,7 @@ impl ConnectorCommon for Noon { message: response.class_description, reason: Some(response.message), attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/noon/transformers.rs b/crates/router/src/connector/noon/transformers.rs index 27a874930bcc..5ff92582051a 100644 --- a/crates/router/src/connector/noon/transformers.rs +++ b/crates/router/src/connector/noon/transformers.rs @@ -512,6 +512,7 @@ impl reason: Some(error_message), status_code: item.http_code, attempt_status: None, + connector_transaction_id: None, }), _ => { let connector_response_reference_id = diff --git a/crates/router/src/connector/nuvei/transformers.rs b/crates/router/src/connector/nuvei/transformers.rs index c23114e2a96b..25562f54bfeb 100644 --- a/crates/router/src/connector/nuvei/transformers.rs +++ b/crates/router/src/connector/nuvei/transformers.rs @@ -1580,6 +1580,7 @@ fn get_error_response( reason: None, status_code: http_code, attempt_status: None, + connector_transaction_id: None, }) } diff --git a/crates/router/src/connector/opayo.rs b/crates/router/src/connector/opayo.rs index ba0fb2046b7c..73a793adcf70 100644 --- a/crates/router/src/connector/opayo.rs +++ b/crates/router/src/connector/opayo.rs @@ -108,6 +108,7 @@ impl ConnectorCommon for Opayo { message: response.message, reason: response.reason, attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/opennode.rs b/crates/router/src/connector/opennode.rs index 41d1e6c3d88c..c4f3d3682dca 100644 --- a/crates/router/src/connector/opennode.rs +++ b/crates/router/src/connector/opennode.rs @@ -111,6 +111,7 @@ impl ConnectorCommon for Opennode { message: response.message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/payeezy.rs b/crates/router/src/connector/payeezy.rs index 33a8ec65152e..0be640f8fbe4 100644 --- a/crates/router/src/connector/payeezy.rs +++ b/crates/router/src/connector/payeezy.rs @@ -124,6 +124,7 @@ impl ConnectorCommon for Payeezy { message: error_messages.join(", "), reason: None, attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/payme.rs b/crates/router/src/connector/payme.rs index 1e67f8a9f350..84367b3a96f6 100644 --- a/crates/router/src/connector/payme.rs +++ b/crates/router/src/connector/payme.rs @@ -98,6 +98,7 @@ impl ConnectorCommon for Payme { response.status_error_details, response.status_additional_info )), attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/payme/transformers.rs b/crates/router/src/connector/payme/transformers.rs index 24b7f2b3a0bd..092a8b49fd86 100644 --- a/crates/router/src/connector/payme/transformers.rs +++ b/crates/router/src/connector/payme/transformers.rs @@ -227,6 +227,7 @@ impl From<(&PaymePaySaleResponse, u16)> for types::ErrorResponse { reason: pay_sale_response.status_error_details.to_owned(), status_code: http_code, attempt_status: None, + connector_transaction_id: None, } } } @@ -310,6 +311,7 @@ impl From<(&SaleQuery, u16)> for types::ErrorResponse { reason: sale_query_response.sale_error_text.clone(), status_code: http_code, attempt_status: None, + connector_transaction_id: None, } } } diff --git a/crates/router/src/connector/paypal.rs b/crates/router/src/connector/paypal.rs index 0e8cff8c0569..4e50bc924b33 100644 --- a/crates/router/src/connector/paypal.rs +++ b/crates/router/src/connector/paypal.rs @@ -92,6 +92,7 @@ impl Paypal { message: response.message.clone(), reason: error_reason.or(Some(response.message)), attempt_status: None, + connector_transaction_id: None, }) } } @@ -245,6 +246,7 @@ impl ConnectorCommon for Paypal { message: response.message.clone(), reason, attempt_status: None, + connector_transaction_id: None, }) } } @@ -380,6 +382,7 @@ impl ConnectorIntegration reason: Some(item.response.response_text), status_code: item.http_code, attempt_status: None, + connector_transaction_id: None, }), ..item.data }) @@ -467,6 +468,7 @@ impl reason: Some(item.response.response_text), status_code: item.http_code, attempt_status: None, + connector_transaction_id: None, }), ..item.data }) @@ -515,6 +517,7 @@ impl reason: Some(item.response.response_text), status_code: item.http_code, attempt_status: None, + connector_transaction_id: None, }), ..item.data }) @@ -625,6 +628,7 @@ impl TryFrom TryFrom { logger::error!(deserialization_error =? error_msg); diff --git a/crates/router/src/connector/rapyd/transformers.rs b/crates/router/src/connector/rapyd/transformers.rs index 08985ba022fc..898b6ed6d147 100644 --- a/crates/router/src/connector/rapyd/transformers.rs +++ b/crates/router/src/connector/rapyd/transformers.rs @@ -458,6 +458,7 @@ impl message: item.response.status.status.unwrap_or_default(), reason: data.failure_message.to_owned(), attempt_status: None, + connector_transaction_id: None, }), ), _ => { @@ -499,6 +500,7 @@ impl message: item.response.status.status.unwrap_or_default(), reason: item.response.status.message, attempt_status: None, + connector_transaction_id: None, }), ), }; diff --git a/crates/router/src/connector/shift4.rs b/crates/router/src/connector/shift4.rs index 6f3a2b802014..dfb4a7de0811 100644 --- a/crates/router/src/connector/shift4.rs +++ b/crates/router/src/connector/shift4.rs @@ -100,6 +100,7 @@ impl ConnectorCommon for Shift4 { message: response.error.message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/square.rs b/crates/router/src/connector/square.rs index d836285755d4..1f1dee6b9e1b 100644 --- a/crates/router/src/connector/square.rs +++ b/crates/router/src/connector/square.rs @@ -124,6 +124,7 @@ impl ConnectorCommon for Square { .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), reason: Some(reason), attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/stax.rs b/crates/router/src/connector/stax.rs index 024211c8caaa..1a0cc54a128d 100644 --- a/crates/router/src/connector/stax.rs +++ b/crates/router/src/connector/stax.rs @@ -110,6 +110,7 @@ impl ConnectorCommon for Stax { .to_owned(), ), attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/stripe.rs b/crates/router/src/connector/stripe.rs index ccf843ec78d6..475105c9cebe 100644 --- a/crates/router/src/connector/stripe.rs +++ b/crates/router/src/connector/stripe.rs @@ -227,6 +227,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -357,6 +358,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -483,6 +485,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -617,6 +620,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -760,6 +764,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -918,6 +923,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -1041,6 +1047,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -1197,6 +1204,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -1318,6 +1326,7 @@ impl services::ConnectorIntegration .or(Some(error.message.clone())), status_code: item.http_code, attempt_status: None, + connector_transaction_id: None, }); let connector_metadata = @@ -2788,6 +2789,12 @@ pub struct ErrorDetails { pub message: Option, pub param: Option, pub decline_code: Option, + pub payment_intent: Option, +} + +#[derive(Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct PaymentIntentErrorResponse { + pub id: String, } #[derive(Debug, Default, Eq, PartialEq, Deserialize, Serialize)] diff --git a/crates/router/src/connector/trustpay.rs b/crates/router/src/connector/trustpay.rs index 65ab5a7ba58d..2430aac6c19f 100644 --- a/crates/router/src/connector/trustpay.rs +++ b/crates/router/src/connector/trustpay.rs @@ -139,6 +139,7 @@ impl ConnectorCommon for Trustpay { .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), reason: reason.or(response_data.description), attempt_status: None, + connector_transaction_id: None, }) } Err(error_msg) => { @@ -298,6 +299,7 @@ impl ConnectorIntegration TryFrom( updated_by: storage_scheme.to_string(), unified_code: option_gsm.clone().map(|gsm| gsm.unified_code), unified_message: option_gsm.map(|gsm| gsm.unified_message), + connector_transaction_id: err.connector_transaction_id, }), ) } diff --git a/crates/router/src/core/payments/retry.rs b/crates/router/src/core/payments/retry.rs index f16f7629578b..c5501ab4dc3b 100644 --- a/crates/router/src/core/payments/retry.rs +++ b/crates/router/src/core/payments/retry.rs @@ -415,6 +415,7 @@ where updated_by: storage_scheme.to_string(), unified_code: option_gsm.clone().map(|gsm| gsm.unified_code), unified_message: option_gsm.map(|gsm| gsm.unified_message), + connector_transaction_id: error_response.connector_transaction_id.clone(), }, storage_scheme, ) diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index aae17195517d..5481d5c5cf9d 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -224,6 +224,7 @@ pub trait ConnectorIntegration: ConnectorIntegrationAny, pub status_code: u16, pub attempt_status: Option, + pub connector_transaction_id: Option, } impl ErrorResponse { @@ -992,6 +993,7 @@ impl ErrorResponse { reason: None, status_code: http::StatusCode::INTERNAL_SERVER_ERROR.as_u16(), attempt_status: None, + connector_transaction_id: None, } } } @@ -1035,6 +1037,7 @@ impl From for ErrorResponse { _ => 500, }, attempt_status: None, + connector_transaction_id: None, } } } diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index b7d2fc8db33e..bcb3a9add553 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -114,6 +114,7 @@ pub trait ConnectorCommon { message: consts::NO_ERROR_MESSAGE.to_string(), reason: None, attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index 83586e51d66a..901e84997e67 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -405,6 +405,7 @@ pub fn handle_json_response_deserialization_failure( message: consts::UNSUPPORTED_ERROR_MESSAGE.to_string(), reason: Some(response_data), attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/workflows/payment_sync.rs b/crates/router/src/workflows/payment_sync.rs index c4b35cd6301a..04f91f30bc7e 100644 --- a/crates/router/src/workflows/payment_sync.rs +++ b/crates/router/src/workflows/payment_sync.rs @@ -140,6 +140,7 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow { updated_by: merchant_account.storage_scheme.to_string(), unified_code: None, unified_message: None, + connector_transaction_id: None, }; payment_data.payment_attempt = db diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index 238a2d75087c..0526fcec9c53 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -1325,6 +1325,7 @@ impl DataModelExt for PaymentAttemptUpdate { updated_by, unified_code, unified_message, + connector_transaction_id, } => DieselPaymentAttemptUpdate::ErrorUpdate { connector, status, @@ -1337,6 +1338,7 @@ impl DataModelExt for PaymentAttemptUpdate { updated_by, unified_code, unified_message, + connector_transaction_id, }, Self::CaptureUpdate { multiple_capture_count, @@ -1588,6 +1590,7 @@ impl DataModelExt for PaymentAttemptUpdate { updated_by, unified_code, unified_message, + connector_transaction_id, } => Self::ErrorUpdate { connector, status, @@ -1600,6 +1603,7 @@ impl DataModelExt for PaymentAttemptUpdate { tax_amount, unified_code, unified_message, + connector_transaction_id, }, DieselPaymentAttemptUpdate::CaptureUpdate { amount_to_capture, From e0bde433282a34eb9eb28a2d9c43c2b17b5e65e5 Mon Sep 17 00:00:00 2001 From: Vedant Khairnar Date: Fri, 24 Nov 2023 14:29:29 +0530 Subject: [PATCH 091/146] docs(README): Updated Community Platform Mentions (#2960) Co-authored-by: Sanchith Hegde <22217505+SanchithHegde@users.noreply.github.com> --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8c5ad9e03b2d..e820b93e63cc 100644 --- a/README.md +++ b/README.md @@ -252,7 +252,8 @@ We welcome contributions from the community. Please read through our Included are directions for opening issues, coding standards, and notes on development. -🦀 **Important note for Rust developers**: We aim for contributions from the community +- We appreciate all types of contributions: code, documentation, demo creation, or something new way you want to contribute to us. We will reward every contribution with a Hyperswitch branded t-shirt. +- 🦀 **Important note for Rust developers**: We aim for contributions from the community across a broad range of tracks. Hence, we have prioritised simplicity and code readability over purely idiomatic code. For example, some of the code in core functions (e.g., `payments_core`) is written to be more readable than @@ -264,10 +265,9 @@ pure-idiomatic. Get updates on Hyperswitch development and chat with the community: -- Read and subscribe to [the official Hyperswitch blog][blog]. -- Join our [Discord server][discord]. -- Join our [Slack workspace][slack]. -- Ask and explore our [GitHub Discussions][github-discussions]. +- [Discord server][discord] for questions related to contributing to hyperswitch, questions about the architecture, components, etc. +- [Slack workspace][slack] for questions related to integrating hyperswitch, integrating a connector in hyperswitch, etc. +- [GitHub Discussions][github-discussions] to drop feature requests or suggest anything payments-related you need for your stack. [blog]: https://hyperswitch.io/blog [discord]: https://discord.gg/wJZ7DVW8mm From 97a38a78e514e4fa3b5db46b6de985be6312dcc3 Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Fri, 24 Nov 2023 17:18:10 +0530 Subject: [PATCH 092/146] fix(core): Error propagation for not supporting partial refund (#2976) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .../src/connector/prophetpay/transformers.rs | 5 +- crates/router/src/core/errors/utils.rs | 90 +++++++++++++++---- 2 files changed, 74 insertions(+), 21 deletions(-) diff --git a/crates/router/src/connector/prophetpay/transformers.rs b/crates/router/src/connector/prophetpay/transformers.rs index a2c3c55c02b8..d81b931edfc9 100644 --- a/crates/router/src/connector/prophetpay/transformers.rs +++ b/crates/router/src/connector/prophetpay/transformers.rs @@ -583,10 +583,7 @@ impl TryFrom<&ProphetpayRouterData<&types::RefundsRouterData>> for Prophet action_type: ProphetpayActionType::get_action_type(&ProphetpayActionType::Refund), }) } else { - Err(errors::ConnectorError::NotImplemented( - "Partial Refund is Not Supported".to_string(), - ) - .into()) + Err(errors::ConnectorError::NotImplemented("Partial Refund".to_string()).into()) } } } diff --git a/crates/router/src/core/errors/utils.rs b/crates/router/src/core/errors/utils.rs index 869a5b6bde95..b62abd0e336e 100644 --- a/crates/router/src/core/errors/utils.rs +++ b/crates/router/src/core/errors/utils.rs @@ -136,25 +136,81 @@ pub trait ConnectorErrorExt { impl ConnectorErrorExt for error_stack::Result { fn to_refund_failed_response(self) -> error_stack::Result { - self.map_err(|err| { - let data = match err.current_context() { - errors::ConnectorError::ProcessingStepFailed(Some(bytes)) => { - let response_str = std::str::from_utf8(bytes); - match response_str { - Ok(s) => serde_json::from_str(s) - .map_err( - |error| logger::error!(%error,"Failed to convert response to JSON"), - ) - .ok(), - Err(error) => { - logger::error!(%error,"Failed to convert response to UTF8 string"); - None - } + self.map_err(|err| match err.current_context() { + errors::ConnectorError::ProcessingStepFailed(Some(bytes)) => { + let response_str = std::str::from_utf8(bytes); + let data = match response_str { + Ok(s) => serde_json::from_str(s) + .map_err( + |error| logger::error!(%error,"Failed to convert response to JSON"), + ) + .ok(), + Err(error) => { + logger::error!(%error,"Failed to convert response to UTF8 string"); + None } + }; + err.change_context(errors::ApiErrorResponse::RefundFailed { data }) + } + errors::ConnectorError::NotImplemented(reason) => { + errors::ApiErrorResponse::NotImplemented { + message: errors::api_error_response::NotImplementedMessage::Reason( + reason.to_string(), + ), } - _ => None, - }; - err.change_context(errors::ApiErrorResponse::RefundFailed { data }) + .into() + } + errors::ConnectorError::FailedToObtainIntegrationUrl + | errors::ConnectorError::RequestEncodingFailed + | errors::ConnectorError::RequestEncodingFailedWithReason(_) + | errors::ConnectorError::ParsingFailed + | errors::ConnectorError::ResponseDeserializationFailed + | errors::ConnectorError::UnexpectedResponseError(_) + | errors::ConnectorError::RoutingRulesParsingError + | errors::ConnectorError::FailedToObtainPreferredConnector + | errors::ConnectorError::ProcessingStepFailed(_) + | errors::ConnectorError::InvalidConnectorName + | errors::ConnectorError::InvalidWallet + | errors::ConnectorError::ResponseHandlingFailed + | errors::ConnectorError::MissingRequiredField { .. } + | errors::ConnectorError::MissingRequiredFields { .. } + | errors::ConnectorError::FailedToObtainAuthType + | errors::ConnectorError::FailedToObtainCertificate + | errors::ConnectorError::NoConnectorMetaData + | errors::ConnectorError::FailedToObtainCertificateKey + | errors::ConnectorError::NotSupported { .. } + | errors::ConnectorError::FlowNotSupported { .. } + | errors::ConnectorError::CaptureMethodNotSupported + | errors::ConnectorError::MissingConnectorMandateID + | errors::ConnectorError::MissingConnectorTransactionID + | errors::ConnectorError::MissingConnectorRefundID + | errors::ConnectorError::MissingApplePayTokenData + | errors::ConnectorError::WebhooksNotImplemented + | errors::ConnectorError::WebhookBodyDecodingFailed + | errors::ConnectorError::WebhookSignatureNotFound + | errors::ConnectorError::WebhookSourceVerificationFailed + | errors::ConnectorError::WebhookVerificationSecretNotFound + | errors::ConnectorError::WebhookVerificationSecretInvalid + | errors::ConnectorError::WebhookReferenceIdNotFound + | errors::ConnectorError::WebhookEventTypeNotFound + | errors::ConnectorError::WebhookResourceObjectNotFound + | errors::ConnectorError::WebhookResponseEncodingFailed + | errors::ConnectorError::InvalidDateFormat + | errors::ConnectorError::DateFormattingFailed + | errors::ConnectorError::InvalidDataFormat { .. } + | errors::ConnectorError::MismatchedPaymentData + | errors::ConnectorError::InvalidWalletToken + | errors::ConnectorError::MissingConnectorRelatedTransactionID { .. } + | errors::ConnectorError::FileValidationFailed { .. } + | errors::ConnectorError::MissingConnectorRedirectionPayload { .. } + | errors::ConnectorError::FailedAtConnector { .. } + | errors::ConnectorError::MissingPaymentMethodType + | errors::ConnectorError::InSufficientBalanceInPaymentMethod + | errors::ConnectorError::RequestTimeoutReceived + | errors::ConnectorError::CurrencyNotSupported { .. } + | errors::ConnectorError::InvalidConnectorConfig { .. } => { + err.change_context(errors::ApiErrorResponse::RefundFailed { data: None }) + } }) } From d56d80557050336d5ed37282f1aa34b6c17389d1 Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Fri, 24 Nov 2023 18:20:21 +0530 Subject: [PATCH 093/146] fix(router): mark refund status as failure for not_implemented error from connector flows (#2978) Co-authored-by: Bernard Eugine <114725419+bernard-eugine@users.noreply.github.com> --- crates/router/src/core/refunds.rs | 39 ++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/crates/router/src/core/refunds.rs b/crates/router/src/core/refunds.rs index b2f73c0b7ce7..aba6e9794e04 100644 --- a/crates/router/src/core/refunds.rs +++ b/crates/router/src/core/refunds.rs @@ -189,15 +189,48 @@ pub async fn trigger_refund_to_gateway( types::RefundsData, types::RefundsResponseData, > = connector.connector.get_connector_integration(); - services::execute_connector_processing_step( + let router_data_res = services::execute_connector_processing_step( state, connector_integration, &router_data, payments::CallConnectorAction::Trigger, None, ) - .await - .to_refund_failed_response()? + .await; + let option_refund_error_update = + router_data_res + .as_ref() + .err() + .and_then(|error| match error.current_context() { + errors::ConnectorError::NotImplemented(message) => { + Some(storage::RefundUpdate::ErrorUpdate { + refund_status: Some(enums::RefundStatus::Failure), + refund_error_message: Some(message.to_string()), + refund_error_code: Some("NOT_IMPLEMENTED".to_string()), + updated_by: storage_scheme.to_string(), + }) + } + _ => None, + }); + // Update the refund status as failure if connector_error is NotImplemented + if let Some(refund_error_update) = option_refund_error_update { + state + .store + .update_refund( + refund.to_owned(), + refund_error_update, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InternalServerError) + .attach_printable_lazy(|| { + format!( + "Failed while updating refund: refund_id: {}", + refund.refund_id + ) + })?; + } + router_data_res.to_refund_failed_response()? } else { router_data }; From 3db721388a7f0e291d7eb186661fc69a57068ea6 Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Fri, 24 Nov 2023 18:44:28 +0530 Subject: [PATCH 094/146] fix: surcharge related status and rules fix (#2974) Co-authored-by: Bernard Eugine <114725419+bernard-eugine@users.noreply.github.com> --- crates/api_models/src/payment_methods.rs | 3 +++ crates/api_models/src/payments.rs | 3 +++ .../src/payments/payment_attempt.rs | 6 ++--- crates/diesel_models/src/payment_attempt.rs | 18 +++++--------- .../src/backend/vir_interpreter/types.rs | 4 ++++ crates/router/src/connector/utils.rs | 3 ++- crates/router/src/core/payments/helpers.rs | 3 ++- .../payments/operations/payment_confirm.rs | 11 +++++++++ .../payments/operations/payment_response.rs | 4 ---- crates/router/src/core/payments/retry.rs | 4 ---- crates/router/src/types.rs | 18 +------------- crates/router/src/workflows/payment_sync.rs | 2 -- .../src/payments/payment_attempt.rs | 24 +++++++------------ .../Payments - Confirm/request.json | 4 ---- .../Payments - Create/request.json | 4 ++++ 15 files changed, 46 insertions(+), 65 deletions(-) diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 8710c69aa5c6..dfb8e8999771 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -352,6 +352,9 @@ impl SurchargeDetailsResponse { request_surcharge_details.surcharge_amount == self.surcharge_amount && request_surcharge_details.tax_amount.unwrap_or(0) == self.tax_on_surcharge_amount } + pub fn get_total_surcharge_amount(&self) -> i64 { + self.surcharge_amount + self.tax_on_surcharge_amount + } } #[derive(Clone, Debug)] diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index a997960edc7e..74559f8ed69a 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -347,6 +347,9 @@ impl RequestSurchargeDetails { final_amount: original_amount + surcharge_amount + tax_on_surcharge_amount, } } + pub fn get_total_surcharge_amount(&self) -> i64 { + self.surcharge_amount + self.tax_amount.unwrap_or(0) + } } #[derive(Default, Debug, Clone, Copy)] diff --git a/crates/data_models/src/payments/payment_attempt.rs b/crates/data_models/src/payments/payment_attempt.rs index b866237745fb..a937c785902f 100644 --- a/crates/data_models/src/payments/payment_attempt.rs +++ b/crates/data_models/src/payments/payment_attempt.rs @@ -264,6 +264,8 @@ pub enum PaymentAttemptUpdate { error_message: Option>, amount_capturable: Option, updated_by: String, + surcharge_amount: Option, + tax_amount: Option, merchant_connector_id: Option, }, RejectUpdate { @@ -291,8 +293,6 @@ pub enum PaymentAttemptUpdate { error_reason: Option>, connector_response_reference_id: Option, amount_capturable: Option, - surcharge_amount: Option, - tax_amount: Option, updated_by: String, authentication_data: Option, encoded_data: Option, @@ -321,8 +321,6 @@ pub enum PaymentAttemptUpdate { error_message: Option>, error_reason: Option>, amount_capturable: Option, - surcharge_amount: Option, - tax_amount: Option, updated_by: String, unified_code: Option>, unified_message: Option>, diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index 42af827f522b..9cc6632c638e 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -180,6 +180,8 @@ pub enum PaymentAttemptUpdate { error_code: Option>, error_message: Option>, amount_capturable: Option, + surcharge_amount: Option, + tax_amount: Option, updated_by: String, merchant_connector_id: Option, }, @@ -208,8 +210,6 @@ pub enum PaymentAttemptUpdate { error_reason: Option>, connector_response_reference_id: Option, amount_capturable: Option, - surcharge_amount: Option, - tax_amount: Option, updated_by: String, authentication_data: Option, encoded_data: Option, @@ -238,8 +238,6 @@ pub enum PaymentAttemptUpdate { error_message: Option>, error_reason: Option>, amount_capturable: Option, - surcharge_amount: Option, - tax_amount: Option, updated_by: String, unified_code: Option>, unified_message: Option>, @@ -443,6 +441,8 @@ impl From for PaymentAttemptUpdateInternal { amount_capturable, updated_by, merchant_connector_id, + surcharge_amount, + tax_amount, } => Self { amount: Some(amount), currency: Some(currency), @@ -463,6 +463,8 @@ impl From for PaymentAttemptUpdateInternal { amount_capturable, updated_by, merchant_connector_id, + surcharge_amount, + tax_amount, ..Default::default() }, PaymentAttemptUpdate::VoidUpdate { @@ -501,8 +503,6 @@ impl From for PaymentAttemptUpdateInternal { error_reason, connector_response_reference_id, amount_capturable, - surcharge_amount, - tax_amount, updated_by, authentication_data, encoded_data, @@ -524,8 +524,6 @@ impl From for PaymentAttemptUpdateInternal { connector_response_reference_id, amount_capturable, updated_by, - surcharge_amount, - tax_amount, authentication_data, encoded_data, unified_code, @@ -539,8 +537,6 @@ impl From for PaymentAttemptUpdateInternal { error_message, error_reason, amount_capturable, - surcharge_amount, - tax_amount, updated_by, unified_code, unified_message, @@ -554,8 +550,6 @@ impl From for PaymentAttemptUpdateInternal { error_reason, amount_capturable, updated_by, - surcharge_amount, - tax_amount, unified_code, unified_message, connector_transaction_id, diff --git a/crates/euclid/src/backend/vir_interpreter/types.rs b/crates/euclid/src/backend/vir_interpreter/types.rs index a144cdaafd08..d0eca5fec2ef 100644 --- a/crates/euclid/src/backend/vir_interpreter/types.rs +++ b/crates/euclid/src/backend/vir_interpreter/types.rs @@ -74,6 +74,10 @@ impl Context { } } + if let Some(card_network) = payment_method.card_network { + enum_values.insert(EuclidValue::CardNetwork(card_network)); + } + if let Some(at) = payment.authentication_type { enum_values.insert(EuclidValue::AuthenticationType(at)); } diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index e096f1878a9c..803c511f3a6b 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -113,7 +113,8 @@ where enums::AttemptStatus::Charged => { let captured_amount = types::Capturable::get_capture_amount(&self.request, payment_data); - if Some(payment_data.payment_attempt.get_total_amount()) == captured_amount { + let total_capturable_amount = payment_data.payment_attempt.get_total_amount(); + if Some(total_capturable_amount) == captured_amount { enums::AttemptStatus::Charged } else if captured_amount.is_some() { enums::AttemptStatus::PartialCharged diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index d813c96ce94b..4b0920a55f51 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1693,7 +1693,8 @@ pub(crate) fn validate_status_with_capture_method( field_name: "payment.status".to_string(), current_flow: "captured".to_string(), current_value: status.to_string(), - states: "requires_capture, partially_captured, processing".to_string() + states: "requires_capture, partially_captured_and_capturable, processing" + .to_string() })) }, ) diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 33270795b343..97b0641d2e7e 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -693,6 +693,15 @@ impl let m_error_message = error_message.clone(); let m_db = state.clone().store; + let surcharge_amount = payment_data + .surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.surcharge_amount); + let tax_amount = payment_data + .surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.tax_on_surcharge_amount); + let payment_attempt_fut = tokio::spawn( async move { m_db.update_payment_attempt_with_attempt_id( @@ -716,6 +725,8 @@ impl amount_capturable: Some(authorized_amount), updated_by: storage_scheme.to_string(), merchant_connector_id, + surcharge_amount, + tax_amount, }, storage_scheme, ) diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index beefa53c56fc..2de5df38dba4 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -372,8 +372,6 @@ async fn payment_response_update_tracker( } else { None }, - surcharge_amount: router_data.request.get_surcharge_amount(), - tax_amount: router_data.request.get_tax_on_surcharge_amount(), updated_by: storage_scheme.to_string(), unified_code: option_gsm.clone().map(|gsm| gsm.unified_code), unified_message: option_gsm.map(|gsm| gsm.unified_message), @@ -497,8 +495,6 @@ async fn payment_response_update_tracker( } else { None }, - surcharge_amount: router_data.request.get_surcharge_amount(), - tax_amount: router_data.request.get_tax_on_surcharge_amount(), updated_by: storage_scheme.to_string(), authentication_data, encoded_data, diff --git a/crates/router/src/core/payments/retry.rs b/crates/router/src/core/payments/retry.rs index c5501ab4dc3b..0fd45c5af3b5 100644 --- a/crates/router/src/core/payments/retry.rs +++ b/crates/router/src/core/payments/retry.rs @@ -382,8 +382,6 @@ where } else { None }, - surcharge_amount: None, - tax_amount: None, updated_by: storage_scheme.to_string(), authentication_data, encoded_data, @@ -410,8 +408,6 @@ where status: storage_enums::AttemptStatus::Failure, error_reason: Some(error_response.reason.clone()), amount_capturable: Some(0), - surcharge_amount: None, - tax_amount: None, updated_by: storage_scheme.to_string(), unified_code: option_gsm.clone().map(|gsm| gsm.unified_code), unified_message: option_gsm.map(|gsm| gsm.unified_message), diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 79c3c11eda14..8c9d030965c9 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -551,12 +551,6 @@ pub trait Capturable { { None } - fn get_surcharge_amount(&self) -> Option { - None - } - fn get_tax_on_surcharge_amount(&self) -> Option { - None - } } impl Capturable for PaymentsAuthorizeData { @@ -570,16 +564,6 @@ impl Capturable for PaymentsAuthorizeData { .map(|surcharge_details| surcharge_details.final_amount); final_amount.or(Some(self.amount)) } - fn get_surcharge_amount(&self) -> Option { - self.surcharge_details - .as_ref() - .map(|surcharge_details| surcharge_details.surcharge_amount) - } - fn get_tax_on_surcharge_amount(&self) -> Option { - self.surcharge_details - .as_ref() - .map(|surcharge_details| surcharge_details.tax_on_surcharge_amount) - } } impl Capturable for PaymentsCaptureData { @@ -620,7 +604,7 @@ impl Capturable for PaymentsSyncData { payment_data .payment_attempt .amount_to_capture - .or(Some(payment_data.payment_attempt.get_total_amount())) + .or_else(|| Some(payment_data.payment_attempt.get_total_amount())) } } diff --git a/crates/router/src/workflows/payment_sync.rs b/crates/router/src/workflows/payment_sync.rs index 04f91f30bc7e..f2760a00582d 100644 --- a/crates/router/src/workflows/payment_sync.rs +++ b/crates/router/src/workflows/payment_sync.rs @@ -135,8 +135,6 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow { consts::REQUEST_TIMEOUT_ERROR_MESSAGE_FROM_PSYNC.to_string(), )), amount_capturable: Some(0), - surcharge_amount: None, - tax_amount: None, updated_by: merchant_account.storage_scheme.to_string(), unified_code: None, unified_message: None, diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index 0526fcec9c53..543cf1059889 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -1215,6 +1215,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, + surcharge_amount, + tax_amount, updated_by, merchant_connector_id: connector_id, } => DieselPaymentAttemptUpdate::ConfirmUpdate { @@ -1234,6 +1236,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, + surcharge_amount, + tax_amount, updated_by, merchant_connector_id: connector_id, }, @@ -1261,8 +1265,6 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, - surcharge_amount, - tax_amount, authentication_data, encoded_data, unified_code, @@ -1282,8 +1284,6 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, - surcharge_amount, - tax_amount, authentication_data, encoded_data, unified_code, @@ -1320,8 +1320,6 @@ impl DataModelExt for PaymentAttemptUpdate { error_message, error_reason, amount_capturable, - tax_amount, - surcharge_amount, updated_by, unified_code, unified_message, @@ -1333,8 +1331,6 @@ impl DataModelExt for PaymentAttemptUpdate { error_message, error_reason, amount_capturable, - surcharge_amount, - tax_amount, updated_by, unified_code, unified_message, @@ -1480,6 +1476,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, + surcharge_amount, + tax_amount, updated_by, merchant_connector_id: connector_id, } => Self::ConfirmUpdate { @@ -1499,6 +1497,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, + surcharge_amount, + tax_amount, updated_by, merchant_connector_id: connector_id, }, @@ -1526,8 +1526,6 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, - surcharge_amount, - tax_amount, authentication_data, encoded_data, unified_code, @@ -1547,8 +1545,6 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, - surcharge_amount, - tax_amount, authentication_data, encoded_data, unified_code, @@ -1585,8 +1581,6 @@ impl DataModelExt for PaymentAttemptUpdate { error_message, error_reason, amount_capturable, - surcharge_amount, - tax_amount, updated_by, unified_code, unified_message, @@ -1599,8 +1593,6 @@ impl DataModelExt for PaymentAttemptUpdate { error_reason, amount_capturable, updated_by, - surcharge_amount, - tax_amount, unified_code, unified_message, connector_transaction_id, diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Confirm/request.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Confirm/request.json index 8559af25e82c..91426564e8e1 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Confirm/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Confirm/request.json @@ -39,10 +39,6 @@ }, "raw_json_formatted": { "client_secret": "{{client_secret}}", - "surcharge_details": { - "surcharge_amount": 5, - "tax_amount": 5 - }, "payment_method": "card", "payment_method_data": { "card": { diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/request.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/request.json index f7d813c34efd..9e084a35c8c9 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/request.json @@ -31,6 +31,10 @@ "description": "Its my first payment request", "authentication_type": "no_three_ds", "return_url": "https://duck.com", + "surcharge_details": { + "surcharge_amount": 5, + "tax_amount": 5 + }, "billing": { "address": { "line1": "1467", From 4c1c6da0d1b3e4145f0bc38b06af2d2a1d643232 Mon Sep 17 00:00:00 2001 From: Sanchith Hegde Date: Fri, 24 Nov 2023 19:13:07 +0530 Subject: [PATCH 095/146] chore(version): v1.89.0 --- CHANGELOG.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e427f33e8fbf..d6197598e564 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,30 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.89.0 (2023-11-24) + +### Features + +- **router:** Add `connector_transaction_id` in error_response from connector flows ([#2972](https://github.com/juspay/hyperswitch/pull/2972)) ([`3322103`](https://github.com/juspay/hyperswitch/commit/3322103f5c9b7c2a5b663980246c6ca36b8dc63e)) + +### Bug Fixes + +- **connector:** [BANKOFAMERICA] Add status VOIDED in enum Bankofameri… ([#2969](https://github.com/juspay/hyperswitch/pull/2969)) ([`203bbd7`](https://github.com/juspay/hyperswitch/commit/203bbd73751e1513206e81d7cf920ec263f83c58)) +- **core:** Error propagation for not supporting partial refund ([#2976](https://github.com/juspay/hyperswitch/pull/2976)) ([`97a38a7`](https://github.com/juspay/hyperswitch/commit/97a38a78e514e4fa3b5db46b6de985be6312dcc3)) +- **router:** Mark refund status as failure for not_implemented error from connector flows ([#2978](https://github.com/juspay/hyperswitch/pull/2978)) ([`d56d805`](https://github.com/juspay/hyperswitch/commit/d56d80557050336d5ed37282f1aa34b6c17389d1)) +- Return none instead of err when payment method data is not found for bank debit during listing ([#2967](https://github.com/juspay/hyperswitch/pull/2967)) ([`5cc829a`](https://github.com/juspay/hyperswitch/commit/5cc829a11f515a413fe19f657a90aa05cebb99b5)) +- Surcharge related status and rules fix ([#2974](https://github.com/juspay/hyperswitch/pull/2974)) ([`3db7213`](https://github.com/juspay/hyperswitch/commit/3db721388a7f0e291d7eb186661fc69a57068ea6)) + +### Documentation + +- **README:** Updated Community Platform Mentions ([#2960](https://github.com/juspay/hyperswitch/pull/2960)) ([`e0bde43`](https://github.com/juspay/hyperswitch/commit/e0bde433282a34eb9eb28a2d9c43c2b17b5e65e5)) +- Add Rust locker information in architecture doc ([#2964](https://github.com/juspay/hyperswitch/pull/2964)) ([`b2f7dd1`](https://github.com/juspay/hyperswitch/commit/b2f7dd13925a1429e316cd9eaf0e2d31d46b6d4a)) + +**Full Changelog:** [`v1.88.0...v1.89.0`](https://github.com/juspay/hyperswitch/compare/v1.88.0...v1.89.0) + +- - - + + ## 1.88.0 (2023-11-23) ### Features From 03c0a772a99000acf4676db8ca2ce916036281d1 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Fri, 24 Nov 2023 19:11:46 +0530 Subject: [PATCH 096/146] feat(auth): Add Authorization for JWT Authentication types (#2973) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/router/src/analytics/routes.rs | 29 ++- crates/router/src/consts.rs | 2 + crates/router/src/consts/user.rs | 6 - crates/router/src/core/user.rs | 4 +- crates/router/src/routes/admin.rs | 37 +++- crates/router/src/routes/api_keys.rs | 10 +- crates/router/src/routes/disputes.rs | 38 +++- crates/router/src/routes/files.rs | 20 ++- crates/router/src/routes/mandates.rs | 8 +- crates/router/src/routes/payment_link.rs | 2 +- crates/router/src/routes/payments.rs | 31 +++- crates/router/src/routes/refunds.rs | 26 ++- crates/router/src/routes/routing.rs | 158 ++++++++++++---- crates/router/src/routes/verification.rs | 14 +- crates/router/src/services.rs | 1 + crates/router/src/services/authentication.rs | 16 +- crates/router/src/services/authorization.rs | 27 +++ .../router/src/services/authorization/info.rs | 168 ++++++++++++++++++ .../src/services/authorization/permissions.rs | 74 ++++++++ .../authorization/predefined_permissions.rs | 79 ++++++++ 20 files changed, 659 insertions(+), 91 deletions(-) create mode 100644 crates/router/src/services/authorization.rs create mode 100644 crates/router/src/services/authorization/info.rs create mode 100644 crates/router/src/services/authorization/permissions.rs create mode 100644 crates/router/src/services/authorization/predefined_permissions.rs diff --git a/crates/router/src/analytics/routes.rs b/crates/router/src/analytics/routes.rs index 298ec61ec903..113312cdf10f 100644 --- a/crates/router/src/analytics/routes.rs +++ b/crates/router/src/analytics/routes.rs @@ -8,7 +8,10 @@ use router_env::AnalyticsFlow; use super::{core::*, payments, refunds, types::AnalyticsDomain}; use crate::{ core::api_locking, - services::{api, authentication as auth, authentication::AuthenticationData}, + services::{ + api, authentication as auth, authentication::AuthenticationData, + authorization::permissions::Permission, + }, AppState, }; @@ -68,7 +71,11 @@ pub async fn get_payment_metrics( |state, auth: AuthenticationData, req| { payments::get_metrics(state.pool.clone(), auth.merchant_account, req) }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::Analytics), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -98,7 +105,11 @@ pub async fn get_refunds_metrics( |state, auth: AuthenticationData, req| { refunds::get_metrics(state.pool.clone(), auth.merchant_account, req) }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::Analytics), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -118,7 +129,11 @@ pub async fn get_payment_filters( |state, auth: AuthenticationData, req| { payment_filters_core(state.pool.clone(), req, auth.merchant_account) }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::Analytics), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -138,7 +153,11 @@ pub async fn get_refund_filters( |state, auth: AuthenticationData, req: GetRefundFilterRequest| { refund_filter_core(state.pool.clone(), req, auth.merchant_account) }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::Analytics), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 410e3c1113b1..c5490ee00e63 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -58,3 +58,5 @@ pub const LOCKER_REDIS_EXPIRY_SECONDS: u32 = 60 * 15; // 15 minutes #[cfg(any(feature = "olap", feature = "oltp"))] pub const JWT_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24 * 2; // 2 days + +pub const ROLE_ID_ORGANIZATION_ADMIN: &str = "org_admin"; diff --git a/crates/router/src/consts/user.rs b/crates/router/src/consts/user.rs index 3a71fed01a12..c570aca76038 100644 --- a/crates/router/src/consts/user.rs +++ b/crates/router/src/consts/user.rs @@ -1,8 +1,2 @@ -#[cfg(feature = "olap")] pub const MAX_NAME_LENGTH: usize = 70; -#[cfg(feature = "olap")] pub const MAX_COMPANY_NAME_LENGTH: usize = 70; - -// USER ROLES -#[cfg(any(feature = "olap", feature = "oltp"))] -pub const ROLE_ID_ORGANIZATION_ADMIN: &str = "org_admin"; diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 710dc9281bfa..8b4cf45fe5ef 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -5,9 +5,7 @@ use masking::{ExposeInterface, Secret}; use router_env::env; use super::errors::{UserErrors, UserResponse}; -use crate::{ - consts::user as consts, routes::AppState, services::ApplicationResponse, types::domain, -}; +use crate::{consts, routes::AppState, services::ApplicationResponse, types::domain}; pub async fn connect_account( state: AppState, diff --git a/crates/router/src/routes/admin.rs b/crates/router/src/routes/admin.rs index eef8cacc5f92..0586faabbf76 100644 --- a/crates/router/src/routes/admin.rs +++ b/crates/router/src/routes/admin.rs @@ -4,7 +4,7 @@ use router_env::{instrument, tracing, Flow}; use super::app::AppState; use crate::{ core::{admin::*, api_locking}, - services::{api, authentication as auth}, + services::{api, authentication as auth, authorization::permissions::Permission}, types::api::admin, }; @@ -77,7 +77,10 @@ pub async fn retrieve_merchant_account( |state, _, req| get_merchant_account(state, req), auth::auth_type( &auth::AdminApiAuth, - &auth::JWTAuthMerchantFromRoute { merchant_id }, + &auth::JWTAuthMerchantFromRoute { + merchant_id, + required_permission: Permission::MerchantAccountRead, + }, req.headers(), ), api_locking::LockAction::NotApplicable, @@ -141,6 +144,7 @@ pub async fn update_merchant_account( &auth::AdminApiAuth, &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), + required_permission: Permission::MerchantAccountWrite, }, req.headers(), ), @@ -220,6 +224,7 @@ pub async fn payment_connector_create( &auth::AdminApiAuth, &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), + required_permission: Permission::MerchantConnectorAccountWrite, }, req.headers(), ), @@ -270,7 +275,10 @@ pub async fn payment_connector_retrieve( }, auth::auth_type( &auth::AdminApiAuth, - &auth::JWTAuthMerchantFromRoute { merchant_id }, + &auth::JWTAuthMerchantFromRoute { + merchant_id, + required_permission: Permission::MerchantConnectorAccountRead, + }, req.headers(), ), api_locking::LockAction::NotApplicable, @@ -312,7 +320,10 @@ pub async fn payment_connector_list( |state, _, merchant_id| list_payment_connectors(state, merchant_id), auth::auth_type( &auth::AdminApiAuth, - &auth::JWTAuthMerchantFromRoute { merchant_id }, + &auth::JWTAuthMerchantFromRoute { + merchant_id, + required_permission: Permission::MerchantConnectorAccountRead, + }, req.headers(), ), api_locking::LockAction::NotApplicable, @@ -359,6 +370,7 @@ pub async fn payment_connector_update( &auth::AdminApiAuth, &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), + required_permission: Permission::MerchantConnectorAccountWrite, }, req.headers(), ), @@ -407,7 +419,10 @@ pub async fn payment_connector_delete( |state, _, req| delete_payment_connector(state, req.merchant_id, req.merchant_connector_id), auth::auth_type( &auth::AdminApiAuth, - &auth::JWTAuthMerchantFromRoute { merchant_id }, + &auth::JWTAuthMerchantFromRoute { + merchant_id, + required_permission: Permission::MerchantConnectorAccountWrite, + }, req.headers(), ), api_locking::LockAction::NotApplicable, @@ -460,6 +475,7 @@ pub async fn business_profile_create( &auth::AdminApiAuth, &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), + required_permission: Permission::MerchantAccountWrite, }, req.headers(), ), @@ -484,7 +500,10 @@ pub async fn business_profile_retrieve( |state, _, profile_id| retrieve_business_profile(state, profile_id), auth::auth_type( &auth::AdminApiAuth, - &auth::JWTAuthMerchantFromRoute { merchant_id }, + &auth::JWTAuthMerchantFromRoute { + merchant_id, + required_permission: Permission::MerchantAccountRead, + }, req.headers(), ), api_locking::LockAction::NotApplicable, @@ -511,6 +530,7 @@ pub async fn business_profile_update( &auth::AdminApiAuth, &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), + required_permission: Permission::MerchantAccountWrite, }, req.headers(), ), @@ -555,7 +575,10 @@ pub async fn business_profiles_list( |state, _, merchant_id| list_business_profile(state, merchant_id), auth::auth_type( &auth::AdminApiAuth, - &auth::JWTAuthMerchantFromRoute { merchant_id }, + &auth::JWTAuthMerchantFromRoute { + merchant_id, + required_permission: Permission::MerchantAccountRead, + }, req.headers(), ), api_locking::LockAction::NotApplicable, diff --git a/crates/router/src/routes/api_keys.rs b/crates/router/src/routes/api_keys.rs index 7299aa696390..5b4c047b1466 100644 --- a/crates/router/src/routes/api_keys.rs +++ b/crates/router/src/routes/api_keys.rs @@ -4,7 +4,7 @@ use router_env::{instrument, tracing, Flow}; use super::app::AppState; use crate::{ core::{api_keys, api_locking}, - services::{api, authentication as auth}, + services::{api, authentication as auth, authorization::permissions::Permission}, types::api as api_types, }; @@ -57,6 +57,7 @@ pub async fn api_key_create( &auth::AdminApiAuth, &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), + required_permission: Permission::ApiKeyWrite, }, req.headers(), ), @@ -101,6 +102,7 @@ pub async fn api_key_retrieve( &auth::AdminApiAuth, &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), + required_permission: Permission::ApiKeyRead, }, req.headers(), ), @@ -189,6 +191,7 @@ pub async fn api_key_revoke( &auth::AdminApiAuth, &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), + required_permission: Permission::ApiKeyWrite, }, req.headers(), ), @@ -237,7 +240,10 @@ pub async fn api_key_list( }, auth::auth_type( &auth::AdminApiAuth, - &auth::JWTAuthMerchantFromRoute { merchant_id }, + &auth::JWTAuthMerchantFromRoute { + merchant_id, + required_permission: Permission::ApiKeyRead, + }, req.headers(), ), api_locking::LockAction::NotApplicable, diff --git a/crates/router/src/routes/disputes.rs b/crates/router/src/routes/disputes.rs index aaeb118645db..7bcd8ad35124 100644 --- a/crates/router/src/routes/disputes.rs +++ b/crates/router/src/routes/disputes.rs @@ -3,7 +3,7 @@ use actix_web::{web, HttpRequest, HttpResponse}; use api_models::disputes as dispute_models; use router_env::{instrument, tracing, Flow}; -use crate::core::api_locking; +use crate::{core::api_locking, services::authorization::permissions::Permission}; pub mod utils; use super::app::AppState; @@ -44,7 +44,11 @@ pub async fn retrieve_dispute( &req, dispute_id, |state, auth, req| disputes::retrieve_dispute(state, auth.merchant_account, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::DisputeRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -87,7 +91,11 @@ pub async fn retrieve_disputes_list( &req, payload, |state, auth, req| disputes::retrieve_disputes_list(state, auth.merchant_account, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::DisputeRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -125,7 +133,11 @@ pub async fn accept_dispute( |state, auth, req| { disputes::accept_dispute(state, auth.merchant_account, auth.key_store, req) }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::DisputeWrite), + req.headers(), + ), api_locking::LockAction::NotApplicable, )) .await @@ -158,7 +170,11 @@ pub async fn submit_dispute_evidence( |state, auth, req| { disputes::submit_evidence(state, auth.merchant_account, auth.key_store, req) }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::DisputeWrite), + req.headers(), + ), api_locking::LockAction::NotApplicable, )) .await @@ -199,7 +215,11 @@ pub async fn attach_dispute_evidence( |state, auth, req| { disputes::attach_evidence(state, auth.merchant_account, auth.key_store, req) }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::DisputeWrite), + req.headers(), + ), api_locking::LockAction::NotApplicable, )) .await @@ -235,7 +255,11 @@ pub async fn retrieve_dispute_evidence( &req, dispute_id, |state, auth, req| disputes::retrieve_dispute_evidence(state, auth.merchant_account, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::DisputeRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, )) .await diff --git a/crates/router/src/routes/files.rs b/crates/router/src/routes/files.rs index bde221ebc161..95f4007cb91b 100644 --- a/crates/router/src/routes/files.rs +++ b/crates/router/src/routes/files.rs @@ -2,7 +2,7 @@ use actix_multipart::Multipart; use actix_web::{web, HttpRequest, HttpResponse}; use router_env::{instrument, tracing, Flow}; -use crate::core::api_locking; +use crate::{core::api_locking, services::authorization::permissions::Permission}; pub mod transformers; use super::app::AppState; @@ -45,7 +45,11 @@ pub async fn files_create( &req, create_file_request, |state, auth, req| files_create_core(state, auth.merchant_account, auth.key_store, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::FileWrite), + req.headers(), + ), api_locking::LockAction::NotApplicable, )) .await @@ -83,7 +87,11 @@ pub async fn files_delete( &req, file_id, |state, auth, req| files_delete_core(state, auth.merchant_account, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::FileWrite), + req.headers(), + ), api_locking::LockAction::NotApplicable, )) .await @@ -121,7 +129,11 @@ pub async fn files_retrieve( &req, file_id, |state, auth, req| files_retrieve_core(state, auth.merchant_account, auth.key_store, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::FileRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, )) .await diff --git a/crates/router/src/routes/mandates.rs b/crates/router/src/routes/mandates.rs index 0213d48ddca7..1e4461362975 100644 --- a/crates/router/src/routes/mandates.rs +++ b/crates/router/src/routes/mandates.rs @@ -4,7 +4,7 @@ use router_env::{instrument, tracing, Flow}; use super::app::AppState; use crate::{ core::{api_locking, mandate}, - services::{api, authentication as auth}, + services::{api, authentication as auth, authorization::permissions::Permission}, types::api::mandates, }; @@ -122,7 +122,11 @@ pub async fn retrieve_mandates_list( &req, payload, |state, auth, req| mandate::retrieve_mandates_list(state, auth.merchant_account, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::MandateRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await diff --git a/crates/router/src/routes/payment_link.rs b/crates/router/src/routes/payment_link.rs index 4c26ea71f7d5..d45d67568b89 100644 --- a/crates/router/src/routes/payment_link.rs +++ b/crates/router/src/routes/payment_link.rs @@ -118,7 +118,7 @@ pub async fn payments_link_list( &req, payload, |state, auth, payload| list_payment_link(state, auth.merchant_account, payload), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, ) .await diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 81e53ade5e96..979b15a3d7f2 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -1,4 +1,7 @@ -use crate::core::api_locking::{self, GetLockingInput}; +use crate::{ + core::api_locking::{self, GetLockingInput}, + services::authorization::permissions::Permission, +}; pub mod helpers; use actix_web::{web, Responder}; @@ -128,7 +131,11 @@ pub async fn payments_create( }, match env::which() { env::Env::Production => &auth::ApiKeyAuth, - _ => auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + _ => auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::PaymentWrite), + req.headers(), + ), }, locking_action, )) @@ -262,7 +269,7 @@ pub async fn payments_retrieve( }, auth::auth_type( &*auth_type, - &auth::JWTAuth, + &auth::JWTAuth(Permission::PaymentRead), req.headers(), ), locking_action, @@ -843,7 +850,11 @@ pub async fn payments_list( &req, payload, |state, auth, req| payments::list_payments(state, auth.merchant_account, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::PaymentRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -863,7 +874,11 @@ pub async fn payments_list_by_filter( &req, payload, |state, auth, req| payments::apply_filters_on_payments(state, auth.merchant_account, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::PaymentRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -883,7 +898,11 @@ pub async fn get_filters_for_payments( &req, payload, |state, auth, req| payments::get_filters_for_payments(state, auth.merchant_account, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::PaymentRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await diff --git a/crates/router/src/routes/refunds.rs b/crates/router/src/routes/refunds.rs index d370af6b8d7a..47e9f2bf42a8 100644 --- a/crates/router/src/routes/refunds.rs +++ b/crates/router/src/routes/refunds.rs @@ -4,7 +4,7 @@ use router_env::{instrument, tracing, Flow}; use super::app::AppState; use crate::{ core::{api_locking, refunds::*}, - services::{api, authentication as auth}, + services::{api, authentication as auth, authorization::permissions::Permission}, types::api::refunds, }; @@ -37,7 +37,11 @@ pub async fn refunds_create( &req, json_payload.into_inner(), |state, auth, req| refund_create_core(state, auth.merchant_account, auth.key_store, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RefundWrite), + req.headers(), + ), api_locking::LockAction::NotApplicable, )) .await @@ -88,7 +92,11 @@ pub async fn refunds_retrieve( refund_retrieve_core, ) }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RefundRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, )) .await @@ -202,7 +210,11 @@ pub async fn refunds_list( &req, payload.into_inner(), |state, auth, req| refund_list(state, auth.merchant_account, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RefundRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -235,7 +247,11 @@ pub async fn refunds_filter_list( &req, payload.into_inner(), |state, auth, req| refund_filter_list(state, auth.merchant_account, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RefundRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await diff --git a/crates/router/src/routes/routing.rs b/crates/router/src/routes/routing.rs index 1d2549bb047a..e7e31cb36aeb 100644 --- a/crates/router/src/routes/routing.rs +++ b/crates/router/src/routes/routing.rs @@ -14,7 +14,7 @@ use router_env::{ use crate::{ core::{api_locking, conditional_config, routing, surcharge_decision_config}, routes::AppState, - services::{api as oss_api, authentication as auth}, + services::{api as oss_api, authentication as auth, authorization::permissions::Permission}, }; #[cfg(feature = "olap")] @@ -34,9 +34,13 @@ pub async fn routing_create_config( routing::create_routing_config(state, auth.merchant_account, auth.key_store, payload) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingWrite), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingWrite), api_locking::LockAction::NotApplicable, )) .await @@ -65,9 +69,13 @@ pub async fn routing_link_config( ) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingWrite), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingWrite), api_locking::LockAction::NotApplicable, )) .await @@ -91,9 +99,13 @@ pub async fn routing_retrieve_config( routing::retrieve_routing_config(state, auth.merchant_account, algorithm_id) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingRead), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingRead), api_locking::LockAction::NotApplicable, )) .await @@ -122,9 +134,13 @@ pub async fn routing_retrieve_dictionary( ) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingRead), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingRead), api_locking::LockAction::NotApplicable, )) .await @@ -142,9 +158,13 @@ pub async fn routing_retrieve_dictionary( routing::retrieve_merchant_routing_dictionary(state, auth.merchant_account) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingRead), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingRead), api_locking::LockAction::NotApplicable, )) .await @@ -172,9 +192,13 @@ pub async fn routing_unlink_config( routing::unlink_routing_config(state, auth.merchant_account, payload_req) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingWrite), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingWrite), api_locking::LockAction::NotApplicable, )) .await @@ -192,9 +216,13 @@ pub async fn routing_unlink_config( routing::unlink_routing_config(state, auth.merchant_account, auth.key_store) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingWrite), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingWrite), api_locking::LockAction::NotApplicable, )) .await @@ -217,9 +245,13 @@ pub async fn routing_update_default_config( routing::update_default_routing_config(state, auth.merchant_account, updated_config) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingWrite), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingWrite), api_locking::LockAction::NotApplicable, ) .await @@ -240,9 +272,13 @@ pub async fn routing_retrieve_default_config( routing::retrieve_default_routing_config(state, auth.merchant_account) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingRead), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingRead), api_locking::LockAction::NotApplicable, ) .await @@ -270,9 +306,13 @@ pub async fn upsert_surcharge_decision_manager_config( ) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::SurchargeDecisionManagerWrite), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::SurchargeDecisionManagerWrite), api_locking::LockAction::NotApplicable, )) .await @@ -297,9 +337,13 @@ pub async fn delete_surcharge_decision_manager_config( ) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::SurchargeDecisionManagerWrite), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::SurchargeDecisionManagerWrite), api_locking::LockAction::NotApplicable, )) .await @@ -324,9 +368,13 @@ pub async fn retrieve_surcharge_decision_manager_config( ) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::SurchargeDecisionManagerRead), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::SurchargeDecisionManagerRead), api_locking::LockAction::NotApplicable, ) .await @@ -354,9 +402,13 @@ pub async fn upsert_decision_manager_config( ) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::SurchargeDecisionManagerRead), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::SurchargeDecisionManagerRead), api_locking::LockAction::NotApplicable, )) .await @@ -382,9 +434,13 @@ pub async fn delete_decision_manager_config( ) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::SurchargeDecisionManagerWrite), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::SurchargeDecisionManagerWrite), api_locking::LockAction::NotApplicable, )) .await @@ -406,9 +462,13 @@ pub async fn retrieve_decision_manager_config( conditional_config::retrieve_conditional_config(state, auth.merchant_account) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::SurchargeDecisionManagerRead), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::SurchargeDecisionManagerRead), api_locking::LockAction::NotApplicable, ) .await @@ -434,9 +494,13 @@ pub async fn routing_retrieve_linked_config( routing::retrieve_linked_routing_config(state, auth.merchant_account, query_params) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingRead), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingRead), api_locking::LockAction::NotApplicable, )) .await @@ -454,9 +518,13 @@ pub async fn routing_retrieve_linked_config( routing::retrieve_linked_routing_config(state, auth.merchant_account) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingRead), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingRead), api_locking::LockAction::NotApplicable, )) .await @@ -478,9 +546,17 @@ pub async fn routing_retrieve_default_config_for_profiles( routing::retrieve_default_routing_config_for_profiles(state, auth.merchant_account) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingRead), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -512,9 +588,13 @@ pub async fn routing_update_default_config_for_profile( ) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingWrite), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingWrite), api_locking::LockAction::NotApplicable, ) .await diff --git a/crates/router/src/routes/verification.rs b/crates/router/src/routes/verification.rs index d0525bb272e8..4bcbacdf9912 100644 --- a/crates/router/src/routes/verification.rs +++ b/crates/router/src/routes/verification.rs @@ -5,7 +5,7 @@ use router_env::{instrument, tracing, Flow}; use super::app::AppState; use crate::{ core::{api_locking, verification}, - services::{api, authentication as auth}, + services::{api, authentication as auth, authorization::permissions::Permission}, }; #[instrument(skip_all, fields(flow = ?Flow::Verification))] @@ -32,7 +32,11 @@ pub async fn apple_pay_merchant_registration( merchant_id.clone(), ) }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::MerchantAccountWrite), + req.headers(), + ), api_locking::LockAction::NotApplicable, )) .await @@ -60,7 +64,11 @@ pub async fn retrieve_apple_pay_verified_domains( mca_id.to_string(), ) }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::MerchantAccountRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await diff --git a/crates/router/src/services.rs b/crates/router/src/services.rs index 21f33f0fa0b8..2d5552b59d17 100644 --- a/crates/router/src/services.rs +++ b/crates/router/src/services.rs @@ -1,5 +1,6 @@ pub mod api; pub mod authentication; +pub mod authorization; pub mod encryption; #[cfg(feature = "olap")] pub mod jwt; diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 4277205b0231..876804b7bb93 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -9,6 +9,7 @@ use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use masking::{PeekInterface, StrongSecret}; use serde::Serialize; +use super::authorization::{self, permissions::Permission}; #[cfg(feature = "olap")] use super::jwt; #[cfg(feature = "olap")] @@ -387,7 +388,7 @@ where } #[derive(Debug)] -pub(crate) struct JWTAuth; +pub(crate) struct JWTAuth(pub Permission); #[derive(serde::Deserialize)] struct JwtAuthPayloadFetchUnit { @@ -406,6 +407,10 @@ where state: &A, ) -> RouterResult<((), AuthenticationType)> { let payload = parse_jwt_payload::(request_headers, state).await?; + + let permissions = authorization::get_permissions(&payload.role_id)?; + authorization::check_authorization(&self.0, permissions)?; + Ok(( (), AuthenticationType::MerchantJWT { @@ -418,6 +423,7 @@ where pub struct JWTAuthMerchantFromRoute { pub merchant_id: String, + pub required_permission: Permission, } #[async_trait] @@ -432,6 +438,9 @@ where ) -> RouterResult<((), AuthenticationType)> { let payload = parse_jwt_payload::(request_headers, state).await?; + let permissions = authorization::get_permissions(&payload.role_id)?; + authorization::check_authorization(&self.required_permission, permissions)?; + // Check if token has access to merchantID that has been requested through query param if payload.merchant_id != self.merchant_id { return Err(report!(errors::ApiErrorResponse::InvalidJwtToken)); @@ -460,6 +469,7 @@ where #[derive(serde::Deserialize)] struct JwtAuthPayloadFetchMerchantAccount { merchant_id: String, + role_id: String, } #[async_trait] @@ -475,6 +485,10 @@ where let payload = parse_jwt_payload::(request_headers, state) .await?; + + let permissions = authorization::get_permissions(&payload.role_id)?; + authorization::check_authorization(&self.0, permissions)?; + let key_store = state .store() .get_merchant_key_store_by_merchant_id( diff --git a/crates/router/src/services/authorization.rs b/crates/router/src/services/authorization.rs new file mode 100644 index 000000000000..cad9b1ece62e --- /dev/null +++ b/crates/router/src/services/authorization.rs @@ -0,0 +1,27 @@ +use crate::core::errors::{ApiErrorResponse, RouterResult}; + +pub mod info; +pub mod permissions; +pub mod predefined_permissions; + +pub fn get_permissions(role: &str) -> RouterResult<&Vec> { + predefined_permissions::PREDEFINED_PERMISSIONS + .get(role) + .map(|role_info| role_info.get_permissions()) + .ok_or(ApiErrorResponse::InvalidJwtToken.into()) +} + +pub fn check_authorization( + required_permission: &permissions::Permission, + permissions: &[permissions::Permission], +) -> RouterResult<()> { + permissions + .contains(required_permission) + .then_some(()) + .ok_or( + ApiErrorResponse::AccessForbidden { + resource: required_permission.to_string(), + } + .into(), + ) +} diff --git a/crates/router/src/services/authorization/info.rs b/crates/router/src/services/authorization/info.rs new file mode 100644 index 000000000000..c6b649f3de5c --- /dev/null +++ b/crates/router/src/services/authorization/info.rs @@ -0,0 +1,168 @@ +use strum::{EnumIter, IntoEnumIterator}; + +use super::permissions::Permission; + +pub fn get_authorization_info() -> Vec { + PermissionModule::iter() + .map(|module| ModuleInfo::new(&module)) + .collect() +} + +pub struct PermissionInfo { + pub enum_name: Permission, + pub description: &'static str, +} + +impl PermissionInfo { + pub fn new(permissions: &[Permission]) -> Vec { + let mut permission_infos = Vec::with_capacity(permissions.len()); + for permission in permissions { + if let Some(description) = Permission::get_permission_description(permission) { + permission_infos.push(Self { + enum_name: permission.clone(), + description, + }) + } + } + permission_infos + } +} + +#[derive(PartialEq, EnumIter, Clone)] +pub enum PermissionModule { + Payments, + Refunds, + MerchantAccount, + Connectors, + Forex, + Routing, + Analytics, + Mandates, + Disputes, + Files, + ThreeDsDecisionManager, + SurchargeDecisionManager, +} + +impl PermissionModule { + pub fn get_module_description(&self) -> &'static str { + match self { + Self::Payments => "Everything related to payments - like creating and viewing payment related information are within this module", + Self::Refunds => "Refunds module encompasses everything related to refunds - like creating and viewing payment related information", + Self::MerchantAccount => "Accounts module permissions allow the user to view and update account details, configure webhooks and much more", + Self::Connectors => "All connector related actions - like configuring new connectors, viewing and updating connector configuration lies with this module", + Self::Routing => "All actions related to new, active, and past routing stacks take place here", + Self::Forex => "Forex module permissions allow the user to view and query the forex rates", + Self::Analytics => "Permission to view and analyse the data relating to payments, refunds, sdk etc.", + Self::Mandates => "Everything related to mandates - like creating and viewing mandate related information are within this module", + Self::Disputes => "Everything related to disputes - like creating and viewing dispute related information are within this module", + Self::Files => "Permissions for uploading, deleting and viewing files for disputes", + Self::ThreeDsDecisionManager => "View and configure 3DS decision rules configured for a merchant", + Self::SurchargeDecisionManager =>"View and configure surcharge decision rules configured for a merchant" + } + } +} + +pub struct ModuleInfo { + pub module: PermissionModule, + pub description: &'static str, + pub permissions: Vec, +} + +impl ModuleInfo { + pub fn new(module: &PermissionModule) -> Self { + let module_name = module.clone(); + let description = module.get_module_description(); + + match module { + PermissionModule::Payments => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[ + Permission::PaymentRead, + Permission::PaymentWrite, + ]), + }, + PermissionModule::Refunds => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[ + Permission::RefundRead, + Permission::RefundWrite, + ]), + }, + PermissionModule::MerchantAccount => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[ + Permission::MerchantAccountRead, + Permission::MerchantAccountWrite, + ]), + }, + PermissionModule::Connectors => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[ + Permission::MerchantConnectorAccountRead, + Permission::MerchantConnectorAccountWrite, + ]), + }, + PermissionModule::Forex => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[Permission::ForexRead]), + }, + PermissionModule::Routing => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[ + Permission::RoutingRead, + Permission::RoutingWrite, + ]), + }, + PermissionModule::Analytics => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[Permission::Analytics]), + }, + PermissionModule::Mandates => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[ + Permission::MandateRead, + Permission::MandateWrite, + ]), + }, + PermissionModule::Disputes => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[ + Permission::DisputeRead, + Permission::DisputeWrite, + ]), + }, + PermissionModule::Files => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[Permission::FileRead, Permission::FileWrite]), + }, + PermissionModule::ThreeDsDecisionManager => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[ + Permission::ThreeDsDecisionManagerWrite, + Permission::ThreeDsDecisionManagerRead, + ]), + }, + + PermissionModule::SurchargeDecisionManager => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[ + Permission::SurchargeDecisionManagerWrite, + Permission::SurchargeDecisionManagerRead, + ]), + }, + } + } +} diff --git a/crates/router/src/services/authorization/permissions.rs b/crates/router/src/services/authorization/permissions.rs new file mode 100644 index 000000000000..708da97e1e39 --- /dev/null +++ b/crates/router/src/services/authorization/permissions.rs @@ -0,0 +1,74 @@ +use strum::Display; + +#[derive(PartialEq, Display, Clone, Debug)] +pub enum Permission { + PaymentRead, + PaymentWrite, + RefundRead, + RefundWrite, + ApiKeyRead, + ApiKeyWrite, + MerchantAccountRead, + MerchantAccountWrite, + MerchantConnectorAccountRead, + MerchantConnectorAccountWrite, + ForexRead, + RoutingRead, + RoutingWrite, + DisputeRead, + DisputeWrite, + MandateRead, + MandateWrite, + FileRead, + FileWrite, + Analytics, + ThreeDsDecisionManagerWrite, + ThreeDsDecisionManagerRead, + SurchargeDecisionManagerWrite, + SurchargeDecisionManagerRead, + UsersRead, + UsersWrite, + MerchantAccountCreate, +} + +impl Permission { + pub fn get_permission_description(&self) -> Option<&'static str> { + match self { + Self::PaymentRead => Some("View all payments"), + Self::PaymentWrite => Some("Create payment, download payments data"), + Self::RefundRead => Some("View all refunds"), + Self::RefundWrite => Some("Create refund, download refunds data"), + Self::ApiKeyRead => Some("View API keys (masked generated for the system"), + Self::ApiKeyWrite => Some("Create and update API keys"), + Self::MerchantAccountRead => Some("View merchant account details"), + Self::MerchantAccountWrite => { + Some("Update merchant account details, configure webhooks, manage api keys") + } + Self::MerchantConnectorAccountRead => Some("View connectors configured"), + Self::MerchantConnectorAccountWrite => { + Some("Create, update, verify and delete connector configurations") + } + Self::ForexRead => Some("Query Forex data"), + Self::RoutingRead => Some("View routing configuration"), + Self::RoutingWrite => Some("Create and activate routing configurations"), + Self::DisputeRead => Some("View disputes"), + Self::DisputeWrite => Some("Create and update disputes"), + Self::MandateRead => Some("View mandates"), + Self::MandateWrite => Some("Create and update mandates"), + Self::FileRead => Some("View files"), + Self::FileWrite => Some("Create, update and delete files"), + Self::Analytics => Some("Access to analytics module"), + Self::ThreeDsDecisionManagerWrite => Some("Create and update 3DS decision rules"), + Self::ThreeDsDecisionManagerRead => { + Some("View all 3DS decision rules configured for a merchant") + } + Self::SurchargeDecisionManagerWrite => { + Some("Create and update the surcharge decision rules") + } + Self::SurchargeDecisionManagerRead => Some("View all the surcharge decision rules"), + Self::UsersRead => Some("View all the users for a merchant"), + Self::UsersWrite => Some("Invite users, assign and update roles"), + Self::MerchantAccountCreate => None, + } + } +} diff --git a/crates/router/src/services/authorization/predefined_permissions.rs b/crates/router/src/services/authorization/predefined_permissions.rs new file mode 100644 index 000000000000..89fa2c8f739c --- /dev/null +++ b/crates/router/src/services/authorization/predefined_permissions.rs @@ -0,0 +1,79 @@ +use std::collections::HashMap; + +use once_cell::sync::Lazy; + +use super::permissions::Permission; +use crate::consts; + +pub struct RoleInfo { + permissions: Vec, + name: Option<&'static str>, + is_invitable: bool, +} + +impl RoleInfo { + pub fn get_permissions(&self) -> &Vec { + &self.permissions + } + + pub fn get_name(&self) -> Option<&'static str> { + self.name + } + + pub fn is_invitable(&self) -> bool { + self.is_invitable + } +} + +pub static PREDEFINED_PERMISSIONS: Lazy> = Lazy::new(|| { + let mut roles = HashMap::new(); + roles.insert( + consts::ROLE_ID_ORGANIZATION_ADMIN, + RoleInfo { + permissions: vec![ + Permission::PaymentRead, + Permission::PaymentWrite, + Permission::RefundRead, + Permission::RefundWrite, + Permission::ApiKeyRead, + Permission::ApiKeyWrite, + Permission::MerchantAccountRead, + Permission::MerchantAccountWrite, + Permission::MerchantConnectorAccountRead, + Permission::MerchantConnectorAccountWrite, + Permission::RoutingRead, + Permission::RoutingWrite, + Permission::ForexRead, + Permission::ThreeDsDecisionManagerWrite, + Permission::ThreeDsDecisionManagerRead, + Permission::SurchargeDecisionManagerWrite, + Permission::SurchargeDecisionManagerRead, + Permission::DisputeRead, + Permission::DisputeWrite, + Permission::MandateRead, + Permission::MandateWrite, + Permission::FileRead, + Permission::FileWrite, + Permission::Analytics, + Permission::UsersRead, + Permission::UsersWrite, + Permission::MerchantAccountCreate, + ], + name: Some("Organization Admin"), + is_invitable: false, + }, + ); + roles +}); + +pub fn get_role_name_from_id(role_id: &str) -> Option<&'static str> { + PREDEFINED_PERMISSIONS + .get(role_id) + .and_then(|role_info| role_info.name) +} + +pub fn is_role_invitable(role_id: &str) -> bool { + PREDEFINED_PERMISSIONS + .get(role_id) + .map_or(false, |role_info| role_info.is_invitable) +} From 107c3b99417dd7bca7b62741ad601485700f37be Mon Sep 17 00:00:00 2001 From: Kartikeya Hegde Date: Fri, 24 Nov 2023 19:25:19 +0530 Subject: [PATCH 097/146] fix: add prefix to connector_transaction_id (#2981) --- crates/storage_impl/src/payments/payment_attempt.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index 543cf1059889..06aacccc769d 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -553,7 +553,7 @@ impl PaymentAttemptInterface for KVRouterStore { } MerchantStorageScheme::RedisKv => { // We assume that PaymentAttempt <=> PaymentIntent is a one-to-one relation for now - let lookup_id = format!("{merchant_id}_{connector_transaction_id}"); + let lookup_id = format!("conn_trans_{merchant_id}_{connector_transaction_id}"); let lookup = self .get_lookup_by_lookup_id(&lookup_id, storage_scheme) .await?; @@ -774,7 +774,7 @@ impl PaymentAttemptInterface for KVRouterStore { .await } MerchantStorageScheme::RedisKv => { - let lookup_id = format!("{merchant_id}_{preprocessing_id}"); + let lookup_id = format!("preprocessing_{merchant_id}_{preprocessing_id}"); let lookup = self .get_lookup_by_lookup_id(&lookup_id, storage_scheme) .await?; @@ -1671,7 +1671,7 @@ async fn add_connector_txn_id_to_reverse_lookup( ) -> CustomResult { let field = format!("pa_{}", updated_attempt_attempt_id); let reverse_lookup_new = ReverseLookupNew { - lookup_id: format!("{}_{}", merchant_id, connector_transaction_id), + lookup_id: format!("conn_trans_{}_{}", merchant_id, connector_transaction_id), pk_id: key.to_owned(), sk_id: field.clone(), source: "payment_attempt".to_string(), @@ -1693,7 +1693,7 @@ async fn add_preprocessing_id_to_reverse_lookup( ) -> CustomResult { let field = format!("pa_{}", updated_attempt_attempt_id); let reverse_lookup_new = ReverseLookupNew { - lookup_id: format!("{}_{}", merchant_id, preprocessing_id), + lookup_id: format!("preprocessing_{}_{}", merchant_id, preprocessing_id), pk_id: key.to_owned(), sk_id: field.clone(), source: "payment_attempt".to_string(), From bfa1645b847fb881eb2370d5dbfef6fd0b53725d Mon Sep 17 00:00:00 2001 From: Apoorv Dixit <64925866+apoorvdixit88@users.noreply.github.com> Date: Fri, 24 Nov 2023 20:34:27 +0530 Subject: [PATCH 098/146] feat(user): implement change password for user (#2959) --- crates/api_models/src/events/user.rs | 4 +- crates/api_models/src/user.rs | 6 ++ crates/router/src/core/errors/user.rs | 13 +++ crates/router/src/core/user.rs | 42 +++++++++- crates/router/src/routes/app.rs | 1 + crates/router/src/routes/lock_utils.rs | 2 +- crates/router/src/routes/user.rs | 18 ++++ crates/router/src/services/authentication.rs | 87 +++++++++++++++++++- crates/router_env/src/logger/types.rs | 2 + 9 files changed, 170 insertions(+), 5 deletions(-) diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index 2a896cc38776..4e9f2f284173 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -1,6 +1,6 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; -use crate::user::{ConnectAccountRequest, ConnectAccountResponse}; +use crate::user::{ChangePasswordRequest, ConnectAccountRequest, ConnectAccountResponse}; impl ApiEventMetric for ConnectAccountResponse { fn get_api_event_type(&self) -> Option { @@ -12,3 +12,5 @@ impl ApiEventMetric for ConnectAccountResponse { } impl ApiEventMetric for ConnectAccountRequest {} + +common_utils::impl_misc_api_event_type!(ChangePasswordRequest); diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index 91f7702c654e..41ea9cc5193a 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -19,3 +19,9 @@ pub struct ConnectAccountResponse { #[serde(skip_serializing)] pub user_id: String, } + +#[derive(serde::Deserialize, Debug, serde::Serialize)] +pub struct ChangePasswordRequest { + pub new_password: Secret, + pub old_password: Secret, +} diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs index b4d48365dc84..b86c395b9814 100644 --- a/crates/router/src/core/errors/user.rs +++ b/crates/router/src/core/errors/user.rs @@ -13,6 +13,8 @@ pub enum UserErrors { InvalidCredentials, #[error("UserExists")] UserExists, + #[error("InvalidOldPassword")] + InvalidOldPassword, #[error("EmailParsingError")] EmailParsingError, #[error("NameParsingError")] @@ -27,6 +29,8 @@ pub enum UserErrors { InvalidEmailError, #[error("DuplicateOrganizationId")] DuplicateOrganizationId, + #[error("MerchantIdNotFound")] + MerchantIdNotFound, } impl common_utils::errors::ErrorSwitch for UserErrors { @@ -49,6 +53,12 @@ impl common_utils::errors::ErrorSwitch AER::BadRequest(ApiError::new( + sub_code, + 6, + "Old password incorrect. Please enter the correct password", + None, + )), Self::EmailParsingError => { AER::BadRequest(ApiError::new(sub_code, 7, "Invalid Email", None)) } @@ -73,6 +83,9 @@ impl common_utils::errors::ErrorSwitch { + AER::BadRequest(ApiError::new(sub_code, 18, "Invalid Merchant ID", None)) + } } } } diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 8b4cf45fe5ef..94cd482a2291 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -1,11 +1,17 @@ use api_models::user as api; use diesel_models::enums::UserStatus; -use error_stack::IntoReport; +use error_stack::{IntoReport, ResultExt}; use masking::{ExposeInterface, Secret}; use router_env::env; use super::errors::{UserErrors, UserResponse}; -use crate::{consts, routes::AppState, services::ApplicationResponse, types::domain}; +use crate::{ + consts, + db::user::UserInterface, + routes::AppState, + services::{authentication::UserFromToken, ApplicationResponse}, + types::domain, +}; pub async fn connect_account( state: AppState, @@ -77,3 +83,35 @@ pub async fn connect_account( Err(UserErrors::InternalServerError.into()) } } + +pub async fn change_password( + state: AppState, + request: api::ChangePasswordRequest, + user_from_token: UserFromToken, +) -> UserResponse<()> { + let user: domain::UserFromStorage = + UserInterface::find_user_by_id(&*state.store, &user_from_token.user_id) + .await + .change_context(UserErrors::InternalServerError)? + .into(); + + user.compare_password(request.old_password) + .change_context(UserErrors::InvalidOldPassword)?; + + let new_password_hash = + crate::utils::user::password::generate_password_hash(request.new_password)?; + + let _ = UserInterface::update_user_by_user_id( + &*state.store, + user.get_user_id(), + diesel_models::user::UserUpdate::AccountUpdate { + name: None, + password: Some(new_password_hash), + is_verified: None, + }, + ) + .await + .change_context(UserErrors::InternalServerError)?; + + Ok(ApplicationResponse::StatusOk) +} diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 96bb47ea4e97..84848e030120 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -759,6 +759,7 @@ impl User { .service(web::resource("/signup").route(web::post().to(user_connect_account))) .service(web::resource("/v2/signin").route(web::post().to(user_connect_account))) .service(web::resource("/v2/signup").route(web::post().to(user_connect_account))) + .service(web::resource("/change_password").route(web::post().to(change_password))) } } diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index a9cf7b44a73d..219948bdd4d2 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -144,7 +144,7 @@ impl From for ApiIdentifier { | Flow::GsmRuleUpdate | Flow::GsmRuleDelete => Self::Gsm, - Flow::UserConnectAccount => Self::User, + Flow::UserConnectAccount | Flow::ChangePassword => Self::User, } } } diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index 0ff11ce087b5..7d3d183eda76 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -29,3 +29,21 @@ pub async fn user_connect_account( )) .await } + +pub async fn change_password( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::ChangePassword; + Box::pin(api::server_wrap( + flow, + state.clone(), + &http_req, + json_payload.into_inner(), + |state, user, req| user::change_password(state, req, user), + &auth::DashboardNoPermissionAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 876804b7bb93..e24c7cebcb2a 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -14,6 +14,8 @@ use super::authorization::{self, permissions::Permission}; use super::jwt; #[cfg(feature = "olap")] use crate::consts; +#[cfg(feature = "olap")] +use crate::core::errors::UserResult; use crate::{ configs::settings, core::{ @@ -97,7 +99,7 @@ impl AuthToken { role_id: String, settings: &settings::Settings, org_id: String, - ) -> errors::UserResult { + ) -> UserResult { let exp_duration = std::time::Duration::from_secs(consts::JWT_TOKEN_TIME_IN_SECS); let exp = jwt::generate_exp(exp_duration)?.as_secs(); let token_payload = Self { @@ -111,6 +113,14 @@ impl AuthToken { } } +#[derive(Clone)] +pub struct UserFromToken { + pub user_id: String, + pub merchant_id: String, + pub role_id: String, + pub org_id: String, +} + pub trait AuthInfo { fn get_merchant_id(&self) -> Option<&str>; } @@ -421,6 +431,34 @@ where } } +#[cfg(feature = "olap")] +#[async_trait] +impl AuthenticateAndFetch for JWTAuth +where + A: AppStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(UserFromToken, AuthenticationType)> { + let payload = parse_jwt_payload::(request_headers, state).await?; + + Ok(( + UserFromToken { + user_id: payload.user_id.clone(), + merchant_id: payload.merchant_id.clone(), + org_id: payload.org_id, + role_id: payload.role_id, + }, + AuthenticationType::MerchantJWT { + merchant_id: payload.merchant_id, + user_id: Some(payload.user_id), + }, + )) + } +} + pub struct JWTAuthMerchantFromRoute { pub merchant_id: String, pub required_permission: Permission, @@ -519,6 +557,53 @@ where } } +pub struct DashboardNoPermissionAuth; + +#[cfg(feature = "olap")] +#[async_trait] +impl AuthenticateAndFetch for DashboardNoPermissionAuth +where + A: AppStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(UserFromToken, AuthenticationType)> { + let payload = parse_jwt_payload::(request_headers, state).await?; + + Ok(( + UserFromToken { + user_id: payload.user_id.clone(), + merchant_id: payload.merchant_id.clone(), + org_id: payload.org_id, + role_id: payload.role_id, + }, + AuthenticationType::MerchantJWT { + merchant_id: payload.merchant_id, + user_id: Some(payload.user_id), + }, + )) + } +} + +#[cfg(feature = "olap")] +#[async_trait] +impl AuthenticateAndFetch<(), A> for DashboardNoPermissionAuth +where + A: AppStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<((), AuthenticationType)> { + parse_jwt_payload::(request_headers, state).await?; + + Ok(((), AuthenticationType::NoAuth)) + } +} + pub trait ClientSecretFetch { fn get_client_secret(&self) -> Option<&String>; } diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 178f837fce18..7978e98e52c0 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -255,6 +255,8 @@ pub enum Flow { DecisionManagerDeleteConfig, /// Retrieve Decision Manager Config DecisionManagerRetrieveConfig, + /// Change password flow + ChangePassword, } /// From 04b7c0384dc9290bd60f49033fd35732527720f1 Mon Sep 17 00:00:00 2001 From: HeetVekariya <91054457+HeetVekariya@users.noreply.github.com> Date: Mon, 27 Nov 2023 01:33:24 +0530 Subject: [PATCH 099/146] refactor(connector): [Nuvei] update error message (#2867) --- crates/router/src/connector/nuvei/transformers.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/crates/router/src/connector/nuvei/transformers.rs b/crates/router/src/connector/nuvei/transformers.rs index 25562f54bfeb..b79b2c892643 100644 --- a/crates/router/src/connector/nuvei/transformers.rs +++ b/crates/router/src/connector/nuvei/transformers.rs @@ -623,11 +623,9 @@ impl TryFrom for NuveiBIC { | api_models::enums::BankNames::TsbBank | api_models::enums::BankNames::TescoBank | api_models::enums::BankNames::UlsterBank => { - Err(errors::ConnectorError::NotSupported { - message: bank.to_string(), - connector: "Nuvei", - } - .into()) + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Nuvei"), + ))? } } } @@ -693,10 +691,9 @@ impl bank_name.map(NuveiBIC::try_from).transpose()?, ) } - _ => Err(errors::ConnectorError::NotSupported { - message: "Bank Redirect".to_string(), - connector: "Nuvei", - })?, + _ => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Nuvei"), + ))?, }; Ok(Self { payment_option: PaymentOption { From 37532d46f599a99e0e021b0455a6f02381005dd7 Mon Sep 17 00:00:00 2001 From: Sahkal Poddar Date: Mon, 27 Nov 2023 10:43:59 +0530 Subject: [PATCH 100/146] fix(router): added validation to check total orderDetails amount equal to amount in request (#2965) Co-authored-by: Sahkal Poddar --- crates/router/src/core/payments/helpers.rs | 19 +++++++++++++++++++ .../payments/operations/payment_confirm.rs | 7 +++++++ .../payments/operations/payment_create.rs | 7 +++++++ .../payments/operations/payment_update.rs | 7 +++++++ 4 files changed, 40 insertions(+) diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 4b0920a55f51..f57c0640f1a8 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -3686,3 +3686,22 @@ pub async fn get_gsm_record( }) .ok() } + +pub fn validate_order_details_amount( + order_details: Vec, + amount: i64, +) -> Result<(), errors::ApiErrorResponse> { + let total_order_details_amount: i64 = order_details + .iter() + .map(|order| order.amount * i64::from(order.quantity)) + .sum(); + + if total_order_details_amount != amount { + Err(errors::ApiErrorResponse::InvalidRequestData { + message: "Total sum of order details doesn't match amount in payment request" + .to_string(), + }) + } else { + Ok(()) + } +} diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 97b0641d2e7e..28b6dbec96ab 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -102,6 +102,13 @@ impl utils::flatten_join_error(mandate_details_fut) )?; + if let Some(order_details) = &request.order_details { + helpers::validate_order_details_amount( + order_details.to_owned(), + payment_intent.amount, + )?; + } + helpers::validate_customer_access(&payment_intent, auth_flow, request)?; helpers::validate_payment_status_against_not_allowed_statuses( diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index ccf9fc3fad1c..c12f28e23390 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -186,6 +186,13 @@ impl payment_id: payment_id.clone(), })?; + if let Some(order_details) = &request.order_details { + helpers::validate_order_details_amount( + order_details.to_owned(), + payment_intent.amount, + )?; + } + payment_attempt = db .insert_payment_attempt(payment_attempt_new, storage_scheme) .await diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 75d3b6b82b4c..1176eeb1dd3f 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -60,6 +60,13 @@ impl .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + if let Some(order_details) = &request.order_details { + helpers::validate_order_details_amount( + order_details.to_owned(), + payment_intent.amount, + )?; + } + payment_intent.setup_future_usage = request .setup_future_usage .or(payment_intent.setup_future_usage); From 34953a046429fe0341e8469bd9b036e176bda205 Mon Sep 17 00:00:00 2001 From: Jagan Date: Mon, 27 Nov 2023 14:06:30 +0530 Subject: [PATCH 101/146] chore(connector): update connector addition script (#2801) Co-authored-by: Bernard Eugine <114725419+bernard-eugine@users.noreply.github.com> --- connector-template/test.rs | 1 + scripts/add_connector.sh | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/connector-template/test.rs b/connector-template/test.rs index 5bbf761dea19..7b093ddb6efa 100644 --- a/connector-template/test.rs +++ b/connector-template/test.rs @@ -17,6 +17,7 @@ impl utils::Connector for {{project-name | downcase | pascal_case}}Test { connector: Box::new(&{{project-name | downcase | pascal_case}}), connector_name: types::Connector::{{project-name | downcase | pascal_case}}, get_token: types::api::GetToken::Connector, + merchant_connector_id: None, } } diff --git a/scripts/add_connector.sh b/scripts/add_connector.sh index 9fdc57bf3c81..9a30fe9d7573 100755 --- a/scripts/add_connector.sh +++ b/scripts/add_connector.sh @@ -45,7 +45,7 @@ cd $SCRIPT/.. # Remove template files if already created for this connector rm -rf $conn/$payment_gateway $conn/$payment_gateway.rs -git checkout $conn.rs $src/types/api.rs $src/configs/settings.rs config/development.toml config/docker_compose.toml config/config.example.toml loadtest/config/development.toml crates/api_models/src/enums.rs $src/core/payments/flows.rs +git checkout $conn.rs $src/types/api.rs $src/configs/settings.rs config/development.toml config/docker_compose.toml config/config.example.toml loadtest/config/development.toml crates/api_models/src/enums.rs crates/euclid/src/enums.rs crates/api_models/src/routing.rs $src/core/payments/flows.rs $src/core/admin.rs $src/core/payments/routing/transformers.rs $src/types/transformers.rs # Add enum for this connector in required places previous_connector='' @@ -54,15 +54,21 @@ previous_connector_camelcase="$(tr '[:lower:]' '[:upper:]' <<< ${previous_connec sed -i'' -e "s|pub mod $previous_connector;|pub mod $previous_connector;\npub mod ${payment_gateway};|" $conn.rs sed -i'' -e "s/};/${payment_gateway}::${payment_gateway_camelcase},\n};/" $conn.rs sed -i'' -e "s|$previous_connector_camelcase \(.*\)|$previous_connector_camelcase \1\n\t\t\tenums::Connector::${payment_gateway_camelcase} => Ok(Box::new(\&connector::${payment_gateway_camelcase})),|" $src/types/api.rs +sed -i'' -e "s|$previous_connector_camelcase \(.*\)|$previous_connector_camelcase \1\n\t\t\tRoutableConnectors::${payment_gateway_camelcase} => euclid_enums::Connector::${payment_gateway_camelcase},|" crates/api_models/src/routing.rs sed -i'' -e "s/pub $previous_connector: \(.*\)/pub $previous_connector: \1\n\tpub ${payment_gateway}: ConnectorParams,/" $src/configs/settings.rs sed -i'' -e "s|$previous_connector.base_url \(.*\)|$previous_connector.base_url \1\n${payment_gateway}.base_url = \"$base_url\"|" config/development.toml config/docker_compose.toml config/config.example.toml loadtest/config/development.toml sed -r -i'' -e "s/\"$previous_connector\",/\"$previous_connector\",\n \"${payment_gateway}\",/" config/development.toml config/docker_compose.toml config/config.example.toml loadtest/config/development.toml sed -i '' -e "s/\(pub enum Connector {\)/\1\n\t${payment_gateway_camelcase},/" crates/api_models/src/enums.rs +sed -i '' -e "s/\(pub enum Connector {\)/\1\n\t${payment_gateway_camelcase},/" crates/euclid/src/enums.rs +sed -i '' -e "s/\(match connector_name {\)/\1\n\t\tapi_enums::Connector::${payment_gateway_camelcase} => {${payment_gateway}::transformers::${payment_gateway_camelcase}AuthType::try_from(val)?;Ok(())}/" $src/core/admin.rs +sed -i'' -e "s|$previous_connector_camelcase \(.*\)|$previous_connector_camelcase \1\n\t\t\tapi_enums::RoutableConnectors::${payment_gateway_camelcase} => Self::${payment_gateway_camelcase},|" $src/core/payments/routing/transformers.rs +sed -i'' -e "s|dsl_enums::Connector::$previous_connector_camelcase \(.*\)|dsl_enums::Connector::$previous_connector_camelcase \1\n\t\t\tdsl_enums::Connector::${payment_gateway_camelcase} => Self::${payment_gateway_camelcase},|" $src/types/transformers.rs +sed -i'' -e "s|api_enums::Connector::$previous_connector_camelcase \(.*\)|api_enums::Connector::$previous_connector_camelcase \1\n\t\t\tapi_enums::Connector::${payment_gateway_camelcase} => Self::${payment_gateway_camelcase},|" $src/types/transformers.rs sed -i'' -e "s/\(pub enum RoutableConnectors {\)/\1\n\t${payment_gateway_camelcase},/" crates/api_models/src/enums.rs sed -i'' -e "s/^default_imp_for_\(.*\)/default_imp_for_\1\n\tconnector::${payment_gateway_camelcase},/" $src/core/payments/flows.rs # Remove temporary files created in above step -rm $conn.rs-e $src/types/api.rs-e $src/configs/settings.rs-e config/development.toml-e config/docker_compose.toml-e config/config.example.toml-e loadtest/config/development.toml-e crates/api_models/src/enums.rs-e $src/core/payments/flows.rs-e +rm $conn.rs-e $src/types/api.rs-e $src/configs/settings.rs-e config/development.toml-e config/docker_compose.toml-e config/config.example.toml-e loadtest/config/development.toml-e crates/api_models/src/enums.rs-e crates/euclid/src/enums.rs-e crates/api_models/src/routing.rs-e $src/core/payments/flows.rs-e $src/core/admin.rs-e $src/core/payments/routing/transformers.rs-e $src/types/transformers.rs-e cd $conn/ # Generate template files for the connector From 0fa8ad1b7c27010bf83e4035de9881d29e192e8a Mon Sep 17 00:00:00 2001 From: Sanchith Hegde <22217505+SanchithHegde@users.noreply.github.com> Date: Mon, 27 Nov 2023 15:58:36 +0530 Subject: [PATCH 102/146] docs(try_local_system): add instructions to run using Docker Compose by pulling standalone images (#2984) --- README.md | 44 +++-- docker-compose-development.yml | 301 +++++++++++++++++++++++++++++++++ docker-compose.yml | 238 +++++++++++++------------- docs/try_local_system.md | 128 +++++++++++--- 4 files changed, 542 insertions(+), 169 deletions(-) create mode 100644 docker-compose-development.yml diff --git a/README.md b/README.md index e820b93e63cc..db8e820ef142 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ Hyperswitch-Logo

-

The open-source payments switch

@@ -35,7 +34,6 @@ The single API to access payment ecosystems across 130+ countries

-
@@ -57,14 +55,14 @@ Using Hyperswitch, you can:

⚡️ Quick Start Guide

-

One-click deployment on AWS cloud

+### One-click deployment on AWS cloud -The fastest and easiest way to try hyperswitch is via our CDK scripts +The fastest and easiest way to try Hyperswitch is via our CDK scripts 1. Click on the following button for a quick standalone deployment on AWS, suitable for prototyping. No code or setup is required in your system and the deployment is covered within the AWS free-tier setup. -   + 2. Sign-in to your AWS console. @@ -72,12 +70,27 @@ The fastest and easiest way to try hyperswitch is via our CDK scripts For an early access to the production-ready setup fill this Early Access Form +### Run it on your system + +You can run Hyperswitch on your system using Docker Compose after cloning this repository: + +```shell +docker compose up -d +``` + +This will start the payments router, the primary component within Hyperswitch. + +Check out the [local setup guide][local-setup-guide] for a more comprehensive +setup, which includes the [scheduler and monitoring services][docker-compose-scheduler-monitoring]. + +[local-setup-guide]: /docs/try_local_system.md +[docker-compose-scheduler-monitoring]: /docs/try_local_system.md#run-the-scheduler-and-monitoring-services +

🔌 Fast Integration for Stripe Users

-If you are already using Stripe, integrating with Hyperswitch is fun, fast & -easy. +If you are already using Stripe, integrating with Hyperswitch is fun, fast & easy. Try the steps below to get a feel for how quick the setup is: 1. Get API keys from our [dashboard]. @@ -96,9 +109,7 @@ Try the steps below to get a feel for how quick the setup is: As of Sept 2023, we support 50+ payment processors and multiple global payment methods. In addition, we are continuously integrating new processors based on their reach and community requests. Our target is to support 100+ processors by H2 2023. -You can find the latest list of payment processors, supported methods, and -features -[here][supported-connectors-and-features]. +You can find the latest list of payment processors, supported methods, and features [here][supported-connectors-and-features]. [supported-connectors-and-features]: https://hyperswitch.io/pm-list @@ -252,12 +263,11 @@ We welcome contributions from the community. Please read through our Included are directions for opening issues, coding standards, and notes on development. -- We appreciate all types of contributions: code, documentation, demo creation, or something new way you want to contribute to us. We will reward every contribution with a Hyperswitch branded t-shirt. -- 🦀 **Important note for Rust developers**: We aim for contributions from the community -across a broad range of tracks. Hence, we have prioritised simplicity and code -readability over purely idiomatic code. For example, some of the code in core -functions (e.g., `payments_core`) is written to be more readable than -pure-idiomatic. +- We appreciate all types of contributions: code, documentation, demo creation, or some new way you want to contribute to us. + We will reward every contribution with a Hyperswitch branded t-shirt. +- 🦀 **Important note for Rust developers**: We aim for contributions from the community across a broad range of tracks. + Hence, we have prioritised simplicity and code readability over purely idiomatic code. + For example, some of the code in core functions (e.g., `payments_core`) is written to be more readable than pure-idiomatic.

👥 Community

@@ -269,7 +279,6 @@ Get updates on Hyperswitch development and chat with the community: - [Slack workspace][slack] for questions related to integrating hyperswitch, integrating a connector in hyperswitch, etc. - [GitHub Discussions][github-discussions] to drop feature requests or suggest anything payments-related you need for your stack. -[blog]: https://hyperswitch.io/blog [discord]: https://discord.gg/wJZ7DVW8mm [slack]: https://join.slack.com/t/hyperswitch-io/shared_invite/zt-1k6cz4lee-SAJzhz6bjmpp4jZCDOtOIg [github-discussions]: https://github.com/juspay/hyperswitch/discussions @@ -314,7 +323,6 @@ Check the [CHANGELOG.md](./CHANGELOG.md) file for details. This product is licensed under the [Apache 2.0 License](LICENSE). -

✨ Thanks to all contributors

diff --git a/docker-compose-development.yml b/docker-compose-development.yml new file mode 100644 index 000000000000..500f397cfa30 --- /dev/null +++ b/docker-compose-development.yml @@ -0,0 +1,301 @@ +version: "3.8" + +volumes: + cargo_cache: + pg_data: + router_build_cache: + scheduler_build_cache: + drainer_build_cache: + redisinsight_store: + +networks: + router_net: + +services: + ### Dependencies + pg: + image: postgres:latest + ports: + - "5432:5432" + networks: + - router_net + volumes: + - pg_data:/VAR/LIB/POSTGRESQL/DATA + environment: + - POSTGRES_USER=db_user + - POSTGRES_PASSWORD=db_pass + - POSTGRES_DB=hyperswitch_db + + redis-standalone: + image: redis:7 + labels: + - redis + networks: + - router_net + ports: + - "6379" + + migration_runner: + image: rust:latest + command: "bash -c 'cargo install diesel_cli --no-default-features --features postgres && diesel migration --database-url postgres://$${DATABASE_USER}:$${DATABASE_PASSWORD}@$${DATABASE_HOST}:$${DATABASE_PORT}/$${DATABASE_NAME} run'" + working_dir: /app + networks: + - router_net + volumes: + - ./:/app + environment: + - DATABASE_USER=db_user + - DATABASE_PASSWORD=db_pass + - DATABASE_HOST=pg + - DATABASE_PORT=5432 + - DATABASE_NAME=hyperswitch_db + + ### Application services + hyperswitch-server: + image: rust:latest + command: cargo run --bin router -- -f ./config/docker_compose.toml + working_dir: /app + ports: + - "8080:8080" + networks: + - router_net + volumes: + - ./:/app + - cargo_cache:/cargo_cache + - router_build_cache:/cargo_build_cache + environment: + - CARGO_HOME=/cargo_cache + - CARGO_TARGET_DIR=/cargo_build_cache + labels: + logs: "promtail" + healthcheck: + test: curl --fail http://localhost:8080/health || exit 1 + interval: 120s + retries: 4 + start_period: 20s + timeout: 10s + + hyperswitch-producer: + image: rust:latest + command: cargo run --bin scheduler -- -f ./config/docker_compose.toml + working_dir: /app + networks: + - router_net + profiles: + - scheduler + volumes: + - ./:/app + - cargo_cache:/cargo_cache + - scheduler_build_cache:/cargo_build_cache + environment: + - CARGO_HOME=/cargo_cache + - CARGO_TARGET_DIR=/cargo_build_cache + - SCHEDULER_FLOW=producer + depends_on: + hyperswitch-consumer: + condition: service_healthy + labels: + logs: "promtail" + + hyperswitch-consumer: + image: rust:latest + command: cargo run --bin scheduler -- -f ./config/docker_compose.toml + working_dir: /app + networks: + - router_net + profiles: + - scheduler + volumes: + - ./:/app + - cargo_cache:/cargo_cache + - scheduler_build_cache:/cargo_build_cache + environment: + - CARGO_HOME=/cargo_cache + - CARGO_TARGET_DIR=/cargo_build_cache + - SCHEDULER_FLOW=consumer + depends_on: + hyperswitch-server: + condition: service_started + labels: + logs: "promtail" + healthcheck: + test: (ps -e | grep scheduler) || exit 1 + interval: 120s + retries: 4 + start_period: 30s + timeout: 10s + + hyperswitch-drainer: + image: rust:latest + command: cargo run --bin drainer -- -f ./config/docker_compose.toml + working_dir: /app + deploy: + replicas: ${DRAINER_INSTANCE_COUNT:-1} + networks: + - router_net + profiles: + - full_kv + volumes: + - ./:/app + - cargo_cache:/cargo_cache + - drainer_build_cache:/cargo_build_cache + environment: + - CARGO_HOME=/cargo_cache + - CARGO_TARGET_DIR=/cargo_build_cache + restart: unless-stopped + depends_on: + hyperswitch-server: + condition: service_started + labels: + logs: "promtail" + + ### Clustered Redis setup + redis-cluster: + image: redis:7 + deploy: + replicas: ${REDIS_CLUSTER_COUNT:-3} + command: redis-server /usr/local/etc/redis/redis.conf + profiles: + - clustered_redis + volumes: + - ./config/redis.conf:/usr/local/etc/redis/redis.conf + labels: + - redis + networks: + - router_net + ports: + - "6379" + - "16379" + + redis-init: + image: redis:7 + profiles: + - clustered_redis + depends_on: + - redis-cluster + networks: + - router_net + command: "bash -c 'export COUNT=${REDIS_CLUSTER_COUNT:-3} + + \ if [ $$COUNT -lt 3 ] + + \ then + + \ echo \"Minimum 3 nodes are needed for redis cluster\" + + \ exit 1 + + \ fi + + \ HOSTS=\"\" + + \ for ((c=1; c<=$$COUNT;c++)) + + \ do + + \ NODE=$COMPOSE_PROJECT_NAME-redis-cluster-$$c:6379 + + \ echo $$NODE + + \ HOSTS=\"$$HOSTS $$NODE\" + + \ done + + \ echo Creating a cluster with $$HOSTS + + \ redis-cli --cluster create $$HOSTS --cluster-yes + + \ '" + + ### Monitoring + grafana: + image: grafana/grafana:latest + ports: + - "3000:3000" + networks: + - router_net + profiles: + - monitoring + restart: unless-stopped + environment: + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_BASIC_ENABLED=false + volumes: + - ./config/grafana.ini:/etc/grafana/grafana.ini + - ./config/grafana-datasource.yaml:/etc/grafana/provisioning/datasources/datasource.yml + + promtail: + image: grafana/promtail:latest + volumes: + - ./logs:/var/log/router + - ./config:/etc/promtail + - /var/run/docker.sock:/var/run/docker.sock + command: -config.file=/etc/promtail/promtail.yaml + profiles: + - monitoring + networks: + - router_net + + loki: + image: grafana/loki:latest + ports: + - "3100" + command: -config.file=/etc/loki/loki.yaml + networks: + - router_net + profiles: + - monitoring + volumes: + - ./config:/etc/loki + + otel-collector: + image: otel/opentelemetry-collector-contrib:latest + command: --config=/etc/otel-collector.yaml + networks: + - router_net + profiles: + - monitoring + volumes: + - ./config/otel-collector.yaml:/etc/otel-collector.yaml + ports: + - "4317" + - "8888" + - "8889" + + prometheus: + image: prom/prometheus:latest + networks: + - router_net + profiles: + - monitoring + volumes: + - ./config/prometheus.yaml:/etc/prometheus/prometheus.yml + ports: + - "9090" + restart: unless-stopped + + tempo: + image: grafana/tempo:latest + command: -config.file=/etc/tempo.yaml + volumes: + - ./config/tempo.yaml:/etc/tempo.yaml + networks: + - router_net + profiles: + - monitoring + ports: + - "3200" # tempo + - "4317" # otlp grpc + restart: unless-stopped + + redis-insight: + image: redislabs/redisinsight:latest + networks: + - router_net + profiles: + - full_kv + ports: + - "8001:8001" + volumes: + - redisinsight_store:/db diff --git a/docker-compose.yml b/docker-compose.yml index f4dce575132e..fd18906143f5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,76 +1,16 @@ -version: "3.7" +version: "3.8" volumes: - cargo_cache: pg_data: - cargo_build_cache: - p_cargo_build_cache: - c_cargo_build_cache: redisinsight_store: - networks: router_net: - services: - promtail: - image: grafana/promtail:latest - volumes: - - ./logs:/var/log/router - - ./config:/etc/promtail - - /var/run/docker.sock:/var/run/docker.sock - command: -config.file=/etc/promtail/promtail.yaml - profiles: - - monitoring - networks: - - router_net - - loki: - image: grafana/loki:latest - ports: - - "3100" - command: -config.file=/etc/loki/loki.yaml - networks: - - router_net - profiles: - - monitoring - volumes: - - ./config:/etc/loki - - otel-collector: - image: otel/opentelemetry-collector-contrib:latest - command: --config=/etc/otel-collector.yaml - networks: - - router_net - profiles: - - monitoring - volumes: - - ./config/otel-collector.yaml:/etc/otel-collector.yaml - ports: - - "4317" - - "8888" - - "8889" - - grafana: - image: grafana/grafana:latest - ports: - - "3000:3000" - networks: - - router_net - profiles: - - monitoring - restart: unless-stopped - environment: - - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin - - GF_AUTH_ANONYMOUS_ENABLED=true - - GF_AUTH_BASIC_ENABLED=false - volumes: - - ./config/grafana.ini:/etc/grafana/grafana.ini - - ./config/grafana-datasource.yaml:/etc/grafana/provisioning/datasources/datasource.yml - + ### Dependencies pg: - image: postgres:14.5 + image: postgres:latest ports: - "5432:5432" networks: @@ -82,52 +22,59 @@ services: - POSTGRES_PASSWORD=db_pass - POSTGRES_DB=hyperswitch_db + redis-standalone: + image: redis:7 + labels: + - redis + networks: + - router_net + ports: + - "6379" + migration_runner: - image: rust:1.70 - command: "bash -c 'cargo install diesel_cli --no-default-features --features \"postgres\" && diesel migration --database-url postgres://db_user:db_pass@pg:5432/hyperswitch_db run'" + image: rust:latest + command: "bash -c 'cargo install diesel_cli --no-default-features --features postgres && diesel migration --database-url postgres://$${DATABASE_USER}:$${DATABASE_PASSWORD}@$${DATABASE_HOST}:$${DATABASE_PORT}/$${DATABASE_NAME} run'" working_dir: /app networks: - router_net volumes: - ./:/app + environment: + - DATABASE_USER=db_user + - DATABASE_PASSWORD=db_pass + - DATABASE_HOST=pg + - DATABASE_PORT=5432 + - DATABASE_NAME=hyperswitch_db + ### Application services hyperswitch-server: - image: rust:1.70 - command: cargo run -- -f ./config/docker_compose.toml - working_dir: /app + image: juspaydotin/hyperswitch-router:standalone + command: /local/bin/router -f /local/config/docker_compose.toml ports: - "8080:8080" networks: - router_net volumes: - - ./:/app - - cargo_cache:/cargo_cache - - cargo_build_cache:/cargo_build_cache - environment: - - CARGO_TARGET_DIR=/cargo_build_cache + - ./config:/local/config labels: logs: "promtail" healthcheck: test: curl --fail http://localhost:8080/health || exit 1 - interval: 100s + interval: 10s retries: 3 - start_period: 20s + start_period: 5s timeout: 10s hyperswitch-producer: - image: rust:1.70 - command: cargo run --bin scheduler -- -f ./config/docker_compose.toml - working_dir: /app + image: juspaydotin/hyperswitch-producer:standalone + command: /local/bin/scheduler -f /local/config/docker_compose.toml networks: - router_net profiles: - scheduler volumes: - - ./:/app - - cargo_cache:/cargo_cache - - p_cargo_build_cache:/cargo_build_cache + - ./config:/local/config environment: - - CARGO_TARGET_DIR=/cargo_build_cache - SCHEDULER_FLOW=producer depends_on: hyperswitch-consumer: @@ -136,39 +83,54 @@ services: logs: "promtail" hyperswitch-consumer: - image: rust:1.70 - command: cargo run --bin scheduler -- -f ./config/docker_compose.toml - working_dir: /app + image: juspaydotin/hyperswitch-consumer:standalone + command: /local/bin/scheduler -f /local/config/docker_compose.toml networks: - router_net profiles: - scheduler volumes: - - ./:/app - - cargo_cache:/cargo_cache - - c_cargo_build_cache:/cargo_build_cache + - ./config:/local/config environment: - - CARGO_TARGET_DIR=/cargo_build_cache - SCHEDULER_FLOW=consumer depends_on: hyperswitch-server: condition: service_started - labels: logs: "promtail" - healthcheck: test: (ps -e | grep scheduler) || exit 1 - interval: 120s + interval: 10s retries: 3 - start_period: 30s + start_period: 5s timeout: 10s + hyperswitch-drainer: + image: juspaydotin/hyperswitch-drainer:standalone + command: /local/bin/drainer -f /local/config/docker_compose.toml + deploy: + replicas: ${DRAINER_INSTANCE_COUNT:-1} + networks: + - router_net + profiles: + - full_kv + volumes: + - ./config:/local/config + restart: unless-stopped + depends_on: + hyperswitch-server: + condition: service_started + labels: + logs: "promtail" + + ### Clustered Redis setup redis-cluster: image: redis:7 deploy: replicas: ${REDIS_CLUSTER_COUNT:-3} command: redis-server /usr/local/etc/redis/redis.conf + profiles: + - clustered_redis volumes: - ./config/redis.conf:/usr/local/etc/redis/redis.conf labels: @@ -179,17 +141,10 @@ services: - "6379" - "16379" - redis-standalone: - image: redis:7 - labels: - - redis - networks: - - router_net - ports: - - "6379" - redis-init: image: redis:7 + profiles: + - clustered_redis depends_on: - redis-cluster networks: @@ -226,16 +181,62 @@ services: \ '" - redis-insight: - image: redislabs/redisinsight:latest + ### Monitoring + grafana: + image: grafana/grafana:latest + ports: + - "3000:3000" networks: - router_net profiles: - - full_kv + - monitoring + restart: unless-stopped + environment: + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_BASIC_ENABLED=false + volumes: + - ./config/grafana.ini:/etc/grafana/grafana.ini + - ./config/grafana-datasource.yaml:/etc/grafana/provisioning/datasources/datasource.yml + + promtail: + image: grafana/promtail:latest + volumes: + - ./logs:/var/log/router + - ./config:/etc/promtail + - /var/run/docker.sock:/var/run/docker.sock + command: -config.file=/etc/promtail/promtail.yaml + profiles: + - monitoring + networks: + - router_net + + loki: + image: grafana/loki:latest ports: - - "8001:8001" + - "3100" + command: -config.file=/etc/loki/loki.yaml + networks: + - router_net + profiles: + - monitoring volumes: - - redisinsight_store:/db + - ./config:/etc/loki + + otel-collector: + image: otel/opentelemetry-collector-contrib:latest + command: --config=/etc/otel-collector.yaml + networks: + - router_net + profiles: + - monitoring + volumes: + - ./config/otel-collector.yaml:/etc/otel-collector.yaml + ports: + - "4317" + - "8888" + - "8889" + prometheus: image: prom/prometheus:latest networks: @@ -261,25 +262,14 @@ services: - "3200" # tempo - "4317" # otlp grpc restart: unless-stopped - hyperswitch-drainer: - image: rust:1.70 - command: cargo run --bin drainer -- -f ./config/docker_compose.toml - working_dir: /app - deploy: - replicas: ${DRAINER_INSTANCE_COUNT:-1} + + redis-insight: + image: redislabs/redisinsight:latest networks: - router_net profiles: - full_kv + ports: + - "8001:8001" volumes: - - ./:/app - - cargo_cache:/cargo_cache - - cargo_build_cache:/cargo_build_cache - environment: - - CARGO_TARGET_DIR=/cargo_build_cache - restart: unless-stopped - depends_on: - hyperswitch-server: - condition: service_started - labels: - logs: "promtail" + - redisinsight_store:/db diff --git a/docs/try_local_system.md b/docs/try_local_system.md index 59df43f24810..a9cd080f26d5 100644 --- a/docs/try_local_system.md +++ b/docs/try_local_system.md @@ -1,23 +1,20 @@ # Try out hyperswitch on your system -**NOTE:** -This guide is aimed at users and developers who wish to set up hyperswitch on -their local systems and requires quite some time and effort. -If you'd prefer trying out hyperswitch quickly without the hassle of setting up -all dependencies, you can [try out hyperswitch sandbox environment][try-sandbox]. - -There are two options to set up hyperswitch on your system: - -1. Use Docker Compose -2. Set up a Rust environment and other dependencies on your system +The simplest way to run hyperswitch locally is +[with Docker Compose](#run-hyperswitch-using-docker-compose) by pulling the +latest images from Docker Hub. +However, if you're willing to modify the code and run it, or are a developer +contributing to hyperswitch, then you can either +[set up a development environment using Docker Compose](#set-up-a-development-environment-using-docker-compose), +or [set up a Rust environment on your system](#set-up-a-rust-environment-and-other-dependencies). Check the Table Of Contents to jump to the relevant section. -[try-sandbox]: ./try_sandbox.md - **Table Of Contents:** -- [Set up hyperswitch using Docker Compose](#set-up-hyperswitch-using-docker-compose) +- [Run hyperswitch using Docker Compose](#run-hyperswitch-using-docker-compose) + - [Run the scheduler and monitoring services](#run-the-scheduler-and-monitoring-services) +- [Set up a development environment using Docker Compose](#set-up-a-development-environment-using-docker-compose) - [Set up a Rust environment and other dependencies](#set-up-a-rust-environment-and-other-dependencies) - [Set up dependencies on Ubuntu-based systems](#set-up-dependencies-on-ubuntu-based-systems) - [Set up dependencies on Windows (Ubuntu on WSL2)](#set-up-dependencies-on-windows-ubuntu-on-wsl2) @@ -33,7 +30,7 @@ Check the Table Of Contents to jump to the relevant section. - [Create a Payment](#create-a-payment) - [Create a Refund](#create-a-refund) -## Set up hyperswitch using Docker Compose +## Run hyperswitch using Docker Compose 1. Install [Docker Compose][docker-compose-install]. 2. Clone the repository and switch to the project directory: @@ -54,15 +51,15 @@ Check the Table Of Contents to jump to the relevant section. docker compose up -d ``` -5. Run database migrations: - - ```shell - docker compose run hyperswitch-server bash -c \ - "cargo install diesel_cli && \ - diesel migration --database-url postgres://db_user:db_pass@pg:5432/hyperswitch_db run" - ``` + This should run the hyperswitch payments router, the primary component within + hyperswitch. + Wait for the `migration_runner` container to finish installing `diesel_cli` + and running migrations (approximately 2 minutes) before proceeding further. + You can also choose to + [run the scheduler and monitoring services](#run-the-scheduler-and-monitoring-services) + in addition to the payments router. -6. Verify that the server is up and running by hitting the health endpoint: +5. Verify that the server is up and running by hitting the health endpoint: ```shell curl --head --request GET 'http://localhost:8080/health' @@ -71,9 +68,86 @@ Check the Table Of Contents to jump to the relevant section. If the command returned a `200 OK` status code, proceed with [trying out our APIs](#try-out-our-apis). +### Run the scheduler and monitoring services + +You can run the scheduler and monitoring services by specifying suitable profile +names to the above Docker Compose command. +To understand more about the hyperswitch architecture and the components +involved, check out the [architecture document][architecture]. + +- To run the scheduler components (consumer and producer), you can specify + `--profile scheduler`: + + ```shell + docker compose --profile scheduler up -d + ``` + +- To run the monitoring services (Grafana, Promtail, Loki, Prometheus and Tempo), + you can specify `--profile monitoring`: + + ```shell + docker compose --profile monitoring up -d + ``` + + You can then access Grafana at `http://localhost:3000` and view application + logs using the "Explore" tab, select Loki as the data source, and select the + container to query logs from. + +- You can also specify multiple profile names by specifying the `--profile` flag + multiple times. + To run both the scheduler components and monitoring services, the Docker + Compose command would be: + + ```shell + docker compose --profile scheduler --profile monitoring up -d + ``` + +Once the services have been confirmed to be up and running, you can proceed with +[trying out our APIs](#try-out-our-apis) + [docker-compose-install]: https://docs.docker.com/compose/install/ [docker-compose-config]: /config/docker_compose.toml [docker-compose-yml]: /docker-compose.yml +[architecture]: /docs/architecture.md + +## Set up a development environment using Docker Compose + +1. Install [Docker Compose][docker-compose-install]. +2. Clone the repository and switch to the project directory: + + ```shell + git clone https://github.com/juspay/hyperswitch + cd hyperswitch + ``` + +3. (Optional) Configure the application using the + [`config/docker_compose.toml`][docker-compose-config] file. + The provided configuration should work as is. + If you do update the `docker_compose.toml` file, ensure to also update the + corresponding values in the [`docker-compose.yml`][docker-compose-yml] file. +4. Start all the services using Docker Compose: + + ```shell + docker compose --file docker-compose-development.yml up -d + ``` + + This will compile the payments router, the primary component within + hyperswitch and then start it. + Depending on the specifications of your machine, compilation can take + around 15 minutes. + +5. (Optional) You can also choose to + [start the scheduler and/or monitoring services](#run-the-scheduler-and-monitoring-services) + in addition to the payments router. + +6. Verify that the server is up and running by hitting the health endpoint: + + ```shell + curl --head --request GET 'http://localhost:8080/health' + ``` + + If the command returned a `200 OK` status code, proceed with + [trying out our APIs](#try-out-our-apis). ## Set up a Rust environment and other dependencies @@ -134,7 +208,7 @@ for your distribution and follow along. 4. Install `diesel_cli` using `cargo`: ```shell - cargo install diesel_cli --no-default-features --features "postgres" + cargo install diesel_cli --no-default-features --features postgres ``` 5. Make sure your system has the `pkg-config` package and OpenSSL installed: @@ -224,7 +298,7 @@ packages for your distribution and follow along. 6. Install `diesel_cli` using `cargo`: ```shell - cargo install diesel_cli --no-default-features --features "postgres" + cargo install diesel_cli --no-default-features --features postgres ``` 7. Make sure your system has the `pkg-config` package and OpenSSL installed: @@ -260,7 +334,7 @@ You can opt to use your favorite package manager instead. 4. Install `diesel_cli` using `cargo`: ```shell - cargo install diesel_cli --no-default-features --features "postgres" + cargo install diesel_cli --no-default-features --features postgres ``` 5. Install OpenSSL with `winget`: @@ -322,7 +396,7 @@ You can opt to use your favorite package manager instead. 4. Install `diesel_cli` using `cargo`: ```shell - cargo install diesel_cli --no-default-features --features "postgres" + cargo install diesel_cli --no-default-features --features postgres ``` If linking `diesel_cli` fails due to missing `libpq` (if the error message is @@ -333,7 +407,7 @@ You can opt to use your favorite package manager instead. brew install libpq export PQ_LIB_DIR="$(brew --prefix libpq)/lib" - cargo install diesel_cli --no-default-features --features "postgres" + cargo install diesel_cli --no-default-features --features postgres ``` You may also choose to persist the value of `PQ_LIB_DIR` in your shell From aee59e088a8e7c1b81aca1015c90c7b4fd07511d Mon Sep 17 00:00:00 2001 From: "hyperswitch-bot[bot]" <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Date: Mon, 27 Nov 2023 14:36:58 +0000 Subject: [PATCH 103/146] test(postman): update postman collection files --- postman/collection-json/paypal.postman_collection.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/postman/collection-json/paypal.postman_collection.json b/postman/collection-json/paypal.postman_collection.json index d9deae47f9af..a6ee545a9497 100644 --- a/postman/collection-json/paypal.postman_collection.json +++ b/postman/collection-json/paypal.postman_collection.json @@ -808,7 +808,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"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://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"paypal\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"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://duck.com\",\"surcharge_details\":{\"surcharge_amount\":5,\"tax_amount\":5},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"paypal\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -990,7 +990,7 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"surcharge_details\":{\"surcharge_amount\":5,\"tax_amount\":5},\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}}}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}}}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", From 34f7e83fe969b630ebe4142605d102340177f7d6 Mon Sep 17 00:00:00 2001 From: Sanchith Hegde Date: Mon, 27 Nov 2023 20:21:45 +0530 Subject: [PATCH 104/146] chore(version): v1.90.0 --- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6197598e564..8b3abf1d5781 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,39 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.90.0 (2023-11-27) + +### Features + +- **auth:** Add Authorization for JWT Authentication types ([#2973](https://github.com/juspay/hyperswitch/pull/2973)) ([`03c0a77`](https://github.com/juspay/hyperswitch/commit/03c0a772a99000acf4676db8ca2ce916036281d1)) +- **user:** Implement change password for user ([#2959](https://github.com/juspay/hyperswitch/pull/2959)) ([`bfa1645`](https://github.com/juspay/hyperswitch/commit/bfa1645b847fb881eb2370d5dbfef6fd0b53725d)) + +### Bug Fixes + +- **router:** Added validation to check total orderDetails amount equal to amount in request ([#2965](https://github.com/juspay/hyperswitch/pull/2965)) ([`37532d4`](https://github.com/juspay/hyperswitch/commit/37532d46f599a99e0e021b0455a6f02381005dd7)) +- Add prefix to connector_transaction_id ([#2981](https://github.com/juspay/hyperswitch/pull/2981)) ([`107c3b9`](https://github.com/juspay/hyperswitch/commit/107c3b99417dd7bca7b62741ad601485700f37be)) + +### Refactors + +- **connector:** [Nuvei] update error message ([#2867](https://github.com/juspay/hyperswitch/pull/2867)) ([`04b7c03`](https://github.com/juspay/hyperswitch/commit/04b7c0384dc9290bd60f49033fd35732527720f1)) + +### Testing + +- **postman:** Update postman collection files ([`aee59e0`](https://github.com/juspay/hyperswitch/commit/aee59e088a8e7c1b81aca1015c90c7b4fd07511d)) + +### Documentation + +- **try_local_system:** Add instructions to run using Docker Compose by pulling standalone images ([#2984](https://github.com/juspay/hyperswitch/pull/2984)) ([`0fa8ad1`](https://github.com/juspay/hyperswitch/commit/0fa8ad1b7c27010bf83e4035de9881d29e192e8a)) + +### Miscellaneous Tasks + +- **connector:** Update connector addition script ([#2801](https://github.com/juspay/hyperswitch/pull/2801)) ([`34953a0`](https://github.com/juspay/hyperswitch/commit/34953a046429fe0341e8469bd9b036e176bda205)) + +**Full Changelog:** [`v1.89.0...v1.90.0`](https://github.com/juspay/hyperswitch/compare/v1.89.0...v1.90.0) + +- - - + + ## 1.89.0 (2023-11-24) ### Features From 54d6b1083fab5d2b0c7637c150524460a16a3fec Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Mon, 27 Nov 2023 20:34:49 +0530 Subject: [PATCH 105/146] CI: update the credentails for generating token in release new version workflow (#2989) --- .github/workflows/release-new-version.yml | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release-new-version.yml b/.github/workflows/release-new-version.yml index b54e240d96fc..2f8ae7e4819f 100644 --- a/.github/workflows/release-new-version.yml +++ b/.github/workflows/release-new-version.yml @@ -23,19 +23,11 @@ jobs: runs-on: ubuntu-latest steps: - - name: Generate a token - if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} - id: generate_token - uses: actions/create-github-app-token@v1 - with: - app-id: ${{ secrets.HYPERSWITCH_BOT_APP_ID }} - private-key: ${{ secrets.HYPERSWITCH_BOT_APP_PRIVATE_KEY }} - - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - token: ${{ steps.generate_token.outputs.token }} + token: ${{ secrets.AUTO_RELEASE_PAT }} - name: Install Rust uses: dtolnay/rust-toolchain@master @@ -51,8 +43,8 @@ jobs: - name: Set Git Configuration shell: bash run: | - git config --local user.name 'hyperswitch-bot[bot]' - git config --local user.email '148525504+hyperswitch-bot[bot]@users.noreply.github.com' + git config --local user.name 'github-actions' + git config --local user.email '41898282+github-actions[bot]@users.noreply.github.com' - name: Update Postman collection files from Postman directories shell: bash From 0e66b1b5dcce6dd87c9d743c9eb73d0cd8e330b2 Mon Sep 17 00:00:00 2001 From: nain-F49FF806 <126972030+nain-F49FF806@users.noreply.github.com> Date: Tue, 28 Nov 2023 08:53:53 +0100 Subject: [PATCH 106/146] refactor(masking): use empty enums as masking:Strategy types (#2874) --- crates/cards/src/validate.rs | 2 +- crates/common_utils/src/pii.rs | 12 ++++++------ crates/masking/src/secret.rs | 6 +++--- crates/masking/src/strategy.rs | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/cards/src/validate.rs b/crates/cards/src/validate.rs index db6957057ecc..d083a420a1e5 100644 --- a/crates/cards/src/validate.rs +++ b/crates/cards/src/validate.rs @@ -72,7 +72,7 @@ impl<'de> Deserialize<'de> for CardNumber { } } -pub struct CardNumberStrategy; +pub enum CardNumberStrategy {} impl Strategy for CardNumberStrategy where diff --git a/crates/common_utils/src/pii.rs b/crates/common_utils/src/pii.rs index c246d2042269..39793de5c2b5 100644 --- a/crates/common_utils/src/pii.rs +++ b/crates/common_utils/src/pii.rs @@ -27,7 +27,7 @@ pub type SecretSerdeValue = Secret; /// Strategy for masking a PhoneNumber #[derive(Debug)] -pub struct PhoneNumberStrategy; +pub enum PhoneNumberStrategy {} /// Phone Number #[derive(Debug, serde::Deserialize, serde::Serialize)] @@ -144,7 +144,7 @@ where /// Strategy for Encryption #[derive(Debug)] -pub struct EncryptionStratergy; +pub enum EncryptionStratergy {} impl Strategy for EncryptionStratergy where @@ -157,7 +157,7 @@ where /// Client secret #[derive(Debug)] -pub struct ClientSecret; +pub enum ClientSecret {} impl Strategy for ClientSecret where @@ -189,7 +189,7 @@ where /// Strategy for masking Email #[derive(Debug)] -pub struct EmailStrategy; +pub enum EmailStrategy {} impl Strategy for EmailStrategy where @@ -305,7 +305,7 @@ impl FromStr for Email { /// IP address #[derive(Debug)] -pub struct IpAddress; +pub enum IpAddress {} impl Strategy for IpAddress where @@ -332,7 +332,7 @@ where /// Strategy for masking UPI VPA's #[derive(Debug)] -pub struct UpiVpaMaskingStrategy; +pub enum UpiVpaMaskingStrategy {} impl Strategy for UpiVpaMaskingStrategy where diff --git a/crates/masking/src/secret.rs b/crates/masking/src/secret.rs index 96411d4632bd..b2e9124688cb 100644 --- a/crates/masking/src/secret.rs +++ b/crates/masking/src/secret.rs @@ -12,8 +12,8 @@ use crate::{strategy::Strategy, PeekInterface}; /// To get access to value use method `expose()` of trait [`crate::ExposeInterface`]. /// /// ## Masking -/// Use the [`crate::strategy::Strategy`] trait to implement a masking strategy on a unit struct -/// and pass the unit struct as a second generic parameter to [`Secret`] while defining it. +/// Use the [`crate::strategy::Strategy`] trait to implement a masking strategy on a zero-variant +/// enum and pass this enum as a second generic parameter to [`Secret`] while defining it. /// [`Secret`] will take care of applying the masking strategy on the inner secret when being /// displayed. /// @@ -24,7 +24,7 @@ use crate::{strategy::Strategy, PeekInterface}; /// use masking::Secret; /// use std::fmt; /// -/// struct MyStrategy; +/// enum MyStrategy {} /// /// impl Strategy for MyStrategy /// where diff --git a/crates/masking/src/strategy.rs b/crates/masking/src/strategy.rs index f744ee1f4b52..8b4d9b0ec34d 100644 --- a/crates/masking/src/strategy.rs +++ b/crates/masking/src/strategy.rs @@ -7,7 +7,7 @@ pub trait Strategy { } /// Debug with type -pub struct WithType; +pub enum WithType {} impl Strategy for WithType { fn fmt(_: &T, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -18,7 +18,7 @@ impl Strategy for WithType { } /// Debug without type -pub struct WithoutType; +pub enum WithoutType {} impl Strategy for WithoutType { fn fmt(_: &T, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { From c0116db271f6afc1b93c04705209bfc346228c68 Mon Sep 17 00:00:00 2001 From: Prajjwal Kumar Date: Tue, 28 Nov 2023 16:05:04 +0530 Subject: [PATCH 107/146] feat(currency_conversion): add currency conversion feature (#2948) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- Cargo.lock | 259 +++++++- config/config.example.toml | 10 + config/development.toml | 9 + config/docker_compose.toml | 9 + crates/api_models/Cargo.toml | 2 +- crates/api_models/src/currency.rs | 21 + crates/api_models/src/lib.rs | 1 + crates/currency_conversion/Cargo.toml | 16 + crates/currency_conversion/src/conversion.rs | 101 +++ crates/currency_conversion/src/error.rs | 8 + crates/currency_conversion/src/lib.rs | 3 + crates/currency_conversion/src/types.rs | 201 ++++++ crates/euclid_wasm/Cargo.toml | 1 + crates/euclid_wasm/src/lib.rs | 36 ++ crates/router/Cargo.toml | 4 +- crates/router/src/configs/settings.rs | 33 + crates/router/src/core.rs | 2 + crates/router/src/core/currency.rs | 51 ++ crates/router/src/lib.rs | 1 + crates/router/src/routes.rs | 4 + crates/router/src/routes/app.rs | 20 +- crates/router/src/routes/currency.rs | 58 ++ crates/router/src/routes/lock_utils.rs | 3 + crates/router/src/utils.rs | 6 +- crates/router/src/utils/currency.rs | 641 +++++++++++++++++++ crates/router_env/src/logger/types.rs | 2 + loadtest/config/development.toml | 9 + 27 files changed, 1501 insertions(+), 10 deletions(-) create mode 100644 crates/api_models/src/currency.rs create mode 100644 crates/currency_conversion/Cargo.toml create mode 100644 crates/currency_conversion/src/conversion.rs create mode 100644 crates/currency_conversion/src/error.rs create mode 100644 crates/currency_conversion/src/lib.rs create mode 100644 crates/currency_conversion/src/types.rs create mode 100644 crates/router/src/core/currency.rs create mode 100644 crates/router/src/routes/currency.rs create mode 100644 crates/router/src/utils/currency.rs diff --git a/Cargo.lock b/Cargo.lock index bf0ee2d110c7..e4a317d74f49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -381,7 +381,7 @@ dependencies = [ "router_derive", "serde", "serde_json", - "strum 0.24.1", + "strum 0.25.0", "time", "url", "utoipa", @@ -1186,6 +1186,18 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake2" version = "0.10.6" @@ -1227,6 +1239,30 @@ dependencies = [ "generic-array", ] +[[package]] +name = "borsh" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf617fabf5cdbdc92f774bfe5062d870f228b80056d41180797abf48bed4056e" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f404657a7ea7b5249e36808dff544bc88a28f26e0ac40009f674b7a009d14be3" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.38", + "syn_derive", +] + [[package]] name = "brotli" version = "3.4.0" @@ -1264,6 +1300,28 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +[[package]] +name = "bytecheck" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6372023ac861f6e6dc89c8344a8f398fb42aaba2b5dbc649ca0c0e9dbcb627" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7ec4c6f261935ad534c0c22dbef2201b45918860eb1c574b972bd213a76af61" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bytecount" version = "0.6.4" @@ -1415,6 +1473,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "checked_int_cast" version = "1.0.0" @@ -1858,6 +1922,17 @@ dependencies = [ "typenum", ] +[[package]] +name = "currency_conversion" +version = "0.1.0" +dependencies = [ + "common_enums", + "rust_decimal", + "rusty-money", + "serde", + "thiserror", +] + [[package]] name = "darling" version = "0.20.3" @@ -2264,6 +2339,7 @@ name = "euclid_wasm" version = "0.1.0" dependencies = [ "api_models", + "currency_conversion", "euclid", "getrandom 0.2.10", "kgraph_utils", @@ -2501,6 +2577,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.1.31" @@ -4266,6 +4348,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "proc-macro-crate" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +dependencies = [ + "toml_edit 0.20.2", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -4342,6 +4433,26 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "pulldown-cmark" version = "0.9.3" @@ -4415,6 +4526,12 @@ dependencies = [ "scheduled-thread-pool", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.7.3" @@ -4643,6 +4760,15 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +[[package]] +name = "rend" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2571463863a6bd50c32f94402933f03457a3fbaf697a707c5be741e459f08fd" +dependencies = [ + "bytecheck", +] + [[package]] name = "reqwest" version = "0.11.22" @@ -4705,6 +4831,34 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "rkyv" +version = "0.7.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0200c8230b013893c0b2d6213d6ec64ed2b9be2e0e016682b7224ff82cff5c58" +dependencies = [ + "bitvec", + "bytecheck", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e06b915b5c230a17d7a736d1e2e63ee753c256a8614ef3f5147b13a4f5541d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "ron" version = "0.7.1" @@ -4755,6 +4909,7 @@ dependencies = [ "common_enums", "common_utils", "config", + "currency_conversion", "data_models", "derive_deref", "diesel", @@ -4793,6 +4948,7 @@ dependencies = [ "router_derive", "router_env", "roxmltree", + "rust_decimal", "rustc-hash", "scheduler", "serde", @@ -4805,7 +4961,7 @@ dependencies = [ "sha-1 0.9.8", "sqlx", "storage_impl", - "strum 0.24.1", + "strum 0.25.0", "tera", "test_utils", "thiserror", @@ -4917,6 +5073,32 @@ dependencies = [ "ordered-multimap", ] +[[package]] +name = "rust_decimal" +version = "1.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06676aec5ccb8fc1da723cc8c0f9a46549f21ebb8753d3915c6c41db1e7f1dc4" +dependencies = [ + "arrayvec", + "borsh", + "bytes 1.5.0", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rust_decimal_macros" +version = "1.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e43721f4ef7060ebc2c3ede757733209564ca8207f47674181bcd425dd76945" +dependencies = [ + "quote", + "rust_decimal", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -5056,6 +5238,16 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "rusty-money" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b28f881005eac7ad8d46b6f075da5f322bd7f4f83a38720fc069694ddadd683" +dependencies = [ + "rust_decimal", + "rust_decimal_macros", +] + [[package]] name = "ryu" version = "1.0.15" @@ -5136,6 +5328,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "security-framework" version = "2.9.2" @@ -5448,6 +5646,12 @@ dependencies = [ "tokio 1.32.0", ] +[[package]] +name = "simdutf8" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" + [[package]] name = "simple_asn1" version = "0.6.2" @@ -5777,6 +5981,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.38", +] + [[package]] name = "sync_wrapper" version = "0.1.2" @@ -5822,6 +6038,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tempfile" version = "3.8.0" @@ -6329,7 +6551,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_edit 0.19.10", ] [[package]] @@ -6351,7 +6573,18 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.4.11", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.0.2", + "toml_datetime", + "winnow 0.5.19", ] [[package]] @@ -7114,6 +7347,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" @@ -7156,6 +7398,15 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x509-parser" version = "0.15.1" diff --git a/config/config.example.toml b/config/config.example.toml index 7815f2400d04..0b8730ca114a 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -53,6 +53,16 @@ default_hash_ttl = 900 # Default TTL for hashes entries, in seconds use_legacy_version = false # Resp protocol for fred crate (set this to true if using RESPv2 or redis version < 6) stream_read_count = 1 # Default number of entries to read from stream if not provided in stream read options +# This section provides configs for currency conversion api +[forex_api] +call_delay = 21600 # Api calls are made after every 6 hrs +local_fetch_retry_count = 5 # Fetch from Local cache has retry count as 5 +local_fetch_retry_delay = 1000 # Retry delay for checking write condition +api_timeout = 20000 # Api timeouts once it crosses 2000 ms +api_key = "YOUR API KEY HERE" # Api key for making request to foreign exchange Api +fallback_api_key = "YOUR API KEY" # Api key for the fallback service +redis_lock_timeout = 26000 # Redis remains write locked for 26000 ms once the acquire_redis_lock is called + # Logging configuration. Logging can be either to file or console or both. # Logging configuration for file logging diff --git a/config/development.toml b/config/development.toml index c82607a704c3..3d64a8791a1c 100644 --- a/config/development.toml +++ b/config/development.toml @@ -52,6 +52,15 @@ host_rs = "" mock_locker = true basilisk_host = "" +[forex_api] +call_delay = 21600 +local_fetch_retry_count = 5 +local_fetch_retry_delay = 1000 +api_timeout = 20000 +api_key = "YOUR API KEY HERE" +fallback_api_key = "YOUR API KEY HERE" +redis_lock_timeout = 26000 + [jwekey] locker_key_identifier1 = "" locker_key_identifier2 = "" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 986240f0a36b..445e1e856846 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -28,6 +28,15 @@ port = 5432 dbname = "hyperswitch_db" pool_size = 5 +[forex_api] +call_delay = 21600 +local_fetch_retry_count = 5 +local_fetch_retry_delay = 1000 +api_timeout = 20000 +api_key = "YOUR API KEY HERE" +fallback_api_key = "YOUR API KEY HERE" +redis_lock_timeout = 26000 + [replica_database] username = "db_user" password = "db_pass" diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index ce882e913282..73c2d673c972 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -25,7 +25,7 @@ mime = "0.3.17" reqwest = { version = "0.11.18", optional = true } serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.96" -strum = { version = "0.24.1", features = ["derive"] } +strum = { version = "0.25", features = ["derive"] } time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } url = { version = "2.4.0", features = ["serde"] } utoipa = { version = "3.3.0", features = ["preserve_order"] } diff --git a/crates/api_models/src/currency.rs b/crates/api_models/src/currency.rs new file mode 100644 index 000000000000..c1d7e422d041 --- /dev/null +++ b/crates/api_models/src/currency.rs @@ -0,0 +1,21 @@ +use common_utils::events::ApiEventMetric; + +/// QueryParams to be send to convert the amount -> from_currency -> to_currency +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct CurrencyConversionParams { + pub amount: i64, + pub to_currency: String, + pub from_currency: String, +} + +/// Response to be send for convert currency route +#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct CurrencyConversionResponse { + pub converted_amount: String, + pub currency: String, +} + +impl ApiEventMetric for CurrencyConversionResponse {} +impl ApiEventMetric for CurrencyConversionParams {} diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index 1abeff7b6ddb..ab40a96582bb 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -5,6 +5,7 @@ pub mod api_keys; pub mod bank_accounts; pub mod cards_info; pub mod conditional_configs; +pub mod currency; pub mod customers; pub mod disputes; pub mod enums; diff --git a/crates/currency_conversion/Cargo.toml b/crates/currency_conversion/Cargo.toml new file mode 100644 index 000000000000..7eb3af7d526d --- /dev/null +++ b/crates/currency_conversion/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "currency_conversion" +description = "Currency conversion for cost based routing" +version = "0.1.0" +edition.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[dependencies] +# First party crates +common_enums = { version = "0.1.0", path = "../common_enums", package = "common_enums" } + +# Third party crates +rust_decimal = "1.29" +rusty-money = { version = "0.4.0", features = ["iso", "crypto"] } +serde = { version = "1.0.163", features = ["derive"] } +thiserror = "1.0.43" diff --git a/crates/currency_conversion/src/conversion.rs b/crates/currency_conversion/src/conversion.rs new file mode 100644 index 000000000000..4cdca8fe0ea2 --- /dev/null +++ b/crates/currency_conversion/src/conversion.rs @@ -0,0 +1,101 @@ +use common_enums::Currency; +use rust_decimal::Decimal; +use rusty_money::Money; + +use crate::{ + error::CurrencyConversionError, + types::{currency_match, ExchangeRates}, +}; + +pub fn convert( + ex_rates: &ExchangeRates, + from_currency: Currency, + to_currency: Currency, + amount: i64, +) -> Result { + let money_minor = Money::from_minor(amount, currency_match(from_currency)); + let base_currency = ex_rates.base_currency; + if to_currency == base_currency { + ex_rates.forward_conversion(*money_minor.amount(), from_currency) + } else if from_currency == base_currency { + ex_rates.backward_conversion(*money_minor.amount(), to_currency) + } else { + let base_conversion_amt = + ex_rates.forward_conversion(*money_minor.amount(), from_currency)?; + ex_rates.backward_conversion(base_conversion_amt, to_currency) + } +} + +#[cfg(test)] +mod tests { + #![allow(clippy::expect_used)] + use std::collections::HashMap; + + use crate::types::CurrencyFactors; + #[test] + fn currency_to_currency_conversion() { + use super::*; + let mut conversion: HashMap = HashMap::new(); + let inr_conversion_rates = + CurrencyFactors::new(Decimal::new(823173, 4), Decimal::new(1214, 5)); + let szl_conversion_rates = + CurrencyFactors::new(Decimal::new(194423, 4), Decimal::new(514, 4)); + let convert_from = Currency::SZL; + let convert_to = Currency::INR; + let amount = 2000; + let base_currency = Currency::USD; + conversion.insert(convert_from, inr_conversion_rates); + conversion.insert(convert_to, szl_conversion_rates); + let sample_rate = ExchangeRates::new(base_currency, conversion); + let res = + convert(&sample_rate, convert_from, convert_to, amount).expect("converted_currency"); + println!( + "The conversion from {} {} to {} is {:?}", + amount, convert_from, convert_to, res + ); + } + + #[test] + fn currency_to_base_conversion() { + use super::*; + let mut conversion: HashMap = HashMap::new(); + let inr_conversion_rates = + CurrencyFactors::new(Decimal::new(823173, 4), Decimal::new(1214, 5)); + let usd_conversion_rates = CurrencyFactors::new(Decimal::new(1, 0), Decimal::new(1, 0)); + let convert_from = Currency::INR; + let convert_to = Currency::USD; + let amount = 2000; + let base_currency = Currency::USD; + conversion.insert(convert_from, inr_conversion_rates); + conversion.insert(convert_to, usd_conversion_rates); + let sample_rate = ExchangeRates::new(base_currency, conversion); + let res = + convert(&sample_rate, convert_from, convert_to, amount).expect("converted_currency"); + println!( + "The conversion from {} {} to {} is {:?}", + amount, convert_from, convert_to, res + ); + } + + #[test] + fn base_to_currency_conversion() { + use super::*; + let mut conversion: HashMap = HashMap::new(); + let inr_conversion_rates = + CurrencyFactors::new(Decimal::new(823173, 4), Decimal::new(1214, 5)); + let usd_conversion_rates = CurrencyFactors::new(Decimal::new(1, 0), Decimal::new(1, 0)); + let convert_from = Currency::USD; + let convert_to = Currency::INR; + let amount = 2000; + let base_currency = Currency::USD; + conversion.insert(convert_from, usd_conversion_rates); + conversion.insert(convert_to, inr_conversion_rates); + let sample_rate = ExchangeRates::new(base_currency, conversion); + let res = + convert(&sample_rate, convert_from, convert_to, amount).expect("converted_currency"); + println!( + "The conversion from {} {} to {} is {:?}", + amount, convert_from, convert_to, res + ); + } +} diff --git a/crates/currency_conversion/src/error.rs b/crates/currency_conversion/src/error.rs new file mode 100644 index 000000000000..b04c147147c3 --- /dev/null +++ b/crates/currency_conversion/src/error.rs @@ -0,0 +1,8 @@ +#[derive(Debug, thiserror::Error, serde::Serialize)] +#[serde(tag = "type", content = "info", rename_all = "snake_case")] +pub enum CurrencyConversionError { + #[error("Currency Conversion isn't possible")] + DecimalMultiplicationFailed, + #[error("Currency not supported: '{0}'")] + ConversionNotSupported(String), +} diff --git a/crates/currency_conversion/src/lib.rs b/crates/currency_conversion/src/lib.rs new file mode 100644 index 000000000000..48e1ae11e5d3 --- /dev/null +++ b/crates/currency_conversion/src/lib.rs @@ -0,0 +1,3 @@ +pub mod conversion; +pub mod error; +pub mod types; diff --git a/crates/currency_conversion/src/types.rs b/crates/currency_conversion/src/types.rs new file mode 100644 index 000000000000..fec25b9fc601 --- /dev/null +++ b/crates/currency_conversion/src/types.rs @@ -0,0 +1,201 @@ +use std::collections::HashMap; + +use common_enums::Currency; +use rust_decimal::Decimal; +use rusty_money::iso; + +use crate::error::CurrencyConversionError; + +/// Cached currency store of base currency +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct ExchangeRates { + pub base_currency: Currency, + pub conversion: HashMap, +} + +/// Stores the multiplicative factor for conversion between currency to base and vice versa +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct CurrencyFactors { + /// The factor that will be multiplied to provide Currency output + pub to_factor: Decimal, + /// The factor that will be multiplied to provide for the base output + pub from_factor: Decimal, +} + +impl CurrencyFactors { + pub fn new(to_factor: Decimal, from_factor: Decimal) -> Self { + Self { + to_factor, + from_factor, + } + } +} + +impl ExchangeRates { + pub fn new(base_currency: Currency, conversion: HashMap) -> Self { + Self { + base_currency, + conversion, + } + } + + /// The flow here is from_currency -> base_currency -> to_currency + /// from to_currency -> base currency + pub fn forward_conversion( + &self, + amt: Decimal, + from_currency: Currency, + ) -> Result { + let from_factor = self + .conversion + .get(&from_currency) + .ok_or_else(|| { + CurrencyConversionError::ConversionNotSupported(from_currency.to_string()) + })? + .from_factor; + amt.checked_mul(from_factor) + .ok_or(CurrencyConversionError::DecimalMultiplicationFailed) + } + + /// from base_currency -> to_currency + pub fn backward_conversion( + &self, + amt: Decimal, + to_currency: Currency, + ) -> Result { + let to_factor = self + .conversion + .get(&to_currency) + .ok_or_else(|| { + CurrencyConversionError::ConversionNotSupported(to_currency.to_string()) + })? + .to_factor; + amt.checked_mul(to_factor) + .ok_or(CurrencyConversionError::DecimalMultiplicationFailed) + } +} + +pub fn currency_match(currency: Currency) -> &'static iso::Currency { + match currency { + Currency::AED => iso::AED, + Currency::ALL => iso::ALL, + Currency::AMD => iso::AMD, + Currency::ANG => iso::ANG, + Currency::ARS => iso::ARS, + Currency::AUD => iso::AUD, + Currency::AWG => iso::AWG, + Currency::AZN => iso::AZN, + Currency::BBD => iso::BBD, + Currency::BDT => iso::BDT, + Currency::BHD => iso::BHD, + Currency::BIF => iso::BIF, + Currency::BMD => iso::BMD, + Currency::BND => iso::BND, + Currency::BOB => iso::BOB, + Currency::BRL => iso::BRL, + Currency::BSD => iso::BSD, + Currency::BWP => iso::BWP, + Currency::BZD => iso::BZD, + Currency::CAD => iso::CAD, + Currency::CHF => iso::CHF, + Currency::CLP => iso::CLP, + Currency::CNY => iso::CNY, + Currency::COP => iso::COP, + Currency::CRC => iso::CRC, + Currency::CUP => iso::CUP, + Currency::CZK => iso::CZK, + Currency::DJF => iso::DJF, + Currency::DKK => iso::DKK, + Currency::DOP => iso::DOP, + Currency::DZD => iso::DZD, + Currency::EGP => iso::EGP, + Currency::ETB => iso::ETB, + Currency::EUR => iso::EUR, + Currency::FJD => iso::FJD, + Currency::GBP => iso::GBP, + Currency::GHS => iso::GHS, + Currency::GIP => iso::GIP, + Currency::GMD => iso::GMD, + Currency::GNF => iso::GNF, + Currency::GTQ => iso::GTQ, + Currency::GYD => iso::GYD, + Currency::HKD => iso::HKD, + Currency::HNL => iso::HNL, + Currency::HRK => iso::HRK, + Currency::HTG => iso::HTG, + Currency::HUF => iso::HUF, + Currency::IDR => iso::IDR, + Currency::ILS => iso::ILS, + Currency::INR => iso::INR, + Currency::JMD => iso::JMD, + Currency::JOD => iso::JOD, + Currency::JPY => iso::JPY, + Currency::KES => iso::KES, + Currency::KGS => iso::KGS, + Currency::KHR => iso::KHR, + Currency::KMF => iso::KMF, + Currency::KRW => iso::KRW, + Currency::KWD => iso::KWD, + Currency::KYD => iso::KYD, + Currency::KZT => iso::KZT, + Currency::LAK => iso::LAK, + Currency::LBP => iso::LBP, + Currency::LKR => iso::LKR, + Currency::LRD => iso::LRD, + Currency::LSL => iso::LSL, + Currency::MAD => iso::MAD, + Currency::MDL => iso::MDL, + Currency::MGA => iso::MGA, + Currency::MKD => iso::MKD, + Currency::MMK => iso::MMK, + Currency::MNT => iso::MNT, + Currency::MOP => iso::MOP, + Currency::MUR => iso::MUR, + Currency::MVR => iso::MVR, + Currency::MWK => iso::MWK, + Currency::MXN => iso::MXN, + Currency::MYR => iso::MYR, + Currency::NAD => iso::NAD, + Currency::NGN => iso::NGN, + Currency::NIO => iso::NIO, + Currency::NOK => iso::NOK, + Currency::NPR => iso::NPR, + Currency::NZD => iso::NZD, + Currency::OMR => iso::OMR, + Currency::PEN => iso::PEN, + Currency::PGK => iso::PGK, + Currency::PHP => iso::PHP, + Currency::PKR => iso::PKR, + Currency::PLN => iso::PLN, + Currency::PYG => iso::PYG, + Currency::QAR => iso::QAR, + Currency::RON => iso::RON, + Currency::RUB => iso::RUB, + Currency::RWF => iso::RWF, + Currency::SAR => iso::SAR, + Currency::SCR => iso::SCR, + Currency::SEK => iso::SEK, + Currency::SGD => iso::SGD, + Currency::SLL => iso::SLL, + Currency::SOS => iso::SOS, + Currency::SSP => iso::SSP, + Currency::SVC => iso::SVC, + Currency::SZL => iso::SZL, + Currency::THB => iso::THB, + Currency::TTD => iso::TTD, + Currency::TRY => iso::TRY, + Currency::TWD => iso::TWD, + Currency::TZS => iso::TZS, + Currency::UGX => iso::UGX, + Currency::USD => iso::USD, + Currency::UYU => iso::UYU, + Currency::UZS => iso::UZS, + Currency::VND => iso::VND, + Currency::VUV => iso::VUV, + Currency::XAF => iso::XAF, + Currency::XOF => iso::XOF, + Currency::XPF => iso::XPF, + Currency::YER => iso::YER, + Currency::ZAR => iso::ZAR, + } +} diff --git a/crates/euclid_wasm/Cargo.toml b/crates/euclid_wasm/Cargo.toml index 4fc8cd970f40..47e349847ef7 100644 --- a/crates/euclid_wasm/Cargo.toml +++ b/crates/euclid_wasm/Cargo.toml @@ -17,6 +17,7 @@ dummy_connector = ["kgraph_utils/dummy_connector"] [dependencies] api_models = { version = "0.1.0", path = "../api_models", package = "api_models" } +currency_conversion = { version = "0.1.0", path = "../currency_conversion" } euclid = { path = "../euclid", features = [] } kgraph_utils = { version = "0.1.0", path = "../kgraph_utils" } diff --git a/crates/euclid_wasm/src/lib.rs b/crates/euclid_wasm/src/lib.rs index e85a002544ff..48d9ac0d82a8 100644 --- a/crates/euclid_wasm/src/lib.rs +++ b/crates/euclid_wasm/src/lib.rs @@ -7,6 +7,9 @@ use std::{ }; use api_models::{admin as admin_api, routing::ConnectorSelection}; +use currency_conversion::{ + conversion::convert as convert_currency, types as currency_conversion_types, +}; use euclid::{ backend::{inputs, interpreter::InterpreterBackend, EuclidBackend}, dssa::{ @@ -33,6 +36,39 @@ struct SeedData<'a> { } static SEED_DATA: OnceCell> = OnceCell::new(); +static SEED_FOREX: OnceCell = OnceCell::new(); + +/// This function can be used by the frontend to educate wasm about the forex rates data. +/// The input argument is a struct fields base_currency and conversion where later is all the conversions associated with the base_currency +/// to all different currencies present. +#[wasm_bindgen(js_name = setForexData)] +pub fn seed_forex(forex: JsValue) -> JsResult { + let forex: currency_conversion_types::ExchangeRates = serde_wasm_bindgen::from_value(forex)?; + SEED_FOREX + .set(forex) + .map_err(|_| "Forex has already been seeded".to_string()) + .err_to_js()?; + + Ok(JsValue::NULL) +} + +/// This function can be used to perform currency_conversion on the input amount, from_currency, +/// to_currency which are all expected to be one of currencies we already have in our Currency +/// enum. +#[wasm_bindgen(js_name = convertCurrency)] +pub fn convert_forex_value(amount: i64, from_currency: JsValue, to_currency: JsValue) -> JsResult { + let forex_data = SEED_FOREX + .get() + .ok_or("Forex Data not seeded") + .err_to_js()?; + let from_currency: enums::Currency = serde_wasm_bindgen::from_value(from_currency)?; + let to_currency: enums::Currency = serde_wasm_bindgen::from_value(to_currency)?; + let converted_amount = convert_currency(forex_data, from_currency, to_currency, amount) + .map_err(|_| "conversion not possible for provided values") + .err_to_js()?; + + Ok(serde_wasm_bindgen::to_value(&converted_amount)?) +} /// This function can be used by the frontend to provide the WASM with information about /// all the merchant's connector accounts. The input argument is a vector of all the merchant's diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 25feb373b734..f0316d69249e 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -76,6 +76,7 @@ regex = "1.8.4" reqwest = { version = "0.11.18", features = ["json", "native-tls", "gzip", "multipart"] } ring = "0.16.20" roxmltree = "0.18.0" +rust_decimal = { version = "1.30.0", features = ["serde-with-float", "serde-with-str"] } rustc-hash = "1.1.0" serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.96" @@ -85,7 +86,7 @@ serde_urlencoded = "0.7.1" serde_with = "3.0.0" sha-1 = { version = "0.9" } sqlx = { version = "0.6.3", features = ["postgres", "runtime-actix", "runtime-actix-native-tls", "time", "bigdecimal"] } -strum = { version = "0.24.1", features = ["derive"] } +strum = { version = "0.25", features = ["derive"] } tera = "1.19.1" thiserror = "1.0.40" time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } @@ -104,6 +105,7 @@ api_models = { version = "0.1.0", path = "../api_models", features = ["errors"] cards = { version = "0.1.0", path = "../cards" } common_enums = { version = "0.1.0", path = "../common_enums" } common_utils = { version = "0.1.0", path = "../common_utils", features = ["signals", "async_ext", "logs"] } +currency_conversion = { version = "0.1.0", path = "../currency_conversion" } data_models = { version = "0.1.0", path = "../data_models", default-features = false } diesel_models = { version = "0.1.0", path = "../diesel_models", features = ["kv_store"] } euclid = { version = "0.1.0", path = "../euclid", features = ["valued_jit"] } diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 0007e636926c..cc273f93ee9a 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -13,6 +13,7 @@ use external_services::email::EmailSettings; use external_services::kms; use redis_interface::RedisSettings; pub use router_env::config::{Log, LogConsole, LogFile, LogTelemetry}; +use rust_decimal::Decimal; use scheduler::SchedulerSettings; use serde::{de::Error, Deserialize, Deserializer}; @@ -70,6 +71,7 @@ pub struct Settings { pub secrets: Secrets, pub locker: Locker, pub connectors: Connectors, + pub forex_api: ForexApi, pub refund: Refund, pub eph_key: EphemeralConfig, pub scheduler: Option, @@ -119,6 +121,37 @@ pub struct PaymentLink { pub sdk_url: String, } +#[derive(Debug, Deserialize, Clone, Default)] +#[serde(default)] +pub struct ForexApi { + pub local_fetch_retry_count: u64, + pub api_key: masking::Secret, + pub fallback_api_key: masking::Secret, + /// in ms + pub call_delay: i64, + /// in ms + pub local_fetch_retry_delay: u64, + /// in ms + pub api_timeout: u64, + /// in ms + pub redis_lock_timeout: u64, +} + +#[derive(Debug, Deserialize, Clone, Default)] +pub struct DefaultExchangeRates { + pub base_currency: String, + pub conversion: HashMap, + pub timestamp: i64, +} + +#[derive(Debug, Deserialize, Clone, Default)] +pub struct Conversion { + #[serde(with = "rust_decimal::serde::str")] + pub to_factor: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub from_factor: Decimal, +} + #[derive(Debug, Deserialize, Clone, Default)] #[serde(default)] pub struct ApplepayMerchantConfigs { diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index a429cab482b4..cff2dc8e58f1 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -5,6 +5,8 @@ pub mod cache; pub mod cards_info; pub mod conditional_config; pub mod configs; +#[cfg(any(feature = "olap", feature = "oltp"))] +pub mod currency; pub mod customers; pub mod disputes; pub mod errors; diff --git a/crates/router/src/core/currency.rs b/crates/router/src/core/currency.rs new file mode 100644 index 000000000000..1ea9454f00a0 --- /dev/null +++ b/crates/router/src/core/currency.rs @@ -0,0 +1,51 @@ +use common_utils::errors::CustomResult; +use error_stack::ResultExt; + +use crate::{ + core::errors::ApiErrorResponse, + services::ApplicationResponse, + utils::currency::{self, convert_currency, get_forex_rates}, + AppState, +}; + +pub async fn retrieve_forex( + state: AppState, +) -> CustomResult, ApiErrorResponse> { + Ok(ApplicationResponse::Json( + get_forex_rates( + &state, + state.conf.forex_api.call_delay, + state.conf.forex_api.local_fetch_retry_delay, + state.conf.forex_api.local_fetch_retry_count, + #[cfg(feature = "kms")] + &state.conf.kms, + ) + .await + .change_context(ApiErrorResponse::GenericNotFoundError { + message: "Unable to fetch forex rates".to_string(), + })?, + )) +} + +pub async fn convert_forex( + state: AppState, + amount: i64, + to_currency: String, + from_currency: String, +) -> CustomResult< + ApplicationResponse, + ApiErrorResponse, +> { + Ok(ApplicationResponse::Json( + Box::pin(convert_currency( + state.clone(), + amount, + to_currency, + from_currency, + #[cfg(feature = "kms")] + &state.conf.kms, + )) + .await + .change_context(ApiErrorResponse::InternalServerError)?, + )) +} diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 0bc8e492c40c..2b1f9c692d86 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -122,6 +122,7 @@ pub fn mk_app( .service(routes::Payments::server(state.clone())) .service(routes::Customers::server(state.clone())) .service(routes::Configs::server(state.clone())) + .service(routes::Forex::server(state.clone())) .service(routes::Refunds::server(state.clone())) .service(routes::MerchantConnectorAccount::server(state.clone())) .service(routes::Mandates::server(state.clone())) diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 5166e326fb91..37cc1339e1a1 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -4,6 +4,8 @@ pub mod app; pub mod cache; pub mod cards_info; pub mod configs; +#[cfg(any(feature = "olap", feature = "oltp"))] +pub mod currency; pub mod customers; pub mod disputes; #[cfg(feature = "dummy_connector")] @@ -32,6 +34,8 @@ pub mod webhooks; pub mod locker_migration; #[cfg(feature = "dummy_connector")] pub use self::app::DummyConnector; +#[cfg(any(feature = "olap", feature = "oltp"))] +pub use self::app::Forex; #[cfg(feature = "payouts")] pub use self::app::Payouts; #[cfg(feature = "olap")] diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 84848e030120..ae0e0f04f598 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -10,6 +10,8 @@ use scheduler::SchedulerInterface; use storage_impl::MockDb; use tokio::sync::oneshot; +#[cfg(any(feature = "olap", feature = "oltp"))] +use super::currency; #[cfg(feature = "dummy_connector")] use super::dummy_connector::*; #[cfg(feature = "payouts")] @@ -28,7 +30,7 @@ use super::{cache::*, health::*}; use super::{configs::*, customers::*, mandates::*, payments::*, refunds::*}; #[cfg(feature = "oltp")] use super::{ephemeral_key::*, payment_methods::*, webhooks::*}; -use crate::{ +pub use crate::{ configs::settings, db::{StorageImpl, StorageInterface}, events::{event_logger::EventLogger, EventHandler}, @@ -302,6 +304,22 @@ impl Payments { } } +#[cfg(any(feature = "olap", feature = "oltp"))] +pub struct Forex; + +#[cfg(any(feature = "olap", feature = "oltp"))] +impl Forex { + pub fn server(state: AppState) -> Scope { + web::scope("/forex") + .app_data(web::Data::new(state.clone())) + .app_data(web::Data::new(state.clone())) + .service(web::resource("/rates").route(web::get().to(currency::retrieve_forex))) + .service( + web::resource("/convert_from_minor").route(web::get().to(currency::convert_forex)), + ) + } +} + #[cfg(feature = "olap")] pub struct Routing; diff --git a/crates/router/src/routes/currency.rs b/crates/router/src/routes/currency.rs new file mode 100644 index 000000000000..1e1858517176 --- /dev/null +++ b/crates/router/src/routes/currency.rs @@ -0,0 +1,58 @@ +use actix_web::{web, HttpRequest, HttpResponse}; +use router_env::Flow; + +use crate::{ + core::{api_locking, currency}, + routes::AppState, + services::{api, authentication as auth, authorization::permissions::Permission}, +}; + +pub async fn retrieve_forex(state: web::Data, req: HttpRequest) -> HttpResponse { + let flow = Flow::RetrieveForexFlow; + Box::pin(api::server_wrap( + flow, + state, + &req, + (), + |state, _auth: auth::AuthenticationData, _| currency::retrieve_forex(state), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::ForexRead), + req.headers(), + ), + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn convert_forex( + state: web::Data, + req: HttpRequest, + params: web::Query, +) -> HttpResponse { + let flow = Flow::RetrieveForexFlow; + let amount = ¶ms.amount; + let to_currency = ¶ms.to_currency; + let from_currency = ¶ms.from_currency; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + (), + |state, _, _| { + currency::convert_forex( + state, + *amount, + to_currency.to_string(), + from_currency.to_string(), + ) + }, + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::ForexRead), + req.headers(), + ), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 219948bdd4d2..5c2ad123749c 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -23,6 +23,7 @@ pub enum ApiIdentifier { ApiKeys, PaymentLink, Routing, + Forex, RustLockerMigration, Gsm, User, @@ -51,6 +52,8 @@ impl From for ApiIdentifier { | Flow::DecisionManagerRetrieveConfig | Flow::DecisionManagerUpsertConfig => Self::Routing, + Flow::RetrieveForexFlow => Self::Forex, + Flow::MerchantConnectorsCreate | Flow::MerchantConnectorsRetrieve | Flow::MerchantConnectorsUpdate diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index 901e84997e67..c936ee858c17 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -1,11 +1,11 @@ +pub mod currency; pub mod custom_serde; pub mod db_utils; pub mod ext_traits; -#[cfg(feature = "olap")] -pub mod user; - #[cfg(feature = "kv_store")] pub mod storage_partitioning; +#[cfg(feature = "olap")] +pub mod user; use std::fmt::Debug; diff --git a/crates/router/src/utils/currency.rs b/crates/router/src/utils/currency.rs new file mode 100644 index 000000000000..118d9df28e22 --- /dev/null +++ b/crates/router/src/utils/currency.rs @@ -0,0 +1,641 @@ +use std::{collections::HashMap, ops::Deref, str::FromStr, sync::Arc, time::Duration}; + +use api_models::enums; +use common_utils::{date_time, errors::CustomResult, events::ApiEventMetric, ext_traits::AsyncExt}; +use currency_conversion::types::{CurrencyFactors, ExchangeRates}; +use error_stack::{IntoReport, ResultExt}; +#[cfg(feature = "kms")] +use external_services::kms; +use masking::PeekInterface; +use once_cell::sync::Lazy; +use redis_interface::DelReply; +use rust_decimal::Decimal; +use strum::IntoEnumIterator; +use tokio::{sync::RwLock, time::sleep}; + +use crate::{ + logger, + routes::app::settings::{Conversion, DefaultExchangeRates}, + services, AppState, +}; +const REDIX_FOREX_CACHE_KEY: &str = "{forex_cache}_lock"; +const REDIX_FOREX_CACHE_DATA: &str = "{forex_cache}_data"; +const FOREX_API_TIMEOUT: u64 = 5; +const FOREX_BASE_URL: &str = "https://openexchangerates.org/api/latest.json?app_id="; +const FOREX_BASE_CURRENCY: &str = "&base=USD"; +const FALLBACK_FOREX_BASE_URL: &str = "http://apilayer.net/api/live?access_key="; +const FALLBACK_FOREX_API_CURRENCY_PREFIX: &str = "USD"; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct FxExchangeRatesCacheEntry { + data: Arc, + timestamp: i64, +} + +static FX_EXCHANGE_RATES_CACHE: Lazy>> = + Lazy::new(|| RwLock::new(None)); + +impl ApiEventMetric for FxExchangeRatesCacheEntry {} + +#[derive(Debug, Clone, thiserror::Error)] +pub enum ForexCacheError { + #[error("API error")] + ApiError, + #[error("API timeout")] + ApiTimeout, + #[error("API unresponsive")] + ApiUnresponsive, + #[error("Conversion error")] + ConversionError, + #[error("Could not acquire the lock for cache entry")] + CouldNotAcquireLock, + #[error("Provided currency not acceptable")] + CurrencyNotAcceptable, + #[error("Incorrect entries in default Currency response")] + DefaultCurrencyParsingError, + #[error("Entry not found in cache")] + EntryNotFound, + #[error("Expiration time invalid")] + InvalidLogExpiry, + #[error("Error reading local")] + LocalReadError, + #[error("Error writing to local cache")] + LocalWriteError, + #[error("Json Parsing error")] + ParsingError, + #[error("Kms decryption error")] + KmsDecryptionFailed, + #[error("Error connecting to redis")] + RedisConnectionError, + #[error("Not able to release write lock")] + RedisLockReleaseFailed, + #[error("Error writing to redis")] + RedisWriteError, + #[error("Not able to acquire write lock")] + WriteLockNotAcquired, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +struct ForexResponse { + pub rates: HashMap, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +struct FallbackForexResponse { + pub quotes: HashMap, +} + +#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)] +#[serde(transparent)] +struct FloatDecimal(#[serde(with = "rust_decimal::serde::float")] Decimal); + +impl Deref for FloatDecimal { + type Target = Decimal; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FxExchangeRatesCacheEntry { + fn new(exchange_rate: ExchangeRates) -> Self { + Self { + data: Arc::new(exchange_rate), + timestamp: date_time::now_unix_timestamp(), + } + } + fn is_expired(&self, call_delay: i64) -> bool { + self.timestamp + call_delay < date_time::now_unix_timestamp() + } +} + +async fn retrieve_forex_from_local() -> Option { + FX_EXCHANGE_RATES_CACHE.read().await.clone() +} + +async fn save_forex_to_local( + exchange_rates_cache_entry: FxExchangeRatesCacheEntry, +) -> CustomResult<(), ForexCacheError> { + let mut local = FX_EXCHANGE_RATES_CACHE.write().await; + *local = Some(exchange_rates_cache_entry); + Ok(()) +} + +// Alternative handler for handling the case, When no data in local as well as redis +#[allow(dead_code)] +async fn waited_fetch_and_update_caches( + state: &AppState, + local_fetch_retry_delay: u64, + local_fetch_retry_count: u64, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, +) -> CustomResult { + for _n in 1..local_fetch_retry_count { + sleep(Duration::from_millis(local_fetch_retry_delay)).await; + //read from redis and update local plus break the loop and return + match retrieve_forex_from_redis(state).await { + Ok(Some(rates)) => { + save_forex_to_local(rates.clone()).await?; + return Ok(rates.clone()); + } + Ok(None) => continue, + Err(e) => { + logger::error!(?e); + continue; + } + } + } + //acquire lock one last time and try to fetch and update local & redis + successive_fetch_and_save_forex( + state, + None, + #[cfg(feature = "kms")] + kms_config, + ) + .await +} + +impl TryFrom for ExchangeRates { + type Error = error_stack::Report; + fn try_from(value: DefaultExchangeRates) -> Result { + let mut conversion_usable: HashMap = HashMap::new(); + for (curr, conversion) in value.conversion { + let enum_curr = enums::Currency::from_str(curr.as_str()) + .into_report() + .change_context(ForexCacheError::ConversionError)?; + conversion_usable.insert(enum_curr, CurrencyFactors::from(conversion)); + } + let base_curr = enums::Currency::from_str(value.base_currency.as_str()) + .into_report() + .change_context(ForexCacheError::ConversionError)?; + Ok(Self { + base_currency: base_curr, + conversion: conversion_usable, + }) + } +} + +impl From for CurrencyFactors { + fn from(value: Conversion) -> Self { + Self { + to_factor: value.to_factor, + from_factor: value.from_factor, + } + } +} +pub async fn get_forex_rates( + state: &AppState, + call_delay: i64, + local_fetch_retry_delay: u64, + local_fetch_retry_count: u64, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, +) -> CustomResult { + if let Some(local_rates) = retrieve_forex_from_local().await { + if local_rates.is_expired(call_delay) { + // expired local data + handler_local_expired( + state, + call_delay, + local_rates, + #[cfg(feature = "kms")] + kms_config, + ) + .await + } else { + // Valid data present in local + Ok(local_rates) + } + } else { + // No data in local + handler_local_no_data( + state, + call_delay, + local_fetch_retry_delay, + local_fetch_retry_count, + #[cfg(feature = "kms")] + kms_config, + ) + .await + } +} + +async fn handler_local_no_data( + state: &AppState, + call_delay: i64, + _local_fetch_retry_delay: u64, + _local_fetch_retry_count: u64, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, +) -> CustomResult { + match retrieve_forex_from_redis(state).await { + Ok(Some(data)) => { + fallback_forex_redis_check( + state, + data, + call_delay, + #[cfg(feature = "kms")] + kms_config, + ) + .await + } + Ok(None) => { + // No data in local as well as redis + Ok(successive_fetch_and_save_forex( + state, + None, + #[cfg(feature = "kms")] + kms_config, + ) + .await?) + } + Err(err) => { + logger::error!(?err); + Ok(successive_fetch_and_save_forex( + state, + None, + #[cfg(feature = "kms")] + kms_config, + ) + .await?) + } + } +} + +async fn successive_fetch_and_save_forex( + state: &AppState, + stale_redis_data: Option, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, +) -> CustomResult { + match acquire_redis_lock(state).await { + Ok(lock_acquired) => { + if !lock_acquired { + return stale_redis_data.ok_or(ForexCacheError::CouldNotAcquireLock.into()); + } + let api_rates = fetch_forex_rates( + state, + #[cfg(feature = "kms")] + kms_config, + ) + .await; + match api_rates { + Ok(rates) => successive_save_data_to_redis_local(state, rates).await, + Err(err) => { + // API not able to fetch data call secondary service + logger::error!(?err); + let secondary_api_rates = fallback_fetch_forex_rates( + state, + #[cfg(feature = "kms")] + kms_config, + ) + .await; + match secondary_api_rates { + Ok(rates) => Ok(successive_save_data_to_redis_local(state, rates).await?), + Err(err) => stale_redis_data.ok_or({ + logger::error!(?err); + ForexCacheError::ApiUnresponsive.into() + }), + } + } + } + } + Err(e) => stale_redis_data.ok_or({ + logger::error!(?e); + ForexCacheError::ApiUnresponsive.into() + }), + } +} + +async fn successive_save_data_to_redis_local( + state: &AppState, + forex: FxExchangeRatesCacheEntry, +) -> CustomResult { + Ok(save_forex_to_redis(state, &forex) + .await + .async_and_then(|_rates| async { release_redis_lock(state).await }) + .await + .async_and_then(|_val| async { Ok(save_forex_to_local(forex.clone()).await) }) + .await + .map_or_else( + |e| { + logger::error!(?e); + forex.clone() + }, + |_| forex.clone(), + )) +} + +async fn fallback_forex_redis_check( + state: &AppState, + redis_data: FxExchangeRatesCacheEntry, + call_delay: i64, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, +) -> CustomResult { + match is_redis_expired(Some(redis_data.clone()).as_ref(), call_delay).await { + Some(redis_forex) => { + // Valid data present in redis + let exchange_rates = FxExchangeRatesCacheEntry::new(redis_forex.as_ref().clone()); + save_forex_to_local(exchange_rates.clone()).await?; + Ok(exchange_rates) + } + None => { + // redis expired + successive_fetch_and_save_forex( + state, + Some(redis_data), + #[cfg(feature = "kms")] + kms_config, + ) + .await + } + } +} + +async fn handler_local_expired( + state: &AppState, + call_delay: i64, + local_rates: FxExchangeRatesCacheEntry, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, +) -> CustomResult { + match retrieve_forex_from_redis(state).await { + Ok(redis_data) => { + match is_redis_expired(redis_data.as_ref(), call_delay).await { + Some(redis_forex) => { + // Valid data present in redis + let exchange_rates = + FxExchangeRatesCacheEntry::new(redis_forex.as_ref().clone()); + save_forex_to_local(exchange_rates.clone()).await?; + Ok(exchange_rates) + } + None => { + // Redis is expired going for API request + successive_fetch_and_save_forex( + state, + Some(local_rates), + #[cfg(feature = "kms")] + kms_config, + ) + .await + } + } + } + Err(e) => { + // data not present in redis waited fetch + logger::error!(?e); + successive_fetch_and_save_forex( + state, + Some(local_rates), + #[cfg(feature = "kms")] + kms_config, + ) + .await + } + } +} + +async fn fetch_forex_rates( + state: &AppState, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, +) -> Result> { + #[cfg(feature = "kms")] + let forex_api_key = kms::get_kms_client(kms_config) + .await + .decrypt(state.conf.forex_api.api_key.peek()) + .await + .change_context(ForexCacheError::KmsDecryptionFailed)?; + + #[cfg(not(feature = "kms"))] + let forex_api_key = state.conf.forex_api.api_key.peek(); + + let forex_url: String = format!("{}{}{}", FOREX_BASE_URL, forex_api_key, FOREX_BASE_CURRENCY); + let forex_request = services::RequestBuilder::new() + .method(services::Method::Get) + .url(&forex_url) + .build(); + + logger::info!(?forex_request); + let response = state + .api_client + .send_request( + &state.clone(), + forex_request, + Some(FOREX_API_TIMEOUT), + false, + ) + .await + .change_context(ForexCacheError::ApiUnresponsive)?; + let forex_response = response + .json::() + .await + .into_report() + .change_context(ForexCacheError::ParsingError)?; + + logger::info!("{:?}", forex_response); + + let mut conversions: HashMap = HashMap::new(); + for enum_curr in enums::Currency::iter() { + match forex_response.rates.get(&enum_curr.to_string()) { + Some(rate) => { + let from_factor = match Decimal::new(1, 0).checked_div(**rate) { + Some(rate) => rate, + None => { + logger::error!("Rates for {} not received from API", &enum_curr); + continue; + } + }; + let currency_factors = CurrencyFactors::new(**rate, from_factor); + conversions.insert(enum_curr, currency_factors); + } + None => { + logger::error!("Rates for {} not received from API", &enum_curr); + } + }; + } + + Ok(FxExchangeRatesCacheEntry::new(ExchangeRates::new( + enums::Currency::USD, + conversions, + ))) +} + +pub async fn fallback_fetch_forex_rates( + state: &AppState, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, +) -> CustomResult { + #[cfg(feature = "kms")] + let fallback_forex_api_key = kms::get_kms_client(kms_config) + .await + .decrypt(state.conf.forex_api.fallback_api_key.peek()) + .await + .change_context(ForexCacheError::KmsDecryptionFailed)?; + + #[cfg(not(feature = "kms"))] + let fallback_forex_api_key = state.conf.forex_api.fallback_api_key.peek(); + + let fallback_forex_url: String = + format!("{}{}", FALLBACK_FOREX_BASE_URL, fallback_forex_api_key,); + let fallback_forex_request = services::RequestBuilder::new() + .method(services::Method::Get) + .url(&fallback_forex_url) + .build(); + + logger::info!(?fallback_forex_request); + let response = state + .api_client + .send_request( + &state.clone(), + fallback_forex_request, + Some(FOREX_API_TIMEOUT), + false, + ) + .await + .change_context(ForexCacheError::ApiUnresponsive)?; + let fallback_forex_response = response + .json::() + .await + .into_report() + .change_context(ForexCacheError::ParsingError)?; + + logger::info!("{:?}", fallback_forex_response); + let mut conversions: HashMap = HashMap::new(); + for enum_curr in enums::Currency::iter() { + match fallback_forex_response.quotes.get( + format!( + "{}{}", + FALLBACK_FOREX_API_CURRENCY_PREFIX, + &enum_curr.to_string() + ) + .as_str(), + ) { + Some(rate) => { + let from_factor = match Decimal::new(1, 0).checked_div(**rate) { + Some(rate) => rate, + None => { + logger::error!("Rates for {} not received from API", &enum_curr); + continue; + } + }; + let currency_factors = CurrencyFactors::new(**rate, from_factor); + conversions.insert(enum_curr, currency_factors); + } + None => { + logger::error!("Rates for {} not received from API", &enum_curr); + } + }; + } + + let rates = + FxExchangeRatesCacheEntry::new(ExchangeRates::new(enums::Currency::USD, conversions)); + match acquire_redis_lock(state).await { + Ok(_) => Ok(successive_save_data_to_redis_local(state, rates).await?), + Err(e) => { + logger::error!(?e); + Ok(rates) + } + } +} + +async fn release_redis_lock( + state: &AppState, +) -> Result> { + state + .store + .get_redis_conn() + .change_context(ForexCacheError::RedisConnectionError)? + .delete_key(REDIX_FOREX_CACHE_KEY) + .await + .change_context(ForexCacheError::RedisLockReleaseFailed) +} + +async fn acquire_redis_lock(app_state: &AppState) -> CustomResult { + app_state + .store + .get_redis_conn() + .change_context(ForexCacheError::RedisConnectionError)? + .set_key_if_not_exists_with_expiry( + REDIX_FOREX_CACHE_KEY, + "", + Some( + (app_state.conf.forex_api.local_fetch_retry_count + * app_state.conf.forex_api.local_fetch_retry_delay + + app_state.conf.forex_api.api_timeout) + .try_into() + .into_report() + .change_context(ForexCacheError::ConversionError)?, + ), + ) + .await + .map(|val| matches!(val, redis_interface::SetnxReply::KeySet)) + .change_context(ForexCacheError::CouldNotAcquireLock) +} + +async fn save_forex_to_redis( + app_state: &AppState, + forex_exchange_cache_entry: &FxExchangeRatesCacheEntry, +) -> CustomResult<(), ForexCacheError> { + app_state + .store + .get_redis_conn() + .change_context(ForexCacheError::RedisConnectionError)? + .serialize_and_set_key(REDIX_FOREX_CACHE_DATA, forex_exchange_cache_entry) + .await + .change_context(ForexCacheError::RedisWriteError) +} + +async fn retrieve_forex_from_redis( + app_state: &AppState, +) -> CustomResult, ForexCacheError> { + app_state + .store + .get_redis_conn() + .change_context(ForexCacheError::RedisConnectionError)? + .get_and_deserialize_key(REDIX_FOREX_CACHE_DATA, "FxExchangeRatesCache") + .await + .change_context(ForexCacheError::EntryNotFound) +} + +async fn is_redis_expired( + redis_cache: Option<&FxExchangeRatesCacheEntry>, + call_delay: i64, +) -> Option> { + redis_cache.and_then(|cache| { + if cache.timestamp + call_delay > date_time::now_unix_timestamp() { + Some(cache.data.clone()) + } else { + None + } + }) +} + +pub async fn convert_currency( + state: AppState, + amount: i64, + to_currency: String, + from_currency: String, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, +) -> CustomResult { + let rates = get_forex_rates( + &state, + state.conf.forex_api.call_delay, + state.conf.forex_api.local_fetch_retry_delay, + state.conf.forex_api.local_fetch_retry_count, + #[cfg(feature = "kms")] + kms_config, + ) + .await + .change_context(ForexCacheError::ApiError)?; + + let to_currency = api_models::enums::Currency::from_str(to_currency.as_str()) + .into_report() + .change_context(ForexCacheError::CurrencyNotAcceptable)?; + + let from_currency = api_models::enums::Currency::from_str(from_currency.as_str()) + .into_report() + .change_context(ForexCacheError::CurrencyNotAcceptable)?; + + let converted_amount = + currency_conversion::conversion::convert(&rates.data, from_currency, to_currency, amount) + .into_report() + .change_context(ForexCacheError::ConversionError)?; + + Ok(api_models::currency::CurrencyConversionResponse { + converted_amount: converted_amount.to_string(), + currency: to_currency.to_string(), + }) +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 7978e98e52c0..2a174f42eb63 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -163,6 +163,8 @@ pub enum Flow { RefundsUpdate, /// Refunds list flow. RefundsList, + // Retrieve forex flow. + RetrieveForexFlow, /// Routing create flow, RoutingCreateConfig, /// Routing link config diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 2fb729fb7b90..bec1074b99d0 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -34,6 +34,15 @@ host_rs = "" mock_locker = true basilisk_host = "" +[forex_api] +call_delay = 21600 +local_fetch_retry_count = 5 +local_fetch_retry_delay = 1000 +api_timeout = 20000 +api_key = "YOUR API KEY HERE" +fallback_api_key = "YOUR API KEY HERE" +redis_lock_timeout = 26000 + [eph_key] validity = 1 From e7ad3a4db8823f3ae8d381771739670d8350e6da Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Tue, 28 Nov 2023 16:22:33 +0530 Subject: [PATCH 108/146] feat(payment_methods): receive `card_holder_name` in confirm flow when using token for payment (#2982) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/payments.rs | 15 +- .../router/src/connector/aci/transformers.rs | 3 +- .../src/connector/adyen/transformers.rs | 6 +- .../src/connector/airwallex/transformers.rs | 3 +- .../connector/bankofamerica/transformers.rs | 3 +- .../src/connector/bluesnap/transformers.rs | 297 +++++++++--------- .../braintree_graphql_transformers.rs | 21 +- .../src/connector/checkout/transformers.rs | 12 +- .../src/connector/cryptopay/transformers.rs | 3 +- .../src/connector/cybersource/transformers.rs | 3 +- .../src/connector/dlocal/transformers.rs | 3 +- .../src/connector/forte/transformers.rs | 3 +- .../src/connector/gocardless/transformers.rs | 23 +- .../src/connector/helcim/transformers.rs | 14 +- crates/router/src/connector/klarna.rs | 3 +- .../connector/multisafepay/transformers.rs | 6 +- .../src/connector/nexinets/transformers.rs | 3 +- .../router/src/connector/nmi/transformers.rs | 3 +- .../router/src/connector/noon/transformers.rs | 3 +- .../src/connector/nuvei/transformers.rs | 6 +- .../src/connector/opayo/transformers.rs | 3 +- .../src/connector/payeezy/transformers.rs | 3 +- .../src/connector/payme/transformers.rs | 10 +- .../src/connector/paypal/transformers.rs | 3 +- .../src/connector/powertranz/transformers.rs | 3 +- .../src/connector/shift4/transformers.rs | 12 +- .../src/connector/square/transformers.rs | 6 +- .../router/src/connector/stax/transformers.rs | 6 +- .../src/connector/stripe/transformers.rs | 16 +- .../src/connector/trustpay/transformers.rs | 3 +- .../router/src/connector/tsys/transformers.rs | 3 +- .../router/src/connector/volt/transformers.rs | 3 +- .../src/connector/worldline/transformers.rs | 3 +- .../src/connector/worldpay/transformers.rs | 3 +- .../router/src/connector/zen/transformers.rs | 3 +- crates/router/src/core/payment_methods.rs | 7 + crates/router/src/core/payments/helpers.rs | 64 +++- crates/router/src/openapi.rs | 1 + crates/router/src/types/transformers.rs | 3 +- openapi/openapi_spec.json | 24 ++ 40 files changed, 375 insertions(+), 237 deletions(-) diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 74559f8ed69a..acb9bbdd6cd4 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -717,6 +717,14 @@ pub struct Card { pub nick_name: Option>, } +#[derive(Eq, PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] +#[serde(rename_all = "snake_case")] +pub struct CardToken { + /// The card holder's name + #[schema(value_type = String, example = "John Test")] + pub card_holder_name: Option>, +} + #[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)] #[serde(rename_all = "snake_case")] pub enum CardRedirectData { @@ -846,6 +854,7 @@ pub enum PaymentMethodData { Upi(UpiData), Voucher(VoucherData), GiftCard(Box), + CardToken(CardToken), } impl PaymentMethodData { @@ -873,7 +882,8 @@ impl PaymentMethodData { | Self::Reward | Self::Upi(_) | Self::Voucher(_) - | Self::GiftCard(_) => None, + | Self::GiftCard(_) + | Self::CardToken(_) => None, } } } @@ -1092,6 +1102,7 @@ pub enum AdditionalPaymentData { GiftCard {}, Voucher {}, CardRedirect {}, + CardToken {}, } #[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize, ToSchema)] @@ -1660,6 +1671,7 @@ pub enum PaymentMethodDataResponse { Voucher, GiftCard, CardRedirect, + CardToken, } #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, ToSchema)] @@ -2455,6 +2467,7 @@ impl From for PaymentMethodDataResponse { AdditionalPaymentData::Voucher {} => Self::Voucher, AdditionalPaymentData::GiftCard {} => Self::GiftCard, AdditionalPaymentData::CardRedirect {} => Self::CardRedirect, + AdditionalPaymentData::CardToken {} => Self::CardToken, } } } diff --git a/crates/router/src/connector/aci/transformers.rs b/crates/router/src/connector/aci/transformers.rs index f56369ed31ab..66aeb3bb6b2b 100644 --- a/crates/router/src/connector/aci/transformers.rs +++ b/crates/router/src/connector/aci/transformers.rs @@ -409,7 +409,8 @@ impl TryFrom<&AciRouterData<&types::PaymentsAuthorizeRouterData>> for AciPayment | api::PaymentMethodData::GiftCard(_) | api::PaymentMethodData::CardRedirect(_) | api::PaymentMethodData::Upi(_) - | api::PaymentMethodData::Voucher(_) => Err(errors::ConnectorError::NotSupported { + | api::PaymentMethodData::Voucher(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { message: format!("{:?}", item.router_data.payment_method), connector: "Aci", })?, diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index a75e3b8ff179..a130ac50cc04 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -1380,7 +1380,8 @@ impl<'a> TryFrom<&AdyenRouterData<&types::PaymentsAuthorizeRouterData>> payments::PaymentMethodData::Crypto(_) | payments::PaymentMethodData::MandatePayment | payments::PaymentMethodData::Reward - | payments::PaymentMethodData::Upi(_) => { + | payments::PaymentMethodData::Upi(_) + | payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotSupported { message: utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "Adyen", @@ -2276,7 +2277,8 @@ impl<'a> | payments::PaymentMethodData::Reward | payments::PaymentMethodData::Upi(_) | payments::PaymentMethodData::Voucher(_) - | payments::PaymentMethodData::GiftCard(_) => { + | payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotSupported { message: "Network tokenization for payment method".to_string(), connector: "Adyen", diff --git a/crates/router/src/connector/airwallex/transformers.rs b/crates/router/src/connector/airwallex/transformers.rs index 457b8d075487..3785e02d4747 100644 --- a/crates/router/src/connector/airwallex/transformers.rs +++ b/crates/router/src/connector/airwallex/transformers.rs @@ -196,7 +196,8 @@ impl TryFrom<&AirwallexRouterData<&types::PaymentsAuthorizeRouterData>> | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("airwallex"), )), }?; diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index 70db9a6d8797..12170deb1a00 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -410,7 +410,8 @@ impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>> | payments::PaymentMethodData::Reward | payments::PaymentMethodData::Upi(_) | payments::PaymentMethodData::Voucher(_) - | payments::PaymentMethodData::GiftCard(_) => { + | payments::PaymentMethodData::GiftCard(_) + | payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Bank of America"), ) diff --git a/crates/router/src/connector/bluesnap/transformers.rs b/crates/router/src/connector/bluesnap/transformers.rs index fe92c337a012..b4ed314e706a 100644 --- a/crates/router/src/connector/bluesnap/transformers.rs +++ b/crates/router/src/connector/bluesnap/transformers.rs @@ -221,7 +221,8 @@ impl TryFrom<&BluesnapRouterData<&types::PaymentsAuthorizeRouterData>> | payments::PaymentMethodData::Upi(_) | payments::PaymentMethodData::CardRedirect(_) | payments::PaymentMethodData::Voucher(_) - | payments::PaymentMethodData::GiftCard(_) => { + | payments::PaymentMethodData::GiftCard(_) + | payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( "Selected payment method via Token flow through bluesnap".to_string(), )) @@ -240,160 +241,160 @@ impl TryFrom<&BluesnapRouterData<&types::PaymentsAuthorizeRouterData>> for Blues Some(enums::CaptureMethod::Manual) => BluesnapTxnType::AuthOnly, _ => BluesnapTxnType::AuthCapture, }; - let (payment_method, card_holder_info) = - match item.router_data.request.payment_method_data.clone() { - api::PaymentMethodData::Card(ref ccard) => Ok(( - PaymentMethodDetails::CreditCard(Card { - card_number: ccard.card_number.clone(), - expiration_month: ccard.card_exp_month.clone(), - expiration_year: ccard.get_expiry_year_4_digit(), - security_code: ccard.card_cvc.clone(), - }), - get_card_holder_info( - item.router_data.get_billing_address()?, - item.router_data.request.get_email()?, - )?, - )), - api::PaymentMethodData::Wallet(wallet_data) => match wallet_data { - api_models::payments::WalletData::GooglePay(payment_method_data) => { - let gpay_object = - Encode::::encode_to_string_of_json( - &BluesnapGooglePayObject { - payment_method_data: utils::GooglePayWalletData::from( - payment_method_data, - ), - }, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(( - PaymentMethodDetails::Wallet(BluesnapWallet { - wallet_type: BluesnapWalletTypes::GooglePay, - encoded_payment_token: Secret::new( - consts::BASE64_ENGINE.encode(gpay_object), - ), - }), - None, - )) + let (payment_method, card_holder_info) = match item + .router_data + .request + .payment_method_data + .clone() + { + api::PaymentMethodData::Card(ref ccard) => Ok(( + PaymentMethodDetails::CreditCard(Card { + card_number: ccard.card_number.clone(), + expiration_month: ccard.card_exp_month.clone(), + expiration_year: ccard.get_expiry_year_4_digit(), + security_code: ccard.card_cvc.clone(), + }), + get_card_holder_info( + item.router_data.get_billing_address()?, + item.router_data.request.get_email()?, + )?, + )), + api::PaymentMethodData::Wallet(wallet_data) => match wallet_data { + api_models::payments::WalletData::GooglePay(payment_method_data) => { + let gpay_object = Encode::::encode_to_string_of_json( + &BluesnapGooglePayObject { + payment_method_data: utils::GooglePayWalletData::from( + payment_method_data, + ), + }, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(( + PaymentMethodDetails::Wallet(BluesnapWallet { + wallet_type: BluesnapWalletTypes::GooglePay, + encoded_payment_token: Secret::new( + consts::BASE64_ENGINE.encode(gpay_object), + ), + }), + None, + )) + } + api_models::payments::WalletData::ApplePay(payment_method_data) => { + let apple_pay_payment_data = payment_method_data + .get_applepay_decoded_payment_data() + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let apple_pay_payment_data: ApplePayEncodedPaymentData = apple_pay_payment_data + .expose()[..] + .as_bytes() + .parse_struct("ApplePayEncodedPaymentData") + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + + let billing = item + .router_data + .address + .billing + .to_owned() + .get_required_value("billing") + .change_context(errors::ConnectorError::MissingRequiredField { + field_name: "billing", + })?; + + let billing_address = billing + .address + .get_required_value("billing_address") + .change_context(errors::ConnectorError::MissingRequiredField { + field_name: "billing", + })?; + + let mut address = Vec::new(); + if let Some(add) = billing_address.line1.to_owned() { + address.push(add) } - api_models::payments::WalletData::ApplePay(payment_method_data) => { - let apple_pay_payment_data = payment_method_data - .get_applepay_decoded_payment_data() - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - let apple_pay_payment_data: ApplePayEncodedPaymentData = - apple_pay_payment_data.expose()[..] - .as_bytes() - .parse_struct("ApplePayEncodedPaymentData") - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - - let billing = item - .router_data - .address - .billing - .to_owned() - .get_required_value("billing") - .change_context(errors::ConnectorError::MissingRequiredField { - field_name: "billing", - })?; - - let billing_address = billing - .address - .get_required_value("billing_address") - .change_context(errors::ConnectorError::MissingRequiredField { - field_name: "billing", - })?; - - let mut address = Vec::new(); - if let Some(add) = billing_address.line1.to_owned() { - address.push(add) - } - if let Some(add) = billing_address.line2.to_owned() { - address.push(add) - } - if let Some(add) = billing_address.line3.to_owned() { - address.push(add) - } - - let apple_pay_object = - Encode::::encode_to_string_of_json( - &EncodedPaymentToken { - token: ApplepayPaymentData { - payment_data: apple_pay_payment_data, - payment_method: payment_method_data - .payment_method - .to_owned() - .into(), - transaction_identifier: payment_method_data - .transaction_identifier, - }, - billing_contact: BillingDetails { - country_code: billing_address.country, - address_lines: Some(address), - family_name: billing_address.last_name.to_owned(), - given_name: billing_address.first_name.to_owned(), - postal_code: billing_address.zip, - }, - }, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - - Ok(( - PaymentMethodDetails::Wallet(BluesnapWallet { - wallet_type: BluesnapWalletTypes::ApplePay, - encoded_payment_token: Secret::new( - consts::BASE64_ENGINE.encode(apple_pay_object), - ), - }), - get_card_holder_info( - item.router_data.get_billing_address()?, - item.router_data.request.get_email()?, - )?, - )) + if let Some(add) = billing_address.line2.to_owned() { + address.push(add) } - payments::WalletData::AliPayQr(_) - | payments::WalletData::AliPayRedirect(_) - | payments::WalletData::AliPayHkRedirect(_) - | payments::WalletData::MomoRedirect(_) - | payments::WalletData::KakaoPayRedirect(_) - | payments::WalletData::GoPayRedirect(_) - | payments::WalletData::GcashRedirect(_) - | payments::WalletData::ApplePayRedirect(_) - | payments::WalletData::ApplePayThirdPartySdk(_) - | payments::WalletData::DanaRedirect {} - | payments::WalletData::GooglePayRedirect(_) - | payments::WalletData::GooglePayThirdPartySdk(_) - | payments::WalletData::MbWayRedirect(_) - | payments::WalletData::MobilePayRedirect(_) - | payments::WalletData::PaypalRedirect(_) - | payments::WalletData::PaypalSdk(_) - | payments::WalletData::SamsungPay(_) - | payments::WalletData::TwintRedirect {} - | payments::WalletData::VippsRedirect {} - | payments::WalletData::TouchNGoRedirect(_) - | payments::WalletData::WeChatPayRedirect(_) - | payments::WalletData::CashappQr(_) - | payments::WalletData::SwishQr(_) - | payments::WalletData::WeChatPayQr(_) => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("bluesnap"), - )) + if let Some(add) = billing_address.line3.to_owned() { + address.push(add) } - }, - payments::PaymentMethodData::PayLater(_) - | payments::PaymentMethodData::BankRedirect(_) - | payments::PaymentMethodData::BankDebit(_) - | payments::PaymentMethodData::BankTransfer(_) - | payments::PaymentMethodData::Crypto(_) - | payments::PaymentMethodData::MandatePayment - | payments::PaymentMethodData::Reward - | payments::PaymentMethodData::Upi(_) - | payments::PaymentMethodData::CardRedirect(_) - | payments::PaymentMethodData::Voucher(_) - | payments::PaymentMethodData::GiftCard(_) => { + + let apple_pay_object = Encode::::encode_to_string_of_json( + &EncodedPaymentToken { + token: ApplepayPaymentData { + payment_data: apple_pay_payment_data, + payment_method: payment_method_data + .payment_method + .to_owned() + .into(), + transaction_identifier: payment_method_data.transaction_identifier, + }, + billing_contact: BillingDetails { + country_code: billing_address.country, + address_lines: Some(address), + family_name: billing_address.last_name.to_owned(), + given_name: billing_address.first_name.to_owned(), + postal_code: billing_address.zip, + }, + }, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + + Ok(( + PaymentMethodDetails::Wallet(BluesnapWallet { + wallet_type: BluesnapWalletTypes::ApplePay, + encoded_payment_token: Secret::new( + consts::BASE64_ENGINE.encode(apple_pay_object), + ), + }), + get_card_holder_info( + item.router_data.get_billing_address()?, + item.router_data.request.get_email()?, + )?, + )) + } + payments::WalletData::AliPayQr(_) + | payments::WalletData::AliPayRedirect(_) + | payments::WalletData::AliPayHkRedirect(_) + | payments::WalletData::MomoRedirect(_) + | payments::WalletData::KakaoPayRedirect(_) + | payments::WalletData::GoPayRedirect(_) + | payments::WalletData::GcashRedirect(_) + | payments::WalletData::ApplePayRedirect(_) + | payments::WalletData::ApplePayThirdPartySdk(_) + | payments::WalletData::DanaRedirect {} + | payments::WalletData::GooglePayRedirect(_) + | payments::WalletData::GooglePayThirdPartySdk(_) + | payments::WalletData::MbWayRedirect(_) + | payments::WalletData::MobilePayRedirect(_) + | payments::WalletData::PaypalRedirect(_) + | payments::WalletData::PaypalSdk(_) + | payments::WalletData::SamsungPay(_) + | payments::WalletData::TwintRedirect {} + | payments::WalletData::VippsRedirect {} + | payments::WalletData::TouchNGoRedirect(_) + | payments::WalletData::WeChatPayRedirect(_) + | payments::WalletData::CashappQr(_) + | payments::WalletData::SwishQr(_) + | payments::WalletData::WeChatPayQr(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("bluesnap"), )) } - }?; + }, + payments::PaymentMethodData::PayLater(_) + | payments::PaymentMethodData::BankRedirect(_) + | payments::PaymentMethodData::BankDebit(_) + | payments::PaymentMethodData::BankTransfer(_) + | payments::PaymentMethodData::Crypto(_) + | payments::PaymentMethodData::MandatePayment + | payments::PaymentMethodData::Reward + | payments::PaymentMethodData::Upi(_) + | payments::PaymentMethodData::CardRedirect(_) + | payments::PaymentMethodData::Voucher(_) + | payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("bluesnap"), + )), + }?; Ok(Self { amount: item.amount.to_owned(), payment_method, diff --git a/crates/router/src/connector/braintree/braintree_graphql_transformers.rs b/crates/router/src/connector/braintree/braintree_graphql_transformers.rs index 5069a9fe38d2..009177e961e7 100644 --- a/crates/router/src/connector/braintree/braintree_graphql_transformers.rs +++ b/crates/router/src/connector/braintree/braintree_graphql_transformers.rs @@ -138,7 +138,8 @@ impl TryFrom<&BraintreeRouterData<&types::PaymentsAuthorizeRouterData>> | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("braintree"), ) @@ -879,12 +880,11 @@ impl TryFrom<&types::TokenizationRouterData> for BraintreeTokenRequest { | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("braintree"), - ) - .into()) - } + | api_models::payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("braintree"), + ) + .into()), } } } @@ -1423,9 +1423,10 @@ fn get_braintree_redirect_form( | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => Err( - errors::ConnectorError::NotImplemented("given payment method".to_owned()), - )?, + | api_models::payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + "given payment method".to_owned(), + ))?, }, }) } diff --git a/crates/router/src/connector/checkout/transformers.rs b/crates/router/src/connector/checkout/transformers.rs index 90e65c8b0474..173ac0b8f585 100644 --- a/crates/router/src/connector/checkout/transformers.rs +++ b/crates/router/src/connector/checkout/transformers.rs @@ -138,7 +138,8 @@ impl TryFrom<&types::TokenizationRouterData> for TokenRequest { | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) | api_models::payments::PaymentMethodData::CardRedirect(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("checkout"), ) @@ -375,11 +376,10 @@ impl TryFrom<&CheckoutRouterData<&types::PaymentsAuthorizeRouterData>> for Payme | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) | api_models::payments::PaymentMethodData::CardRedirect(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("checkout"), - )) - } + | api_models::payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("checkout"), + )), }?; let three_ds = match item.router_data.auth_type { diff --git a/crates/router/src/connector/cryptopay/transformers.rs b/crates/router/src/connector/cryptopay/transformers.rs index 0bc4ff3b3ae6..446da0761d1f 100644 --- a/crates/router/src/connector/cryptopay/transformers.rs +++ b/crates/router/src/connector/cryptopay/transformers.rs @@ -80,7 +80,8 @@ impl TryFrom<&CryptopayRouterData<&types::PaymentsAuthorizeRouterData>> | api_models::payments::PaymentMethodData::Reward {} | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotSupported { message: utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "CryptoPay", diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 33b8fa56d00e..656c45b6d6b6 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -367,7 +367,8 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> | payments::PaymentMethodData::Reward | payments::PaymentMethodData::Upi(_) | payments::PaymentMethodData::Voucher(_) - | payments::PaymentMethodData::GiftCard(_) => { + | payments::PaymentMethodData::GiftCard(_) + | payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Cybersource"), ))? diff --git a/crates/router/src/connector/dlocal/transformers.rs b/crates/router/src/connector/dlocal/transformers.rs index 668a335cce88..a9033e53d666 100644 --- a/crates/router/src/connector/dlocal/transformers.rs +++ b/crates/router/src/connector/dlocal/transformers.rs @@ -168,7 +168,8 @@ impl TryFrom<&DlocalRouterData<&types::PaymentsAuthorizeRouterData>> for DlocalP | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( crate::connector::utils::get_unimplemented_payment_method_error_message("Dlocal"), ))?, } diff --git a/crates/router/src/connector/forte/transformers.rs b/crates/router/src/connector/forte/transformers.rs index dd78324c9b8b..2197b4558a20 100644 --- a/crates/router/src/connector/forte/transformers.rs +++ b/crates/router/src/connector/forte/transformers.rs @@ -112,7 +112,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for FortePaymentsRequest { | api_models::payments::PaymentMethodData::Reward {} | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotSupported { message: utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "Forte", diff --git a/crates/router/src/connector/gocardless/transformers.rs b/crates/router/src/connector/gocardless/transformers.rs index 72204b511518..63e199657af0 100644 --- a/crates/router/src/connector/gocardless/transformers.rs +++ b/crates/router/src/connector/gocardless/transformers.rs @@ -108,7 +108,8 @@ impl TryFrom<&types::ConnectorCustomerRouterData> for GocardlessCustomerRequest | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Gocardless"), )) @@ -297,12 +298,11 @@ impl TryFrom<&types::TokenizationRouterData> for CustomerBankAccount { | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Gocardless"), - ) - .into()) - } + | api_models::payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Gocardless"), + ) + .into()), } } } @@ -483,11 +483,10 @@ impl TryFrom<&types::SetupMandateRouterData> for GocardlessMandateRequest { | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { - Err(errors::ConnectorError::NotImplemented( - "Setup Mandate flow for selected payment method through Gocardless".to_string(), - )) - } + | api_models::payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + "Setup Mandate flow for selected payment method through Gocardless".to_string(), + )), }?; let payment_method_token = item.get_payment_method_token()?; let customer_bank_account = match payment_method_token { diff --git a/crates/router/src/connector/helcim/transformers.rs b/crates/router/src/connector/helcim/transformers.rs index 9510ff6e67ad..9f405e2e2ea1 100644 --- a/crates/router/src/connector/helcim/transformers.rs +++ b/crates/router/src/connector/helcim/transformers.rs @@ -141,7 +141,8 @@ impl TryFrom<&types::SetupMandateRouterData> for HelcimVerifyRequest { | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotSupported { message: format!("{:?}", item.request.payment_method_data), connector: "Helcim", @@ -223,12 +224,11 @@ impl TryFrom<&HelcimRouterData<&types::PaymentsAuthorizeRouterData>> for HelcimP | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { - Err(errors::ConnectorError::NotSupported { - message: format!("{:?}", item.router_data.request.payment_method_data), - connector: "Helcim", - })? - } + | api_models::payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { + message: format!("{:?}", item.router_data.request.payment_method_data), + connector: "Helcim", + })?, } } } diff --git a/crates/router/src/connector/klarna.rs b/crates/router/src/connector/klarna.rs index 3bd3407c3aef..91eaf94c01ee 100644 --- a/crates/router/src/connector/klarna.rs +++ b/crates/router/src/connector/klarna.rs @@ -406,7 +406,8 @@ impl | api_payments::PaymentMethodData::Reward | api_payments::PaymentMethodData::Upi(_) | api_payments::PaymentMethodData::Voucher(_) - | api_payments::PaymentMethodData::GiftCard(_) => Err(error_stack::report!( + | api_payments::PaymentMethodData::GiftCard(_) + | api_payments::PaymentMethodData::CardToken(_) => Err(error_stack::report!( errors::ConnectorError::MismatchedPaymentData )), } diff --git a/crates/router/src/connector/multisafepay/transformers.rs b/crates/router/src/connector/multisafepay/transformers.rs index a067818b743d..1780b77379c7 100644 --- a/crates/router/src/connector/multisafepay/transformers.rs +++ b/crates/router/src/connector/multisafepay/transformers.rs @@ -365,7 +365,8 @@ impl TryFrom<&MultisafepayRouterData<&types::PaymentsAuthorizeRouterData>> | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("multisafepay"), ))?, }; @@ -509,7 +510,8 @@ impl TryFrom<&MultisafepayRouterData<&types::PaymentsAuthorizeRouterData>> | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("multisafepay"), ))?, }; diff --git a/crates/router/src/connector/nexinets/transformers.rs b/crates/router/src/connector/nexinets/transformers.rs index 2af3ee0a1bb8..15cbe9a7e28e 100644 --- a/crates/router/src/connector/nexinets/transformers.rs +++ b/crates/router/src/connector/nexinets/transformers.rs @@ -624,7 +624,8 @@ fn get_payment_details_and_product( | PaymentMethodData::Reward | PaymentMethodData::Upi(_) | PaymentMethodData::Voucher(_) - | PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | PaymentMethodData::GiftCard(_) + | PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("nexinets"), ))?, } diff --git a/crates/router/src/connector/nmi/transformers.rs b/crates/router/src/connector/nmi/transformers.rs index c8721d0d8f6b..ff3a1e6a1c54 100644 --- a/crates/router/src/connector/nmi/transformers.rs +++ b/crates/router/src/connector/nmi/transformers.rs @@ -188,7 +188,8 @@ impl TryFrom<&api_models::payments::PaymentMethodData> for PaymentMethod { | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotSupported { + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { message: utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "nmi", }) diff --git a/crates/router/src/connector/noon/transformers.rs b/crates/router/src/connector/noon/transformers.rs index 5ff92582051a..ee3a8ba8c532 100644 --- a/crates/router/src/connector/noon/transformers.rs +++ b/crates/router/src/connector/noon/transformers.rs @@ -284,7 +284,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for NoonPaymentsRequest { | api::PaymentMethodData::Reward {} | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => { + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotSupported { message: conn_utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "Noon", diff --git a/crates/router/src/connector/nuvei/transformers.rs b/crates/router/src/connector/nuvei/transformers.rs index b79b2c892643..36244b8bc0d8 100644 --- a/crates/router/src/connector/nuvei/transformers.rs +++ b/crates/router/src/connector/nuvei/transformers.rs @@ -856,8 +856,9 @@ impl | payments::PaymentMethodData::Reward | payments::PaymentMethodData::Upi(_) | payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::CardRedirect(_) - | payments::PaymentMethodData::GiftCard(_) => { + | payments::PaymentMethodData::CardRedirect(_) + | payments::PaymentMethodData::GiftCard(_) + | payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("nuvei"), ) @@ -1037,6 +1038,7 @@ impl TryFrom<(&types::PaymentsCompleteAuthorizeRouterData, String)> for NuveiPay | Some(api::PaymentMethodData::CardRedirect(..)) | Some(api::PaymentMethodData::Reward) | Some(api::PaymentMethodData::Upi(..)) + | Some(api::PaymentMethodData::CardToken(..)) | None => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("nuvei"), )), diff --git a/crates/router/src/connector/opayo/transformers.rs b/crates/router/src/connector/opayo/transformers.rs index 41bcc1500ed1..5e9fb066c78d 100644 --- a/crates/router/src/connector/opayo/transformers.rs +++ b/crates/router/src/connector/opayo/transformers.rs @@ -52,7 +52,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for OpayoPaymentsRequest { | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Opayo"), ) .into()), diff --git a/crates/router/src/connector/payeezy/transformers.rs b/crates/router/src/connector/payeezy/transformers.rs index 817ab43ac717..90c58c3a9bce 100644 --- a/crates/router/src/connector/payeezy/transformers.rs +++ b/crates/router/src/connector/payeezy/transformers.rs @@ -260,7 +260,8 @@ fn get_payment_method_data( | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Payeezy"), ))?, } diff --git a/crates/router/src/connector/payme/transformers.rs b/crates/router/src/connector/payme/transformers.rs index 092a8b49fd86..e751de20e219 100644 --- a/crates/router/src/connector/payme/transformers.rs +++ b/crates/router/src/connector/payme/transformers.rs @@ -431,7 +431,8 @@ impl TryFrom<&PaymentMethodData> for SalePaymentMethod { | PaymentMethodData::GiftCard(_) | PaymentMethodData::CardRedirect(_) | PaymentMethodData::Upi(_) - | api::PaymentMethodData::Voucher(_) => { + | PaymentMethodData::Voucher(_) + | PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()) } } @@ -666,7 +667,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PayRequest { | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("payme"), ))?, } @@ -725,6 +727,7 @@ impl TryFrom<&types::PaymentsCompleteAuthorizeRouterData> for Pay3dsRequest { | Some(api::PaymentMethodData::Upi(_)) | Some(api::PaymentMethodData::Voucher(_)) | Some(api::PaymentMethodData::GiftCard(_)) + | Some(api::PaymentMethodData::CardToken(_)) | None => { Err(errors::ConnectorError::NotImplemented("Tokenize Flow".to_string()).into()) } @@ -761,7 +764,8 @@ impl TryFrom<&types::TokenizationRouterData> for CaptureBuyerRequest { | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => { + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented("Tokenize Flow".to_string()).into()) } } diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs index d023077ff008..e59ff09a1f60 100644 --- a/crates/router/src/connector/paypal/transformers.rs +++ b/crates/router/src/connector/paypal/transformers.rs @@ -584,7 +584,8 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP } api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Crypto(_) - | api_models::payments::PaymentMethodData::Upi(_) => { + | api_models::payments::PaymentMethodData::Upi(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotSupported { message: utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "Paypal", diff --git a/crates/router/src/connector/powertranz/transformers.rs b/crates/router/src/connector/powertranz/transformers.rs index 7f62c1939c07..a631a126ed3f 100644 --- a/crates/router/src/connector/powertranz/transformers.rs +++ b/crates/router/src/connector/powertranz/transformers.rs @@ -113,7 +113,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PowertranzPaymentsRequest | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotSupported { + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { message: utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "powertranz", }) diff --git a/crates/router/src/connector/shift4/transformers.rs b/crates/router/src/connector/shift4/transformers.rs index 0dd3b8583490..c272a5b6fc12 100644 --- a/crates/router/src/connector/shift4/transformers.rs +++ b/crates/router/src/connector/shift4/transformers.rs @@ -166,11 +166,14 @@ impl TryFrom<&types::RouterData Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Shift4", + | payments::PaymentMethodData::Upi(_) + | payments::PaymentMethodData::CardToken(_) => { + Err(errors::ConnectorError::NotSupported { + message: utils::SELECTED_PAYMENT_METHOD.to_string(), + connector: "Shift4", + } + .into()) } - .into()), } } } @@ -397,6 +400,7 @@ impl TryFrom<&types::RouterData Err(errors::ConnectorError::NotSupported { message: "Flow".to_string(), connector: "Shift4", diff --git a/crates/router/src/connector/square/transformers.rs b/crates/router/src/connector/square/transformers.rs index dfb49e8e6775..6024a20fa6ab 100644 --- a/crates/router/src/connector/square/transformers.rs +++ b/crates/router/src/connector/square/transformers.rs @@ -191,7 +191,8 @@ impl TryFrom<&types::TokenizationRouterData> for SquareTokenRequest { | api::PaymentMethodData::MandatePayment | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) - | api::PaymentMethodData::Voucher(_) => Err(errors::ConnectorError::NotSupported { + | api::PaymentMethodData::Voucher(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { message: format!("{:?}", item.request.payment_method_data), connector: "Square", })?, @@ -307,7 +308,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for SquarePaymentsRequest { | api::PaymentMethodData::MandatePayment | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) - | api::PaymentMethodData::Voucher(_) => Err(errors::ConnectorError::NotSupported { + | api::PaymentMethodData::Voucher(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { message: format!("{:?}", item.request.payment_method_data), connector: "Square", })?, diff --git a/crates/router/src/connector/stax/transformers.rs b/crates/router/src/connector/stax/transformers.rs index f2aae442ddd6..bb37bf1fc9e7 100644 --- a/crates/router/src/connector/stax/transformers.rs +++ b/crates/router/src/connector/stax/transformers.rs @@ -118,7 +118,8 @@ impl TryFrom<&StaxRouterData<&types::PaymentsAuthorizeRouterData>> for StaxPayme | api::PaymentMethodData::Voucher(_) | api::PaymentMethodData::GiftCard(_) | api::PaymentMethodData::CardRedirect(_) - | api::PaymentMethodData::Upi(_) => Err(errors::ConnectorError::NotSupported { + | api::PaymentMethodData::Upi(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { message: "SELECTED_PAYMENT_METHOD".to_string(), connector: "Stax", })?, @@ -268,7 +269,8 @@ impl TryFrom<&types::TokenizationRouterData> for StaxTokenRequest { | api::PaymentMethodData::Voucher(_) | api::PaymentMethodData::GiftCard(_) | api::PaymentMethodData::CardRedirect(_) - | api::PaymentMethodData::Upi(_) => Err(errors::ConnectorError::NotSupported { + | api::PaymentMethodData::Upi(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { message: "SELECTED_PAYMENT_METHOD".to_string(), connector: "Stax", })?, diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index 56eebc2df3bd..ae7fe59be96c 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -1431,13 +1431,13 @@ fn create_stripe_payment_method( .into()), }, - payments::PaymentMethodData::Upi(_) | payments::PaymentMethodData::MandatePayment => { - Err(errors::ConnectorError::NotSupported { - message: connector_util::SELECTED_PAYMENT_METHOD.to_string(), - connector: "stripe", - } - .into()) + payments::PaymentMethodData::Upi(_) + | payments::PaymentMethodData::MandatePayment + | payments::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { + message: connector_util::SELECTED_PAYMENT_METHOD.to_string(), + connector: "stripe", } + .into()), } } @@ -2995,6 +2995,7 @@ impl TryFrom<&types::PaymentsPreProcessingRouterData> for StripeCreditTransferSo | Some(payments::PaymentMethodData::GiftCard(..)) | Some(payments::PaymentMethodData::CardRedirect(..)) | Some(payments::PaymentMethodData::Voucher(..)) + | Some(payments::PaymentMethodData::CardToken(..)) | None => Err(errors::ConnectorError::NotImplemented( connector_util::get_unimplemented_payment_method_error_message("stripe"), ) @@ -3416,7 +3417,8 @@ impl | api::PaymentMethodData::GiftCard(_) | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::CardRedirect(_) - | api::PaymentMethodData::Voucher(_) => Err(errors::ConnectorError::NotSupported { + | api::PaymentMethodData::Voucher(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { message: format!("{pm_type:?}"), connector: "Stripe", })?, diff --git a/crates/router/src/connector/trustpay/transformers.rs b/crates/router/src/connector/trustpay/transformers.rs index 0210d3ca2d92..e891501d6d0a 100644 --- a/crates/router/src/connector/trustpay/transformers.rs +++ b/crates/router/src/connector/trustpay/transformers.rs @@ -445,7 +445,8 @@ impl TryFrom<&TrustpayRouterData<&types::PaymentsAuthorizeRouterData>> for Trust | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("trustpay"), ) .into()), diff --git a/crates/router/src/connector/tsys/transformers.rs b/crates/router/src/connector/tsys/transformers.rs index c60aeb64898b..863b754fc89c 100644 --- a/crates/router/src/connector/tsys/transformers.rs +++ b/crates/router/src/connector/tsys/transformers.rs @@ -77,7 +77,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for TsysPaymentsRequest { | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("tsys"), ))?, } diff --git a/crates/router/src/connector/volt/transformers.rs b/crates/router/src/connector/volt/transformers.rs index e603ef2db06c..efed7c797c76 100644 --- a/crates/router/src/connector/volt/transformers.rs +++ b/crates/router/src/connector/volt/transformers.rs @@ -148,7 +148,8 @@ impl TryFrom<&VoltRouterData<&types::PaymentsAuthorizeRouterData>> for VoltPayme | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotSupported { message: utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "Volt", diff --git a/crates/router/src/connector/worldline/transformers.rs b/crates/router/src/connector/worldline/transformers.rs index 049453e325ae..282e1b3a8adb 100644 --- a/crates/router/src/connector/worldline/transformers.rs +++ b/crates/router/src/connector/worldline/transformers.rs @@ -257,7 +257,8 @@ impl | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("worldline"), ))?, }; diff --git a/crates/router/src/connector/worldpay/transformers.rs b/crates/router/src/connector/worldpay/transformers.rs index d31f4d65e78c..e35a51552c03 100644 --- a/crates/router/src/connector/worldpay/transformers.rs +++ b/crates/router/src/connector/worldpay/transformers.rs @@ -120,7 +120,8 @@ fn fetch_payment_instrument( | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) | api_models::payments::PaymentMethodData::CardRedirect(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("worldpay"), ) diff --git a/crates/router/src/connector/zen/transformers.rs b/crates/router/src/connector/zen/transformers.rs index 689894176b26..64f6d5bf1a07 100644 --- a/crates/router/src/connector/zen/transformers.rs +++ b/crates/router/src/connector/zen/transformers.rs @@ -707,7 +707,8 @@ impl TryFrom<&ZenRouterData<&types::PaymentsAuthorizeRouterData>> for ZenPayment api_models::payments::PaymentMethodData::Crypto(_) | api_models::payments::PaymentMethodData::MandatePayment | api_models::payments::PaymentMethodData::Reward - | api_models::payments::PaymentMethodData::Upi(_) => { + | api_models::payments::PaymentMethodData::Upi(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Zen"), ))? diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index 80cec01e9166..1049137a9470 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -3,6 +3,7 @@ pub mod surcharge_decision_configs; pub mod transformers; pub mod vault; +use api_models::payments::CardToken; pub use api_models::{ enums::{Connector, PayoutConnectors}, payouts as payout_types, @@ -42,6 +43,7 @@ pub trait PaymentMethodRetrieve { token: &storage::PaymentTokenData, payment_intent: &PaymentIntent, card_cvc: Option>, + card_token_data: Option<&CardToken>, ) -> RouterResult>; } @@ -125,6 +127,7 @@ impl PaymentMethodRetrieve for Oss { token_data: &storage::PaymentTokenData, payment_intent: &PaymentIntent, card_cvc: Option>, + card_token_data: Option<&CardToken>, ) -> RouterResult> { match token_data { storage::PaymentTokenData::TemporaryGeneric(generic_token) => { @@ -134,6 +137,7 @@ impl PaymentMethodRetrieve for Oss { payment_intent, card_cvc, merchant_key_store, + card_token_data, ) .await } @@ -145,6 +149,7 @@ impl PaymentMethodRetrieve for Oss { payment_intent, card_cvc, merchant_key_store, + card_token_data, ) .await } @@ -155,6 +160,7 @@ impl PaymentMethodRetrieve for Oss { &card_token.token, payment_intent, card_cvc, + card_token_data, ) .await .map(|card| Some((card, enums::PaymentMethod::Card))) @@ -166,6 +172,7 @@ impl PaymentMethodRetrieve for Oss { &card_token.token, payment_intent, card_cvc, + card_token_data, ) .await .map(|card| Some((card, enums::PaymentMethod::Card))) diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index f57c0640f1a8..68b8128d7909 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; -use api_models::payments::GetPaymentMethodType; +use api_models::payments::{CardToken, GetPaymentMethodType}; use base64::Engine; use common_utils::{ ext_traits::{AsyncExt, ByteSliceExt, ValueExt}, @@ -1356,6 +1356,7 @@ pub async fn retrieve_payment_method_with_temporary_token( payment_intent: &PaymentIntent, card_cvc: Option>, merchant_key_store: &domain::MerchantKeyStore, + card_token_data: Option<&CardToken>, ) -> RouterResult> { let (pm, supplementary_data) = vault::Vault::get_payment_method_data_from_locker(state, token, merchant_key_store) @@ -1375,9 +1376,29 @@ pub async fn retrieve_payment_method_with_temporary_token( Ok::<_, error_stack::Report>(match pm { Some(api::PaymentMethodData::Card(card)) => { + let mut updated_card = card.clone(); + let mut is_card_updated = false; + + let name_on_card = if card.card_holder_name.clone().expose().is_empty() { + card_token_data + .and_then(|token_data| { + is_card_updated = true; + token_data.card_holder_name.clone() + }) + .filter(|name_on_card| !name_on_card.clone().expose().is_empty()) + .ok_or(errors::ApiErrorResponse::MissingRequiredField { + field_name: "card_holder_name", + })? + } else { + card.card_holder_name.clone() + }; + updated_card.card_holder_name = name_on_card; + if let Some(cvc) = card_cvc { - let mut updated_card = card; + is_card_updated = true; updated_card.card_cvc = cvc; + } + if is_card_updated { let updated_pm = api::PaymentMethodData::Card(updated_card); vault::Vault::store_payment_method_data_in_locker( state, @@ -1423,6 +1444,7 @@ pub async fn retrieve_card_with_permanent_token( token: &str, payment_intent: &PaymentIntent, card_cvc: Option>, + card_token_data: Option<&CardToken>, ) -> RouterResult { let customer_id = payment_intent .customer_id @@ -1437,13 +1459,26 @@ pub async fn retrieve_card_with_permanent_token( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("failed to fetch card information from the permanent locker")?; + let name = card + .name_on_card + .get_required_value("name_on_card") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("card holder name was not saved in permanent locker")?; + + let name_on_card = if name.clone().expose().is_empty() { + card_token_data + .and_then(|token_data| token_data.card_holder_name.clone()) + .filter(|name_on_card| !name_on_card.clone().expose().is_empty()) + .ok_or(errors::ApiErrorResponse::MissingRequiredField { + field_name: "card_holder_name", + })? + } else { + name + }; + let api_card = api::Card { card_number: card.card_number, - card_holder_name: card - .name_on_card - .get_required_value("name_on_card") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("card holder name was not saved in permanent locker")?, + card_holder_name: name_on_card, card_exp_month: card.card_exp_month, card_exp_year: card.card_exp_year, card_cvc: card_cvc.unwrap_or_default(), @@ -1529,6 +1564,11 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( let card_cvc = payment_data.card_cvc.clone(); + let card_token_data = request.as_ref().and_then(|pmd| match pmd { + api_models::payments::PaymentMethodData::CardToken(token_data) => Some(token_data), + _ => None, + }); + // TODO: Handle case where payment method and token both are present in request properly. let payment_method = match (request, hyperswitch_token) { (_, Some(hyperswitch_token)) => { @@ -1538,6 +1578,7 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( &hyperswitch_token, &payment_data.payment_intent, card_cvc, + card_token_data, ) .await .attach_printable("in 'make_pm_data'")?; @@ -3316,6 +3357,9 @@ pub async fn get_additional_payment_data( api_models::payments::PaymentMethodData::GiftCard(_) => { api_models::payments::AdditionalPaymentData::GiftCard {} } + api_models::payments::PaymentMethodData::CardToken(_) => { + api_models::payments::AdditionalPaymentData::CardToken {} + } } } @@ -3615,6 +3659,12 @@ pub fn get_key_params_for_surcharge_details( gift_card.get_payment_method_type(), None, )), + api_models::payments::PaymentMethodData::CardToken(_) => { + Err(errors::ApiErrorResponse::InvalidDataValue { + field_name: "payment_method_data", + } + .into()) + } } } diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index d191890b8cdb..ec38389cdc42 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -248,6 +248,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::OnlineMandate, api_models::payments::Card, api_models::payments::CardRedirectData, + api_models::payments::CardToken, api_models::payments::CustomerAcceptance, api_models::payments::PaymentsRequest, api_models::payments::PaymentsCreateRequest, diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 45aad93371e2..5bd28db3c158 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -522,7 +522,8 @@ impl ForeignTryFrom for api_enums::Paym payment_method_data: api_models::payments::PaymentMethodData, ) -> Result { match payment_method_data { - api_models::payments::PaymentMethodData::Card(..) => Ok(Self::Card), + api_models::payments::PaymentMethodData::Card(..) + | api_models::payments::PaymentMethodData::CardToken(..) => Ok(Self::Card), api_models::payments::PaymentMethodData::Wallet(..) => Ok(Self::Wallet), api_models::payments::PaymentMethodData::PayLater(..) => Ok(Self::PayLater), api_models::payments::PaymentMethodData::BankRedirect(..) => Ok(Self::BankRedirect), diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 88a0d115ff01..08f415782963 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -4053,6 +4053,19 @@ } ] }, + "CardToken": { + "type": "object", + "required": [ + "card_holder_name" + ], + "properties": { + "card_holder_name": { + "type": "string", + "description": "The card holder's name", + "example": "John Test" + } + } + }, "CashappQr": { "type": "object" }, @@ -8657,6 +8670,17 @@ "$ref": "#/components/schemas/GiftCardData" } } + }, + { + "type": "object", + "required": [ + "card_token" + ], + "properties": { + "card_token": { + "$ref": "#/components/schemas/CardToken" + } + } } ] }, From 77fc92c99a99aaf76d270ba5b981928183a05768 Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Tue, 28 Nov 2023 16:40:42 +0530 Subject: [PATCH 109/146] feat(core): [Paypal] Add Preprocessing flow to CompleteAuthorize for Card 3DS Auth Verification (#2757) --- crates/router/src/connector/paypal.rs | 156 ++++++++++++++++++ .../src/connector/paypal/transformers.rs | 68 ++++++++ crates/router/src/consts.rs | 2 + crates/router/src/core/payments.rs | 16 +- crates/router/src/core/payments/flows.rs | 1 - .../src/core/payments/flows/authorize_flow.rs | 24 +++ .../payments/flows/complete_authorize_flow.rs | 72 +++++++- .../router/src/core/payments/transformers.rs | 1 + crates/router/src/types.rs | 1 + 9 files changed, 338 insertions(+), 3 deletions(-) diff --git a/crates/router/src/connector/paypal.rs b/crates/router/src/connector/paypal.rs index 4e50bc924b33..9ab19b295570 100644 --- a/crates/router/src/connector/paypal.rs +++ b/crates/router/src/connector/paypal.rs @@ -30,6 +30,7 @@ use crate::{ types::{ self, api::{self, CompleteAuthorize, ConnectorCommon, ConnectorCommonExt, VerifyWebhookSource}, + storage::enums as storage_enums, transformers::ForeignFrom, ConnectorAuthType, ErrorResponse, Response, }, @@ -506,6 +507,161 @@ impl ConnectorIntegration for Paypal +{ + fn get_headers( + &self, + req: &types::PaymentsPreProcessingRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_url( + &self, + req: &types::PaymentsPreProcessingRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let order_id = req + .request + .connector_transaction_id + .to_owned() + .ok_or(errors::ConnectorError::MissingConnectorTransactionID)?; + Ok(format!( + "{}v2/checkout/orders/{}?fields=payment_source", + self.base_url(connectors), + order_id, + )) + } + + fn build_request( + &self, + req: &types::PaymentsPreProcessingRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Get) + .url(&types::PaymentsPreProcessingType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsPreProcessingType::get_headers( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsPreProcessingRouterData, + res: Response, + ) -> CustomResult { + let response: paypal::PaypalPreProcessingResponse = res + .response + .parse_struct("paypal PaypalPreProcessingResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + // permutation for status to continue payment + match ( + response + .payment_source + .card + .authentication_result + .three_d_secure + .enrollment_status + .as_ref(), + response + .payment_source + .card + .authentication_result + .three_d_secure + .authentication_status + .as_ref(), + response + .payment_source + .card + .authentication_result + .liability_shift + .clone(), + ) { + ( + Some(paypal::EnrollementStatus::Ready), + Some(paypal::AuthenticationStatus::Success), + paypal::LiabilityShift::Possible, + ) + | ( + Some(paypal::EnrollementStatus::Ready), + Some(paypal::AuthenticationStatus::Attempted), + paypal::LiabilityShift::Possible, + ) + | (Some(paypal::EnrollementStatus::NotReady), None, paypal::LiabilityShift::No) + | (Some(paypal::EnrollementStatus::Unavailable), None, paypal::LiabilityShift::No) + | (Some(paypal::EnrollementStatus::Bypassed), None, paypal::LiabilityShift::No) => { + Ok(types::PaymentsPreProcessingRouterData { + status: storage_enums::AttemptStatus::AuthenticationSuccessful, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::NoResponseId, + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + }), + ..data.clone() + }) + } + _ => Ok(types::PaymentsPreProcessingRouterData { + response: Err(ErrorResponse { + attempt_status: Some(enums::AttemptStatus::Failure), + code: consts::NO_ERROR_CODE.to_string(), + message: consts::NO_ERROR_MESSAGE.to_string(), + connector_transaction_id: None, + reason: Some(format!("{} Connector Responsded with LiabilityShift: {:?}, EnrollmentStatus: {:?}, and AuthenticationStatus: {:?}", + consts::CANNOT_CONTINUE_AUTH, + response + .payment_source + .card + .authentication_result + .liability_shift, + response + .payment_source + .card + .authentication_result + .three_d_secure + .enrollment_status + .unwrap_or(paypal::EnrollementStatus::Null), + response + .payment_source + .card + .authentication_result + .three_d_secure + .authentication_status + .unwrap_or(paypal::AuthenticationStatus::Null), + )), + status_code: res.status_code, + }), + ..data.clone() + }), + } + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + impl ConnectorIntegration< CompleteAuthorize, diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs index e59ff09a1f60..04328cead233 100644 --- a/crates/router/src/connector/paypal/transformers.rs +++ b/crates/router/src/connector/paypal/transformers.rs @@ -925,6 +925,74 @@ pub struct PaypalThreeDsResponse { links: Vec, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaypalPreProcessingResponse { + pub payment_source: CardParams, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CardParams { + pub card: AuthResult, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AuthResult { + pub authentication_result: PaypalThreeDsParams, +} +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaypalThreeDsParams { + pub liability_shift: LiabilityShift, + pub three_d_secure: ThreeDsCheck, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ThreeDsCheck { + pub enrollment_status: Option, + pub authentication_status: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum LiabilityShift { + Possible, + No, + Unknown, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum EnrollementStatus { + Null, + #[serde(rename = "Y")] + Ready, + #[serde(rename = "N")] + NotReady, + #[serde(rename = "U")] + Unavailable, + #[serde(rename = "B")] + Bypassed, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum AuthenticationStatus { + Null, + #[serde(rename = "Y")] + Success, + #[serde(rename = "N")] + Failed, + #[serde(rename = "R")] + Rejected, + #[serde(rename = "A")] + Attempted, + #[serde(rename = "U")] + Unable, + #[serde(rename = "C")] + ChallengeRequired, + #[serde(rename = "I")] + InfoOnly, + #[serde(rename = "D")] + Decoupled, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PaypalOrdersResponse { id: String, diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index c5490ee00e63..8937764409f8 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -28,6 +28,8 @@ pub(crate) const NO_ERROR_MESSAGE: &str = "No error message"; pub(crate) const NO_ERROR_CODE: &str = "No error code"; pub(crate) const UNSUPPORTED_ERROR_MESSAGE: &str = "Unsupported response type"; pub(crate) const CONNECTOR_UNAUTHORIZED_ERROR: &str = "Authentication Error from the connector"; +pub(crate) const CANNOT_CONTINUE_AUTH: &str = + "Cannot continue with Authorization due to failed Liability Shift."; // General purpose base64 engines pub(crate) const BASE64_ENGINE: base64::engine::GeneralPurpose = diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 1c40ef81f497..4fe7ea848008 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1418,7 +1418,21 @@ where (router_data, should_continue_payment) } } - _ => (router_data, should_continue_payment), + _ => { + // 3DS validation for paypal cards after verification (authorize call) + if connector.connector_name == router_types::Connector::Paypal + && payment_data.payment_attempt.payment_method + == Some(storage_enums::PaymentMethod::Card) + && matches!(format!("{operation:?}").as_str(), "CompleteAuthorize") + { + router_data = router_data.preprocessing_steps(state, connector).await?; + let is_error_in_response = router_data.response.is_err(); + // If is_error_in_response is true, should_continue_payment should be false, we should throw the error + (router_data, !is_error_in_response) + } else { + (router_data, should_continue_payment) + } + } }; Ok(router_data_and_should_continue_payment) diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 46eaca26f7cc..d983cd19bdb5 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -863,7 +863,6 @@ default_imp_for_pre_processing_steps!( connector::Opayo, connector::Opennode, connector::Payeezy, - connector::Paypal, connector::Payu, connector::Powertranz, connector::Prophetpay, diff --git a/crates/router/src/core/payments/flows/authorize_flow.rs b/crates/router/src/core/payments/flows/authorize_flow.rs index 04bd7f0b4338..4ef23f481a2c 100644 --- a/crates/router/src/core/payments/flows/authorize_flow.rs +++ b/crates/router/src/core/payments/flows/authorize_flow.rs @@ -417,6 +417,30 @@ impl TryFrom for types::PaymentsPreProcessingData complete_authorize_url: data.complete_authorize_url, browser_info: data.browser_info, surcharge_details: data.surcharge_details, + connector_transaction_id: None, + }) + } +} + +impl TryFrom for types::PaymentsPreProcessingData { + type Error = error_stack::Report; + + fn try_from(data: types::CompleteAuthorizeData) -> Result { + Ok(Self { + payment_method_data: data.payment_method_data, + amount: Some(data.amount), + email: data.email, + currency: Some(data.currency), + payment_method_type: None, + setup_mandate_details: data.setup_mandate_details, + capture_method: data.capture_method, + order_details: None, + router_return_url: None, + webhook_url: None, + complete_authorize_url: None, + browser_info: data.browser_info, + surcharge_details: None, + connector_transaction_id: data.connector_transaction_id, }) } } diff --git a/crates/router/src/core/payments/flows/complete_authorize_flow.rs b/crates/router/src/core/payments/flows/complete_authorize_flow.rs index 44d8728fd4d2..2d52a145feae 100644 --- a/crates/router/src/core/payments/flows/complete_authorize_flow.rs +++ b/crates/router/src/core/payments/flows/complete_authorize_flow.rs @@ -6,7 +6,7 @@ use crate::{ errors::{self, ConnectorErrorExt, RouterResult}, payments::{self, access_token, helpers, transformers, PaymentData}, }, - routes::AppState, + routes::{metrics, AppState}, services, types::{self, api, domain}, utils::OptionExt, @@ -144,6 +144,76 @@ impl Feature Ok((request, true)) } + + async fn preprocessing_steps<'a>( + self, + state: &AppState, + connector: &api::ConnectorData, + ) -> RouterResult { + complete_authorize_preprocessing_steps(state, &self, true, connector).await + } +} + +pub async fn complete_authorize_preprocessing_steps( + state: &AppState, + router_data: &types::RouterData, + confirm: bool, + connector: &api::ConnectorData, +) -> RouterResult> { + if confirm { + let connector_integration: services::BoxedConnectorIntegration< + '_, + api::PreProcessing, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, + > = connector.connector.get_connector_integration(); + + let preprocessing_request_data = + types::PaymentsPreProcessingData::try_from(router_data.request.to_owned())?; + + let preprocessing_response_data: Result = + Err(types::ErrorResponse::default()); + + let preprocessing_router_data = + payments::helpers::router_data_type_conversion::<_, api::PreProcessing, _, _, _, _>( + router_data.clone(), + preprocessing_request_data, + preprocessing_response_data, + ); + + let resp = services::execute_connector_processing_step( + state, + connector_integration, + &preprocessing_router_data, + payments::CallConnectorAction::Trigger, + None, + ) + .await + .to_payment_failed_response()?; + + metrics::PREPROCESSING_STEPS_COUNT.add( + &metrics::CONTEXT, + 1, + &[ + metrics::request::add_attributes("connector", connector.connector_name.to_string()), + metrics::request::add_attributes( + "payment_method", + router_data.payment_method.to_string(), + ), + ], + ); + + let authorize_router_data = + payments::helpers::router_data_type_conversion::<_, F, _, _, _, _>( + resp.clone(), + router_data.request.to_owned(), + resp.response, + ); + + Ok(authorize_router_data) + } else { + Ok(router_data.clone()) + } } impl TryFrom for types::PaymentMethodTokenizationData { diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index f395c023128c..000bbb0fc00b 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -1428,6 +1428,7 @@ impl TryFrom> for types::PaymentsPreProce complete_authorize_url, browser_info, surcharge_details: payment_data.surcharge_details, + connector_transaction_id: payment_data.payment_attempt.connector_transaction_id, }) } } diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 8c9d030965c9..db126b81451d 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -442,6 +442,7 @@ pub struct PaymentsPreProcessingData { pub complete_authorize_url: Option, pub surcharge_details: Option, pub browser_info: Option, + pub connector_transaction_id: Option, } #[derive(Debug, Clone)] From bd889c834dd5e201b055233016f7226fa2187aea Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Tue, 28 Nov 2023 17:32:53 +0530 Subject: [PATCH 110/146] fix(connector): [Adyen] `ErrorHandling` in case of Balance Check for Gift Cards (#1976) --- crates/router/src/connector/adyen.rs | 155 ++++++------------ .../src/connector/adyen/transformers.rs | 46 ++++-- crates/router/src/consts.rs | 1 + crates/router/src/core/payments.rs | 11 ++ crates/router/src/core/payments/flows.rs | 1 - crates/router/src/types.rs | 2 +- .../.meta.json | 3 + .../Payments - Create/.event.meta.json | 3 + .../Payments - Create/event.test.js | 71 ++++++++ .../Payments - Create/request.json | 88 ++++++++++ .../Payments - Create/response.json | 1 + .../Payments - Retrieve/.event.meta.json | 3 + .../Payments - Retrieve/event.test.js | 71 ++++++++ .../Payments - Retrieve/request.json | 28 ++++ .../Payments - Retrieve/response.json | 1 + .../Payment Connector - Create/request.json | 12 ++ .../QuickStart/Payments - Create/request.json | 4 + .../.meta.json | 3 + .../Payments - Create/.event.meta.json | 3 + .../Payments - Create/event.test.js | 81 +++++++++ .../Payments - Create/request.json | 88 ++++++++++ .../Payments - Create/response.json | 1 + .../Payments - Retrieve/.event.meta.json | 3 + .../Payments - Retrieve/event.test.js | 71 ++++++++ .../Payments - Retrieve/request.json | 28 ++++ .../Payments - Retrieve/response.json | 1 + 26 files changed, 660 insertions(+), 120 deletions(-) create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/.meta.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/.event.meta.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/event.test.js create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/request.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/response.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/.event.meta.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/event.test.js create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/request.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/response.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/.meta.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/.event.meta.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/event.test.js create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/request.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/response.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/.event.meta.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/event.test.js create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/request.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/response.json diff --git a/crates/router/src/connector/adyen.rs b/crates/router/src/connector/adyen.rs index e101b796b8d4..ddd93bc289a9 100644 --- a/crates/router/src/connector/adyen.rs +++ b/crates/router/src/connector/adyen.rs @@ -14,11 +14,8 @@ use crate::{ configs::settings, connector::utils as connector_utils, consts, - core::{ - self, - errors::{self, CustomResult}, - }, - headers, logger, routes, + core::errors::{self, CustomResult}, + headers, logger, services::{ self, request::{self, Mask}, @@ -560,7 +557,6 @@ impl } } -#[async_trait::async_trait] impl services::ConnectorIntegration< api::Authorize, @@ -568,49 +564,6 @@ impl types::PaymentsResponseData, > for Adyen { - async fn execute_pretasks( - &self, - router_data: &mut types::PaymentsAuthorizeRouterData, - app_state: &routes::AppState, - ) -> CustomResult<(), errors::ConnectorError> { - match &router_data.request.payment_method_data { - api_models::payments::PaymentMethodData::GiftCard(gift_card_data) => { - match gift_card_data.as_ref() { - api_models::payments::GiftCardData::Givex(_) => { - let integ: Box< - &(dyn services::ConnectorIntegration< - api::Balance, - types::PaymentsAuthorizeData, - types::PaymentsResponseData, - > + Send - + Sync - + 'static), - > = Box::new(&Self); - - let authorize_data = &types::PaymentsBalanceRouterData::from(( - &router_data.to_owned(), - router_data.request.clone(), - )); - - let resp = services::execute_connector_processing_step( - app_state, - integ, - authorize_data, - core::payments::CallConnectorAction::Trigger, - None, - ) - .await?; - router_data.payment_method_balance = resp.payment_method_balance; - - Ok(()) - } - _ => Ok(()), - } - } - _ => Ok(()), - } - } - fn get_headers( &self, req: &types::PaymentsAuthorizeRouterData, @@ -667,7 +620,6 @@ impl req: &types::PaymentsAuthorizeRouterData, connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - check_for_payment_method_balance(req)?; Ok(Some( services::RequestBuilder::new() .method(services::Method::Post) @@ -725,28 +677,23 @@ impl } } +impl api::PaymentsPreProcessing for Adyen {} + impl services::ConnectorIntegration< - api::Balance, - types::PaymentsAuthorizeData, + api::PreProcessing, + types::PaymentsPreProcessingData, types::PaymentsResponseData, > for Adyen { fn get_headers( &self, - req: &types::PaymentsBalanceRouterData, + req: &types::PaymentsPreProcessingRouterData, _connectors: &settings::Connectors, - ) -> CustomResult)>, errors::ConnectorError> - where - Self: services::ConnectorIntegration< - api::Balance, - types::PaymentsAuthorizeData, - types::PaymentsResponseData, - >, - { + ) -> CustomResult)>, errors::ConnectorError> { let mut header = vec![( headers::CONTENT_TYPE.to_string(), - types::PaymentsBalanceType::get_content_type(self) + types::PaymentsPreProcessingType::get_content_type(self) .to_string() .into(), )]; @@ -757,7 +704,7 @@ impl fn get_url( &self, - _req: &types::PaymentsBalanceRouterData, + _req: &types::PaymentsPreProcessingRouterData, connectors: &settings::Connectors, ) -> CustomResult { Ok(format!( @@ -768,7 +715,7 @@ impl fn get_request_body( &self, - req: &types::PaymentsBalanceRouterData, + req: &types::PaymentsPreProcessingRouterData, _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = adyen::AdyenBalanceRequest::try_from(req)?; @@ -783,18 +730,20 @@ impl fn build_request( &self, - req: &types::PaymentsBalanceRouterData, + req: &types::PaymentsPreProcessingRouterData, connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { Ok(Some( services::RequestBuilder::new() .method(services::Method::Post) - .url(&types::PaymentsBalanceType::get_url(self, req, connectors)?) + .url(&types::PaymentsPreProcessingType::get_url( + self, req, connectors, + )?) .attach_default_headers() - .headers(types::PaymentsBalanceType::get_headers( + .headers(types::PaymentsPreProcessingType::get_headers( self, req, connectors, )?) - .body(types::PaymentsBalanceType::get_request_body( + .body(types::PaymentsPreProcessingType::get_request_body( self, req, connectors, )?) .build(), @@ -803,19 +752,47 @@ impl fn handle_response( &self, - data: &types::PaymentsBalanceRouterData, + data: &types::PaymentsPreProcessingRouterData, res: types::Response, - ) -> CustomResult { + ) -> CustomResult { let response: adyen::AdyenBalanceResponse = res .response .parse_struct("AdyenBalanceResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - types::RouterData::try_from(types::ResponseRouterData { - response, - data: data.clone(), - http_code: res.status_code, - }) - .change_context(errors::ConnectorError::ResponseHandlingFailed) + + let currency = match data.request.currency { + Some(currency) => currency, + None => Err(errors::ConnectorError::MissingRequiredField { + field_name: "currency", + })?, + }; + let amount = match data.request.amount { + Some(amount) => amount, + None => Err(errors::ConnectorError::MissingRequiredField { + field_name: "amount", + })?, + }; + + if response.balance.currency != currency || response.balance.value < amount { + Ok(types::RouterData { + response: Err(types::ErrorResponse { + code: consts::NO_ERROR_CODE.to_string(), + message: consts::NO_ERROR_MESSAGE.to_string(), + reason: Some(consts::LOW_BALANCE_ERROR_MESSAGE.to_string()), + status_code: res.status_code, + attempt_status: Some(enums::AttemptStatus::Failure), + connector_transaction_id: None, + }), + ..data.clone() + }) + } else { + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } } fn get_error_response( @@ -1634,7 +1611,7 @@ impl api::IncomingWebhook for Adyen { .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; Ok(api::disputes::DisputePayload { amount: notif.amount.value.to_string(), - currency: notif.amount.currency, + currency: notif.amount.currency.to_string(), dispute_stage: api_models::enums::DisputeStage::from(notif.event_code.clone()), connector_dispute_id: notif.psp_reference, connector_reason: notif.reason, @@ -1646,27 +1623,3 @@ impl api::IncomingWebhook for Adyen { }) } } - -pub fn check_for_payment_method_balance( - req: &types::PaymentsAuthorizeRouterData, -) -> CustomResult<(), errors::ConnectorError> { - match &req.request.payment_method_data { - api_models::payments::PaymentMethodData::GiftCard(gift_card) => match gift_card.as_ref() { - api_models::payments::GiftCardData::Givex(_) => { - let payment_method_balance = req - .payment_method_balance - .as_ref() - .ok_or(errors::ConnectorError::RequestEncodingFailed)?; - if payment_method_balance.currency != req.request.currency.to_string() - || payment_method_balance.amount < req.request.amount - { - Err(errors::ConnectorError::InSufficientBalanceInPaymentMethod.into()) - } else { - Ok(()) - } - } - _ => Ok(()), - }, - _ => Ok(()), - } -} diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index a130ac50cc04..cfa601112677 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -213,8 +213,8 @@ pub struct AdyenBalanceRequest<'a> { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AdyenBalanceResponse { - psp_reference: String, - balance: Amount, + pub psp_reference: String, + pub balance: Amount, } /// This implementation will be used only in Authorize, Automatic capture flow. @@ -397,8 +397,8 @@ pub enum ActionType { #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct Amount { - currency: String, - value: i64, + pub currency: storage_enums::Currency, + pub value: i64, } #[derive(Debug, Clone, Serialize)] @@ -1392,11 +1392,11 @@ impl<'a> TryFrom<&AdyenRouterData<&types::PaymentsAuthorizeRouterData>> } } -impl<'a> TryFrom<&types::PaymentsBalanceRouterData> for AdyenBalanceRequest<'a> { +impl<'a> TryFrom<&types::PaymentsPreProcessingRouterData> for AdyenBalanceRequest<'a> { type Error = Error; - fn try_from(item: &types::PaymentsBalanceRouterData) -> Result { + fn try_from(item: &types::PaymentsPreProcessingRouterData) -> Result { let payment_method = match &item.request.payment_method_data { - payments::PaymentMethodData::GiftCard(gift_card_data) => { + Some(payments::PaymentMethodData::GiftCard(gift_card_data)) => { match gift_card_data.as_ref() { payments::GiftCardData::Givex(gift_card_data) => { let balance_pm = BalancePmData { @@ -1510,7 +1510,7 @@ fn get_channel_type(pm_type: &Option) -> Optio fn get_amount_data(item: &AdyenRouterData<&types::PaymentsAuthorizeRouterData>) -> Amount { Amount { - currency: item.router_data.request.currency.to_string(), + currency: item.router_data.request.currency, value: item.amount.to_owned(), } } @@ -2857,12 +2857,24 @@ impl TryFrom> } } -impl TryFrom> - for types::PaymentsBalanceRouterData +impl + TryFrom< + types::ResponseRouterData< + F, + AdyenBalanceResponse, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, + >, + > for types::RouterData { type Error = Error; fn try_from( - item: types::PaymentsBalanceResponseRouterData, + item: types::ResponseRouterData< + F, + AdyenBalanceResponse, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, + >, ) -> Result { Ok(Self { response: Ok(types::PaymentsResponseData::TransactionResponse { @@ -3457,7 +3469,7 @@ impl TryFrom<&AdyenRouterData<&types::PaymentsCaptureRouterData>> for AdyenCaptu merchant_account: auth_type.merchant_account, reference, amount: Amount { - currency: item.router_data.request.currency.to_string(), + currency: item.router_data.request.currency, value: item.amount.to_owned(), }, }) @@ -3547,7 +3559,7 @@ impl TryFrom<&AdyenRouterData<&types::RefundsRouterData>> for AdyenRefundR Ok(Self { merchant_account: auth_type.merchant_account, amount: Amount { - currency: item.router_data.request.currency.to_string(), + currency: item.router_data.request.currency, value: item.router_data.request.refund_amount, }, merchant_refund_reason: item.router_data.request.reason.clone(), @@ -3629,7 +3641,7 @@ pub struct AdyenAdditionalDataWH { #[derive(Debug, Deserialize)] pub struct AdyenAmountWH { pub value: i64, - pub currency: String, + pub currency: storage_enums::Currency, } #[derive(Clone, Debug, Deserialize, Serialize, strum::Display, PartialEq)] @@ -3955,7 +3967,7 @@ impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutE )?; Ok(Self { amount: Amount { - currency: item.router_data.request.destination_currency.to_string(), + currency: item.router_data.request.destination_currency, value: item.amount.to_owned(), }, merchant_account: auth_type.merchant_account, @@ -4030,7 +4042,7 @@ impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutC Ok(Self { amount: Amount { value: item.amount.to_owned(), - currency: item.router_data.request.destination_currency.to_string(), + currency: item.router_data.request.destination_currency, }, recurring: RecurringContract { contract: Contract::Payout, @@ -4077,7 +4089,7 @@ impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutF Ok(Self::Card(Box::new(PayoutFulfillCardRequest { amount: Amount { value: item.amount.to_owned(), - currency: item.router_data.request.destination_currency.to_string(), + currency: item.router_data.request.destination_currency, }, card: get_payout_card_details(&item.router_data.get_payout_method_data()?) .map_or( diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 8937764409f8..4f19562c83ce 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -27,6 +27,7 @@ pub const DEFAULT_FULFILLMENT_TIME: i64 = 15 * 60; pub(crate) const NO_ERROR_MESSAGE: &str = "No error message"; pub(crate) const NO_ERROR_CODE: &str = "No error code"; pub(crate) const UNSUPPORTED_ERROR_MESSAGE: &str = "Unsupported response type"; +pub(crate) const LOW_BALANCE_ERROR_MESSAGE: &str = "Insufficient balance in the payment method"; pub(crate) const CONNECTOR_UNAUTHORIZED_ERROR: &str = "Authentication Error from the connector"; pub(crate) const CANNOT_CONTINUE_AUTH: &str = "Cannot continue with Authorization due to failed Liability Shift."; diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 4fe7ea848008..8cfded8463eb 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1408,6 +1408,17 @@ where (router_data, should_continue_payment) } } + Some(api_models::payments::PaymentMethodData::GiftCard(_)) => { + if connector.connector_name == router_types::Connector::Adyen { + router_data = router_data.preprocessing_steps(state, connector).await?; + + let is_error_in_response = router_data.response.is_err(); + // If is_error_in_response is true, should_continue_payment should be false, we should throw the error + (router_data, !is_error_in_response) + } else { + (router_data, should_continue_payment) + } + } Some(api_models::payments::PaymentMethodData::BankDebit(_)) => { if connector.connector_name == router_types::Connector::Gocardless { router_data = router_data.preprocessing_steps(state, connector).await?; diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index d983cd19bdb5..9be6f5905b8b 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -832,7 +832,6 @@ impl default_imp_for_pre_processing_steps!( connector::Aci, - connector::Adyen, connector::Airwallex, connector::Authorizedotnet, connector::Bambora, diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index db126b81451d..cd37fbb549d9 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -323,7 +323,7 @@ pub struct ApplePayCryptogramData { #[derive(Debug, Clone)] pub struct PaymentMethodBalance { pub amount: i64, - pub currency: String, + pub currency: storage_enums::Currency, } #[cfg(feature = "payouts")] diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/.meta.json new file mode 100644 index 000000000000..69b505c6d863 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["Payments - Create", "Payments - Retrieve"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/event.test.js new file mode 100644 index 000000000000..c48d8e2d054e --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'succeeded'", + function () { + pm.expect(jsonData.status).to.eql("succeeded"); + }, + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/request.json new file mode 100644 index 000000000000..0915e9894bb6 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/request.json @@ -0,0 +1,88 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 1100, + "currency": "EUR", + "confirm": true, + "capture_method": "automatic", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 1100, + "customer_id": "StripeCustomer", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "payment_method": "gift_card", + "payment_method_type": "givex", + "payment_method_data": { + "gift_card": { + "givex": { + "number": "6364530000000000", + "cvc": "122222" + } + } + }, + "routing": { + "type": "single", + "data": "adyen" + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": ["{{baseUrl}}"], + "path": ["payments"] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..0652a2d92fd4 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[GET]::/payments/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/payments/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[GET]::/payments/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "Succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'", + function () { + pm.expect(jsonData.status).to.eql("succeeded"); + }, + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/request.json new file mode 100644 index 000000000000..6cd4b7d96c52 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/request.json @@ -0,0 +1,28 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": ["{{baseUrl}}"], + "path": ["payments", ":id"], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/request.json index 592cff807510..fe25f6f5e682 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/request.json +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/request.json @@ -190,6 +190,18 @@ } ] }, + { + "payment_method": "gift_card", + "payment_method_types": [ + { + "payment_method_type": "givex", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, { "payment_method": "bank_redirect", "payment_method_types": [ diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payments - Create/request.json index 8ac3ed14b0a7..ed9dbeaa9c49 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payments - Create/request.json +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payments - Create/request.json @@ -43,6 +43,10 @@ "card_cvc": "7373" } }, + "routing": { + "type": "single", + "data": "adyen" + }, "billing": { "address": { "line1": "1467", diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/.meta.json new file mode 100644 index 000000000000..69b505c6d863 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["Payments - Create", "Payments - Retrieve"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/event.test.js new file mode 100644 index 000000000000..601f4f8fa7f5 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/event.test.js @@ -0,0 +1,81 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "failed" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'failed'", + function () { + pm.expect(jsonData.status).to.eql("failed"); + }, + ); +} + +// Response body should have error message as "Insufficient balance in the payment method" +if (jsonData?.error_message) { + pm.test( + "[POST]::/payments - Content check if value for 'error_message' matches 'Insufficient balance in the payment method'", + function () { + pm.expect(jsonData.error_message).to.eql("Insufficient balance in the payment method"); + }, + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/request.json new file mode 100644 index 000000000000..11437ff57659 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/request.json @@ -0,0 +1,88 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 14100, + "currency": "EUR", + "confirm": true, + "capture_method": "automatic", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 14100, + "customer_id": "StripeCustomer", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "payment_method": "gift_card", + "payment_method_type": "givex", + "payment_method_data": { + "gift_card": { + "givex": { + "number": "6364530000000000", + "cvc": "122222" + } + } + }, + "routing": { + "type": "single", + "data": "adyen" + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": ["{{baseUrl}}"], + "path": ["payments"] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..ff2099305d7a --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[GET]::/payments/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/payments/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[GET]::/payments/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "Failed" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id - Content check if value for 'status' matches 'failed'", + function () { + pm.expect(jsonData.status).to.eql("failed"); + }, + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/request.json new file mode 100644 index 000000000000..6cd4b7d96c52 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/request.json @@ -0,0 +1,28 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": ["{{baseUrl}}"], + "path": ["payments", ":id"], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] From b3c51e6eb55c58adc024ee32b59c3910b2b72131 Mon Sep 17 00:00:00 2001 From: akshay-97 Date: Tue, 28 Nov 2023 18:58:46 +0530 Subject: [PATCH 111/146] refactor: Added min idle and max lifetime for database config (#2900) Co-authored-by: akshay.s Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Gnanasundari24 <118818938+Gnanasundari24@users.noreply.github.com> Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- config/development.toml | 1 + crates/router/src/configs/defaults.rs | 2 ++ crates/router/src/configs/kms.rs | 2 ++ crates/router/src/configs/settings.rs | 4 ++++ crates/storage_impl/src/config.rs | 2 ++ crates/storage_impl/src/database/store.rs | 4 +++- 6 files changed, 14 insertions(+), 1 deletion(-) diff --git a/config/development.toml b/config/development.toml index 3d64a8791a1c..bcf561dd5857 100644 --- a/config/development.toml +++ b/config/development.toml @@ -20,6 +20,7 @@ port = 5432 dbname = "hyperswitch_db" pool_size = 5 connection_timeout = 10 +min_idle = 2 [replica_database] username = "db_user" diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index 2eddaf3084d7..a92e63d67639 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -30,6 +30,8 @@ impl Default for super::settings::Database { pool_size: 5, connection_timeout: 10, queue_strategy: Default::default(), + min_idle: None, + max_lifetime: None, } } } diff --git a/crates/router/src/configs/kms.rs b/crates/router/src/configs/kms.rs index 205169fa291b..c2f159d16cf1 100644 --- a/crates/router/src/configs/kms.rs +++ b/crates/router/src/configs/kms.rs @@ -64,6 +64,8 @@ impl KmsDecrypt for settings::Database { pool_size: self.pool_size, connection_timeout: self.connection_timeout, queue_strategy: self.queue_strategy.into(), + min_idle: self.min_idle, + max_lifetime: self.max_lifetime, }) } } diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index cc273f93ee9a..918ae6647eef 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -517,6 +517,8 @@ pub struct Database { pub pool_size: u32, pub connection_timeout: u64, pub queue_strategy: QueueStrategy, + pub min_idle: Option, + pub max_lifetime: Option, } #[derive(Debug, Deserialize, Clone, Default)] @@ -548,6 +550,8 @@ impl From for storage_impl::config::Database { pool_size: val.pool_size, connection_timeout: val.connection_timeout, queue_strategy: val.queue_strategy.into(), + min_idle: val.min_idle, + max_lifetime: val.max_lifetime, } } } diff --git a/crates/storage_impl/src/config.rs b/crates/storage_impl/src/config.rs index ceed3da81b39..f53507831b11 100644 --- a/crates/storage_impl/src/config.rs +++ b/crates/storage_impl/src/config.rs @@ -10,4 +10,6 @@ pub struct Database { pub pool_size: u32, pub connection_timeout: u64, pub queue_strategy: bb8::QueueStrategy, + pub min_idle: Option, + pub max_lifetime: Option, } diff --git a/crates/storage_impl/src/database/store.rs b/crates/storage_impl/src/database/store.rs index a09f1b752561..c36575e37c97 100644 --- a/crates/storage_impl/src/database/store.rs +++ b/crates/storage_impl/src/database/store.rs @@ -88,8 +88,10 @@ pub async fn diesel_make_pg_pool( let manager = async_bb8_diesel::ConnectionManager::::new(database_url); let mut pool = bb8::Pool::builder() .max_size(database.pool_size) + .min_idle(database.min_idle) .queue_strategy(database.queue_strategy) - .connection_timeout(std::time::Duration::from_secs(database.connection_timeout)); + .connection_timeout(std::time::Duration::from_secs(database.connection_timeout)) + .max_lifetime(database.max_lifetime.map(std::time::Duration::from_secs)); if test_transaction { pool = pool.connection_customizer(Box::new(TestTransaction)); From cdbb3853cd44443f8487abc16a9ba5d99f22e475 Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Tue, 28 Nov 2023 19:17:17 +0530 Subject: [PATCH 112/146] refactor(router): add openapi spec support for merchant_connector apis (#2997) --- crates/router/src/openapi.rs | 10 +- crates/router/src/routes/admin.rs | 2 +- openapi/openapi_spec.json | 253 ++++++++++++++++++++++++++++++ 3 files changed, 259 insertions(+), 6 deletions(-) diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index ec38389cdc42..cfb0268a9f80 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -73,11 +73,11 @@ Never share your secret api keys. Keep them guarded and secure. // crate::routes::admin::retrieve_merchant_account, // crate::routes::admin::update_merchant_account, // crate::routes::admin::delete_merchant_account, - // crate::routes::admin::payment_connector_create, - // crate::routes::admin::payment_connector_retrieve, - // crate::routes::admin::payment_connector_list, - // crate::routes::admin::payment_connector_update, - // crate::routes::admin::payment_connector_delete, + crate::routes::admin::payment_connector_create, + crate::routes::admin::payment_connector_retrieve, + crate::routes::admin::payment_connector_list, + crate::routes::admin::payment_connector_update, + crate::routes::admin::payment_connector_delete, crate::routes::mandates::get_mandate, crate::routes::mandates::revoke_mandate, crate::routes::payments::payments_create, diff --git a/crates/router/src/routes/admin.rs b/crates/router/src/routes/admin.rs index 0586faabbf76..ce6a2a97e28d 100644 --- a/crates/router/src/routes/admin.rs +++ b/crates/router/src/routes/admin.rs @@ -190,7 +190,7 @@ pub async fn delete_merchant_account( ) .await } -/// PaymentsConnectors - Create +/// Merchant Connector - Create /// /// Create a new Merchant Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialized services like Fraud / Accounting etc." #[utoipa::path( diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 08f415782963..86dc053d2d77 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -129,6 +129,259 @@ ] } }, + "/accounts/{account_id}/connectors": { + "get": { + "tags": [ + "Merchant Connector Account" + ], + "summary": "Merchant Connector - List", + "description": "Merchant Connector - List\n\nList Merchant Connector Details for the merchant", + "operationId": "List all Merchant Connectors", + "parameters": [ + { + "name": "account_id", + "in": "path", + "description": "The unique identifier for the merchant account", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Merchant Connector list retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MerchantConnectorResponse" + } + } + } + } + }, + "401": { + "description": "Unauthorized request" + }, + "404": { + "description": "Merchant Connector does not exist in records" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + }, + "post": { + "tags": [ + "Merchant Connector Account" + ], + "summary": "Merchant Connector - Create", + "description": "Merchant Connector - Create\n\nCreate a new Merchant Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialized services like Fraud / Accounting etc.\"", + "operationId": "Create a Merchant Connector", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MerchantConnectorCreate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Merchant Connector Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MerchantConnectorResponse" + } + } + } + }, + "400": { + "description": "Missing Mandatory fields" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + } + }, + "/accounts/{account_id}/connectors/{connector_id}": { + "get": { + "tags": [ + "Merchant Connector Account" + ], + "summary": "Merchant Connector - Retrieve", + "description": "Merchant Connector - Retrieve\n\nRetrieve Merchant Connector Details", + "operationId": "Retrieve a Merchant Connector", + "parameters": [ + { + "name": "account_id", + "in": "path", + "description": "The unique identifier for the merchant account", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "connector_id", + "in": "path", + "description": "The unique identifier for the Merchant Connector", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Merchant Connector retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MerchantConnectorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized request" + }, + "404": { + "description": "Merchant Connector does not exist in records" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + }, + "post": { + "tags": [ + "Merchant Connector Account" + ], + "summary": "Merchant Connector - Update", + "description": "Merchant Connector - Update\n\nTo update an existing Merchant Connector. Helpful in enabling / disabling different payment methods and other settings for the connector etc.", + "operationId": "Update a Merchant Connector", + "parameters": [ + { + "name": "account_id", + "in": "path", + "description": "The unique identifier for the merchant account", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "connector_id", + "in": "path", + "description": "The unique identifier for the Merchant Connector", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MerchantConnectorUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Merchant Connector Updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MerchantConnectorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized request" + }, + "404": { + "description": "Merchant Connector does not exist in records" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + }, + "delete": { + "tags": [ + "Merchant Connector Account" + ], + "summary": "Merchant Connector - Delete", + "description": "Merchant Connector - Delete\n\nDelete or Detach a Merchant Connector from Merchant Account", + "operationId": "Delete a Merchant Connector", + "parameters": [ + { + "name": "account_id", + "in": "path", + "description": "The unique identifier for the merchant account", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "connector_id", + "in": "path", + "description": "The unique identifier for the Merchant Connector", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Merchant Connector Deleted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MerchantConnectorDeleteResponse" + } + } + } + }, + "401": { + "description": "Unauthorized request" + }, + "404": { + "description": "Merchant Connector does not exist in records" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + } + }, "/customers": { "post": { "tags": [ From 1c5a9b5452afc33b18f45389bf3bdfd80820f476 Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Tue, 28 Nov 2023 20:09:50 +0530 Subject: [PATCH 113/146] fix: remove error propagation if card name not found in locker (#2998) --- crates/router/src/core/payments/helpers.rs | 23 ++++++++++------------ 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 68b8128d7909..266792f98758 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1459,26 +1459,23 @@ pub async fn retrieve_card_with_permanent_token( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("failed to fetch card information from the permanent locker")?; - let name = card - .name_on_card - .get_required_value("name_on_card") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("card holder name was not saved in permanent locker")?; - - let name_on_card = if name.clone().expose().is_empty() { + let name_on_card = if let Some(name_on_card) = card.name_on_card.clone() { + if card.name_on_card.unwrap_or_default().expose().is_empty() { + card_token_data + .and_then(|token_data| token_data.card_holder_name.clone()) + .filter(|name_on_card| !name_on_card.clone().expose().is_empty()) + } else { + Some(name_on_card) + } + } else { card_token_data .and_then(|token_data| token_data.card_holder_name.clone()) .filter(|name_on_card| !name_on_card.clone().expose().is_empty()) - .ok_or(errors::ApiErrorResponse::MissingRequiredField { - field_name: "card_holder_name", - })? - } else { - name }; let api_card = api::Card { card_number: card.card_number, - card_holder_name: name_on_card, + card_holder_name: name_on_card.unwrap_or(masking::Secret::from("".to_string())), card_exp_month: card.card_exp_month, card_exp_year: card.card_exp_year, card_cvc: card_cvc.unwrap_or_default(), From ff6a0dd0b515778b64a3e28ef905154eee85ec78 Mon Sep 17 00:00:00 2001 From: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Date: Tue, 28 Nov 2023 20:34:30 +0530 Subject: [PATCH 114/146] fix(core): Replace euclid enum with RoutableConnectors enum (#2994) Signed-off-by: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- Cargo.lock | 2 + connector-template/mod.rs | 3 +- crates/api_models/src/enums.rs | 98 ----------------- crates/api_models/src/routing.rs | 67 +----------- crates/common_enums/Cargo.toml | 4 + crates/common_enums/src/enums.rs | 99 +++++++++++++++++ crates/euclid/src/enums.rs | 101 +----------------- crates/euclid/src/frontend/ast.rs | 8 +- crates/euclid/src/frontend/dir.rs | 8 +- crates/euclid/src/frontend/dir/enums.rs | 4 +- crates/euclid_wasm/Cargo.toml | 1 + crates/euclid_wasm/src/lib.rs | 12 ++- crates/kgraph_utils/Cargo.toml | 1 + crates/kgraph_utils/src/mca.rs | 7 +- crates/router/src/core/payments.rs | 2 +- .../src/core/payments/routing/transformers.rs | 75 +------------ crates/router/src/types/transformers.rs | 88 ++------------- scripts/add_connector.sh | 10 +- 18 files changed, 147 insertions(+), 443 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e4a317d74f49..2ca33b6910a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2339,6 +2339,7 @@ name = "euclid_wasm" version = "0.1.0" dependencies = [ "api_models", + "common_enums", "currency_conversion", "euclid", "getrandom 0.2.10", @@ -3306,6 +3307,7 @@ name = "kgraph_utils" version = "0.1.0" dependencies = [ "api_models", + "common_enums", "criterion", "euclid", "masking", diff --git a/connector-template/mod.rs b/connector-template/mod.rs index e441b0e5879a..e9945a726a95 100644 --- a/connector-template/mod.rs +++ b/connector-template/mod.rs @@ -106,6 +106,7 @@ impl ConnectorCommon for {{project-name | downcase | pascal_case}} { message: response.message, reason: response.reason, attempt_status: None, + connector_transaction_id: None, }) } } @@ -485,7 +486,7 @@ impl api::IncomingWebhook for {{project-name | downcase | pascal_case}} { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index ffefaa2ad2c4..535be4dfb159 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -147,104 +147,6 @@ impl Connector { } } -#[derive( - Clone, - Copy, - Debug, - Eq, - Hash, - PartialEq, - serde::Serialize, - serde::Deserialize, - strum::Display, - strum::EnumString, - strum::EnumIter, - strum::EnumVariantNames, -)] -#[serde(rename_all = "snake_case")] -#[strum(serialize_all = "snake_case")] -pub enum RoutableConnectors { - #[cfg(feature = "dummy_connector")] - #[serde(rename = "phonypay")] - #[strum(serialize = "phonypay")] - DummyConnector1, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "fauxpay")] - #[strum(serialize = "fauxpay")] - DummyConnector2, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "pretendpay")] - #[strum(serialize = "pretendpay")] - DummyConnector3, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "stripe_test")] - #[strum(serialize = "stripe_test")] - DummyConnector4, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "adyen_test")] - #[strum(serialize = "adyen_test")] - DummyConnector5, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "checkout_test")] - #[strum(serialize = "checkout_test")] - DummyConnector6, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "paypal_test")] - #[strum(serialize = "paypal_test")] - DummyConnector7, - Aci, - Adyen, - Airwallex, - Authorizedotnet, - Bankofamerica, - Bitpay, - Bambora, - Bluesnap, - Boku, - Braintree, - Cashtocode, - Checkout, - Coinbase, - Cryptopay, - Cybersource, - Dlocal, - Fiserv, - Forte, - Globalpay, - Globepay, - Gocardless, - Helcim, - Iatapay, - Klarna, - Mollie, - Multisafepay, - Nexinets, - Nmi, - Noon, - Nuvei, - // Opayo, added as template code for future usage - Opennode, - // Payeezy, As psync and rsync are not supported by this connector, it is added as template code for future usage - Payme, - Paypal, - Payu, - Powertranz, - Prophetpay, - Rapyd, - Shift4, - Square, - Stax, - Stripe, - Trustpay, - // Tsys, - Tsys, - Volt, - Wise, - Worldline, - Worldpay, - Zen, -} - #[cfg(feature = "payouts")] #[derive( Clone, diff --git a/crates/api_models/src/routing.rs b/crates/api_models/src/routing.rs index 47a44ea7443e..2236714da1d1 100644 --- a/crates/api_models/src/routing.rs +++ b/crates/api_models/src/routing.rs @@ -4,7 +4,6 @@ use common_utils::errors::ParsingError; use error_stack::IntoReport; use euclid::{ dssa::types::EuclidAnalysable, - enums as euclid_enums, frontend::{ ast, dir::{DirKeyKind, EuclidDirFilter}, @@ -287,71 +286,7 @@ impl From for RoutableChoiceSerde { impl From for ast::ConnectorChoice { fn from(value: RoutableConnectorChoice) -> Self { Self { - connector: match value.connector { - #[cfg(feature = "dummy_connector")] - RoutableConnectors::DummyConnector1 => euclid_enums::Connector::DummyConnector1, - #[cfg(feature = "dummy_connector")] - RoutableConnectors::DummyConnector2 => euclid_enums::Connector::DummyConnector2, - #[cfg(feature = "dummy_connector")] - RoutableConnectors::DummyConnector3 => euclid_enums::Connector::DummyConnector3, - #[cfg(feature = "dummy_connector")] - RoutableConnectors::DummyConnector4 => euclid_enums::Connector::DummyConnector4, - #[cfg(feature = "dummy_connector")] - RoutableConnectors::DummyConnector5 => euclid_enums::Connector::DummyConnector5, - #[cfg(feature = "dummy_connector")] - RoutableConnectors::DummyConnector6 => euclid_enums::Connector::DummyConnector6, - #[cfg(feature = "dummy_connector")] - RoutableConnectors::DummyConnector7 => euclid_enums::Connector::DummyConnector7, - RoutableConnectors::Aci => euclid_enums::Connector::Aci, - RoutableConnectors::Adyen => euclid_enums::Connector::Adyen, - RoutableConnectors::Airwallex => euclid_enums::Connector::Airwallex, - RoutableConnectors::Authorizedotnet => euclid_enums::Connector::Authorizedotnet, - RoutableConnectors::Bambora => euclid_enums::Connector::Bambora, - RoutableConnectors::Bankofamerica => euclid_enums::Connector::Bankofamerica, - RoutableConnectors::Bitpay => euclid_enums::Connector::Bitpay, - RoutableConnectors::Bluesnap => euclid_enums::Connector::Bluesnap, - RoutableConnectors::Boku => euclid_enums::Connector::Boku, - RoutableConnectors::Braintree => euclid_enums::Connector::Braintree, - RoutableConnectors::Cashtocode => euclid_enums::Connector::Cashtocode, - RoutableConnectors::Checkout => euclid_enums::Connector::Checkout, - RoutableConnectors::Coinbase => euclid_enums::Connector::Coinbase, - RoutableConnectors::Cryptopay => euclid_enums::Connector::Cryptopay, - RoutableConnectors::Cybersource => euclid_enums::Connector::Cybersource, - RoutableConnectors::Dlocal => euclid_enums::Connector::Dlocal, - RoutableConnectors::Fiserv => euclid_enums::Connector::Fiserv, - RoutableConnectors::Forte => euclid_enums::Connector::Forte, - RoutableConnectors::Globalpay => euclid_enums::Connector::Globalpay, - RoutableConnectors::Globepay => euclid_enums::Connector::Globepay, - RoutableConnectors::Gocardless => euclid_enums::Connector::Gocardless, - RoutableConnectors::Helcim => euclid_enums::Connector::Helcim, - RoutableConnectors::Iatapay => euclid_enums::Connector::Iatapay, - RoutableConnectors::Klarna => euclid_enums::Connector::Klarna, - RoutableConnectors::Mollie => euclid_enums::Connector::Mollie, - RoutableConnectors::Multisafepay => euclid_enums::Connector::Multisafepay, - RoutableConnectors::Nexinets => euclid_enums::Connector::Nexinets, - RoutableConnectors::Nmi => euclid_enums::Connector::Nmi, - RoutableConnectors::Noon => euclid_enums::Connector::Noon, - RoutableConnectors::Nuvei => euclid_enums::Connector::Nuvei, - RoutableConnectors::Opennode => euclid_enums::Connector::Opennode, - RoutableConnectors::Payme => euclid_enums::Connector::Payme, - RoutableConnectors::Paypal => euclid_enums::Connector::Paypal, - RoutableConnectors::Payu => euclid_enums::Connector::Payu, - RoutableConnectors::Powertranz => euclid_enums::Connector::Powertranz, - RoutableConnectors::Prophetpay => euclid_enums::Connector::Prophetpay, - RoutableConnectors::Rapyd => euclid_enums::Connector::Rapyd, - RoutableConnectors::Shift4 => euclid_enums::Connector::Shift4, - RoutableConnectors::Square => euclid_enums::Connector::Square, - RoutableConnectors::Stax => euclid_enums::Connector::Stax, - RoutableConnectors::Stripe => euclid_enums::Connector::Stripe, - RoutableConnectors::Trustpay => euclid_enums::Connector::Trustpay, - RoutableConnectors::Tsys => euclid_enums::Connector::Tsys, - RoutableConnectors::Volt => euclid_enums::Connector::Volt, - RoutableConnectors::Wise => euclid_enums::Connector::Wise, - RoutableConnectors::Worldline => euclid_enums::Connector::Worldline, - RoutableConnectors::Worldpay => euclid_enums::Connector::Worldpay, - RoutableConnectors::Zen => euclid_enums::Connector::Zen, - }, - + connector: value.connector, #[cfg(not(feature = "connector_choice_mca_id"))] sub_label: value.sub_label, } diff --git a/crates/common_enums/Cargo.toml b/crates/common_enums/Cargo.toml index 88628825ca64..cd061970bff3 100644 --- a/crates/common_enums/Cargo.toml +++ b/crates/common_enums/Cargo.toml @@ -7,6 +7,10 @@ rust-version.workspace = true readme = "README.md" license.workspace = true +[features] +default = ["dummy_connector"] +dummy_connector = [] + [dependencies] diesel = { version = "2.1.0", features = ["postgres"] } serde = { version = "1.0.160", features = ["derive"] } diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 063e35933c43..3f343965130e 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -59,6 +59,105 @@ pub enum AttemptStatus { DeviceDataCollectionPending, } +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + PartialEq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumString, + strum::EnumIter, + strum::EnumVariantNames, +)] +#[router_derive::diesel_enum(storage_type = "db_enum")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum RoutableConnectors { + #[cfg(feature = "dummy_connector")] + #[serde(rename = "phonypay")] + #[strum(serialize = "phonypay")] + DummyConnector1, + #[cfg(feature = "dummy_connector")] + #[serde(rename = "fauxpay")] + #[strum(serialize = "fauxpay")] + DummyConnector2, + #[cfg(feature = "dummy_connector")] + #[serde(rename = "pretendpay")] + #[strum(serialize = "pretendpay")] + DummyConnector3, + #[cfg(feature = "dummy_connector")] + #[serde(rename = "stripe_test")] + #[strum(serialize = "stripe_test")] + DummyConnector4, + #[cfg(feature = "dummy_connector")] + #[serde(rename = "adyen_test")] + #[strum(serialize = "adyen_test")] + DummyConnector5, + #[cfg(feature = "dummy_connector")] + #[serde(rename = "checkout_test")] + #[strum(serialize = "checkout_test")] + DummyConnector6, + #[cfg(feature = "dummy_connector")] + #[serde(rename = "paypal_test")] + #[strum(serialize = "paypal_test")] + DummyConnector7, + Aci, + Adyen, + Airwallex, + Authorizedotnet, + Bankofamerica, + Bitpay, + Bambora, + Bluesnap, + Boku, + Braintree, + Cashtocode, + Checkout, + Coinbase, + Cryptopay, + Cybersource, + Dlocal, + Fiserv, + Forte, + Globalpay, + Globepay, + Gocardless, + Helcim, + Iatapay, + Klarna, + Mollie, + Multisafepay, + Nexinets, + Nmi, + Noon, + Nuvei, + // Opayo, added as template code for future usage + Opennode, + // Payeezy, As psync and rsync are not supported by this connector, it is added as template code for future usage + Payme, + Paypal, + Payu, + Powertranz, + Prophetpay, + Rapyd, + Shift4, + Square, + Stax, + Stripe, + Trustpay, + // Tsys, + Tsys, + Volt, + Wise, + Worldline, + Worldpay, + Zen, +} + impl AttemptStatus { pub fn is_terminal_status(self) -> bool { match self { diff --git a/crates/euclid/src/enums.rs b/crates/euclid/src/enums.rs index dc6d9f66a58f..68e081c7aa92 100644 --- a/crates/euclid/src/enums.rs +++ b/crates/euclid/src/enums.rs @@ -1,8 +1,7 @@ pub use common_enums::{ AuthenticationType, CaptureMethod, CardNetwork, Country, Currency, - FutureUsage as SetupFutureUsage, PaymentMethod, PaymentMethodType, + FutureUsage as SetupFutureUsage, PaymentMethod, PaymentMethodType, RoutableConnectors, }; -use serde::{Deserialize, Serialize}; use strum::VariantNames; pub trait CollectVariants { @@ -24,6 +23,7 @@ macro_rules! collect_variants { pub(crate) use collect_variants; collect_variants!(PaymentMethod); +collect_variants!(RoutableConnectors); collect_variants!(PaymentType); collect_variants!(MandateType); collect_variants!(MandateAcceptanceType); @@ -33,105 +33,8 @@ collect_variants!(AuthenticationType); collect_variants!(CaptureMethod); collect_variants!(Currency); collect_variants!(Country); -collect_variants!(Connector); collect_variants!(SetupFutureUsage); -#[derive( - Debug, - Copy, - Clone, - PartialEq, - Eq, - Hash, - Serialize, - Deserialize, - strum::Display, - strum::EnumVariantNames, - strum::EnumIter, - strum::EnumString, - frunk::LabelledGeneric, -)] -#[serde(rename_all = "snake_case")] -#[strum(serialize_all = "snake_case")] -pub enum Connector { - #[cfg(feature = "dummy_connector")] - #[serde(rename = "phonypay")] - #[strum(serialize = "phonypay")] - DummyConnector1, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "fauxpay")] - #[strum(serialize = "fauxpay")] - DummyConnector2, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "pretendpay")] - #[strum(serialize = "pretendpay")] - DummyConnector3, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "stripe_test")] - #[strum(serialize = "stripe_test")] - DummyConnector4, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "adyen_test")] - #[strum(serialize = "adyen_test")] - DummyConnector5, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "checkout_test")] - #[strum(serialize = "checkout_test")] - DummyConnector6, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "paypal_test")] - #[strum(serialize = "paypal_test")] - DummyConnector7, - Aci, - Adyen, - Airwallex, - Authorizedotnet, - Bambora, - Bankofamerica, - Bitpay, - Bluesnap, - Boku, - Braintree, - Cashtocode, - Checkout, - Coinbase, - Cryptopay, - Cybersource, - Dlocal, - Fiserv, - Forte, - Globalpay, - Globepay, - Gocardless, - Helcim, - Iatapay, - Klarna, - Mollie, - Multisafepay, - Nexinets, - Nmi, - Noon, - Nuvei, - Opennode, - Payme, - Paypal, - Payu, - Powertranz, - Prophetpay, - Rapyd, - Shift4, - Square, - Stax, - Stripe, - Trustpay, - Tsys, - Volt, - Wise, - Worldline, - Worldpay, - Zen, -} - #[derive( Clone, Debug, diff --git a/crates/euclid/src/frontend/ast.rs b/crates/euclid/src/frontend/ast.rs index 3adb06ab1873..0dad9b53c323 100644 --- a/crates/euclid/src/frontend/ast.rs +++ b/crates/euclid/src/frontend/ast.rs @@ -2,16 +2,14 @@ pub mod lowering; #[cfg(feature = "ast_parser")] pub mod parser; +use common_enums::RoutableConnectors; use serde::{Deserialize, Serialize}; -use crate::{ - enums::Connector, - types::{DataType, Metadata}, -}; +use crate::types::{DataType, Metadata}; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] pub struct ConnectorChoice { - pub connector: Connector, + pub connector: RoutableConnectors, #[cfg(not(feature = "connector_choice_mca_id"))] pub sub_label: Option, } diff --git a/crates/euclid/src/frontend/dir.rs b/crates/euclid/src/frontend/dir.rs index 7f2fc252d232..f8cef1f92955 100644 --- a/crates/euclid/src/frontend/dir.rs +++ b/crates/euclid/src/frontend/dir.rs @@ -13,7 +13,7 @@ macro_rules! dirval { (Connector = $name:ident) => { $crate::frontend::dir::DirValue::Connector(Box::new( $crate::frontend::ast::ConnectorChoice { - connector: $crate::frontend::dir::enums::Connector::$name, + connector: $crate::enums::RoutableConnectors::$name, }, )) }; @@ -51,7 +51,7 @@ macro_rules! dirval { (Connector = $name:ident) => { $crate::frontend::dir::DirValue::Connector(Box::new( $crate::frontend::ast::ConnectorChoice { - connector: $crate::frontend::dir::enums::Connector::$name, + connector: $crate::enums::RoutableConnectors::$name, sub_label: None, }, )) @@ -60,7 +60,7 @@ macro_rules! dirval { (Connector = ($name:ident, $sub_label:literal)) => { $crate::frontend::dir::DirValue::Connector(Box::new( $crate::frontend::ast::ConnectorChoice { - connector: $crate::frontend::dir::enums::Connector::$name, + connector: $crate::enums::RoutableConnectors::$name, sub_label: Some($sub_label.to_string()), }, )) @@ -464,7 +464,7 @@ impl DirKeyKind { .collect(), ), Self::Connector => Some( - enums::Connector::iter() + common_enums::RoutableConnectors::iter() .map(|connector| { DirValue::Connector(Box::new(ast::ConnectorChoice { connector, diff --git a/crates/euclid/src/frontend/dir/enums.rs b/crates/euclid/src/frontend/dir/enums.rs index f049ad35328e..0b71f916d033 100644 --- a/crates/euclid/src/frontend/dir/enums.rs +++ b/crates/euclid/src/frontend/dir/enums.rs @@ -2,9 +2,9 @@ use strum::VariantNames; use crate::enums::collect_variants; pub use crate::enums::{ - AuthenticationType, CaptureMethod, CardNetwork, Connector, Country, Country as BusinessCountry, + AuthenticationType, CaptureMethod, CardNetwork, Country, Country as BusinessCountry, Country as BillingCountry, Currency as PaymentCurrency, MandateAcceptanceType, MandateType, - PaymentMethod, PaymentType, SetupFutureUsage, + PaymentMethod, PaymentType, RoutableConnectors, SetupFutureUsage, }; #[derive( diff --git a/crates/euclid_wasm/Cargo.toml b/crates/euclid_wasm/Cargo.toml index 47e349847ef7..8c96a7f67da2 100644 --- a/crates/euclid_wasm/Cargo.toml +++ b/crates/euclid_wasm/Cargo.toml @@ -20,6 +20,7 @@ api_models = { version = "0.1.0", path = "../api_models", package = "api_models" currency_conversion = { version = "0.1.0", path = "../currency_conversion" } euclid = { path = "../euclid", features = [] } kgraph_utils = { version = "0.1.0", path = "../kgraph_utils" } +common_enums = { version = "0.1.0", path = "../common_enums" } # Third party crates getrandom = { version = "0.2.10", features = ["js"] } diff --git a/crates/euclid_wasm/src/lib.rs b/crates/euclid_wasm/src/lib.rs index 48d9ac0d82a8..cab82f8ce411 100644 --- a/crates/euclid_wasm/src/lib.rs +++ b/crates/euclid_wasm/src/lib.rs @@ -7,6 +7,7 @@ use std::{ }; use api_models::{admin as admin_api, routing::ConnectorSelection}; +use common_enums::RoutableConnectors; use currency_conversion::{ conversion::convert as convert_currency, types as currency_conversion_types, }; @@ -17,7 +18,6 @@ use euclid::{ graph::{self, Memoization}, state_machine, truth, }, - enums, frontend::{ ast, dir::{self, enums as dir_enums}, @@ -61,8 +61,8 @@ pub fn convert_forex_value(amount: i64, from_currency: JsValue, to_currency: JsV .get() .ok_or("Forex Data not seeded") .err_to_js()?; - let from_currency: enums::Currency = serde_wasm_bindgen::from_value(from_currency)?; - let to_currency: enums::Currency = serde_wasm_bindgen::from_value(to_currency)?; + let from_currency: common_enums::Currency = serde_wasm_bindgen::from_value(from_currency)?; + let to_currency: common_enums::Currency = serde_wasm_bindgen::from_value(to_currency)?; let converted_amount = convert_currency(forex_data, from_currency, to_currency, amount) .map_err(|_| "conversion not possible for provided values") .err_to_js()?; @@ -80,7 +80,7 @@ pub fn seed_knowledge_graph(mcas: JsValue) -> JsResult { .iter() .map(|mca| { Ok::<_, strum::ParseError>(ast::ConnectorChoice { - connector: dir_enums::Connector::from_str(&mca.connector_name)?, + connector: RoutableConnectors::from_str(&mca.connector_name)?, #[cfg(not(feature = "connector_choice_mca_id"))] sub_label: mca.business_sub_label.clone(), }) @@ -183,7 +183,9 @@ pub fn run_program(program: JsValue, input: JsValue) -> JsResult { #[wasm_bindgen(js_name = getAllConnectors)] pub fn get_all_connectors() -> JsResult { - Ok(serde_wasm_bindgen::to_value(enums::Connector::VARIANTS)?) + Ok(serde_wasm_bindgen::to_value( + common_enums::RoutableConnectors::VARIANTS, + )?) } #[wasm_bindgen(js_name = getAllKeys)] diff --git a/crates/kgraph_utils/Cargo.toml b/crates/kgraph_utils/Cargo.toml index cd0adf0bc8af..44a73dae4d77 100644 --- a/crates/kgraph_utils/Cargo.toml +++ b/crates/kgraph_utils/Cargo.toml @@ -11,6 +11,7 @@ connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connect [dependencies] api_models = { version = "0.1.0", path = "../api_models", package = "api_models" } +common_enums = { version = "0.1.0", path = "../common_enums" } euclid = { version = "0.1.0", path = "../euclid" } masking = { version = "0.1.0", path = "../masking/" } diff --git a/crates/kgraph_utils/src/mca.rs b/crates/kgraph_utils/src/mca.rs index deea51bd8808..0e224a8f3d9d 100644 --- a/crates/kgraph_utils/src/mca.rs +++ b/crates/kgraph_utils/src/mca.rs @@ -5,10 +5,7 @@ use api_models::{ }; use euclid::{ dssa::graph::{self, DomainIdentifier}, - frontend::{ - ast, - dir::{self, enums as dir_enums}, - }, + frontend::{ast, dir}, types::{NumValue, NumValueRefinement}, }; @@ -277,7 +274,7 @@ fn compile_merchant_connector_graph( builder: &mut graph::KnowledgeGraphBuilder<'_>, mca: admin_api::MerchantConnectorResponse, ) -> Result<(), KgraphError> { - let connector = dir_enums::Connector::from_str(&mca.connector_name) + let connector = common_enums::RoutableConnectors::from_str(&mca.connector_name) .map_err(|_| KgraphError::InvalidConnectorName(mca.connector_name.clone()))?; let mut agg_nodes: Vec<(graph::NodeId, graph::Relation)> = Vec::new(); diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 8cfded8463eb..db83dce487a6 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -81,7 +81,7 @@ pub async fn payments_operation_core( req: Req, call_connector_action: CallConnectorAction, auth_flow: services::AuthFlow, - eligible_connectors: Option>, + eligible_connectors: Option>, header_payload: HeaderPayload, ) -> RouterResult<( PaymentData, diff --git a/crates/router/src/core/payments/routing/transformers.rs b/crates/router/src/core/payments/routing/transformers.rs index 5704f82f4983..b273f18f3fd8 100644 --- a/crates/router/src/core/payments/routing/transformers.rs +++ b/crates/router/src/core/payments/routing/transformers.rs @@ -1,15 +1,15 @@ -use api_models::{self, enums as api_enums, routing as routing_types}; +use api_models::{self, routing as routing_types}; use diesel_models::enums as storage_enums; use euclid::{enums as dsl_enums, frontend::ast as dsl_ast}; -use crate::types::transformers::{ForeignFrom, ForeignInto}; +use crate::types::transformers::ForeignFrom; impl ForeignFrom for dsl_ast::ConnectorChoice { fn foreign_from(from: routing_types::RoutableConnectorChoice) -> Self { Self { // #[cfg(feature = "backwards_compatibility")] // choice_kind: from.choice_kind.foreign_into(), - connector: from.connector.foreign_into(), + connector: from.connector, #[cfg(not(feature = "connector_choice_mca_id"))] sub_label: from.sub_label, } @@ -52,72 +52,3 @@ impl ForeignFrom for dsl_enums::MandateType { } } } - -impl ForeignFrom for dsl_enums::Connector { - fn foreign_from(from: api_enums::RoutableConnectors) -> Self { - match from { - #[cfg(feature = "dummy_connector")] - api_enums::RoutableConnectors::DummyConnector1 => Self::DummyConnector1, - #[cfg(feature = "dummy_connector")] - api_enums::RoutableConnectors::DummyConnector2 => Self::DummyConnector2, - #[cfg(feature = "dummy_connector")] - api_enums::RoutableConnectors::DummyConnector3 => Self::DummyConnector3, - #[cfg(feature = "dummy_connector")] - api_enums::RoutableConnectors::DummyConnector4 => Self::DummyConnector4, - #[cfg(feature = "dummy_connector")] - api_enums::RoutableConnectors::DummyConnector5 => Self::DummyConnector5, - #[cfg(feature = "dummy_connector")] - api_enums::RoutableConnectors::DummyConnector6 => Self::DummyConnector6, - #[cfg(feature = "dummy_connector")] - api_enums::RoutableConnectors::DummyConnector7 => Self::DummyConnector7, - api_enums::RoutableConnectors::Aci => Self::Aci, - api_enums::RoutableConnectors::Adyen => Self::Adyen, - api_enums::RoutableConnectors::Airwallex => Self::Airwallex, - api_enums::RoutableConnectors::Authorizedotnet => Self::Authorizedotnet, - api_enums::RoutableConnectors::Bambora => Self::Bambora, - api_enums::RoutableConnectors::Bankofamerica => Self::Bankofamerica, - api_enums::RoutableConnectors::Bitpay => Self::Bitpay, - api_enums::RoutableConnectors::Bluesnap => Self::Bluesnap, - api_enums::RoutableConnectors::Boku => Self::Boku, - api_enums::RoutableConnectors::Braintree => Self::Braintree, - api_enums::RoutableConnectors::Cashtocode => Self::Cashtocode, - api_enums::RoutableConnectors::Checkout => Self::Checkout, - api_enums::RoutableConnectors::Coinbase => Self::Coinbase, - api_enums::RoutableConnectors::Cryptopay => Self::Cryptopay, - api_enums::RoutableConnectors::Cybersource => Self::Cybersource, - api_enums::RoutableConnectors::Dlocal => Self::Dlocal, - api_enums::RoutableConnectors::Fiserv => Self::Fiserv, - api_enums::RoutableConnectors::Forte => Self::Forte, - api_enums::RoutableConnectors::Globalpay => Self::Globalpay, - api_enums::RoutableConnectors::Globepay => Self::Globepay, - api_enums::RoutableConnectors::Gocardless => Self::Gocardless, - api_enums::RoutableConnectors::Helcim => Self::Helcim, - api_enums::RoutableConnectors::Iatapay => Self::Iatapay, - api_enums::RoutableConnectors::Klarna => Self::Klarna, - api_enums::RoutableConnectors::Mollie => Self::Mollie, - api_enums::RoutableConnectors::Multisafepay => Self::Multisafepay, - api_enums::RoutableConnectors::Nexinets => Self::Nexinets, - api_enums::RoutableConnectors::Nmi => Self::Nmi, - api_enums::RoutableConnectors::Noon => Self::Noon, - api_enums::RoutableConnectors::Nuvei => Self::Nuvei, - api_enums::RoutableConnectors::Opennode => Self::Opennode, - api_enums::RoutableConnectors::Payme => Self::Payme, - api_enums::RoutableConnectors::Paypal => Self::Paypal, - api_enums::RoutableConnectors::Payu => Self::Payu, - api_enums::RoutableConnectors::Powertranz => Self::Powertranz, - api_enums::RoutableConnectors::Prophetpay => Self::Prophetpay, - api_enums::RoutableConnectors::Rapyd => Self::Rapyd, - api_enums::RoutableConnectors::Shift4 => Self::Shift4, - api_enums::RoutableConnectors::Square => Self::Square, - api_enums::RoutableConnectors::Stax => Self::Stax, - api_enums::RoutableConnectors::Stripe => Self::Stripe, - api_enums::RoutableConnectors::Trustpay => Self::Trustpay, - api_enums::RoutableConnectors::Tsys => Self::Tsys, - api_enums::RoutableConnectors::Volt => Self::Volt, - api_enums::RoutableConnectors::Wise => Self::Wise, - api_enums::RoutableConnectors::Worldline => Self::Worldline, - api_enums::RoutableConnectors::Worldpay => Self::Worldpay, - api_enums::RoutableConnectors::Zen => Self::Zen, - } - } -} diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 5bd28db3c158..99096864a000 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -9,7 +9,6 @@ use common_utils::{ }; use diesel_models::enums as storage_enums; use error_stack::{IntoReport, ResultExt}; -use euclid::enums as dsl_enums; use masking::{ExposeInterface, PeekInterface}; use super::domain; @@ -174,25 +173,11 @@ impl ForeignFrom for api_models::payments::Manda } } -impl ForeignTryFrom for api_enums::RoutableConnectors { +impl ForeignTryFrom for common_enums::RoutableConnectors { type Error = error_stack::Report; fn foreign_try_from(from: api_enums::Connector) -> Result { Ok(match from { - #[cfg(feature = "dummy_connector")] - api_enums::Connector::DummyConnector1 => Self::DummyConnector1, - #[cfg(feature = "dummy_connector")] - api_enums::Connector::DummyConnector2 => Self::DummyConnector2, - #[cfg(feature = "dummy_connector")] - api_enums::Connector::DummyConnector3 => Self::DummyConnector3, - #[cfg(feature = "dummy_connector")] - api_enums::Connector::DummyConnector4 => Self::DummyConnector4, - #[cfg(feature = "dummy_connector")] - api_enums::Connector::DummyConnector5 => Self::DummyConnector5, - #[cfg(feature = "dummy_connector")] - api_enums::Connector::DummyConnector6 => Self::DummyConnector6, - #[cfg(feature = "dummy_connector")] - api_enums::Connector::DummyConnector7 => Self::DummyConnector7, api_enums::Connector::Aci => Self::Aci, api_enums::Connector::Adyen => Self::Adyen, api_enums::Connector::Airwallex => Self::Airwallex, @@ -253,76 +238,21 @@ impl ForeignTryFrom for api_enums::RoutableConnectors { api_enums::Connector::Worldline => Self::Worldline, api_enums::Connector::Worldpay => Self::Worldpay, api_enums::Connector::Zen => Self::Zen, - }) - } -} - -impl ForeignFrom for api_enums::RoutableConnectors { - fn foreign_from(from: dsl_enums::Connector) -> Self { - match from { #[cfg(feature = "dummy_connector")] - dsl_enums::Connector::DummyConnector1 => Self::DummyConnector1, + api_enums::Connector::DummyConnector1 => Self::DummyConnector1, #[cfg(feature = "dummy_connector")] - dsl_enums::Connector::DummyConnector2 => Self::DummyConnector2, + api_enums::Connector::DummyConnector2 => Self::DummyConnector2, #[cfg(feature = "dummy_connector")] - dsl_enums::Connector::DummyConnector3 => Self::DummyConnector3, + api_enums::Connector::DummyConnector3 => Self::DummyConnector3, #[cfg(feature = "dummy_connector")] - dsl_enums::Connector::DummyConnector4 => Self::DummyConnector4, + api_enums::Connector::DummyConnector4 => Self::DummyConnector4, #[cfg(feature = "dummy_connector")] - dsl_enums::Connector::DummyConnector5 => Self::DummyConnector5, + api_enums::Connector::DummyConnector5 => Self::DummyConnector5, #[cfg(feature = "dummy_connector")] - dsl_enums::Connector::DummyConnector6 => Self::DummyConnector6, + api_enums::Connector::DummyConnector6 => Self::DummyConnector6, #[cfg(feature = "dummy_connector")] - dsl_enums::Connector::DummyConnector7 => Self::DummyConnector7, - dsl_enums::Connector::Aci => Self::Aci, - dsl_enums::Connector::Adyen => Self::Adyen, - dsl_enums::Connector::Airwallex => Self::Airwallex, - dsl_enums::Connector::Authorizedotnet => Self::Authorizedotnet, - dsl_enums::Connector::Bambora => Self::Bambora, - dsl_enums::Connector::Bankofamerica => Self::Bankofamerica, - dsl_enums::Connector::Bitpay => Self::Bitpay, - dsl_enums::Connector::Bluesnap => Self::Bluesnap, - dsl_enums::Connector::Boku => Self::Boku, - dsl_enums::Connector::Braintree => Self::Braintree, - dsl_enums::Connector::Cashtocode => Self::Cashtocode, - dsl_enums::Connector::Checkout => Self::Checkout, - dsl_enums::Connector::Coinbase => Self::Coinbase, - dsl_enums::Connector::Cryptopay => Self::Cryptopay, - dsl_enums::Connector::Cybersource => Self::Cybersource, - dsl_enums::Connector::Dlocal => Self::Dlocal, - dsl_enums::Connector::Fiserv => Self::Fiserv, - dsl_enums::Connector::Forte => Self::Forte, - dsl_enums::Connector::Globalpay => Self::Globalpay, - dsl_enums::Connector::Globepay => Self::Globepay, - dsl_enums::Connector::Gocardless => Self::Gocardless, - dsl_enums::Connector::Helcim => Self::Helcim, - dsl_enums::Connector::Iatapay => Self::Iatapay, - dsl_enums::Connector::Klarna => Self::Klarna, - dsl_enums::Connector::Mollie => Self::Mollie, - dsl_enums::Connector::Multisafepay => Self::Multisafepay, - dsl_enums::Connector::Nexinets => Self::Nexinets, - dsl_enums::Connector::Nmi => Self::Nmi, - dsl_enums::Connector::Noon => Self::Noon, - dsl_enums::Connector::Nuvei => Self::Nuvei, - dsl_enums::Connector::Opennode => Self::Opennode, - dsl_enums::Connector::Payme => Self::Payme, - dsl_enums::Connector::Paypal => Self::Paypal, - dsl_enums::Connector::Payu => Self::Payu, - dsl_enums::Connector::Powertranz => Self::Powertranz, - dsl_enums::Connector::Prophetpay => Self::Prophetpay, - dsl_enums::Connector::Rapyd => Self::Rapyd, - dsl_enums::Connector::Shift4 => Self::Shift4, - dsl_enums::Connector::Square => Self::Square, - dsl_enums::Connector::Stax => Self::Stax, - dsl_enums::Connector::Stripe => Self::Stripe, - dsl_enums::Connector::Trustpay => Self::Trustpay, - dsl_enums::Connector::Tsys => Self::Tsys, - dsl_enums::Connector::Volt => Self::Volt, - dsl_enums::Connector::Wise => Self::Wise, - dsl_enums::Connector::Worldline => Self::Worldline, - dsl_enums::Connector::Worldpay => Self::Worldpay, - dsl_enums::Connector::Zen => Self::Zen, - } + api_enums::Connector::DummyConnector7 => Self::DummyConnector7, + }) } } diff --git a/scripts/add_connector.sh b/scripts/add_connector.sh index 9a30fe9d7573..7ed5e65151e1 100755 --- a/scripts/add_connector.sh +++ b/scripts/add_connector.sh @@ -45,7 +45,7 @@ cd $SCRIPT/.. # Remove template files if already created for this connector rm -rf $conn/$payment_gateway $conn/$payment_gateway.rs -git checkout $conn.rs $src/types/api.rs $src/configs/settings.rs config/development.toml config/docker_compose.toml config/config.example.toml loadtest/config/development.toml crates/api_models/src/enums.rs crates/euclid/src/enums.rs crates/api_models/src/routing.rs $src/core/payments/flows.rs $src/core/admin.rs $src/core/payments/routing/transformers.rs $src/types/transformers.rs +git checkout $conn.rs $src/types/api.rs $src/configs/settings.rs config/development.toml config/docker_compose.toml config/config.example.toml loadtest/config/development.toml crates/api_models/src/enums.rs crates/euclid/src/enums.rs crates/api_models/src/routing.rs $src/core/payments/flows.rs crates/common_enums/src/enums.rs $src/types/transformers.rs $src/core/admin.rs # Add enum for this connector in required places previous_connector='' @@ -61,14 +61,12 @@ sed -r -i'' -e "s/\"$previous_connector\",/\"$previous_connector\",\n \"${pa sed -i '' -e "s/\(pub enum Connector {\)/\1\n\t${payment_gateway_camelcase},/" crates/api_models/src/enums.rs sed -i '' -e "s/\(pub enum Connector {\)/\1\n\t${payment_gateway_camelcase},/" crates/euclid/src/enums.rs sed -i '' -e "s/\(match connector_name {\)/\1\n\t\tapi_enums::Connector::${payment_gateway_camelcase} => {${payment_gateway}::transformers::${payment_gateway_camelcase}AuthType::try_from(val)?;Ok(())}/" $src/core/admin.rs -sed -i'' -e "s|$previous_connector_camelcase \(.*\)|$previous_connector_camelcase \1\n\t\t\tapi_enums::RoutableConnectors::${payment_gateway_camelcase} => Self::${payment_gateway_camelcase},|" $src/core/payments/routing/transformers.rs -sed -i'' -e "s|dsl_enums::Connector::$previous_connector_camelcase \(.*\)|dsl_enums::Connector::$previous_connector_camelcase \1\n\t\t\tdsl_enums::Connector::${payment_gateway_camelcase} => Self::${payment_gateway_camelcase},|" $src/types/transformers.rs -sed -i'' -e "s|api_enums::Connector::$previous_connector_camelcase \(.*\)|api_enums::Connector::$previous_connector_camelcase \1\n\t\t\tapi_enums::Connector::${payment_gateway_camelcase} => Self::${payment_gateway_camelcase},|" $src/types/transformers.rs -sed -i'' -e "s/\(pub enum RoutableConnectors {\)/\1\n\t${payment_gateway_camelcase},/" crates/api_models/src/enums.rs +sed -i'' -e "s/\(pub enum RoutableConnectors {\)/\1\n\t${payment_gateway_camelcase},/" crates/common_enums/src/enums.rs +sed -i'' -e "s|$previous_connector_camelcase \(.*\)|$previous_connector_camelcase \1\n\t\t\tapi_enums::Connector::${payment_gateway_camelcase} => Self::${payment_gateway_camelcase},|" $src/types/transformers.rs sed -i'' -e "s/^default_imp_for_\(.*\)/default_imp_for_\1\n\tconnector::${payment_gateway_camelcase},/" $src/core/payments/flows.rs # Remove temporary files created in above step -rm $conn.rs-e $src/types/api.rs-e $src/configs/settings.rs-e config/development.toml-e config/docker_compose.toml-e config/config.example.toml-e loadtest/config/development.toml-e crates/api_models/src/enums.rs-e crates/euclid/src/enums.rs-e crates/api_models/src/routing.rs-e $src/core/payments/flows.rs-e $src/core/admin.rs-e $src/core/payments/routing/transformers.rs-e $src/types/transformers.rs-e +rm $conn.rs-e $src/types/api.rs-e $src/configs/settings.rs-e config/development.toml-e config/docker_compose.toml-e config/config.example.toml-e loadtest/config/development.toml-e crates/api_models/src/enums.rs-e crates/euclid/src/enums.rs-e crates/api_models/src/routing.rs-e $src/core/payments/flows.rs-e crates/common_enums/src/enums.rs-e $src/types/transformers.rs-e $src/core/admin.rs-e cd $conn/ # Generate template files for the connector From 837480d935cce8cc35f07c5ccb3560285909bc52 Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Tue, 28 Nov 2023 20:44:55 +0530 Subject: [PATCH 115/146] feat(core): enable payment refund when payment is partially captured (#2991) Co-authored-by: Gnanasundari24 <118818938+Gnanasundari24@users.noreply.github.com> --- .../src/payments/payment_attempt.rs | 7 ++ .../src/query/payment_attempt.rs | 36 ++++++++++ crates/router/src/core/refunds.rs | 14 ++-- .../src/mock_db/payment_attempt.rs | 20 ++++++ .../src/payments/payment_attempt.rs | 72 +++++++++++++++++++ 5 files changed, 145 insertions(+), 4 deletions(-) diff --git a/crates/data_models/src/payments/payment_attempt.rs b/crates/data_models/src/payments/payment_attempt.rs index a937c785902f..44aa48b142ad 100644 --- a/crates/data_models/src/payments/payment_attempt.rs +++ b/crates/data_models/src/payments/payment_attempt.rs @@ -36,6 +36,13 @@ pub trait PaymentAttemptInterface { storage_scheme: storage_enums::MerchantStorageScheme, ) -> error_stack::Result; + async fn find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + &self, + payment_id: &str, + merchant_id: &str, + storage_scheme: storage_enums::MerchantStorageScheme, + ) -> error_stack::Result; + async fn find_payment_attempt_by_merchant_id_connector_txn_id( &self, merchant_id: &str, diff --git a/crates/diesel_models/src/query/payment_attempt.rs b/crates/diesel_models/src/query/payment_attempt.rs index 4737233e3048..9e9195f5e0bb 100644 --- a/crates/diesel_models/src/query/payment_attempt.rs +++ b/crates/diesel_models/src/query/payment_attempt.rs @@ -120,6 +120,42 @@ impl PaymentAttempt { ) } + pub async fn find_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + conn: &PgPooledConn, + payment_id: &str, + merchant_id: &str, + ) -> StorageResult { + // perform ordering on the application level instead of database level + generics::generic_filter::< + ::Table, + _, + <::Table as Table>::PrimaryKey, + Self, + >( + conn, + dsl::payment_id + .eq(payment_id.to_owned()) + .and(dsl::merchant_id.eq(merchant_id.to_owned())) + .and( + dsl::status + .eq(enums::AttemptStatus::Charged) + .or(dsl::status.eq(enums::AttemptStatus::PartialCharged)), + ), + None, + None, + None, + ) + .await? + .into_iter() + .fold( + Err(DatabaseError::NotFound).into_report(), + |acc, cur| match acc { + Ok(value) if value.modified_at > cur.modified_at => Ok(value), + _ => Ok(cur), + }, + ) + } + #[instrument(skip(conn))] pub async fn find_by_merchant_id_connector_txn_id( conn: &PgPooledConn, diff --git a/crates/router/src/core/refunds.rs b/crates/router/src/core/refunds.rs index aba6e9794e04..2d572cee9513 100644 --- a/crates/router/src/core/refunds.rs +++ b/crates/router/src/core/refunds.rs @@ -50,10 +50,16 @@ pub async fn refund_create_core( .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; utils::when( - payment_intent.status != enums::IntentStatus::Succeeded, + !(payment_intent.status == enums::IntentStatus::Succeeded + || payment_intent.status == enums::IntentStatus::PartiallyCaptured), || { - Err(report!(errors::ApiErrorResponse::PaymentNotSucceeded) - .attach_printable("unable to refund for a unsuccessful payment intent")) + Err(report!(errors::ApiErrorResponse::PaymentUnexpectedState { + current_flow: "refund".into(), + field_name: "status".into(), + current_value: payment_intent.status.to_string(), + states: "succeeded, partially_captured".to_string() + }) + .attach_printable("unable to refund for a unsuccessful payment intent")) }, )?; @@ -75,7 +81,7 @@ pub async fn refund_create_core( })?; payment_attempt = db - .find_payment_attempt_last_successful_attempt_by_payment_id_merchant_id( + .find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( &req.payment_id, merchant_id, merchant_account.storage_scheme, diff --git a/crates/storage_impl/src/mock_db/payment_attempt.rs b/crates/storage_impl/src/mock_db/payment_attempt.rs index fe244b10325f..6137b444f963 100644 --- a/crates/storage_impl/src/mock_db/payment_attempt.rs +++ b/crates/storage_impl/src/mock_db/payment_attempt.rs @@ -205,4 +205,24 @@ impl PaymentAttemptInterface for MockDb { .cloned() .unwrap()) } + #[allow(clippy::unwrap_used)] + async fn find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + &self, + payment_id: &str, + merchant_id: &str, + _storage_scheme: storage_enums::MerchantStorageScheme, + ) -> CustomResult { + let payment_attempts = self.payment_attempts.lock().await; + + Ok(payment_attempts + .iter() + .find(|payment_attempt| { + payment_attempt.payment_id == payment_id + && payment_attempt.merchant_id == merchant_id + && (payment_attempt.status == storage_enums::AttemptStatus::PartialCharged + || payment_attempt.status == storage_enums::AttemptStatus::Charged) + }) + .cloned() + .unwrap()) + } } diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index 06aacccc769d..e86119e41af6 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -115,6 +115,27 @@ impl PaymentAttemptInterface for RouterStore { .map(PaymentAttempt::from_storage_model) } + #[instrument(skip_all)] + async fn find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + &self, + payment_id: &str, + merchant_id: &str, + _storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + let conn = pg_connection_read(self).await?; + DieselPaymentAttempt::find_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + &conn, + payment_id, + merchant_id, + ) + .await + .map_err(|er| { + let new_err = diesel_error_to_data_error(er.current_context()); + er.change_context(new_err) + }) + .map(PaymentAttempt::from_storage_model) + } + async fn find_payment_attempt_by_merchant_id_connector_txn_id( &self, merchant_id: &str, @@ -618,6 +639,57 @@ impl PaymentAttemptInterface for KVRouterStore { } } + async fn find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + &self, + payment_id: &str, + merchant_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result { + let database_call = || { + self.router_store + .find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + payment_id, + merchant_id, + storage_scheme, + ) + }; + match storage_scheme { + MerchantStorageScheme::PostgresOnly => database_call().await, + MerchantStorageScheme::RedisKv => { + let key = format!("mid_{merchant_id}_pid_{payment_id}"); + let pattern = "pa_*"; + + let redis_fut = async { + let kv_result = kv_wrapper::( + self, + KvOperation::::Scan(pattern), + key, + ) + .await? + .try_into_scan(); + kv_result.and_then(|mut payment_attempts| { + payment_attempts.sort_by(|a, b| b.modified_at.cmp(&a.modified_at)); + payment_attempts + .iter() + .find(|&pa| { + pa.status == api_models::enums::AttemptStatus::Charged + || pa.status == api_models::enums::AttemptStatus::PartialCharged + }) + .cloned() + .ok_or(error_stack::report!( + redis_interface::errors::RedisError::NotFound + )) + }) + }; + Box::pin(try_redis_get_else_try_database_get( + redis_fut, + database_call, + )) + .await + } + } + } + async fn find_payment_attempt_by_merchant_id_connector_txn_id( &self, merchant_id: &str, From d63f6f7224f35018e7c707353508bbacc2baed5c Mon Sep 17 00:00:00 2001 From: ShivanshMathurJuspay <104988143+ShivanshMathurJuspay@users.noreply.github.com> Date: Tue, 28 Nov 2023 21:35:42 +0530 Subject: [PATCH 116/146] refactor(events): Adding changes to type of API events to Kafka (#2992) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/router/src/events/api_logs.rs | 10 ++++----- crates/router/src/services/authentication.rs | 22 ++++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/crates/router/src/events/api_logs.rs b/crates/router/src/events/api_logs.rs index 3f0bf651c464..3f598e88394b 100644 --- a/crates/router/src/events/api_logs.rs +++ b/crates/router/src/events/api_logs.rs @@ -31,11 +31,11 @@ pub struct ApiEvent { status_code: i64, #[serde(flatten)] auth_type: AuthenticationType, - request: serde_json::Value, + request: String, user_agent: Option, ip_addr: Option, url_path: String, - response: Option, + response: Option, error: Option, #[serde(flatten)] event_type: ApiEventsType, @@ -59,12 +59,12 @@ impl ApiEvent { ) -> Self { Self { api_flow: api_flow.to_string(), - created_at_timestamp: OffsetDateTime::now_utc().unix_timestamp_nanos(), + created_at_timestamp: OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000_000, request_id: request_id.as_hyphenated().to_string(), latency, status_code, - request, - response, + request: request.to_string(), + response: response.map(|resp| resp.to_string()), auth_type, error, ip_addr: http_req diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index e24c7cebcb2a..b01e3762bfab 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -47,11 +47,11 @@ pub enum AuthenticationType { key_id: String, }, AdminApiKey, - MerchantJWT { + MerchantJwt { merchant_id: String, user_id: Option, }, - MerchantID { + MerchantId { merchant_id: String, }, PublishableKey { @@ -70,9 +70,9 @@ impl AuthenticationType { merchant_id, key_id: _, } - | Self::MerchantID { merchant_id } + | Self::MerchantId { merchant_id } | Self::PublishableKey { merchant_id } - | Self::MerchantJWT { + | Self::MerchantJwt { merchant_id, user_id: _, } @@ -352,7 +352,7 @@ where }; Ok(( auth.clone(), - AuthenticationType::MerchantID { + AuthenticationType::MerchantId { merchant_id: auth.merchant_account.merchant_id.clone(), }, )) @@ -423,7 +423,7 @@ where Ok(( (), - AuthenticationType::MerchantJWT { + AuthenticationType::MerchantJwt { merchant_id: payload.merchant_id, user_id: Some(payload.user_id), }, @@ -451,7 +451,7 @@ where org_id: payload.org_id, role_id: payload.role_id, }, - AuthenticationType::MerchantJWT { + AuthenticationType::MerchantJwt { merchant_id: payload.merchant_id, user_id: Some(payload.user_id), }, @@ -479,13 +479,13 @@ where let permissions = authorization::get_permissions(&payload.role_id)?; authorization::check_authorization(&self.required_permission, permissions)?; - // Check if token has access to merchantID that has been requested through query param + // Check if token has access to MerchantId that has been requested through query param if payload.merchant_id != self.merchant_id { return Err(report!(errors::ApiErrorResponse::InvalidJwtToken)); } Ok(( (), - AuthenticationType::MerchantJWT { + AuthenticationType::MerchantJwt { merchant_id: payload.merchant_id, user_id: Some(payload.user_id), }, @@ -549,7 +549,7 @@ where }; Ok(( auth.clone(), - AuthenticationType::MerchantJWT { + AuthenticationType::MerchantJwt { merchant_id: auth.merchant_account.merchant_id.clone(), user_id: None, }, @@ -579,7 +579,7 @@ where org_id: payload.org_id, role_id: payload.role_id, }, - AuthenticationType::MerchantJWT { + AuthenticationType::MerchantJwt { merchant_id: payload.merchant_id, user_id: Some(payload.user_id), }, From af6b05c504b6fdbec7db77fa7f71535d7fea3e7a Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 16:25:56 +0000 Subject: [PATCH 117/146] test(postman): update postman collection files --- .../adyen_uk.postman_collection.json | 498 +++++++++++++++++- 1 file changed, 496 insertions(+), 2 deletions(-) diff --git a/postman/collection-json/adyen_uk.postman_collection.json b/postman/collection-json/adyen_uk.postman_collection.json index 33aadeb6f970..400f04241c27 100644 --- a/postman/collection-json/adyen_uk.postman_collection.json +++ b/postman/collection-json/adyen_uk.postman_collection.json @@ -472,7 +472,7 @@ "language": "json" } }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"adyen\",\"connector_account_details\":{\"auth_type\":\"BodyKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\",\"api_secret\":\"{{connector_api_secret}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"pay_bright\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"walley\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mobile_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"ali_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mb_way\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"giropay\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"blik\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"trustly\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_czech_republic\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_finland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_poland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_slovakia\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bancontact_card\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\"}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUdKakNDQlE2Z0F3SUJBZ0lRRENzRmFrVkNLU01uc2JacTc1YTI0ekFOQmdrcWhraUc5dzBCQVFzRkFEQjEKTVVRd1FnWURWUVFERER0QmNIQnNaU0JYYjNKc1pIZHBaR1VnUkdWMlpXeHZjR1Z5SUZKbGJHRjBhVzl1Y3lCRApaWEowYVdacFkyRjBhVzl1SUVGMWRHaHZjbWwwZVRFTE1Ba0dBMVVFQ3d3Q1J6TXhFekFSQmdOVkJBb01Da0Z3CmNHeGxJRWx1WXk0eEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJeU1USXdPREE1TVRJeE1Wb1hEVEkxTURFd05qQTUKTVRJeE1Gb3dnYWd4SmpBa0Jnb0praWFKay9Jc1pBRUJEQlp0WlhKamFHRnVkQzVqYjIwdVlXUjVaVzR1YzJGdQpNVHN3T1FZRFZRUUREREpCY0hCc1pTQlFZWGtnVFdWeVkyaGhiblFnU1dSbGJuUnBkSGs2YldWeVkyaGhiblF1ClkyOXRMbUZrZVdWdUxuTmhiakVUTUJFR0ExVUVDd3dLV1UwNVZUY3pXakpLVFRFc01Db0dBMVVFQ2d3alNsVlQKVUVGWklGUkZRMGhPVDB4UFIwbEZVeUJRVWtsV1FWUkZJRXhKVFVsVVJVUXdnZ0VpTUEwR0NTcUdTSWIzRFFFQgpBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRDhIUy81ZmJZNVJLaElYU3pySEpoeTVrNmY0YUdMaEltYklLaXFYRUlUCnVSQ2RHcGcyMExZM1VhTlBlYXZXTVRIUTBpK3d1RzlZWFVhYzV5eGE0cHg5eHlmQlVIejhzeU9pMjdYNVZaVG8KTlFhd2F6dGM5aGpZc1B2K0s2UW9oaWRTQWZ3cDhMdThkQ0lVZlhQWHBjdjFqVVRyRCtlc1RJTFZUb1FUTmhDcwplQlJtUS9nK05WdTB5c3BqeUYxU2l6VG9BK1BML3NrMlJEYWNaWC9vWTB1R040VWd4c0JYWHdZM0dKbTFSQ3B1CjM0Y2d0UC9kaHNBM1Ixb1VOb0gyQkZBSm9xK3pyUnl3U1RCSEhNMGpEQ2lncVU1RktwL1pBbHdzYmg1WVZOU00KWksrQ0pTK1BPTzlVNGVkeHJmTGlBVkhnQTgzRG43Z2U4K29nV1Y0Z0hUNmhBZ01CQUFHamdnSjhNSUlDZURBTQpCZ05WSFJNQkFmOEVBakFBTUI4R0ExVWRJd1FZTUJhQUZBbit3QldRK2E5a0NwSVN1U1lvWXd5WDdLZXlNSEFHCkNDc0dBUVVGQndFQkJHUXdZakF0QmdnckJnRUZCUWN3QW9ZaGFIUjBjRG92TDJObGNuUnpMbUZ3Y0d4bExtTnYKYlM5M2QyUnlaek11WkdWeU1ERUdDQ3NHQVFVRkJ6QUJoaVZvZEhSd09pOHZiMk56Y0M1aGNIQnNaUzVqYjIwdgpiMk56Y0RBekxYZDNaSEpuTXpBNU1JSUJMUVlEVlIwZ0JJSUJKRENDQVNBd2dnRWNCZ2txaGtpRzkyTmtCUUV3CmdnRU5NSUhSQmdnckJnRUZCUWNDQWpDQnhBeUJ3VkpsYkdsaGJtTmxJRzl1SUhSb2FYTWdRMlZ5ZEdsbWFXTmgKZEdVZ1lua2dZVzU1SUhCaGNuUjVJRzkwYUdWeUlIUm9ZVzRnUVhCd2JHVWdhWE1nY0hKdmFHbGlhWFJsWkM0ZwpVbVZtWlhJZ2RHOGdkR2hsSUdGd2NHeHBZMkZpYkdVZ2MzUmhibVJoY21RZ2RHVnliWE1nWVc1a0lHTnZibVJwCmRHbHZibk1nYjJZZ2RYTmxMQ0JqWlhKMGFXWnBZMkYwWlNCd2IyeHBZM2tnWVc1a0lHTmxjblJwWm1sallYUnAKYjI0Z2NISmhZM1JwWTJVZ2MzUmhkR1Z0Wlc1MGN5NHdOd1lJS3dZQkJRVUhBZ0VXSzJoMGRIQnpPaTh2ZDNkMwpMbUZ3Y0d4bExtTnZiUzlqWlhKMGFXWnBZMkYwWldGMWRHaHZjbWwwZVM4d0V3WURWUjBsQkF3d0NnWUlLd1lCCkJRVUhBd0l3SFFZRFZSME9CQllFRk5RSysxcUNHbDRTQ1p6SzFSUmpnb05nM0hmdk1BNEdBMVVkRHdFQi93UUUKQXdJSGdEQlBCZ2txaGtpRzkyTmtCaUFFUWd4QVFVUkNRemxDTmtGRE5USkVRems0TnpCRk5qYzJNVFpFUkRJdwpPVUkwTWtReE1UVXlSVVpFTURVeFFVRXhRekV6T0ROR00wUkROa1V5TkVNelFqRkVSVEFQQmdrcWhraUc5Mk5rCkJpNEVBZ1VBTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFBSFR6NTU2RUs5VVp6R0RVd2cvcmFibmYrUXFSYkgKcllVS0ZNcWQwUDhFTHZGMmYrTzN0ZXlDWHNBckF4TmVMY2hRSGVTNUFJOHd2azdMQ0xLUmJCdWJQQy9NVmtBKwpCZ3h5STg2ejJOVUNDWml4QVM1d2JFQWJYOStVMFp2RHp5Y01BbUNrdVVHZjNwWXR5TDNDaEplSGRvOEwwdmdvCnJQWElUSzc4ZjQzenNzYjBTNE5xbTE0eS9LNCs1ZkUzcFUxdEJqME5tUmdKUVJLRnB6MENpV2RPd1BRTk5BYUMKYUNNU2NiYXdwUTBjWEhaZDJWVjNtem4xdUlITEVtaU5GTWVxeEprbjZhUXFFQnNndDUzaUFxcmZMNjEzWStScAppd0tENXVmeU0wYzBweTYyZmkvWEwwS2c4ajEwWU1VdWJpd2dHajAzZThQWTB6bWUvcGZCZ3p6VQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==\",\"display_name\":\"applepay\",\"certificate_keys\":\"LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRRDhIUy81ZmJZNVJLaEkKWFN6ckhKaHk1azZmNGFHTGhJbWJJS2lxWEVJVHVSQ2RHcGcyMExZM1VhTlBlYXZXTVRIUTBpK3d1RzlZWFVhYwo1eXhhNHB4OXh5ZkJVSHo4c3lPaTI3WDVWWlRvTlFhd2F6dGM5aGpZc1B2K0s2UW9oaWRTQWZ3cDhMdThkQ0lVCmZYUFhwY3YxalVUckQrZXNUSUxWVG9RVE5oQ3NlQlJtUS9nK05WdTB5c3BqeUYxU2l6VG9BK1BML3NrMlJEYWMKWlgvb1kwdUdONFVneHNCWFh3WTNHSm0xUkNwdTM0Y2d0UC9kaHNBM1Ixb1VOb0gyQkZBSm9xK3pyUnl3U1RCSApITTBqRENpZ3FVNUZLcC9aQWx3c2JoNVlWTlNNWksrQ0pTK1BPTzlVNGVkeHJmTGlBVkhnQTgzRG43Z2U4K29nCldWNGdIVDZoQWdNQkFBRUNnZ0VBZFNaRzVhTFJxdmpKVFo3bVFYWHZMT3p4dWY5SlpxQTJwUHZmQkJLTXJjZC8KL2RDZXpGSGRhZ1VvWXNUQjRXekluaVVjL2Z3bDJTUzJyREFMZjB2dnRjNTJIYkQ5OHhwMnc3VmVjTGFnMCtuWAo2dUJaSEZCS3FWNU1LZ1l6YUpVMTdqaDM2VEV3dTFnbmdlZnRqVlpBV1NERTFvbDBlSzZ3Mk5kOExjVWdxRkxUCjVHYUlBV01nd0NKL3pzQmwydUV1Y0Q4S21WL1Z2MkVCQVJLWGZtci92UU1NelZrNkhhalprVGZqbWY2cWFVQVMKQWlFblROcHBic2ZrdTk2VGdIa2owWm10VWc0SFkzSU9qWFlpaGJsSjJzQ1JjS3p6cXkxa3B3WlpHcHo1NXEzbgphSXEwenJ3RjlpTUZubEhCa04yK3FjSnhzcDNTalhRdFRLTTY4WHRrVlFLQmdRRC8wemtCVlExR2Q1U0Mzb2czCnM3QWRCZ243dnVMdUZHZFFZY3c0OUppRGw1a1BZRXlKdGQvTVpNOEpFdk1nbVVTeUZmczNZcGtmQ2VGbUp0S3QKMnNNNEdCRWxqTVBQNjI3Q0QrV3c4L3JpWmlOZEg3OUhPRjRneTRGbjBycDNqanlLSWF1OHJISDQwRUUvSkVyOQpxWFQ1SGdWMmJQOGhMcW5sSjFmSDhpY2Zkd0tCZ1FEOFNWQ3ZDV2txQkh2SzE5ZDVTNlArdm5hcXhnTWo0U0srCnJ6L1I1c3pTaW5lS045VEhyeVkxYUZJbVFJZjJYOUdaQXBocUhrckxtQ3BIcURHOWQ3WDVQdUxxQzhmc09kVTYKRzhWaFRXeXdaSVNjdGRSYkk5S2xKUFk2V2ZDQjk0ODNVaDJGbW1xV2JuNWcwZUJxZWZzamVVdEtHekNRaGJDYworR1dBREVRSXB3S0JnUURmaWYvN3pBZm5sVUh1QU9saVV0OEczV29IMGtxVTRydE1IOGpGMCtVWXgzVDFYSjVFCmp1blp2aFN5eHg0dlUvNFU1dVEzQnk3cFVrYmtiZlFWK2x3dlBjaHQyVXlZK0E0MkFKSWlSMjdvT1h1Wk9jNTQKT3liMDNSNWNUR1NuWjJBN0N5VDNubStRak5rV2hXNEpyUE1MWTFJK293dGtRVlF2YW10bnlZNnFEUUtCZ0ZYWgpLT0IzSmxjSzhZa0R5Nm5WeUhkZUhvbGNHaU55YjkxTlN6MUUrWHZIYklnWEdZdmRtUFhoaXRyRGFNQzR1RjBGCjJoRjZQMTlxWnpDOUZqZnY3WGRrSTlrYXF5eENQY0dwUTVBcHhZdDhtUGV1bEJWemFqR1NFMHVsNFVhSWxDNXgKL2VQQnVQVjVvZjJXVFhST0Q5eHhZT0pWd0QvZGprekw1ZFlkMW1UUEFvR0JBTWVwY3diVEphZ3BoZk5zOHY0WAprclNoWXplbVpxY2EwQzRFc2QwNGYwTUxHSlVNS3Zpck4zN0g1OUFjT2IvNWtZcTU5WFRwRmJPWjdmYlpHdDZnCkxnM2hWSHRacElOVGJ5Ni9GOTBUZ09Za3RxUnhNVmc3UFBxbjFqdEFiVU15eVpVZFdHcFNNMmI0bXQ5dGhlUDEKblhMR09NWUtnS2JYbjZXWWN5K2U5eW9ICi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0KCg==\",\"initiative_context\":\"hyperswitch-sdk-test.netlify.app\",\"merchant_identifier\":\"merchant.com.adyen.san\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"adyen\",\"connector_account_details\":{\"auth_type\":\"BodyKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\",\"api_secret\":\"{{connector_api_secret}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"pay_bright\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"walley\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mobile_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"ali_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mb_way\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"gift_card\",\"payment_method_types\":[{\"payment_method_type\":\"givex\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"giropay\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"blik\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"trustly\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_czech_republic\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_finland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_poland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_slovakia\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bancontact_card\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\"}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUdKakNDQlE2Z0F3SUJBZ0lRRENzRmFrVkNLU01uc2JacTc1YTI0ekFOQmdrcWhraUc5dzBCQVFzRkFEQjEKTVVRd1FnWURWUVFERER0QmNIQnNaU0JYYjNKc1pIZHBaR1VnUkdWMlpXeHZjR1Z5SUZKbGJHRjBhVzl1Y3lCRApaWEowYVdacFkyRjBhVzl1SUVGMWRHaHZjbWwwZVRFTE1Ba0dBMVVFQ3d3Q1J6TXhFekFSQmdOVkJBb01Da0Z3CmNHeGxJRWx1WXk0eEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJeU1USXdPREE1TVRJeE1Wb1hEVEkxTURFd05qQTUKTVRJeE1Gb3dnYWd4SmpBa0Jnb0praWFKay9Jc1pBRUJEQlp0WlhKamFHRnVkQzVqYjIwdVlXUjVaVzR1YzJGdQpNVHN3T1FZRFZRUUREREpCY0hCc1pTQlFZWGtnVFdWeVkyaGhiblFnU1dSbGJuUnBkSGs2YldWeVkyaGhiblF1ClkyOXRMbUZrZVdWdUxuTmhiakVUTUJFR0ExVUVDd3dLV1UwNVZUY3pXakpLVFRFc01Db0dBMVVFQ2d3alNsVlQKVUVGWklGUkZRMGhPVDB4UFIwbEZVeUJRVWtsV1FWUkZJRXhKVFVsVVJVUXdnZ0VpTUEwR0NTcUdTSWIzRFFFQgpBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRDhIUy81ZmJZNVJLaElYU3pySEpoeTVrNmY0YUdMaEltYklLaXFYRUlUCnVSQ2RHcGcyMExZM1VhTlBlYXZXTVRIUTBpK3d1RzlZWFVhYzV5eGE0cHg5eHlmQlVIejhzeU9pMjdYNVZaVG8KTlFhd2F6dGM5aGpZc1B2K0s2UW9oaWRTQWZ3cDhMdThkQ0lVZlhQWHBjdjFqVVRyRCtlc1RJTFZUb1FUTmhDcwplQlJtUS9nK05WdTB5c3BqeUYxU2l6VG9BK1BML3NrMlJEYWNaWC9vWTB1R040VWd4c0JYWHdZM0dKbTFSQ3B1CjM0Y2d0UC9kaHNBM1Ixb1VOb0gyQkZBSm9xK3pyUnl3U1RCSEhNMGpEQ2lncVU1RktwL1pBbHdzYmg1WVZOU00KWksrQ0pTK1BPTzlVNGVkeHJmTGlBVkhnQTgzRG43Z2U4K29nV1Y0Z0hUNmhBZ01CQUFHamdnSjhNSUlDZURBTQpCZ05WSFJNQkFmOEVBakFBTUI4R0ExVWRJd1FZTUJhQUZBbit3QldRK2E5a0NwSVN1U1lvWXd5WDdLZXlNSEFHCkNDc0dBUVVGQndFQkJHUXdZakF0QmdnckJnRUZCUWN3QW9ZaGFIUjBjRG92TDJObGNuUnpMbUZ3Y0d4bExtTnYKYlM5M2QyUnlaek11WkdWeU1ERUdDQ3NHQVFVRkJ6QUJoaVZvZEhSd09pOHZiMk56Y0M1aGNIQnNaUzVqYjIwdgpiMk56Y0RBekxYZDNaSEpuTXpBNU1JSUJMUVlEVlIwZ0JJSUJKRENDQVNBd2dnRWNCZ2txaGtpRzkyTmtCUUV3CmdnRU5NSUhSQmdnckJnRUZCUWNDQWpDQnhBeUJ3VkpsYkdsaGJtTmxJRzl1SUhSb2FYTWdRMlZ5ZEdsbWFXTmgKZEdVZ1lua2dZVzU1SUhCaGNuUjVJRzkwYUdWeUlIUm9ZVzRnUVhCd2JHVWdhWE1nY0hKdmFHbGlhWFJsWkM0ZwpVbVZtWlhJZ2RHOGdkR2hsSUdGd2NHeHBZMkZpYkdVZ2MzUmhibVJoY21RZ2RHVnliWE1nWVc1a0lHTnZibVJwCmRHbHZibk1nYjJZZ2RYTmxMQ0JqWlhKMGFXWnBZMkYwWlNCd2IyeHBZM2tnWVc1a0lHTmxjblJwWm1sallYUnAKYjI0Z2NISmhZM1JwWTJVZ2MzUmhkR1Z0Wlc1MGN5NHdOd1lJS3dZQkJRVUhBZ0VXSzJoMGRIQnpPaTh2ZDNkMwpMbUZ3Y0d4bExtTnZiUzlqWlhKMGFXWnBZMkYwWldGMWRHaHZjbWwwZVM4d0V3WURWUjBsQkF3d0NnWUlLd1lCCkJRVUhBd0l3SFFZRFZSME9CQllFRk5RSysxcUNHbDRTQ1p6SzFSUmpnb05nM0hmdk1BNEdBMVVkRHdFQi93UUUKQXdJSGdEQlBCZ2txaGtpRzkyTmtCaUFFUWd4QVFVUkNRemxDTmtGRE5USkVRems0TnpCRk5qYzJNVFpFUkRJdwpPVUkwTWtReE1UVXlSVVpFTURVeFFVRXhRekV6T0ROR00wUkROa1V5TkVNelFqRkVSVEFQQmdrcWhraUc5Mk5rCkJpNEVBZ1VBTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFBSFR6NTU2RUs5VVp6R0RVd2cvcmFibmYrUXFSYkgKcllVS0ZNcWQwUDhFTHZGMmYrTzN0ZXlDWHNBckF4TmVMY2hRSGVTNUFJOHd2azdMQ0xLUmJCdWJQQy9NVmtBKwpCZ3h5STg2ejJOVUNDWml4QVM1d2JFQWJYOStVMFp2RHp5Y01BbUNrdVVHZjNwWXR5TDNDaEplSGRvOEwwdmdvCnJQWElUSzc4ZjQzenNzYjBTNE5xbTE0eS9LNCs1ZkUzcFUxdEJqME5tUmdKUVJLRnB6MENpV2RPd1BRTk5BYUMKYUNNU2NiYXdwUTBjWEhaZDJWVjNtem4xdUlITEVtaU5GTWVxeEprbjZhUXFFQnNndDUzaUFxcmZMNjEzWStScAppd0tENXVmeU0wYzBweTYyZmkvWEwwS2c4ajEwWU1VdWJpd2dHajAzZThQWTB6bWUvcGZCZ3p6VQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==\",\"display_name\":\"applepay\",\"certificate_keys\":\"LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRRDhIUy81ZmJZNVJLaEkKWFN6ckhKaHk1azZmNGFHTGhJbWJJS2lxWEVJVHVSQ2RHcGcyMExZM1VhTlBlYXZXTVRIUTBpK3d1RzlZWFVhYwo1eXhhNHB4OXh5ZkJVSHo4c3lPaTI3WDVWWlRvTlFhd2F6dGM5aGpZc1B2K0s2UW9oaWRTQWZ3cDhMdThkQ0lVCmZYUFhwY3YxalVUckQrZXNUSUxWVG9RVE5oQ3NlQlJtUS9nK05WdTB5c3BqeUYxU2l6VG9BK1BML3NrMlJEYWMKWlgvb1kwdUdONFVneHNCWFh3WTNHSm0xUkNwdTM0Y2d0UC9kaHNBM1Ixb1VOb0gyQkZBSm9xK3pyUnl3U1RCSApITTBqRENpZ3FVNUZLcC9aQWx3c2JoNVlWTlNNWksrQ0pTK1BPTzlVNGVkeHJmTGlBVkhnQTgzRG43Z2U4K29nCldWNGdIVDZoQWdNQkFBRUNnZ0VBZFNaRzVhTFJxdmpKVFo3bVFYWHZMT3p4dWY5SlpxQTJwUHZmQkJLTXJjZC8KL2RDZXpGSGRhZ1VvWXNUQjRXekluaVVjL2Z3bDJTUzJyREFMZjB2dnRjNTJIYkQ5OHhwMnc3VmVjTGFnMCtuWAo2dUJaSEZCS3FWNU1LZ1l6YUpVMTdqaDM2VEV3dTFnbmdlZnRqVlpBV1NERTFvbDBlSzZ3Mk5kOExjVWdxRkxUCjVHYUlBV01nd0NKL3pzQmwydUV1Y0Q4S21WL1Z2MkVCQVJLWGZtci92UU1NelZrNkhhalprVGZqbWY2cWFVQVMKQWlFblROcHBic2ZrdTk2VGdIa2owWm10VWc0SFkzSU9qWFlpaGJsSjJzQ1JjS3p6cXkxa3B3WlpHcHo1NXEzbgphSXEwenJ3RjlpTUZubEhCa04yK3FjSnhzcDNTalhRdFRLTTY4WHRrVlFLQmdRRC8wemtCVlExR2Q1U0Mzb2czCnM3QWRCZ243dnVMdUZHZFFZY3c0OUppRGw1a1BZRXlKdGQvTVpNOEpFdk1nbVVTeUZmczNZcGtmQ2VGbUp0S3QKMnNNNEdCRWxqTVBQNjI3Q0QrV3c4L3JpWmlOZEg3OUhPRjRneTRGbjBycDNqanlLSWF1OHJISDQwRUUvSkVyOQpxWFQ1SGdWMmJQOGhMcW5sSjFmSDhpY2Zkd0tCZ1FEOFNWQ3ZDV2txQkh2SzE5ZDVTNlArdm5hcXhnTWo0U0srCnJ6L1I1c3pTaW5lS045VEhyeVkxYUZJbVFJZjJYOUdaQXBocUhrckxtQ3BIcURHOWQ3WDVQdUxxQzhmc09kVTYKRzhWaFRXeXdaSVNjdGRSYkk5S2xKUFk2V2ZDQjk0ODNVaDJGbW1xV2JuNWcwZUJxZWZzamVVdEtHekNRaGJDYworR1dBREVRSXB3S0JnUURmaWYvN3pBZm5sVUh1QU9saVV0OEczV29IMGtxVTRydE1IOGpGMCtVWXgzVDFYSjVFCmp1blp2aFN5eHg0dlUvNFU1dVEzQnk3cFVrYmtiZlFWK2x3dlBjaHQyVXlZK0E0MkFKSWlSMjdvT1h1Wk9jNTQKT3liMDNSNWNUR1NuWjJBN0N5VDNubStRak5rV2hXNEpyUE1MWTFJK293dGtRVlF2YW10bnlZNnFEUUtCZ0ZYWgpLT0IzSmxjSzhZa0R5Nm5WeUhkZUhvbGNHaU55YjkxTlN6MUUrWHZIYklnWEdZdmRtUFhoaXRyRGFNQzR1RjBGCjJoRjZQMTlxWnpDOUZqZnY3WGRrSTlrYXF5eENQY0dwUTVBcHhZdDhtUGV1bEJWemFqR1NFMHVsNFVhSWxDNXgKL2VQQnVQVjVvZjJXVFhST0Q5eHhZT0pWd0QvZGprekw1ZFlkMW1UUEFvR0JBTWVwY3diVEphZ3BoZk5zOHY0WAprclNoWXplbVpxY2EwQzRFc2QwNGYwTUxHSlVNS3Zpck4zN0g1OUFjT2IvNWtZcTU5WFRwRmJPWjdmYlpHdDZnCkxnM2hWSHRacElOVGJ5Ni9GOTBUZ09Za3RxUnhNVmc3UFBxbjFqdEFiVU15eVpVZFdHcFNNMmI0bXQ5dGhlUDEKblhMR09NWUtnS2JYbjZXWWN5K2U5eW9ICi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0KCg==\",\"initiative_context\":\"hyperswitch-sdk-test.netlify.app\",\"merchant_identifier\":\"merchant.com.adyen.san\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" }, "url": { "raw": "{{baseUrl}}/account/:account_id/connectors", @@ -735,7 +735,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"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://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"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://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"routing\":{\"type\":\"single\",\"data\":\"adyen\"},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -1214,6 +1214,248 @@ { "name": "Happy Cases", "item": [ + { + "name": "Scenario22-Create Gift Card payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":1100,\"currency\":\"EUR\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1100,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"gift_card\",\"payment_method_type\":\"givex\",\"payment_method_data\":{\"gift_card\":{\"givex\":{\"number\":\"6364530000000000\",\"cvc\":\"122222\"}}},\"routing\":{\"type\":\"single\",\"data\":\"adyen\"},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, { "name": "Scenario1-Create payment with confirm true", "item": [ @@ -10722,6 +10964,258 @@ { "name": "Variation Cases", "item": [ + { + "name": "Scenario10-Create Gift Card payment where it fails due to insufficient balance", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"failed\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'failed'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"failed\");", + " },", + " );", + "}", + "", + "// Response body should have error message as \"Insufficient balance in the payment method\"", + "if (jsonData?.error_message) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error_message' matches 'Insufficient balance in the payment method'\",", + " function () {", + " pm.expect(jsonData.error_message).to.eql(\"Insufficient balance in the payment method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":14100,\"currency\":\"EUR\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":14100,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"gift_card\",\"payment_method_type\":\"givex\",\"payment_method_data\":{\"gift_card\":{\"givex\":{\"number\":\"6364530000000000\",\"cvc\":\"122222\"}}},\"routing\":{\"type\":\"single\",\"data\":\"adyen\"},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Failed\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"failed\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, { "name": "Scenario1-Create payment with Invalid card details", "item": [ From 1be197f6f0b664c281650caed4c2971e96360759 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 16:25:57 +0000 Subject: [PATCH 118/146] chore(version): v1.91.0 --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b3abf1d5781..dfe703192a3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,38 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.91.0 (2023-11-28) + +### Features + +- **core:** + - [Paypal] Add Preprocessing flow to CompleteAuthorize for Card 3DS Auth Verification ([#2757](https://github.com/juspay/hyperswitch/pull/2757)) ([`77fc92c`](https://github.com/juspay/hyperswitch/commit/77fc92c99a99aaf76d270ba5b981928183a05768)) + - Enable payment refund when payment is partially captured ([#2991](https://github.com/juspay/hyperswitch/pull/2991)) ([`837480d`](https://github.com/juspay/hyperswitch/commit/837480d935cce8cc35f07c5ccb3560285909bc52)) +- **currency_conversion:** Add currency conversion feature ([#2948](https://github.com/juspay/hyperswitch/pull/2948)) ([`c0116db`](https://github.com/juspay/hyperswitch/commit/c0116db271f6afc1b93c04705209bfc346228c68)) +- **payment_methods:** Receive `card_holder_name` in confirm flow when using token for payment ([#2982](https://github.com/juspay/hyperswitch/pull/2982)) ([`e7ad3a4`](https://github.com/juspay/hyperswitch/commit/e7ad3a4db8823f3ae8d381771739670d8350e6da)) + +### Bug Fixes + +- **connector:** [Adyen] `ErrorHandling` in case of Balance Check for Gift Cards ([#1976](https://github.com/juspay/hyperswitch/pull/1976)) ([`bd889c8`](https://github.com/juspay/hyperswitch/commit/bd889c834dd5e201b055233016f7226fa2187aea)) +- **core:** Replace euclid enum with RoutableConnectors enum ([#2994](https://github.com/juspay/hyperswitch/pull/2994)) ([`ff6a0dd`](https://github.com/juspay/hyperswitch/commit/ff6a0dd0b515778b64a3e28ef905154eee85ec78)) +- Remove error propagation if card name not found in locker ([#2998](https://github.com/juspay/hyperswitch/pull/2998)) ([`1c5a9b5`](https://github.com/juspay/hyperswitch/commit/1c5a9b5452afc33b18f45389bf3bdfd80820f476)) + +### Refactors + +- **events:** Adding changes to type of API events to Kafka ([#2992](https://github.com/juspay/hyperswitch/pull/2992)) ([`d63f6f7`](https://github.com/juspay/hyperswitch/commit/d63f6f7224f35018e7c707353508bbacc2baed5c)) +- **masking:** Use empty enums as masking:Strategy types ([#2874](https://github.com/juspay/hyperswitch/pull/2874)) ([`0e66b1b`](https://github.com/juspay/hyperswitch/commit/0e66b1b5dcce6dd87c9d743c9eb73d0cd8e330b2)) +- **router:** Add openapi spec support for merchant_connector apis ([#2997](https://github.com/juspay/hyperswitch/pull/2997)) ([`cdbb385`](https://github.com/juspay/hyperswitch/commit/cdbb3853cd44443f8487abc16a9ba5d99f22e475)) +- Added min idle and max lifetime for database config ([#2900](https://github.com/juspay/hyperswitch/pull/2900)) ([`b3c51e6`](https://github.com/juspay/hyperswitch/commit/b3c51e6eb55c58adc024ee32b59c3910b2b72131)) + +### Testing + +- **postman:** Update postman collection files ([`af6b05c`](https://github.com/juspay/hyperswitch/commit/af6b05c504b6fdbec7db77fa7f71535d7fea3e7a)) + +**Full Changelog:** [`v1.90.0...v1.91.0`](https://github.com/juspay/hyperswitch/compare/v1.90.0...v1.91.0) + +- - - + + ## 1.90.0 (2023-11-27) ### Features From 1c2f35af92608fca5836448710eca9f9c23a776a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Nov 2023 13:39:42 +0530 Subject: [PATCH 119/146] chore(deps): bump openssl from 0.10.57 to 0.10.60 (#3004) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- crates/router/Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2ca33b6910a0..a16dde18e83e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3861,9 +3861,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.57" +version = "0.10.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" +checksum = "79a4c6c3a2b158f7f8f2a2fc5a969fa3a068df6fc9dbb4a43845436e3af7c800" dependencies = [ "bitflags 2.4.0", "cfg-if 1.0.0", @@ -3893,9 +3893,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.93" +version = "0.9.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" +checksum = "3812c071ba60da8b5677cc12bcb1d42989a65553772897a7e0355545a819838f" dependencies = [ "cc", "libc", diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index f0316d69249e..a5f8b2f6b847 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -68,7 +68,7 @@ mime = "0.3.17" nanoid = "0.4.0" num_cpus = "1.15.0" once_cell = "1.18.0" -openssl = "0.10.55" +openssl = "0.10.60" qrcode = "0.12.0" rand = "0.8.5" rand_chacha = "0.3.1" From bb593ab0cd1a30190b6c305f2432de83ac7fde93 Mon Sep 17 00:00:00 2001 From: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Date: Wed, 29 Nov 2023 13:42:36 +0530 Subject: [PATCH 120/146] fix: remove `dummy_connector` from `default` features in `common_enums` (#3005) Signed-off-by: chikke srujan <121822803+srujanchikke@users.noreply.github.com> --- crates/api_models/Cargo.toml | 2 +- crates/common_enums/Cargo.toml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index 73c2d673c972..cb2e243745de 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -14,7 +14,7 @@ connector_choice_bcompat = [] errors = ["dep:actix-web", "dep:reqwest"] backwards_compatibility = ["connector_choice_bcompat"] connector_choice_mca_id = ["euclid/connector_choice_mca_id"] -dummy_connector = ["euclid/dummy_connector"] +dummy_connector = ["euclid/dummy_connector", "common_enums/dummy_connector"] detailed_errors = [] payouts = [] diff --git a/crates/common_enums/Cargo.toml b/crates/common_enums/Cargo.toml index cd061970bff3..72d9f6bb0bb1 100644 --- a/crates/common_enums/Cargo.toml +++ b/crates/common_enums/Cargo.toml @@ -8,7 +8,6 @@ readme = "README.md" license.workspace = true [features] -default = ["dummy_connector"] dummy_connector = [] [dependencies] From 5c32b3739e2c5895fe7f5cf8cc92f917c2639eac Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Wed, 29 Nov 2023 13:47:16 +0530 Subject: [PATCH 121/146] fix: remove error propagation if card name not found in locker in case of temporary token (#3006) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/router/src/core/payments/helpers.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 266792f98758..7a8a76e1123a 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1381,18 +1381,19 @@ pub async fn retrieve_payment_method_with_temporary_token( let name_on_card = if card.card_holder_name.clone().expose().is_empty() { card_token_data - .and_then(|token_data| { + .and_then(|token_data| token_data.card_holder_name.clone()) + .filter(|name_on_card| !name_on_card.clone().expose().is_empty()) + .map(|name_on_card| { is_card_updated = true; - token_data.card_holder_name.clone() + name_on_card }) - .filter(|name_on_card| !name_on_card.clone().expose().is_empty()) - .ok_or(errors::ApiErrorResponse::MissingRequiredField { - field_name: "card_holder_name", - })? } else { - card.card_holder_name.clone() + Some(card.card_holder_name.clone()) }; - updated_card.card_holder_name = name_on_card; + + if let Some(name_on_card) = name_on_card { + updated_card.card_holder_name = name_on_card; + } if let Some(cvc) = card_cvc { is_card_updated = true; From d289524869f0c3835db9cf90d57ebedf560e0291 Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Wed, 29 Nov 2023 13:54:16 +0530 Subject: [PATCH 122/146] fix: few fields were not getting updated in apply_changeset function (#3002) --- crates/diesel_models/src/business_profile.rs | 46 ++++--- crates/diesel_models/src/capture.rs | 21 +++- crates/diesel_models/src/payment_attempt.rs | 123 +++++++++++-------- crates/diesel_models/src/payment_intent.rs | 91 ++++++++------ crates/diesel_models/src/refund.rs | 32 +++-- 5 files changed, 190 insertions(+), 123 deletions(-) diff --git a/crates/diesel_models/src/business_profile.rs b/crates/diesel_models/src/business_profile.rs index 1f6c4f604958..700104aaaecc 100644 --- a/crates/diesel_models/src/business_profile.rs +++ b/crates/diesel_models/src/business_profile.rs @@ -103,25 +103,39 @@ impl From for BusinessProfile { impl BusinessProfileUpdateInternal { pub fn apply_changeset(self, source: BusinessProfile) -> BusinessProfile { + let Self { + profile_name, + modified_at: _, + return_url, + enable_payment_response_hash, + payment_response_hash_key, + redirect_to_merchant_with_http_post, + webhook_details, + metadata, + routing_algorithm, + intent_fulfillment_time, + frm_routing_algorithm, + payout_routing_algorithm, + is_recon_enabled, + applepay_verified_domains, + } = self; BusinessProfile { - profile_name: self.profile_name.unwrap_or(source.profile_name), - modified_at: self.modified_at.unwrap_or(source.modified_at), - return_url: self.return_url, - enable_payment_response_hash: self - .enable_payment_response_hash + profile_name: profile_name.unwrap_or(source.profile_name), + modified_at: common_utils::date_time::now(), + return_url, + enable_payment_response_hash: enable_payment_response_hash .unwrap_or(source.enable_payment_response_hash), - payment_response_hash_key: self.payment_response_hash_key, - redirect_to_merchant_with_http_post: self - .redirect_to_merchant_with_http_post + payment_response_hash_key, + redirect_to_merchant_with_http_post: redirect_to_merchant_with_http_post .unwrap_or(source.redirect_to_merchant_with_http_post), - webhook_details: self.webhook_details, - metadata: self.metadata, - routing_algorithm: self.routing_algorithm, - intent_fulfillment_time: self.intent_fulfillment_time, - frm_routing_algorithm: self.frm_routing_algorithm, - payout_routing_algorithm: self.payout_routing_algorithm, - is_recon_enabled: self.is_recon_enabled.unwrap_or(source.is_recon_enabled), - applepay_verified_domains: self.applepay_verified_domains, + webhook_details, + metadata, + routing_algorithm, + intent_fulfillment_time, + frm_routing_algorithm, + payout_routing_algorithm, + is_recon_enabled: is_recon_enabled.unwrap_or(source.is_recon_enabled), + applepay_verified_domains, ..source } } diff --git a/crates/diesel_models/src/capture.rs b/crates/diesel_models/src/capture.rs index 30eee900cff1..adc313ca3dde 100644 --- a/crates/diesel_models/src/capture.rs +++ b/crates/diesel_models/src/capture.rs @@ -83,13 +83,24 @@ pub struct CaptureUpdateInternal { impl CaptureUpdate { pub fn apply_changeset(self, source: Capture) -> Capture { - let capture_update: CaptureUpdateInternal = self.into(); + let CaptureUpdateInternal { + status, + error_message, + error_code, + error_reason, + modified_at: _, + connector_capture_id, + connector_response_reference_id, + } = self.into(); Capture { - status: capture_update.status.unwrap_or(source.status), - error_message: capture_update.error_message.or(source.error_message), - error_code: capture_update.error_code.or(source.error_code), - error_reason: capture_update.error_reason.or(source.error_reason), + status: status.unwrap_or(source.status), + error_message: error_message.or(source.error_message), + error_code: error_code.or(source.error_code), + error_reason: error_reason.or(source.error_reason), modified_at: common_utils::date_time::now(), + connector_capture_id: connector_capture_id.or(source.connector_capture_id), + connector_response_reference_id: connector_response_reference_id + .or(source.connector_response_reference_id), ..source } } diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index 9cc6632c638e..216801fa8fb1 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -314,60 +314,83 @@ pub struct PaymentAttemptUpdateInternal { impl PaymentAttemptUpdate { pub fn apply_changeset(self, source: PaymentAttempt) -> PaymentAttempt { - let pa_update: PaymentAttemptUpdateInternal = self.into(); + let PaymentAttemptUpdateInternal { + amount, + currency, + status, + connector_transaction_id, + amount_to_capture, + connector, + authentication_type, + payment_method, + error_message, + payment_method_id, + cancellation_reason, + modified_at: _, + mandate_id, + browser_info, + payment_token, + error_code, + connector_metadata, + payment_method_data, + payment_method_type, + payment_experience, + business_sub_label, + straight_through_algorithm, + preprocessing_step_id, + error_reason, + capture_method, + connector_response_reference_id, + multiple_capture_count, + surcharge_amount, + tax_amount, + amount_capturable, + updated_by, + merchant_connector_id, + authentication_data, + encoded_data, + unified_code, + unified_message, + } = self.into(); PaymentAttempt { - amount: pa_update.amount.unwrap_or(source.amount), - currency: pa_update.currency.or(source.currency), - status: pa_update.status.unwrap_or(source.status), - connector_transaction_id: pa_update - .connector_transaction_id - .or(source.connector_transaction_id), - amount_to_capture: pa_update.amount_to_capture.or(source.amount_to_capture), - connector: pa_update.connector.or(source.connector), - authentication_type: pa_update.authentication_type.or(source.authentication_type), - payment_method: pa_update.payment_method.or(source.payment_method), - error_message: pa_update.error_message.unwrap_or(source.error_message), - payment_method_id: pa_update - .payment_method_id - .unwrap_or(source.payment_method_id), - cancellation_reason: pa_update.cancellation_reason.or(source.cancellation_reason), + amount: amount.unwrap_or(source.amount), + currency: currency.or(source.currency), + status: status.unwrap_or(source.status), + connector_transaction_id: connector_transaction_id.or(source.connector_transaction_id), + amount_to_capture: amount_to_capture.or(source.amount_to_capture), + connector: connector.or(source.connector), + authentication_type: authentication_type.or(source.authentication_type), + payment_method: payment_method.or(source.payment_method), + error_message: error_message.unwrap_or(source.error_message), + payment_method_id: payment_method_id.unwrap_or(source.payment_method_id), + cancellation_reason: cancellation_reason.or(source.cancellation_reason), modified_at: common_utils::date_time::now(), - mandate_id: pa_update.mandate_id.or(source.mandate_id), - browser_info: pa_update.browser_info.or(source.browser_info), - payment_token: pa_update.payment_token.or(source.payment_token), - error_code: pa_update.error_code.unwrap_or(source.error_code), - connector_metadata: pa_update.connector_metadata.or(source.connector_metadata), - payment_method_data: pa_update.payment_method_data.or(source.payment_method_data), - payment_method_type: pa_update.payment_method_type.or(source.payment_method_type), - payment_experience: pa_update.payment_experience.or(source.payment_experience), - business_sub_label: pa_update.business_sub_label.or(source.business_sub_label), - straight_through_algorithm: pa_update - .straight_through_algorithm + mandate_id: mandate_id.or(source.mandate_id), + browser_info: browser_info.or(source.browser_info), + payment_token: payment_token.or(source.payment_token), + error_code: error_code.unwrap_or(source.error_code), + connector_metadata: connector_metadata.or(source.connector_metadata), + payment_method_data: payment_method_data.or(source.payment_method_data), + payment_method_type: payment_method_type.or(source.payment_method_type), + payment_experience: payment_experience.or(source.payment_experience), + business_sub_label: business_sub_label.or(source.business_sub_label), + straight_through_algorithm: straight_through_algorithm .or(source.straight_through_algorithm), - preprocessing_step_id: pa_update - .preprocessing_step_id - .or(source.preprocessing_step_id), - error_reason: pa_update.error_reason.unwrap_or(source.error_reason), - capture_method: pa_update.capture_method.or(source.capture_method), - connector_response_reference_id: pa_update - .connector_response_reference_id + preprocessing_step_id: preprocessing_step_id.or(source.preprocessing_step_id), + error_reason: error_reason.unwrap_or(source.error_reason), + capture_method: capture_method.or(source.capture_method), + connector_response_reference_id: connector_response_reference_id .or(source.connector_response_reference_id), - multiple_capture_count: pa_update - .multiple_capture_count - .or(source.multiple_capture_count), - surcharge_amount: pa_update.surcharge_amount.or(source.surcharge_amount), - tax_amount: pa_update.tax_amount.or(source.tax_amount), - amount_capturable: pa_update - .amount_capturable - .unwrap_or(source.amount_capturable), - updated_by: pa_update.updated_by, - merchant_connector_id: pa_update - .merchant_connector_id - .or(source.merchant_connector_id), - authentication_data: pa_update.authentication_data.or(source.authentication_data), - encoded_data: pa_update.encoded_data.or(source.encoded_data), - unified_code: pa_update.unified_code.unwrap_or(source.unified_code), - unified_message: pa_update.unified_message.unwrap_or(source.unified_message), + multiple_capture_count: multiple_capture_count.or(source.multiple_capture_count), + surcharge_amount: surcharge_amount.or(source.surcharge_amount), + tax_amount: tax_amount.or(source.tax_amount), + amount_capturable: amount_capturable.unwrap_or(source.amount_capturable), + updated_by, + merchant_connector_id: merchant_connector_id.or(source.merchant_connector_id), + authentication_data: authentication_data.or(source.authentication_data), + encoded_data: encoded_data.or(source.encoded_data), + unified_code: unified_code.unwrap_or(source.unified_code), + unified_message: unified_message.unwrap_or(source.unified_message), ..source } } diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 2ffa857026ba..b6ff4fcf8d8d 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -217,50 +217,61 @@ pub struct PaymentIntentUpdateInternal { impl PaymentIntentUpdate { pub fn apply_changeset(self, source: PaymentIntent) -> PaymentIntent { - let internal_update: PaymentIntentUpdateInternal = self.into(); + let PaymentIntentUpdateInternal { + amount, + currency, + status, + amount_captured, + customer_id, + return_url, + setup_future_usage, + off_session, + metadata, + billing_address_id, + shipping_address_id, + modified_at: _, + active_attempt_id, + business_country, + business_label, + description, + statement_descriptor_name, + statement_descriptor_suffix, + order_details, + attempt_count, + profile_id, + merchant_decision, + payment_confirm_source, + updated_by, + surcharge_applicable, + } = self.into(); PaymentIntent { - amount: internal_update.amount.unwrap_or(source.amount), - currency: internal_update.currency.or(source.currency), - status: internal_update.status.unwrap_or(source.status), - amount_captured: internal_update.amount_captured.or(source.amount_captured), - customer_id: internal_update.customer_id.or(source.customer_id), - return_url: internal_update.return_url.or(source.return_url), - setup_future_usage: internal_update - .setup_future_usage - .or(source.setup_future_usage), - off_session: internal_update.off_session.or(source.off_session), - metadata: internal_update.metadata.or(source.metadata), - billing_address_id: internal_update - .billing_address_id - .or(source.billing_address_id), - shipping_address_id: internal_update - .shipping_address_id - .or(source.shipping_address_id), + amount: amount.unwrap_or(source.amount), + currency: currency.or(source.currency), + status: status.unwrap_or(source.status), + amount_captured: amount_captured.or(source.amount_captured), + customer_id: customer_id.or(source.customer_id), + return_url: return_url.or(source.return_url), + setup_future_usage: setup_future_usage.or(source.setup_future_usage), + off_session: off_session.or(source.off_session), + metadata: metadata.or(source.metadata), + billing_address_id: billing_address_id.or(source.billing_address_id), + shipping_address_id: shipping_address_id.or(source.shipping_address_id), modified_at: common_utils::date_time::now(), - active_attempt_id: internal_update - .active_attempt_id - .unwrap_or(source.active_attempt_id), - business_country: internal_update.business_country.or(source.business_country), - business_label: internal_update.business_label.or(source.business_label), - description: internal_update.description.or(source.description), - statement_descriptor_name: internal_update - .statement_descriptor_name + active_attempt_id: active_attempt_id.unwrap_or(source.active_attempt_id), + business_country: business_country.or(source.business_country), + business_label: business_label.or(source.business_label), + description: description.or(source.description), + statement_descriptor_name: statement_descriptor_name .or(source.statement_descriptor_name), - statement_descriptor_suffix: internal_update - .statement_descriptor_suffix + statement_descriptor_suffix: statement_descriptor_suffix .or(source.statement_descriptor_suffix), - order_details: internal_update.order_details.or(source.order_details), - attempt_count: internal_update - .attempt_count - .unwrap_or(source.attempt_count), - profile_id: internal_update.profile_id.or(source.profile_id), - merchant_decision: internal_update - .merchant_decision - .or(source.merchant_decision), - payment_confirm_source: internal_update - .payment_confirm_source - .or(source.payment_confirm_source), - updated_by: internal_update.updated_by, + order_details: order_details.or(source.order_details), + attempt_count: attempt_count.unwrap_or(source.attempt_count), + profile_id: profile_id.or(source.profile_id), + merchant_decision: merchant_decision.or(source.merchant_decision), + payment_confirm_source: payment_confirm_source.or(source.payment_confirm_source), + updated_by, + surcharge_applicable: surcharge_applicable.or(source.surcharge_applicable), ..source } } diff --git a/crates/diesel_models/src/refund.rs b/crates/diesel_models/src/refund.rs index 62aec3fb27d8..bb805fb646c5 100644 --- a/crates/diesel_models/src/refund.rs +++ b/crates/diesel_models/src/refund.rs @@ -202,19 +202,27 @@ impl From for RefundUpdateInternal { impl RefundUpdate { pub fn apply_changeset(self, source: Refund) -> Refund { - let pa_update: RefundUpdateInternal = self.into(); + let RefundUpdateInternal { + connector_refund_id, + refund_status, + sent_to_gateway, + refund_error_message, + refund_arn, + metadata, + refund_reason, + refund_error_code, + updated_by, + } = self.into(); Refund { - connector_refund_id: pa_update.connector_refund_id.or(source.connector_refund_id), - refund_status: pa_update.refund_status.unwrap_or(source.refund_status), - sent_to_gateway: pa_update.sent_to_gateway.unwrap_or(source.sent_to_gateway), - refund_error_message: pa_update - .refund_error_message - .or(source.refund_error_message), - refund_error_code: pa_update.refund_error_code.or(source.refund_error_code), - refund_arn: pa_update.refund_arn.or(source.refund_arn), - metadata: pa_update.metadata.or(source.metadata), - refund_reason: pa_update.refund_reason.or(source.refund_reason), - updated_by: pa_update.updated_by, + connector_refund_id: connector_refund_id.or(source.connector_refund_id), + refund_status: refund_status.unwrap_or(source.refund_status), + sent_to_gateway: sent_to_gateway.unwrap_or(source.sent_to_gateway), + refund_error_message: refund_error_message.or(source.refund_error_message), + refund_error_code: refund_error_code.or(source.refund_error_code), + refund_arn: refund_arn.or(source.refund_arn), + metadata: metadata.or(source.metadata), + refund_reason: refund_reason.or(source.refund_reason), + updated_by, ..source } } From 37ab392488350c22d1d1352edc90f46af25d40be Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 29 Nov 2023 08:44:23 +0000 Subject: [PATCH 123/146] chore(version): v1.91.1 --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfe703192a3a..5a63dcc2cae0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,23 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.91.1 (2023-11-29) + +### Bug Fixes + +- Remove `dummy_connector` from `default` features in `common_enums` ([#3005](https://github.com/juspay/hyperswitch/pull/3005)) ([`bb593ab`](https://github.com/juspay/hyperswitch/commit/bb593ab0cd1a30190b6c305f2432de83ac7fde93)) +- Remove error propagation if card name not found in locker in case of temporary token ([#3006](https://github.com/juspay/hyperswitch/pull/3006)) ([`5c32b37`](https://github.com/juspay/hyperswitch/commit/5c32b3739e2c5895fe7f5cf8cc92f917c2639eac)) +- Few fields were not getting updated in apply_changeset function ([#3002](https://github.com/juspay/hyperswitch/pull/3002)) ([`d289524`](https://github.com/juspay/hyperswitch/commit/d289524869f0c3835db9cf90d57ebedf560e0291)) + +### Miscellaneous Tasks + +- **deps:** Bump openssl from 0.10.57 to 0.10.60 ([#3004](https://github.com/juspay/hyperswitch/pull/3004)) ([`1c2f35a`](https://github.com/juspay/hyperswitch/commit/1c2f35af92608fca5836448710eca9f9c23a776a)) + +**Full Changelog:** [`v1.91.0...v1.91.1`](https://github.com/juspay/hyperswitch/compare/v1.91.0...v1.91.1) + +- - - + + ## 1.91.0 (2023-11-28) ### Features From 5f5e895f638701a0e6ab3deea9101ef39033dd16 Mon Sep 17 00:00:00 2001 From: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com> Date: Wed, 29 Nov 2023 16:12:12 +0530 Subject: [PATCH 124/146] feat(ses_email): add email services to hyperswitch (#2977) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Gnanasundari24 <118818938+Gnanasundari24@users.noreply.github.com> --- Cargo.lock | 45 +++ config/config.example.toml | 17 +- config/development.toml | 10 +- crates/external_services/Cargo.toml | 3 + crates/external_services/src/email.rs | 194 ++++++----- crates/external_services/src/email/ses.rs | 257 +++++++++++++++ crates/router/Cargo.toml | 2 +- crates/router/src/consts.rs | 2 + crates/router/src/core/user.rs | 22 ++ crates/router/src/routes/app.rs | 20 +- crates/router/src/services.rs | 3 + crates/router/src/services/email.rs | 1 + .../src/services/email/assets/invite.html | 243 ++++++++++++++ .../src/services/email/assets/magic_link.html | 260 +++++++++++++++ .../email/assets/recon_activated.html | 309 ++++++++++++++++++ .../src/services/email/assets/reset.html | 229 +++++++++++++ .../src/services/email/assets/verify.html | 253 ++++++++++++++ crates/router/src/services/email/types.rs | 80 +++++ 18 files changed, 1857 insertions(+), 93 deletions(-) create mode 100644 crates/external_services/src/email/ses.rs create mode 100644 crates/router/src/services/email.rs create mode 100644 crates/router/src/services/email/assets/invite.html create mode 100644 crates/router/src/services/email/assets/magic_link.html create mode 100644 crates/router/src/services/email/assets/recon_activated.html create mode 100644 crates/router/src/services/email/assets/reset.html create mode 100644 crates/router/src/services/email/assets/verify.html create mode 100644 crates/router/src/services/email/types.rs diff --git a/Cargo.lock b/Cargo.lock index a16dde18e83e..96bdcff3f86e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2366,11 +2366,14 @@ dependencies = [ "aws-config", "aws-sdk-kms", "aws-sdk-sesv2", + "aws-sdk-sts", "aws-smithy-client", "base64 0.21.4", "common_utils", "dyn-clone", "error-stack", + "hyper", + "hyper-proxy", "masking", "once_cell", "router_env", @@ -2867,6 +2870,30 @@ dependencies = [ "hashbrown 0.14.1", ] +[[package]] +name = "headers" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" +dependencies = [ + "base64 0.21.4", + "bytes 1.5.0", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.4.1" @@ -2994,6 +3021,24 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-proxy" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca815a891b24fdfb243fa3239c86154392b0953ee584aa1a2a1f66d20cbe75cc" +dependencies = [ + "bytes 1.5.0", + "futures 0.3.28", + "headers", + "http", + "hyper", + "hyper-tls", + "native-tls", + "tokio 1.32.0", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-rustls" version = "0.23.2" diff --git a/config/config.example.toml b/config/config.example.toml index 0b8730ca114a..d935a4e7f20d 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -322,9 +322,17 @@ region = "" # The AWS region used by the KMS SDK for decrypting data. # EmailClient configuration. Only applicable when the `email` feature flag is enabled. [email] -from_email = "notify@example.com" # Sender email -aws_region = "" # AWS region used by AWS SES -base_url = "" # Base url used when adding links that should redirect to self +sender_email = "example@example.com" # Sender email +aws_region = "" # AWS region used by AWS SES +base_url = "" # Base url used when adding links that should redirect to self +allowed_unverified_days = 1 # Number of days the api calls ( with jwt token ) can be made without verifying the email +active_email_client = "SES" # The currently active email client + +# Configuration for aws ses, applicable when the active email client is SES +[email.aws_ses] +email_role_arn = "" # The amazon resource name ( arn ) of the role which has permission to send emails +sts_role_session_name = "" # An identifier for the assumed role session, used to uniquely identify a session. + #tokenization configuration which describe token lifetime and payment method for specific connector [tokenization] @@ -427,9 +435,6 @@ credit = { currency = "USD" } debit = { currency = "USD" } ach = { currency = "USD" } -[pm_filters.stripe] -cashapp = { country = "US", currency = "USD" } - [pm_filters.prophetpay] card_redirect = { currency = "USD" } diff --git a/config/development.toml b/config/development.toml index bcf561dd5857..f2620bd37135 100644 --- a/config/development.toml +++ b/config/development.toml @@ -212,9 +212,15 @@ disabled = false consumer_group = "SCHEDULER_GROUP" [email] -from_email = "notify@example.com" +sender_email = "example@example.com" aws_region = "" -base_url = "" +base_url = "http://localhost:8080" +allowed_unverified_days = 1 +active_email_client = "SES" + +[email.aws_ses] +email_role_arn = "" +sts_role_session_name = "" [bank_config.eps] stripe = { banks = "arzte_und_apotheker_bank,austrian_anadi_bank_ag,bank_austria,bankhaus_carl_spangler,bankhaus_schelhammer_und_schattera_ag,bawag_psk_ag,bks_bank_ag,brull_kallmus_bank_ag,btv_vier_lander_bank,capital_bank_grawe_gruppe_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_alpeadriabank_international_ag,hypo_noe_lb_fur_niederosterreich_u_wien,hypo_oberosterreich_salzburg_steiermark,hypo_tirol_bank_ag,hypo_vorarlberg_bank_ag,hypo_bank_burgenland_aktiengesellschaft,marchfelder_bank,oberbank_ag,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag,vr_bank_braunau" } diff --git a/crates/external_services/Cargo.toml b/crates/external_services/Cargo.toml index 4700c2a81d75..54a636a382b2 100644 --- a/crates/external_services/Cargo.toml +++ b/crates/external_services/Cargo.toml @@ -16,6 +16,7 @@ async-trait = "0.1.68" aws-config = { version = "0.55.3", optional = true } aws-sdk-kms = { version = "0.28.0", optional = true } aws-sdk-sesv2 = "0.28.0" +aws-sdk-sts = "0.28.0" aws-smithy-client = "0.55.3" base64 = "0.21.2" dyn-clone = "1.0.11" @@ -24,6 +25,8 @@ once_cell = "1.18.0" serde = { version = "1.0.163", features = ["derive"] } thiserror = "1.0.40" tokio = "1.28.2" +hyper-proxy = "0.9.1" +hyper = "0.14.26" # First party crates common_utils = { version = "0.1.0", path = "../common_utils" } diff --git a/crates/external_services/src/email.rs b/crates/external_services/src/email.rs index b2bf99d8e01d..1d389f58298a 100644 --- a/crates/external_services/src/email.rs +++ b/crates/external_services/src/email.rs @@ -1,127 +1,163 @@ //! Interactions with the AWS SES SDK -use aws_config::meta::region::RegionProviderChain; -use aws_sdk_sesv2::{ - config::Region, - operation::send_email::SendEmailError, - types::{Body, Content, Destination, EmailContent, Message}, - Client, -}; +use aws_sdk_sesv2::types::Body; use common_utils::{errors::CustomResult, pii}; -use error_stack::{IntoReport, ResultExt}; -use masking::PeekInterface; use serde::Deserialize; +/// Implementation of aws ses client +pub mod ses; + /// Custom Result type alias for Email operations. pub type EmailResult = CustomResult; /// A trait that defines the methods that must be implemented to send email. #[async_trait::async_trait] pub trait EmailClient: Sync + Send + dyn_clone::DynClone { + /// The rich text type of the email client + type RichText; + /// Sends an email to the specified recipient with the given subject and body. async fn send_email( &self, recipient: pii::Email, subject: String, - body: String, + body: Self::RichText, + proxy_url: Option<&String>, + ) -> EmailResult<()>; + + /// Convert Stringified HTML to client native rich text format + /// This has to be done because not all clients may format html as the same + fn convert_to_rich_text( + &self, + intermediate_string: IntermediateString, + ) -> CustomResult + where + Self::RichText: Send; +} + +/// A super trait which is automatically implemented for all EmailClients +#[async_trait::async_trait] +pub trait EmailService: Sync + Send + dyn_clone::DynClone { + /// Compose and send email using the email data + async fn compose_and_send_email( + &self, + email_data: Box, + proxy_url: Option<&String>, ) -> EmailResult<()>; } -dyn_clone::clone_trait_object!(EmailClient); +#[async_trait::async_trait] +impl EmailService for T +where + T: EmailClient, + ::RichText: Send, +{ + async fn compose_and_send_email( + &self, + email_data: Box, + proxy_url: Option<&String>, + ) -> EmailResult<()> { + let email_data = email_data.get_email_data(); + let email_data = email_data.await?; + + let EmailContents { + subject, + body, + recipient, + } = email_data; + + let rich_text_string = self.convert_to_rich_text(body)?; + + self.send_email(recipient, subject, rich_text_string, proxy_url) + .await + } +} + +/// This is a struct used to create Intermediate String for rich text ( html ) +#[derive(Debug)] +pub struct IntermediateString(String); + +impl IntermediateString { + /// Create a new Instance of IntermediateString using a string + pub fn new(inner: String) -> Self { + Self(inner) + } + + /// Get the inner String + pub fn into_inner(self) -> String { + self.0 + } +} + +/// Temporary output for the email subject +#[derive(Debug)] +pub struct EmailContents { + /// The subject of email + pub subject: String, + + /// This will be the intermediate representation of the the email body in a generic format. + /// The email clients can convert this intermediate representation to their client specific rich text format + pub body: IntermediateString, + + /// The email of the recipient to whom the email has to be sent + pub recipient: pii::Email, +} + +/// A trait which will contain the logic of generating the email subject and body +#[async_trait::async_trait] +pub trait EmailData { + /// Get the email contents + async fn get_email_data(&self) -> CustomResult; +} + +dyn_clone::clone_trait_object!(EmailClient); + +/// List of available email clients to choose from +#[derive(Debug, Clone, Default, Deserialize)] +pub enum AvailableEmailClients { + #[default] + /// AWS ses email client + SES, +} /// Struct that contains the settings required to construct an EmailClient. #[derive(Debug, Clone, Default, Deserialize)] pub struct EmailSettings { - /// Sender email. - pub from_email: String, - /// The AWS region to send SES requests to. pub aws_region: String, /// Base-url used when adding links that should redirect to self pub base_url: String, -} -/// Client for AWS SES operation -#[derive(Debug, Clone)] -pub struct AwsSes { - ses_client: Client, - from_email: String, -} + /// Number of days for verification of the email + pub allowed_unverified_days: i64, -impl AwsSes { - /// Constructs a new AwsSes client - pub async fn new(conf: &EmailSettings) -> Self { - let region_provider = RegionProviderChain::first_try(Region::new(conf.aws_region.clone())); - let sdk_config = aws_config::from_env().region(region_provider).load().await; + /// Sender email + pub sender_email: String, - Self { - ses_client: Client::new(&sdk_config), - from_email: conf.from_email.clone(), - } - } -} + /// Configs related to AWS Simple Email Service + pub aws_ses: Option, -#[async_trait::async_trait] -impl EmailClient for AwsSes { - async fn send_email( - &self, - recipient: pii::Email, - subject: String, - body: String, - ) -> EmailResult<()> { - self.ses_client - .send_email() - .from_email_address(self.from_email.to_owned()) - .destination( - Destination::builder() - .to_addresses(recipient.peek()) - .build(), - ) - .content( - EmailContent::builder() - .simple( - Message::builder() - .subject(Content::builder().data(subject).build()) - .body( - Body::builder() - .text(Content::builder().data(body).charset("UTF-8").build()) - .build(), - ) - .build(), - ) - .build(), - ) - .send() - .await - .map_err(AwsSesError::SendingFailure) - .into_report() - .change_context(EmailError::EmailSendingFailure)?; - - Ok(()) - } + /// The active email client to use + pub active_email_client: AvailableEmailClients, } -#[allow(missing_docs)] /// Errors that could occur from EmailClient. #[derive(Debug, thiserror::Error)] pub enum EmailError { /// An error occurred when building email client. #[error("Error building email client")] ClientBuildingFailure, + /// An error occurred when sending email #[error("Error sending email to recipient")] EmailSendingFailure, + + /// Failed to generate the email token #[error("Failed to generate email token")] TokenGenerationFailure, + + /// The expected feature is not implemented #[error("Feature not implemented")] NotImplemented, } - -/// Errors that could occur during SES operations. -#[derive(Debug, thiserror::Error)] -pub enum AwsSesError { - /// An error occurred in the SDK while sending email. - #[error("Failed to Send Email {0:?}")] - SendingFailure(aws_smithy_client::SdkError), -} diff --git a/crates/external_services/src/email/ses.rs b/crates/external_services/src/email/ses.rs new file mode 100644 index 000000000000..7e521a5bc1c4 --- /dev/null +++ b/crates/external_services/src/email/ses.rs @@ -0,0 +1,257 @@ +use std::time::{Duration, SystemTime}; + +use aws_sdk_sesv2::{ + config::Region, + operation::send_email::SendEmailError, + types::{Body, Content, Destination, EmailContent, Message}, + Client, +}; +use aws_sdk_sts::config::Credentials; +use common_utils::{errors::CustomResult, ext_traits::OptionExt, pii}; +use error_stack::{report, IntoReport, ResultExt}; +use hyper::Uri; +use masking::PeekInterface; +use router_env::logger; +use tokio::sync::OnceCell; + +use crate::email::{EmailClient, EmailError, EmailResult, EmailSettings, IntermediateString}; + +/// Client for AWS SES operation +#[derive(Debug, Clone)] +pub struct AwsSes { + ses_client: OnceCell, + sender: String, + settings: EmailSettings, +} + +/// Struct that contains the AWS ses specific configs required to construct an SES email client +#[derive(Debug, Clone, Default, serde::Deserialize)] +pub struct SESConfig { + /// The arn of email role + pub email_role_arn: String, + + /// The name of sts_session role + pub sts_role_session_name: String, +} + +/// Errors that could occur during SES operations. +#[derive(Debug, thiserror::Error)] +pub enum AwsSesError { + /// An error occurred in the SDK while sending email. + #[error("Failed to Send Email {0:?}")] + SendingFailure(aws_smithy_client::SdkError), + + /// Configuration variable is missing to construct the email client + #[error("Missing configuration variable {0}")] + MissingConfigurationVariable(&'static str), + + /// Failed to assume the given STS role + #[error("Failed to STS assume role: Role ARN: {role_arn}, Session name: {session_name}, Region: {region}")] + AssumeRoleFailure { + /// Aws region + region: String, + + /// arn of email role + role_arn: String, + + /// The name of sts_session role + session_name: String, + }, + + /// Temporary credentials are missing + #[error("Assumed role does not contain credentials for role user: {0:?}")] + TemporaryCredentialsMissing(String), + + /// The proxy Connector cannot be built + #[error("The proxy build cannot be built")] + BuildingProxyConnectorFailed, +} + +impl AwsSes { + /// Constructs a new AwsSes client + pub async fn create(conf: &EmailSettings, proxy_url: Option>) -> Self { + Self { + ses_client: OnceCell::new_with( + Self::create_client(conf, proxy_url) + .await + .map_err(|error| logger::error!(?error, "Failed to initialize SES Client")) + .ok(), + ), + sender: conf.sender_email.clone(), + settings: conf.clone(), + } + } + + /// A helper function to create ses client + pub async fn create_client( + conf: &EmailSettings, + proxy_url: Option>, + ) -> CustomResult { + let sts_config = Self::get_shared_config(conf.aws_region.to_owned(), proxy_url.as_ref())? + .load() + .await; + + let ses_config = conf + .aws_ses + .as_ref() + .get_required_value("aws ses configuration") + .attach_printable("The selected email client is aws ses, but configuration is missing") + .change_context(AwsSesError::MissingConfigurationVariable("aws_ses"))?; + + let role = aws_sdk_sts::Client::new(&sts_config) + .assume_role() + .role_arn(&ses_config.email_role_arn) + .role_session_name(&ses_config.sts_role_session_name) + .send() + .await + .into_report() + .change_context(AwsSesError::AssumeRoleFailure { + region: conf.aws_region.to_owned(), + role_arn: ses_config.email_role_arn.to_owned(), + session_name: ses_config.sts_role_session_name.to_owned(), + })?; + + let creds = role.credentials().ok_or( + report!(AwsSesError::TemporaryCredentialsMissing(format!( + "{role:?}" + ))) + .attach_printable("Credentials object not available"), + )?; + + let credentials = Credentials::new( + creds + .access_key_id() + .ok_or( + report!(AwsSesError::TemporaryCredentialsMissing(format!( + "{role:?}" + ))) + .attach_printable("Access Key ID not found"), + )? + .to_owned(), + creds + .secret_access_key() + .ok_or( + report!(AwsSesError::TemporaryCredentialsMissing(format!( + "{role:?}" + ))) + .attach_printable("Secret Access Key not found"), + )? + .to_owned(), + creds.session_token().map(|s| s.to_owned()), + creds.expiration().and_then(|dt| { + SystemTime::UNIX_EPOCH + .checked_add(Duration::from_nanos(u64::try_from(dt.as_nanos()).ok()?)) + }), + "custom_provider", + ); + + logger::debug!( + "Obtained SES temporary credentials with expiry {:?}", + credentials.expiry() + ); + + let ses_config = Self::get_shared_config(conf.aws_region.to_owned(), proxy_url)? + .credentials_provider(credentials) + .load() + .await; + + Ok(Client::new(&ses_config)) + } + + fn get_shared_config( + region: String, + proxy_url: Option>, + ) -> CustomResult { + let region_provider = Region::new(region); + let mut config = aws_config::from_env().region(region_provider); + if let Some(proxy_url) = proxy_url { + let proxy_connector = Self::get_proxy_connector(proxy_url)?; + let provider_config = aws_config::provider_config::ProviderConfig::default() + .with_tcp_connector(proxy_connector.clone()); + let http_connector = + aws_smithy_client::hyper_ext::Adapter::builder().build(proxy_connector); + config = config + .configure(provider_config) + .http_connector(http_connector); + }; + Ok(config) + } + + fn get_proxy_connector( + proxy_url: impl AsRef, + ) -> CustomResult, AwsSesError> { + let proxy_uri = proxy_url + .as_ref() + .parse::() + .into_report() + .attach_printable("Unable to parse the proxy url {proxy_url}") + .change_context(AwsSesError::BuildingProxyConnectorFailed)?; + + let proxy = hyper_proxy::Proxy::new(hyper_proxy::Intercept::All, proxy_uri); + + hyper_proxy::ProxyConnector::from_proxy(hyper::client::HttpConnector::new(), proxy) + .into_report() + .change_context(AwsSesError::BuildingProxyConnectorFailed) + } +} + +#[async_trait::async_trait] +impl EmailClient for AwsSes { + type RichText = Body; + + fn convert_to_rich_text( + &self, + intermediate_string: IntermediateString, + ) -> CustomResult { + let email_body = Body::builder() + .html( + Content::builder() + .data(intermediate_string.into_inner()) + .charset("UTF-8") + .build(), + ) + .build(); + + Ok(email_body) + } + + async fn send_email( + &self, + recipient: pii::Email, + subject: String, + body: Self::RichText, + proxy_url: Option<&String>, + ) -> EmailResult<()> { + self.ses_client + .get_or_try_init(|| async { + Self::create_client(&self.settings, proxy_url) + .await + .change_context(EmailError::ClientBuildingFailure) + }) + .await? + .send_email() + .from_email_address(self.sender.to_owned()) + .destination( + Destination::builder() + .to_addresses(recipient.peek()) + .build(), + ) + .content( + EmailContent::builder() + .simple( + Message::builder() + .subject(Content::builder().data(subject).build()) + .body(body) + .build(), + ) + .build(), + ) + .send() + .await + .map_err(AwsSesError::SendingFailure) + .into_report() + .change_context(EmailError::EmailSendingFailure)?; + + Ok(()) + } +} diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index a5f8b2f6b847..b51dc045b20d 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -12,7 +12,7 @@ license.workspace = true default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "profile_specific_fallback_routing", "retry"] s3 = ["dep:aws-sdk-s3", "dep:aws-config"] kms = ["external_services/kms", "dep:aws-config"] -email = ["external_services/email", "dep:aws-config"] +email = ["external_services/email", "dep:aws-config", "olap"] basilisk = ["kms"] stripe = ["dep:serde_qs"] release = ["kms", "stripe", "basilisk", "s3", "email", "business_profile_routing", "accounts_cache", "kv_store", "profile_specific_fallback_routing"] diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 4f19562c83ce..61072d06221b 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -62,4 +62,6 @@ pub const LOCKER_REDIS_EXPIRY_SECONDS: u32 = 60 * 15; // 15 minutes #[cfg(any(feature = "olap", feature = "oltp"))] pub const JWT_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24 * 2; // 2 days +#[cfg(feature = "email")] +pub const EMAIL_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24; // 1 day pub const ROLE_ID_ORGANIZATION_ADMIN: &str = "org_admin"; diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 94cd482a2291..1dc0e2e1a112 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -70,6 +70,28 @@ pub async fn connect_account( .get_jwt_auth_token(state.clone(), user_role.org_id) .await?; + #[cfg(feature = "email")] + { + use router_env::logger; + + use crate::services::email::types as email_types; + + let email_contents = email_types::WelcomeEmail { + recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?, + settings: state.conf.clone(), + }; + + let send_email_result = state + .email_client + .compose_and_send_email( + Box::new(email_contents), + state.conf.proxy.https_url.as_ref(), + ) + .await; + + logger::info!(?send_email_result); + } + return Ok(ApplicationResponse::Json(api::ConnectAccountResponse { token: Secret::new(jwt_token), merchant_id: user_role.merchant_id, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index ae0e0f04f598..1a6f36363d1d 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use actix_web::{web, Scope}; #[cfg(feature = "email")] -use external_services::email::{AwsSes, EmailClient}; +use external_services::email::{ses::AwsSes, EmailService}; #[cfg(feature = "kms")] use external_services::kms::{self, decrypt::KmsDecrypt}; use router_env::tracing_actix_web::RequestId; @@ -45,7 +45,7 @@ pub struct AppState { pub conf: Arc, pub event_handler: Box, #[cfg(feature = "email")] - pub email_client: Arc, + pub email_client: Arc, #[cfg(feature = "kms")] pub kms_secrets: Arc, pub api_client: Box, @@ -64,7 +64,7 @@ pub trait AppStateInfo { fn store(&self) -> Box; fn event_handler(&self) -> Box; #[cfg(feature = "email")] - fn email_client(&self) -> Arc; + fn email_client(&self) -> Arc; fn add_request_id(&mut self, request_id: RequestId); fn add_merchant_id(&mut self, merchant_id: Option); fn add_flow_name(&mut self, flow_name: String); @@ -79,7 +79,7 @@ impl AppStateInfo for AppState { self.store.to_owned() } #[cfg(feature = "email")] - fn email_client(&self) -> Arc { + fn email_client(&self) -> Arc { self.email_client.to_owned() } fn event_handler(&self) -> Box { @@ -107,6 +107,15 @@ impl AsRef for AppState { } } +#[cfg(feature = "email")] +pub async fn create_email_client(settings: &settings::Settings) -> impl EmailService { + match settings.email.active_email_client { + external_services::email::AvailableEmailClients::SES => { + AwsSes::create(&settings.email, settings.proxy.https_url.to_owned()).await + } + } +} + impl AppState { /// # Panics /// @@ -154,7 +163,8 @@ impl AppState { .expect("Failed while performing KMS decryption"); #[cfg(feature = "email")] - let email_client = Arc::new(AwsSes::new(&conf.email).await); + let email_client = Arc::new(create_email_client(&conf).await); + Self { flow_name: String::from("default"), store, diff --git a/crates/router/src/services.rs b/crates/router/src/services.rs index 2d5552b59d17..faea707f2a14 100644 --- a/crates/router/src/services.rs +++ b/crates/router/src/services.rs @@ -6,6 +6,9 @@ pub mod encryption; pub mod jwt; pub mod logger; +#[cfg(feature = "email")] +pub mod email; + #[cfg(feature = "kms")] use data_models::errors::StorageError; use data_models::errors::StorageResult; diff --git a/crates/router/src/services/email.rs b/crates/router/src/services/email.rs new file mode 100644 index 000000000000..cd408564ea08 --- /dev/null +++ b/crates/router/src/services/email.rs @@ -0,0 +1 @@ +pub mod types; diff --git a/crates/router/src/services/email/assets/invite.html b/crates/router/src/services/email/assets/invite.html new file mode 100644 index 000000000000..307ec6cead85 --- /dev/null +++ b/crates/router/src/services/email/assets/invite.html @@ -0,0 +1,243 @@ + +Welcome to HyperSwitch! + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Welcome to HyperSwitch! +
+
+ Hi {username}
+
+
+ You have received this email because your administrator has invited you as a new user on + Hyperswitch. +
+
+
+ To get started, click on the button below. +
+ + + + +
+ Click here to Join +
+
+
+ If the link has already expired, you can request a new link from your administrator or reach out to + your internal support for more assistance.
+
+ Thanks,
+ Team Hyperswitch +
+
+ diff --git a/crates/router/src/services/email/assets/magic_link.html b/crates/router/src/services/email/assets/magic_link.html new file mode 100644 index 000000000000..6439c83f227c --- /dev/null +++ b/crates/router/src/services/email/assets/magic_link.html @@ -0,0 +1,260 @@ + +Login to Hyperswitch + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Welcome to Hyperswitch! +

Dear {user_name},

+ We are thrilled to welcome you into our community! + +
+
+ Simply click on the link below, and you'll be granted instant access + to your Hyperswitch account. Note that this link expires in 24 hours + and can only be used once.
+
+ + + + +
+ Unlock Hyperswitch +
+
+ Thanks,
+ Team Hyperswitch +
+
+ diff --git a/crates/router/src/services/email/assets/recon_activated.html b/crates/router/src/services/email/assets/recon_activated.html new file mode 100644 index 000000000000..7feffacb09df --- /dev/null +++ b/crates/router/src/services/email/assets/recon_activated.html @@ -0,0 +1,309 @@ + +Access Granted to HyperSwitch Recon Dashboard! + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Access Granted to HyperSwitch Recon Dashboard! +
+
+ Dear {username}
+
+
+ We are pleased to inform you that your Reconciliation access request + has been approved. As a result, you now have authorized access to the + Recon dashboard, allowing you to test its functionality and experience + its benefits firsthand. +
+
+
+ To access the Recon dashboard, please follow these steps +
+
+
    +
  1. + Visit our website at + Hyperswitch Dashboard. +
  2. +
  3. Click on the "Login" button.
  4. +
  5. Enter your login credentials to log in.
  6. +
  7. + Once logged in, you will have full access to the Recon dashboard, + where you can explore its comprehensive features. +
  8. +
+ Should you have any inquiries or require any form of assistance, + please do not hesitate to reach out to our team on + Slack , + and we will be more than willing to assist you promptly.

+ Wishing you a seamless and successful experience as you explore the + capabilities of Hyperswitch.
+
+ Thanks,
+ Team Hyperswitch +
+
+ \ No newline at end of file diff --git a/crates/router/src/services/email/assets/reset.html b/crates/router/src/services/email/assets/reset.html new file mode 100644 index 000000000000..98ddf8a7bd16 --- /dev/null +++ b/crates/router/src/services/email/assets/reset.html @@ -0,0 +1,229 @@ + +Hyperswitch Merchant + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Reset Your Password +
+
+ Hey {username}
+
+
+ We have received a request to reset your password associated with +
+ username : + {username}
+
+
+ Click on the below button to reset your password.
+
+ + + + +
+ Reset Password +
+
+ Thanks,
+ Team Hyperswitch +
+
+ diff --git a/crates/router/src/services/email/assets/verify.html b/crates/router/src/services/email/assets/verify.html new file mode 100644 index 000000000000..47d0e3b5c6d5 --- /dev/null +++ b/crates/router/src/services/email/assets/verify.html @@ -0,0 +1,253 @@ + +Hyperswitch Merchant + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Thanks for signing up!
We need a confirmation of your email address to complete your + registration. +
+
+ Click below to confirm your email address.
+
+ + + + +
+ Verify Email Now +
+
+ Thanks,
+ Team Hyperswitch +
+
+ diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs new file mode 100644 index 000000000000..8650e1c27c22 --- /dev/null +++ b/crates/router/src/services/email/types.rs @@ -0,0 +1,80 @@ +use common_utils::errors::CustomResult; +use error_stack::ResultExt; +use external_services::email::{EmailContents, EmailData, EmailError}; +use masking::ExposeInterface; + +use crate::{configs, consts}; +#[cfg(feature = "olap")] +use crate::{core::errors::UserErrors, services::jwt, types::domain::UserEmail}; + +pub enum EmailBody { + Verify { link: String }, +} + +pub mod html { + use crate::services::email::types::EmailBody; + + pub fn get_html_body(email_body: EmailBody) -> String { + match email_body { + EmailBody::Verify { link } => { + format!(include_str!("assets/verify.html"), link = link) + } + } + } +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct EmailToken { + email: String, + expiration: u64, +} + +impl EmailToken { + pub async fn new_token( + email: UserEmail, + settings: &configs::settings::Settings, + ) -> CustomResult { + let expiration_duration = std::time::Duration::from_secs(consts::EMAIL_TOKEN_TIME_IN_SECS); + let expiration = jwt::generate_exp(expiration_duration)?.as_secs(); + let token_payload = Self { + email: email.get_secret().expose(), + expiration, + }; + jwt::generate_jwt(&token_payload, settings).await + } +} + +pub struct WelcomeEmail { + pub recipient_email: UserEmail, + pub settings: std::sync::Arc, +} + +pub fn get_email_verification_link( + base_url: impl std::fmt::Display, + token: impl std::fmt::Display, +) -> String { + format!("{base_url}/user/verify_email/?token={token}") +} + +/// Currently only HTML is supported +#[async_trait::async_trait] +impl EmailData for WelcomeEmail { + async fn get_email_data(&self) -> CustomResult { + let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings) + .await + .change_context(EmailError::TokenGenerationFailure)?; + + let verify_email_link = get_email_verification_link(&self.settings.server.base_url, token); + + let body = html::get_html_body(EmailBody::Verify { + link: verify_email_link, + }); + let subject = "Welcome to the Hyperswitch community!".to_string(); + + Ok(EmailContents { + subject, + body: external_services::email::IntermediateString::new(body), + recipient: self.recipient_email.clone().into_inner(), + }) + } +} From 2e57745352c547323ac2df2554f6bc2dbd6da37f Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Wed, 29 Nov 2023 16:52:35 +0530 Subject: [PATCH 125/146] fix(router): make use of warning to log errors when apple pay metadata parsing fails (#3010) --- crates/router/src/core/payments.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index db83dce487a6..33afa29397e1 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1554,7 +1554,7 @@ fn check_apple_pay_metadata( }) }) .map_err( - |error| logger::error!(%error, "Failed to Parse Value to ApplepaySessionTokenData"), + |error| logger::warn!(%error, "Failed to Parse Value to ApplepaySessionTokenData"), ); parsed_metadata.ok().map(|metadata| match metadata { From 9df4e0193ffeb6d1cc323bdebb7e2bdfb2a375e2 Mon Sep 17 00:00:00 2001 From: Sampras Lopes Date: Wed, 29 Nov 2023 17:04:53 +0530 Subject: [PATCH 126/146] feat(analytics): Add Clickhouse based analytics (#2988) Co-authored-by: harsh_sharma_juspay Co-authored-by: Ivor Dsouza Co-authored-by: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Co-authored-by: nain-F49FF806 <126972030+nain-F49FF806@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: akshay.s Co-authored-by: Gnanasundari24 <118818938+Gnanasundari24@users.noreply.github.com> --- Cargo.lock | 121 +- Dockerfile | 4 +- config/development.toml | 30 + config/docker_compose.toml | 18 +- crates/analytics/Cargo.toml | 37 + crates/analytics/docs/clickhouse/README.md | 45 + .../docs/clickhouse/cluster_setup/README.md | 347 +++ .../config/clickhouse_config.xml | 370 ++++ .../config/clickhouse_metrika.xml | 60 + .../cluster_setup/config/macros/macros-01.xml | 9 + .../cluster_setup/config/macros/macros-02.xml | 9 + .../cluster_setup/config/macros/macros-03.xml | 9 + .../cluster_setup/config/macros/macros-04.xml | 9 + .../cluster_setup/config/macros/macros-05.xml | 9 + .../cluster_setup/config/macros/macros-06.xml | 9 + .../clickhouse/cluster_setup/config/users.xml | 117 + .../cluster_setup/docker-compose.yml | 198 ++ .../clickhouse/cluster_setup/kafka-script.sh | 11 + .../cluster_setup/scripts/api_event_logs.sql | 237 ++ .../scripts/payment_attempts.sql | 217 ++ .../cluster_setup/scripts/payment_intents.sql | 165 ++ .../scripts/refund_analytics.sql | 173 ++ .../cluster_setup/scripts/sdk_events.sql | 156 ++ .../cluster_setup/scripts/seed_scripts.sql | 1 + .../docs/clickhouse/scripts/api_events_v2.sql | 134 ++ .../clickhouse/scripts/payment_attempts.sql | 156 ++ .../clickhouse/scripts/payment_intents.sql | 116 + .../docs/clickhouse/scripts/refunds.sql | 121 ++ crates/analytics/src/api_event.rs | 9 + crates/analytics/src/api_event/core.rs | 176 ++ crates/analytics/src/api_event/events.rs | 105 + crates/analytics/src/api_event/filters.rs | 53 + crates/analytics/src/api_event/metrics.rs | 110 + .../src/api_event/metrics/api_count.rs | 106 + .../src/api_event/metrics/latency.rs | 138 ++ .../api_event/metrics/status_code_count.rs | 103 + crates/analytics/src/api_event/types.rs | 33 + crates/analytics/src/clickhouse.rs | 458 ++++ crates/analytics/src/core.rs | 31 + .../src/analytics => analytics/src}/errors.rs | 0 crates/analytics/src/lambda_utils.rs | 36 + crates/analytics/src/lib.rs | 509 +++++ crates/analytics/src/main.rs | 3 + .../analytics => analytics/src}/metrics.rs | 0 .../src}/metrics/request.rs | 28 +- crates/analytics/src/payments.rs | 16 + .../src}/payments/accumulator.rs | 72 +- crates/analytics/src/payments/core.rs | 303 +++ crates/analytics/src/payments/distribution.rs | 92 + .../distribution/payment_error_message.rs | 176 ++ .../src}/payments/filters.rs | 10 +- .../src}/payments/metrics.rs | 41 +- .../src}/payments/metrics/avg_ticket_size.rs | 16 +- .../metrics/connector_success_rate.rs | 130 ++ .../src}/payments/metrics/payment_count.rs | 8 +- .../metrics/payment_processed_amount.rs | 10 +- .../payments/metrics/payment_success_count.rs | 10 +- .../src/payments/metrics/retries_count.rs | 122 ++ .../src}/payments/metrics/success_rate.rs | 8 +- .../src}/payments/types.rs | 11 +- .../src/analytics => analytics/src}/query.rs | 272 ++- .../analytics => analytics/src}/refunds.rs | 2 +- .../src}/refunds/accumulator.rs | 4 +- crates/analytics/src/refunds/core.rs | 203 ++ .../src}/refunds/filters.rs | 10 +- .../src}/refunds/metrics.rs | 13 +- .../src}/refunds/metrics/refund_count.rs | 9 +- .../metrics/refund_processed_amount.rs | 9 +- .../refunds/metrics/refund_success_count.rs | 9 +- .../refunds/metrics/refund_success_rate.rs | 7 +- .../src}/refunds/types.rs | 2 +- crates/analytics/src/sdk_events.rs | 14 + .../analytics/src/sdk_events/accumulator.rs | 98 + crates/analytics/src/sdk_events/core.rs | 201 ++ crates/analytics/src/sdk_events/events.rs | 80 + crates/analytics/src/sdk_events/filters.rs | 56 + crates/analytics/src/sdk_events/metrics.rs | 181 ++ .../metrics/average_payment_time.rs | 129 ++ .../sdk_events/metrics/payment_attempts.rs | 118 + .../metrics/payment_data_filled_count.rs | 118 + .../metrics/payment_method_selected_count.rs | 118 + .../metrics/payment_methods_call_count.rs | 126 ++ .../metrics/payment_success_count.rs | 118 + .../sdk_events/metrics/sdk_initiated_count.rs | 118 + .../sdk_events/metrics/sdk_rendered_count.rs | 118 + crates/analytics/src/sdk_events/types.rs | 50 + .../src/analytics => analytics/src}/sqlx.rs | 189 +- .../src/analytics => analytics/src}/types.rs | 26 +- .../src/analytics => analytics/src}/utils.rs | 18 + crates/api_models/src/analytics.rs | 154 +- crates/api_models/src/analytics/api_event.rs | 148 ++ crates/api_models/src/analytics/payments.rs | 52 +- crates/api_models/src/analytics/refunds.rs | 21 +- crates/api_models/src/analytics/sdk_events.rs | 215 ++ crates/api_models/src/events.rs | 36 +- crates/api_models/src/payments.rs | 2 + crates/data_models/Cargo.toml | 1 - crates/router/Cargo.toml | 4 +- crates/router/src/analytics.rs | 655 +++++- crates/router/src/analytics/core.rs | 96 - crates/router/src/analytics/payments.rs | 13 - crates/router/src/analytics/payments/core.rs | 129 -- crates/router/src/analytics/refunds/core.rs | 104 - crates/router/src/analytics/routes.rs | 164 -- crates/router/src/bin/scheduler.rs | 2 - crates/router/src/configs/kms.rs | 2 +- crates/router/src/configs/settings.rs | 25 +- crates/router/src/core/refunds.rs | 4 +- crates/router/src/core/webhooks.rs | 2 + crates/router/src/db.rs | 94 +- crates/router/src/db/kafka_store.rs | 1917 +++++++++++++++++ crates/router/src/events.rs | 64 +- crates/router/src/events/api_logs.rs | 6 + crates/router/src/events/event_logger.rs | 2 +- crates/router/src/events/kafka_handler.rs | 29 + crates/router/src/lib.rs | 9 +- crates/router/src/routes/app.rs | 49 +- crates/router/src/services.rs | 1 + crates/router/src/services/api.rs | 2 + crates/router/src/services/kafka.rs | 314 +++ crates/router/src/services/kafka/api_event.rs | 108 + .../src/services/kafka/outgoing_request.rs | 19 + .../src/services/kafka/payment_attempt.rs | 92 + .../src/services/kafka/payment_intent.rs | 71 + crates/router/src/services/kafka/refund.rs | 68 + .../src/types/storage/payment_attempt.rs | 4 - crates/router/tests/connectors/aci.rs | 4 + crates/router/tests/connectors/utils.rs | 9 + crates/router/tests/payments2.rs | 1 + crates/router/tests/utils.rs | 1 + crates/router_env/src/lib.rs | 13 +- crates/scheduler/Cargo.toml | 2 +- crates/storage_impl/src/config.rs | 38 +- crates/storage_impl/src/database/store.rs | 2 +- docker-compose.yml | 63 + 135 files changed, 12141 insertions(+), 897 deletions(-) create mode 100644 crates/analytics/Cargo.toml create mode 100644 crates/analytics/docs/clickhouse/README.md create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/README.md create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/config/clickhouse_config.xml create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/config/clickhouse_metrika.xml create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-01.xml create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-02.xml create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-03.xml create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-04.xml create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-05.xml create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-06.xml create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/config/users.xml create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/docker-compose.yml create mode 100755 crates/analytics/docs/clickhouse/cluster_setup/kafka-script.sh create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/scripts/api_event_logs.sql create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/scripts/payment_attempts.sql create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/scripts/payment_intents.sql create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/scripts/refund_analytics.sql create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/scripts/sdk_events.sql create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/scripts/seed_scripts.sql create mode 100644 crates/analytics/docs/clickhouse/scripts/api_events_v2.sql create mode 100644 crates/analytics/docs/clickhouse/scripts/payment_attempts.sql create mode 100644 crates/analytics/docs/clickhouse/scripts/payment_intents.sql create mode 100644 crates/analytics/docs/clickhouse/scripts/refunds.sql create mode 100644 crates/analytics/src/api_event.rs create mode 100644 crates/analytics/src/api_event/core.rs create mode 100644 crates/analytics/src/api_event/events.rs create mode 100644 crates/analytics/src/api_event/filters.rs create mode 100644 crates/analytics/src/api_event/metrics.rs create mode 100644 crates/analytics/src/api_event/metrics/api_count.rs create mode 100644 crates/analytics/src/api_event/metrics/latency.rs create mode 100644 crates/analytics/src/api_event/metrics/status_code_count.rs create mode 100644 crates/analytics/src/api_event/types.rs create mode 100644 crates/analytics/src/clickhouse.rs create mode 100644 crates/analytics/src/core.rs rename crates/{router/src/analytics => analytics/src}/errors.rs (100%) create mode 100644 crates/analytics/src/lambda_utils.rs create mode 100644 crates/analytics/src/lib.rs create mode 100644 crates/analytics/src/main.rs rename crates/{router/src/analytics => analytics/src}/metrics.rs (100%) rename crates/{router/src/analytics => analytics/src}/metrics/request.rs (51%) create mode 100644 crates/analytics/src/payments.rs rename crates/{router/src/analytics => analytics/src}/payments/accumulator.rs (62%) create mode 100644 crates/analytics/src/payments/core.rs create mode 100644 crates/analytics/src/payments/distribution.rs create mode 100644 crates/analytics/src/payments/distribution/payment_error_message.rs rename crates/{router/src/analytics => analytics/src}/payments/filters.rs (87%) rename crates/{router/src/analytics => analytics/src}/payments/metrics.rs (76%) rename crates/{router/src/analytics => analytics/src}/payments/metrics/avg_ticket_size.rs (90%) create mode 100644 crates/analytics/src/payments/metrics/connector_success_rate.rs rename crates/{router/src/analytics => analytics/src}/payments/metrics/payment_count.rs (94%) rename crates/{router/src/analytics => analytics/src}/payments/metrics/payment_processed_amount.rs (94%) rename crates/{router/src/analytics => analytics/src}/payments/metrics/payment_success_count.rs (94%) create mode 100644 crates/analytics/src/payments/metrics/retries_count.rs rename crates/{router/src/analytics => analytics/src}/payments/metrics/success_rate.rs (95%) rename crates/{router/src/analytics => analytics/src}/payments/types.rs (82%) rename crates/{router/src/analytics => analytics/src}/query.rs (65%) rename crates/{router/src/analytics => analytics/src}/refunds.rs (81%) rename crates/{router/src/analytics => analytics/src}/refunds/accumulator.rs (98%) create mode 100644 crates/analytics/src/refunds/core.rs rename crates/{router/src/analytics => analytics/src}/refunds/filters.rs (90%) rename crates/{router/src/analytics => analytics/src}/refunds/metrics.rs (91%) rename crates/{router/src/analytics => analytics/src}/refunds/metrics/refund_count.rs (94%) rename crates/{router/src/analytics => analytics/src}/refunds/metrics/refund_processed_amount.rs (95%) rename crates/{router/src/analytics => analytics/src}/refunds/metrics/refund_success_count.rs (95%) rename crates/{router/src/analytics => analytics/src}/refunds/metrics/refund_success_rate.rs (96%) rename crates/{router/src/analytics => analytics/src}/refunds/types.rs (98%) create mode 100644 crates/analytics/src/sdk_events.rs create mode 100644 crates/analytics/src/sdk_events/accumulator.rs create mode 100644 crates/analytics/src/sdk_events/core.rs create mode 100644 crates/analytics/src/sdk_events/events.rs create mode 100644 crates/analytics/src/sdk_events/filters.rs create mode 100644 crates/analytics/src/sdk_events/metrics.rs create mode 100644 crates/analytics/src/sdk_events/metrics/average_payment_time.rs create mode 100644 crates/analytics/src/sdk_events/metrics/payment_attempts.rs create mode 100644 crates/analytics/src/sdk_events/metrics/payment_data_filled_count.rs create mode 100644 crates/analytics/src/sdk_events/metrics/payment_method_selected_count.rs create mode 100644 crates/analytics/src/sdk_events/metrics/payment_methods_call_count.rs create mode 100644 crates/analytics/src/sdk_events/metrics/payment_success_count.rs create mode 100644 crates/analytics/src/sdk_events/metrics/sdk_initiated_count.rs create mode 100644 crates/analytics/src/sdk_events/metrics/sdk_rendered_count.rs create mode 100644 crates/analytics/src/sdk_events/types.rs rename crates/{router/src/analytics => analytics/src}/sqlx.rs (64%) rename crates/{router/src/analytics => analytics/src}/types.rs (83%) rename crates/{router/src/analytics => analytics/src}/utils.rs (52%) create mode 100644 crates/api_models/src/analytics/api_event.rs create mode 100644 crates/api_models/src/analytics/sdk_events.rs delete mode 100644 crates/router/src/analytics/core.rs delete mode 100644 crates/router/src/analytics/payments.rs delete mode 100644 crates/router/src/analytics/payments/core.rs delete mode 100644 crates/router/src/analytics/refunds/core.rs delete mode 100644 crates/router/src/analytics/routes.rs create mode 100644 crates/router/src/db/kafka_store.rs create mode 100644 crates/router/src/events/kafka_handler.rs create mode 100644 crates/router/src/services/kafka.rs create mode 100644 crates/router/src/services/kafka/api_event.rs create mode 100644 crates/router/src/services/kafka/outgoing_request.rs create mode 100644 crates/router/src/services/kafka/payment_attempt.rs create mode 100644 crates/router/src/services/kafka/payment_intent.rs create mode 100644 crates/router/src/services/kafka/refund.rs diff --git a/Cargo.lock b/Cargo.lock index 96bdcff3f86e..417e6d85db6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -332,6 +332,36 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +[[package]] +name = "analytics" +version = "0.1.0" +dependencies = [ + "actix-web", + "api_models", + "async-trait", + "aws-config", + "aws-sdk-lambda", + "aws-smithy-types", + "bigdecimal", + "common_utils", + "diesel_models", + "error-stack", + "external_services", + "futures 0.3.28", + "masking", + "once_cell", + "reqwest", + "router_env", + "serde", + "serde_json", + "sqlx", + "storage_impl", + "strum 0.25.0", + "thiserror", + "time", + "tokio 1.32.0", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -729,6 +759,31 @@ dependencies = [ "tracing", ] +[[package]] +name = "aws-sdk-lambda" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3ad176ffaa3aafa532246eb6a9f18a7d68da19950704ecc95d33d9dc3c62a9b" +dependencies = [ + "aws-credential-types", + "aws-endpoint", + "aws-http", + "aws-sig-auth", + "aws-smithy-async", + "aws-smithy-client", + "aws-smithy-http", + "aws-smithy-http-tower", + "aws-smithy-json", + "aws-smithy-types", + "aws-types", + "bytes 1.5.0", + "http", + "regex", + "tokio-stream", + "tower", + "tracing", +] + [[package]] name = "aws-sdk-s3" version = "0.28.0" @@ -1148,6 +1203,7 @@ dependencies = [ "num-bigint", "num-integer", "num-traits", + "serde", ] [[package]] @@ -1256,7 +1312,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f404657a7ea7b5249e36808dff544bc88a28f26e0ac40009f674b7a009d14be3" dependencies = [ "once_cell", - "proc-macro-crate", + "proc-macro-crate 2.0.0", "proc-macro2", "quote", "syn 2.0.38", @@ -3862,6 +3918,27 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "object" version = "0.32.1" @@ -4395,6 +4472,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.10", +] + [[package]] name = "proc-macro-crate" version = "2.0.0" @@ -4688,6 +4775,36 @@ dependencies = [ "crossbeam-utils 0.8.16", ] +[[package]] +name = "rdkafka" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54f02a5a40220f8a2dfa47ddb38ba9064475a5807a69504b6f91711df2eea63" +dependencies = [ + "futures-channel", + "futures-util", + "libc", + "log", + "rdkafka-sys", + "serde", + "serde_derive", + "serde_json", + "slab", + "tokio 1.32.0", +] + +[[package]] +name = "rdkafka-sys" +version = "4.7.0+2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55e0d2f9ba6253f6ec72385e453294f8618e9e15c2c6aba2a5c01ccf9622d615" +dependencies = [ + "libc", + "libz-sys", + "num_enum", + "pkg-config", +] + [[package]] name = "redis-protocol" version = "4.1.0" @@ -4939,6 +5056,7 @@ dependencies = [ "actix-multipart", "actix-rt", "actix-web", + "analytics", "api_models", "argon2", "async-bb8-diesel", @@ -4988,6 +5106,7 @@ dependencies = [ "qrcode", "rand 0.8.5", "rand_chacha 0.3.1", + "rdkafka", "redis_interface", "regex", "reqwest", diff --git a/Dockerfile b/Dockerfile index 8eb321dd2afd..e9591e5e9f27 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:slim-bookworm as builder +FROM rust:bookworm as builder ARG EXTRA_FEATURES="" @@ -36,7 +36,7 @@ RUN cargo build --release --features release ${EXTRA_FEATURES} -FROM debian:bookworm-slim +FROM debian:bookworm # Placing config and binary executable in different directories ARG CONFIG_DIR=/local/config diff --git a/config/development.toml b/config/development.toml index f2620bd37135..fa5fddb0d60a 100644 --- a/config/development.toml +++ b/config/development.toml @@ -475,3 +475,33 @@ delay_between_retries_in_milliseconds = 500 [kv_config] ttl = 900 # 15 * 60 seconds + +[events] +source = "logs" + +[events.kafka] +brokers = ["localhost:9092"] +intent_analytics_topic = "hyperswitch-payment-intent-events" +attempt_analytics_topic = "hyperswitch-payment-attempt-events" +refund_analytics_topic = "hyperswitch-refund-events" +api_logs_topic = "hyperswitch-api-log-events" +connector_events_topic = "hyperswitch-connector-api-events" + +[analytics] +source = "sqlx" + +[analytics.clickhouse] +username = "default" +# password = "" +host = "http://localhost:8123" +database_name = "default" + +[analytics.sqlx] +username = "db_user" +password = "db_pass" +host = "localhost" +port = 5432 +dbname = "hyperswitch_db" +pool_size = 5 +connection_timeout = 10 +queue_strategy = "Fifo" \ No newline at end of file diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 445e1e856846..4d50600e1bf8 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -333,16 +333,32 @@ supported_connectors = "braintree" redis_lock_expiry_seconds = 180 # 3 * 60 seconds delay_between_retries_in_milliseconds = 500 +[events.kafka] +brokers = ["localhost:9092"] +intent_analytics_topic = "hyperswitch-payment-intent-events" +attempt_analytics_topic = "hyperswitch-payment-attempt-events" +refund_analytics_topic = "hyperswitch-refund-events" +api_logs_topic = "hyperswitch-api-log-events" +connector_events_topic = "hyperswitch-connector-api-events" + [analytics] source = "sqlx" +[analytics.clickhouse] +username = "default" +# password = "" +host = "http://localhost:8123" +database_name = "default" + [analytics.sqlx] username = "db_user" password = "db_pass" -host = "pg" +host = "localhost" port = 5432 dbname = "hyperswitch_db" pool_size = 5 +connection_timeout = 10 +queue_strategy = "Fifo" [kv_config] ttl = 900 # 15 * 60 seconds diff --git a/crates/analytics/Cargo.toml b/crates/analytics/Cargo.toml new file mode 100644 index 000000000000..f49fe322ae3b --- /dev/null +++ b/crates/analytics/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "analytics" +version = "0.1.0" +description = "Analytics / Reports related functionality" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + + +[dependencies] +# First party crates +api_models = { version = "0.1.0", path = "../api_models" , features = ["errors"]} +storage_impl = { version = "0.1.0", path = "../storage_impl", default-features = false } +common_utils = { version = "0.1.0", path = "../common_utils"} +external_services = { version = "0.1.0", path = "../external_services", default-features = false} +masking = { version = "0.1.0", path = "../masking" } +router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"] } +diesel_models = { version = "0.1.0", path = "../diesel_models", features = ["kv_store"] } + +#Third Party dependencies +actix-web = "4.3.1" +async-trait = "0.1.68" +aws-config = { version = "0.55.3" } +aws-sdk-lambda = { version = "0.28.0" } +aws-smithy-types = { version = "0.55.3" } +bigdecimal = { version = "0.3.1", features = ["serde"] } +error-stack = "0.3.1" +futures = "0.3.28" +once_cell = "1.18.0" +reqwest = { version = "0.11.18", features = ["serde_json"] } +serde = { version = "1.0.163", features = ["derive", "rc"] } +serde_json = "1.0.96" +sqlx = { version = "0.6.3", features = ["postgres", "runtime-actix", "runtime-actix-native-tls", "time", "bigdecimal"] } +strum = { version = "0.25.0", features = ["derive"] } +thiserror = "1.0.43" +time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } +tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] } diff --git a/crates/analytics/docs/clickhouse/README.md b/crates/analytics/docs/clickhouse/README.md new file mode 100644 index 000000000000..2fd48a30c29f --- /dev/null +++ b/crates/analytics/docs/clickhouse/README.md @@ -0,0 +1,45 @@ +#### Starting the containers + +In our use case we rely on kafka for ingesting events. +hence we can use docker compose to start all the components + +``` +docker compose up -d clickhouse-server kafka-ui +``` + +> kafka-ui is a visual tool for inspecting kafka on localhost:8090 + +#### Setting up Clickhouse + +Once clickhouse is up & running you need to create the required tables for it + +you can either visit the url (http://localhost:8123/play) in which the clickhouse-server is running to get a playground +Alternatively you can bash into the clickhouse container & execute commands manually +``` +# On your local terminal +docker compose exec clickhouse-server bash + +# Inside the clickhouse-server container shell +clickhouse-client --user default + +# Inside the clickhouse-client shell +SHOW TABLES; +CREATE TABLE ...... +``` + +The table creation scripts are provided [here](./scripts) + +#### Running/Debugging your application +Once setup you can run your application either via docker compose or normally via cargo run + +Remember to enable the kafka_events via development.toml/docker_compose.toml files + +Inspect the [kafka-ui](http://localhost:8090) to check the messages being inserted in queue + +If the messages/topic are available then you can run select queries on your clickhouse table to ensure data is being populated... + +If the data is not being populated in clickhouse, you can check the error logs in clickhouse server via +``` +# Inside the clickhouse-server container shell +tail -f /var/log/clickhouse-server/clickhouse-server.err.log +``` \ No newline at end of file diff --git a/crates/analytics/docs/clickhouse/cluster_setup/README.md b/crates/analytics/docs/clickhouse/cluster_setup/README.md new file mode 100644 index 000000000000..cd5f2dfeb023 --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/README.md @@ -0,0 +1,347 @@ +# Tutorial for set up clickhouse server + + +## Single server with docker + + +- Run server + +``` +docker run -d --name clickhouse-server -p 9000:9000 --ulimit nofile=262144:262144 yandex/clickhouse-server + +``` + +- Run client + +``` +docker run -it --rm --link clickhouse-server:clickhouse-server yandex/clickhouse-client --host clickhouse-server +``` + +Now you can see if it success setup or not. + + +## Setup Cluster + + +This part we will setup + +- 1 cluster, with 3 shards +- Each shard has 2 replica server +- Use ReplicatedMergeTree & Distributed table to setup our table. + + +### Cluster + +Let's see our docker-compose.yml first. + +``` +version: '3' + +services: + clickhouse-zookeeper: + image: zookeeper + ports: + - "2181:2181" + - "2182:2182" + container_name: clickhouse-zookeeper + hostname: clickhouse-zookeeper + + clickhouse-01: + image: yandex/clickhouse-server + hostname: clickhouse-01 + container_name: clickhouse-01 + ports: + - 9001:9000 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-01.xml:/etc/clickhouse-server/config.d/macros.xml + # - ./data/server-01:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + clickhouse-02: + image: yandex/clickhouse-server + hostname: clickhouse-02 + container_name: clickhouse-02 + ports: + - 9002:9000 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-02.xml:/etc/clickhouse-server/config.d/macros.xml + # - ./data/server-02:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + clickhouse-03: + image: yandex/clickhouse-server + hostname: clickhouse-03 + container_name: clickhouse-03 + ports: + - 9003:9000 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-03.xml:/etc/clickhouse-server/config.d/macros.xml + # - ./data/server-03:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + clickhouse-04: + image: yandex/clickhouse-server + hostname: clickhouse-04 + container_name: clickhouse-04 + ports: + - 9004:9000 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-04.xml:/etc/clickhouse-server/config.d/macros.xml + # - ./data/server-04:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + clickhouse-05: + image: yandex/clickhouse-server + hostname: clickhouse-05 + container_name: clickhouse-05 + ports: + - 9005:9000 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-05.xml:/etc/clickhouse-server/config.d/macros.xml + # - ./data/server-05:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + clickhouse-06: + image: yandex/clickhouse-server + hostname: clickhouse-06 + container_name: clickhouse-06 + ports: + - 9006:9000 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-06.xml:/etc/clickhouse-server/config.d/macros.xml + # - ./data/server-06:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" +networks: + default: + external: + name: clickhouse-net +``` + + +We have 6 clickhouse server container and one zookeeper container. + + +**To enable replication ZooKeeper is required. ClickHouse will take care of data consistency on all replicas and run restore procedure after failure automatically. It's recommended to deploy ZooKeeper cluster to separate servers.** + +**ZooKeeper is not a requirement — in some simple cases you can duplicate the data by writing it into all the replicas from your application code. This approach is not recommended — in this case ClickHouse is not able to guarantee data consistency on all replicas. This remains the responsibility of your application.** + + +Let's see config file. + +`./config/clickhouse_config.xml` is the default config file in docker, we copy it out and add this line + +``` + + /etc/clickhouse-server/metrika.xml +``` + + +So lets see `clickhouse_metrika.xml` + +``` + + + + + 1 + true + + clickhouse-01 + 9000 + + + clickhouse-06 + 9000 + + + + 1 + true + + clickhouse-02 + 9000 + + + clickhouse-03 + 9000 + + + + 1 + true + + + clickhouse-04 + 9000 + + + clickhouse-05 + 9000 + + + + + + + clickhouse-zookeeper + 2181 + + + + ::/0 + + + + 10000000000 + 0.01 + lz4 + + + +``` + +and macros.xml, each instances has there own macros settings, like server 1: + +``` + + + clickhouse-01 + 01 + 01 + + +``` + + +**Make sure your macros settings is equal to remote server settings in metrika.xml** + +So now you can start the server. + +``` +docker network create clickhouse-net +docker-compose up -d +``` + +Conn to server and see if the cluster settings fine; + +``` +docker run -it --rm --network="clickhouse-net" --link clickhouse-01:clickhouse-server yandex/clickhouse-client --host clickhouse-server +``` + +```sql +clickhouse-01 :) select * from system.clusters; + +SELECT * +FROM system.clusters + +┌─cluster─────────────────────┬─shard_num─┬─shard_weight─┬─replica_num─┬─host_name─────┬─host_address─┬─port─┬─is_local─┬─user────┬─default_database─┐ +│ cluster_1 │ 1 │ 1 │ 1 │ clickhouse-01 │ 172.21.0.4 │ 9000 │ 1 │ default │ │ +│ cluster_1 │ 1 │ 1 │ 2 │ clickhouse-06 │ 172.21.0.5 │ 9000 │ 1 │ default │ │ +│ cluster_1 │ 2 │ 1 │ 1 │ clickhouse-02 │ 172.21.0.8 │ 9000 │ 0 │ default │ │ +│ cluster_1 │ 2 │ 1 │ 2 │ clickhouse-03 │ 172.21.0.6 │ 9000 │ 0 │ default │ │ +│ cluster_1 │ 3 │ 1 │ 1 │ clickhouse-04 │ 172.21.0.7 │ 9000 │ 0 │ default │ │ +│ cluster_1 │ 3 │ 1 │ 2 │ clickhouse-05 │ 172.21.0.3 │ 9000 │ 0 │ default │ │ +│ test_shard_localhost │ 1 │ 1 │ 1 │ localhost │ 127.0.0.1 │ 9000 │ 1 │ default │ │ +│ test_shard_localhost_secure │ 1 │ 1 │ 1 │ localhost │ 127.0.0.1 │ 9440 │ 0 │ default │ │ +└─────────────────────────────┴───────────┴──────────────┴─────────────┴───────────────┴──────────────┴──────┴──────────┴─────────┴──────────────────┘ +``` + +If you see this, it means cluster's settings work well(but not conn fine). + + +### Replica Table + +So now we have a cluster and replica settings. For clickhouse, we need to create ReplicatedMergeTree Table as a local table in every server. + +```sql +CREATE TABLE ttt (id Int32) ENGINE = ReplicatedMergeTree('/clickhouse/tables/{layer}-{shard}/ttt', '{replica}') PARTITION BY id ORDER BY id +``` + +and Create Distributed Table conn to local table + +```sql +CREATE TABLE ttt_all as ttt ENGINE = Distributed(cluster_1, default, ttt, rand()); +``` + + +### Insert and test + +gen some data and test. + + +``` +# docker exec into client server 1 and +for ((idx=1;idx<=100;++idx)); do clickhouse-client --host clickhouse-server --query "Insert into default.ttt_all values ($idx)"; done; +``` + +For Distributed table. + +``` +select count(*) from ttt_all; +``` + +For loacl table. + +``` +select count(*) from ttt; +``` + + +## Authentication + +Please see config/users.xml + + +- Conn +```bash +docker run -it --rm --network="clickhouse-net" --link clickhouse-01:clickhouse-server yandex/clickhouse-client --host clickhouse-server -u user1 --password 123456 +``` + +## Source + +- https://clickhouse.yandex/docs/en/operations/table_engines/replication/#creating-replicated-tables diff --git a/crates/analytics/docs/clickhouse/cluster_setup/config/clickhouse_config.xml b/crates/analytics/docs/clickhouse/cluster_setup/config/clickhouse_config.xml new file mode 100644 index 000000000000..94c854dc273a --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/config/clickhouse_config.xml @@ -0,0 +1,370 @@ + + + + + error + 1000M + 1 + 10 + + + + 8123 + 9000 + + + + + + + + + /etc/clickhouse-server/server.crt + /etc/clickhouse-server/server.key + + /etc/clickhouse-server/dhparam.pem + none + true + true + sslv2,sslv3 + true + + + + true + true + sslv2,sslv3 + true + + + + RejectCertificateHandler + + + + + + + + + 9009 + + + + + + + + + + + + + + + + + + + + 4096 + 3 + + + 100 + + + + + + 8589934592 + + + 5368709120 + + + + /var/lib/clickhouse/ + + + /var/lib/clickhouse/tmp/ + + + /var/lib/clickhouse/user_files/ + + + users.xml + + + default + + + + + + default + + + + + + + + + + + + + + localhost + 9000 + + + + + + + localhost + 9440 + 1 + + + + + + + + /etc/clickhouse-server/metrika.xml + + + + + + + + + 3600 + + + + 3600 + + + 60 + + + + + + + + + + system + query_log
+ + toYYYYMM(event_date) + + 7500 +
+ + + + + + + + + + + + + + + + *_dictionary.xml + + + + + + + + + + /clickhouse/task_queue/ddl + + + + + + + + + + + + + + + + click_cost + any + + 0 + 3600 + + + 86400 + 60 + + + + max + + 0 + 60 + + + 3600 + 300 + + + 86400 + 3600 + + + + + + /var/lib/clickhouse/format_schemas/ + + + +
+ diff --git a/crates/analytics/docs/clickhouse/cluster_setup/config/clickhouse_metrika.xml b/crates/analytics/docs/clickhouse/cluster_setup/config/clickhouse_metrika.xml new file mode 100644 index 000000000000..b58ffc34bc29 --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/config/clickhouse_metrika.xml @@ -0,0 +1,60 @@ + + + + + 1 + true + + clickhouse-01 + 9000 + + + clickhouse-06 + 9000 + + + + 1 + true + + clickhouse-02 + 9000 + + + clickhouse-03 + 9000 + + + + 1 + true + + + clickhouse-04 + 9000 + + + clickhouse-05 + 9000 + + + + + + + clickhouse-zookeeper + 2181 + + + + ::/0 + + + + 10000000000 + 0.01 + lz4 + + + + diff --git a/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-01.xml b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-01.xml new file mode 100644 index 000000000000..75df1c5916e8 --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-01.xml @@ -0,0 +1,9 @@ + + + clickhouse-01 + 01 + 01 + data + cluster_1 + + diff --git a/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-02.xml b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-02.xml new file mode 100644 index 000000000000..67e4a545b30c --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-02.xml @@ -0,0 +1,9 @@ + + + clickhouse-02 + 02 + 01 + data + cluster_1 + + diff --git a/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-03.xml b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-03.xml new file mode 100644 index 000000000000..e9278191b80f --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-03.xml @@ -0,0 +1,9 @@ + + + clickhouse-03 + 02 + 01 + data + cluster_1 + + diff --git a/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-04.xml b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-04.xml new file mode 100644 index 000000000000..033c0ad1152e --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-04.xml @@ -0,0 +1,9 @@ + + + clickhouse-04 + 03 + 01 + data + cluster_1 + + diff --git a/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-05.xml b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-05.xml new file mode 100644 index 000000000000..c63314c5acea --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-05.xml @@ -0,0 +1,9 @@ + + + clickhouse-05 + 03 + 01 + data + cluster_1 + + diff --git a/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-06.xml b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-06.xml new file mode 100644 index 000000000000..4b01bda9948c --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-06.xml @@ -0,0 +1,9 @@ + + + clickhouse-06 + 01 + 01 + data + cluster_1 + + diff --git a/crates/analytics/docs/clickhouse/cluster_setup/config/users.xml b/crates/analytics/docs/clickhouse/cluster_setup/config/users.xml new file mode 100644 index 000000000000..e1b8de78e37a --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/config/users.xml @@ -0,0 +1,117 @@ + + + + + + + + 10000000000 + + + 0 + + + random + + + + + 1 + + + + + + + 123456 + + ::/0 + + default + default + + + + + + + + + ::/0 + + + + default + + + default + + + + + + + ::1 + 127.0.0.1 + + readonly + default + + + + + + + + + + + 3600 + + + 0 + 0 + 0 + 0 + 0 + + + + diff --git a/crates/analytics/docs/clickhouse/cluster_setup/docker-compose.yml b/crates/analytics/docs/clickhouse/cluster_setup/docker-compose.yml new file mode 100644 index 000000000000..96d7618b47e6 --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/docker-compose.yml @@ -0,0 +1,198 @@ +version: '3' + +networks: + ckh_net: + +services: + clickhouse-zookeeper: + image: zookeeper + ports: + - "2181:2181" + - "2182:2182" + container_name: clickhouse-zookeeper + hostname: clickhouse-zookeeper + networks: + - ckh_net + + clickhouse-01: + image: clickhouse/clickhouse-server + hostname: clickhouse-01 + container_name: clickhouse-01 + networks: + - ckh_net + ports: + - 9001:9000 + - 8124:8123 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-01.xml:/etc/clickhouse-server/config.d/macros.xml + - ./config/users.xml:/etc/clickhouse-server/users.xml + # - ./data/server-01:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + clickhouse-02: + image: clickhouse/clickhouse-server + hostname: clickhouse-02 + container_name: clickhouse-02 + networks: + - ckh_net + ports: + - 9002:9000 + - 8125:8123 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-02.xml:/etc/clickhouse-server/config.d/macros.xml + - ./config/users.xml:/etc/clickhouse-server/users.xml + # - ./data/server-02:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + clickhouse-03: + image: clickhouse/clickhouse-server + hostname: clickhouse-03 + container_name: clickhouse-03 + networks: + - ckh_net + ports: + - 9003:9000 + - 8126:8123 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-03.xml:/etc/clickhouse-server/config.d/macros.xml + - ./config/users.xml:/etc/clickhouse-server/users.xml + # - ./data/server-03:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + clickhouse-04: + image: clickhouse/clickhouse-server + hostname: clickhouse-04 + container_name: clickhouse-04 + networks: + - ckh_net + ports: + - 9004:9000 + - 8127:8123 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-04.xml:/etc/clickhouse-server/config.d/macros.xml + - ./config/users.xml:/etc/clickhouse-server/users.xml + # - ./data/server-04:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + clickhouse-05: + image: clickhouse/clickhouse-server + hostname: clickhouse-05 + container_name: clickhouse-05 + networks: + - ckh_net + ports: + - 9005:9000 + - 8128:8123 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-05.xml:/etc/clickhouse-server/config.d/macros.xml + - ./config/users.xml:/etc/clickhouse-server/users.xml + # - ./data/server-05:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + clickhouse-06: + image: clickhouse/clickhouse-server + hostname: clickhouse-06 + container_name: clickhouse-06 + networks: + - ckh_net + ports: + - 9006:9000 + - 8129:8123 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-06.xml:/etc/clickhouse-server/config.d/macros.xml + - ./config/users.xml:/etc/clickhouse-server/users.xml + # - ./data/server-06:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + kafka0: + image: confluentinc/cp-kafka:7.0.5 + hostname: kafka0 + container_name: kafka0 + ports: + - 9092:9092 + - 9093 + - 9997 + - 29092 + environment: + KAFKA_BROKER_ID: 1 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092 + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_PROCESS_ROLES: 'broker,controller' + KAFKA_NODE_ID: 1 + KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka0:29093' + KAFKA_LISTENERS: 'PLAINTEXT://kafka0:29092,CONTROLLER://kafka0:29093,PLAINTEXT_HOST://0.0.0.0:9092' + KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER' + KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs' + JMX_PORT: 9997 + KAFKA_JMX_OPTS: -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=kafka0 -Dcom.sun.management.jmxremote.rmi.port=9997 + volumes: + - ./kafka-script.sh:/tmp/update_run.sh + command: "bash -c 'if [ ! -f /tmp/update_run.sh ]; then echo \"ERROR: Did you forget the update_run.sh file that came with this docker-compose.yml file?\" && exit 1 ; else /tmp/update_run.sh && /etc/confluent/docker/run ; fi'" + networks: + ckh_net: + aliases: + - hyper-c1-kafka-brokers.kafka-cluster.svc.cluster.local + + + # Kafka UI for debugging kafka queues + kafka-ui: + container_name: kafka-ui + image: provectuslabs/kafka-ui:latest + ports: + - 8090:8080 + depends_on: + - kafka0 + networks: + - ckh_net + environment: + KAFKA_CLUSTERS_0_NAME: local + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092 + KAFKA_CLUSTERS_0_JMXPORT: 9997 + diff --git a/crates/analytics/docs/clickhouse/cluster_setup/kafka-script.sh b/crates/analytics/docs/clickhouse/cluster_setup/kafka-script.sh new file mode 100755 index 000000000000..023c832b4e1b --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/kafka-script.sh @@ -0,0 +1,11 @@ +# This script is required to run kafka cluster (without zookeeper) +#!/bin/sh + +# Docker workaround: Remove check for KAFKA_ZOOKEEPER_CONNECT parameter +sed -i '/KAFKA_ZOOKEEPER_CONNECT/d' /etc/confluent/docker/configure + +# Docker workaround: Ignore cub zk-ready +sed -i 's/cub zk-ready/echo ignore zk-ready/' /etc/confluent/docker/ensure + +# KRaft required step: Format the storage directory with a new cluster ID +echo "kafka-storage format --ignore-formatted -t $(kafka-storage random-uuid) -c /etc/kafka/kafka.properties" >> /etc/confluent/docker/ensure \ No newline at end of file diff --git a/crates/analytics/docs/clickhouse/cluster_setup/scripts/api_event_logs.sql b/crates/analytics/docs/clickhouse/cluster_setup/scripts/api_event_logs.sql new file mode 100644 index 000000000000..0fe194a0e676 --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/scripts/api_event_logs.sql @@ -0,0 +1,237 @@ +CREATE TABLE hyperswitch.api_events_queue on cluster '{cluster}' ( + `merchant_id` String, + `payment_id` Nullable(String), + `refund_id` Nullable(String), + `payment_method_id` Nullable(String), + `payment_method` Nullable(String), + `payment_method_type` Nullable(String), + `customer_id` Nullable(String), + `user_id` Nullable(String), + `request_id` String, + `flow_type` LowCardinality(String), + `api_name` LowCardinality(String), + `request` String, + `response` String, + `status_code` UInt32, + `url_path` LowCardinality(Nullable(String)), + `event_type` LowCardinality(Nullable(String)), + `created_at` DateTime CODEC(T64, LZ4), + `latency` Nullable(UInt128), + `user_agent` Nullable(String), + `ip_addr` Nullable(String) +) ENGINE = Kafka SETTINGS kafka_broker_list = 'hyper-c1-kafka-brokers.kafka-cluster.svc.cluster.local:9092', +kafka_topic_list = 'hyperswitch-api-log-events', +kafka_group_name = 'hyper-c1', +kafka_format = 'JSONEachRow', +kafka_handle_error_mode = 'stream'; + + +CREATE TABLE hyperswitch.api_events_clustered on cluster '{cluster}' ( + `merchant_id` String, + `payment_id` Nullable(String), + `refund_id` Nullable(String), + `payment_method_id` Nullable(String), + `payment_method` Nullable(String), + `payment_method_type` Nullable(String), + `customer_id` Nullable(String), + `user_id` Nullable(String), + `request_id` Nullable(String), + `flow_type` LowCardinality(String), + `api_name` LowCardinality(String), + `request` String, + `response` String, + `status_code` UInt32, + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `latency` Nullable(UInt128), + `user_agent` Nullable(String), + `ip_addr` Nullable(String), + INDEX flowIndex flow_type TYPE bloom_filter GRANULARITY 1, + INDEX apiIndex api_name TYPE bloom_filter GRANULARITY 1, + INDEX statusIndex status_code TYPE bloom_filter GRANULARITY 1 +) ENGINE = ReplicatedMergeTree( + '/clickhouse/{installation}/{cluster}/tables/{shard}/hyperswitch/api_events_clustered', + '{replica}' +) +PARTITION BY toStartOfDay(created_at) +ORDER BY + (created_at, merchant_id, flow_type, status_code, api_name) +TTL created_at + toIntervalMonth(6) +; + + +CREATE TABLE hyperswitch.api_events_dist on cluster '{cluster}' ( + `merchant_id` String, + `payment_id` Nullable(String), + `refund_id` Nullable(String), + `payment_method_id` Nullable(String), + `payment_method` Nullable(String), + `payment_method_type` Nullable(String), + `customer_id` Nullable(String), + `user_id` Nullable(String), + `request_id` Nullable(String), + `flow_type` LowCardinality(String), + `api_name` LowCardinality(String), + `request` String, + `response` String, + `status_code` UInt32, + `url_path` LowCardinality(Nullable(String)), + `event_type` LowCardinality(Nullable(String)), + `inserted_at` DateTime64(3), + `created_at` DateTime64(3), + `latency` Nullable(UInt128), + `user_agent` Nullable(String), + `ip_addr` Nullable(String) +) ENGINE = Distributed('{cluster}', 'hyperswitch', 'api_events_clustered', rand()); + +CREATE MATERIALIZED VIEW hyperswitch.api_events_mv on cluster '{cluster}' TO hyperswitch.api_events_dist ( + `merchant_id` String, + `payment_id` Nullable(String), + `refund_id` Nullable(String), + `payment_method_id` Nullable(String), + `payment_method` Nullable(String), + `payment_method_type` Nullable(String), + `customer_id` Nullable(String), + `user_id` Nullable(String), + `request_id` Nullable(String), + `flow_type` LowCardinality(String), + `api_name` LowCardinality(String), + `request` String, + `response` String, + `status_code` UInt32, + `url_path` LowCardinality(Nullable(String)), + `event_type` LowCardinality(Nullable(String)), + `inserted_at` DateTime64(3), + `created_at` DateTime64(3), + `latency` Nullable(UInt128), + `user_agent` Nullable(String), + `ip_addr` Nullable(String) +) AS +SELECT + merchant_id, + payment_id, + refund_id, + payment_method_id, + payment_method, + payment_method_type, + customer_id, + user_id, + request_id, + flow_type, + api_name, + request, + response, + status_code, + url_path, + event_type, + now() as inserted_at, + created_at, + latency, + user_agent, + ip_addr +FROM + hyperswitch.api_events_queue +WHERE length(_error) = 0; + + +CREATE MATERIALIZED VIEW hyperswitch.api_events_parse_errors on cluster '{cluster}' +( + `topic` String, + `partition` Int64, + `offset` Int64, + `raw` String, + `error` String +) +ENGINE = MergeTree +ORDER BY (topic, partition, offset) +SETTINGS index_granularity = 8192 AS +SELECT + _topic AS topic, + _partition AS partition, + _offset AS offset, + _raw_message AS raw, + _error AS error +FROM hyperswitch.api_events_queue +WHERE length(_error) > 0 +; + + +ALTER TABLE hyperswitch.api_events_clustered on cluster '{cluster}' ADD COLUMN `url_path` LowCardinality(Nullable(String)); +ALTER TABLE hyperswitch.api_events_clustered on cluster '{cluster}' ADD COLUMN `event_type` LowCardinality(Nullable(String)); + + +CREATE TABLE hyperswitch.api_audit_log ON CLUSTER '{cluster}' ( + `merchant_id` LowCardinality(String), + `payment_id` String, + `refund_id` Nullable(String), + `payment_method_id` Nullable(String), + `payment_method` Nullable(String), + `payment_method_type` Nullable(String), + `user_id` Nullable(String), + `request_id` Nullable(String), + `flow_type` LowCardinality(String), + `api_name` LowCardinality(String), + `request` String, + `response` String, + `status_code` UInt32, + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `latency` Nullable(UInt128), + `user_agent` Nullable(String), + `ip_addr` Nullable(String), + `url_path` LowCardinality(Nullable(String)), + `event_type` LowCardinality(Nullable(String)), + `customer_id` LowCardinality(Nullable(String)) +) ENGINE = ReplicatedMergeTree( '/clickhouse/{installation}/{cluster}/tables/{shard}/hyperswitch/api_audit_log', '{replica}' ) PARTITION BY merchant_id +ORDER BY (merchant_id, payment_id) +TTL created_at + toIntervalMonth(18) +SETTINGS index_granularity = 8192 + + +CREATE MATERIALIZED VIEW hyperswitch.api_audit_log_mv ON CLUSTER `{cluster}` TO hyperswitch.api_audit_log( + `merchant_id` LowCardinality(String), + `payment_id` String, + `refund_id` Nullable(String), + `payment_method_id` Nullable(String), + `payment_method` Nullable(String), + `payment_method_type` Nullable(String), + `customer_id` Nullable(String), + `user_id` Nullable(String), + `request_id` Nullable(String), + `flow_type` LowCardinality(String), + `api_name` LowCardinality(String), + `request` String, + `response` String, + `status_code` UInt32, + `url_path` LowCardinality(Nullable(String)), + `event_type` LowCardinality(Nullable(String)), + `inserted_at` DateTime64(3), + `created_at` DateTime64(3), + `latency` Nullable(UInt128), + `user_agent` Nullable(String), + `ip_addr` Nullable(String) +) AS +SELECT + merchant_id, + multiIf(payment_id IS NULL, '', payment_id) AS payment_id, + refund_id, + payment_method_id, + payment_method, + payment_method_type, + customer_id, + user_id, + request_id, + flow_type, + api_name, + request, + response, + status_code, + url_path, + api_event_type AS event_type, + now() AS inserted_at, + created_at, + latency, + user_agent, + ip_addr +FROM hyperswitch.api_events_queue +WHERE length(_error) = 0 \ No newline at end of file diff --git a/crates/analytics/docs/clickhouse/cluster_setup/scripts/payment_attempts.sql b/crates/analytics/docs/clickhouse/cluster_setup/scripts/payment_attempts.sql new file mode 100644 index 000000000000..3a6281ae9050 --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/scripts/payment_attempts.sql @@ -0,0 +1,217 @@ +CREATE TABLE hyperswitch.payment_attempt_queue on cluster '{cluster}' ( + `payment_id` String, + `merchant_id` String, + `attempt_id` String, + `status` LowCardinality(String), + `amount` Nullable(UInt32), + `currency` LowCardinality(Nullable(String)), + `connector` LowCardinality(Nullable(String)), + `save_to_locker` Nullable(Bool), + `error_message` Nullable(String), + `offer_amount` Nullable(UInt32), + `surcharge_amount` Nullable(UInt32), + `tax_amount` Nullable(UInt32), + `payment_method_id` Nullable(String), + `payment_method` LowCardinality(Nullable(String)), + `payment_method_type` LowCardinality(Nullable(String)), + `connector_transaction_id` Nullable(String), + `capture_method` LowCardinality(Nullable(String)), + `capture_on` Nullable(DateTime) CODEC(T64, LZ4), + `confirm` Bool, + `authentication_type` LowCardinality(Nullable(String)), + `cancellation_reason` Nullable(String), + `amount_to_capture` Nullable(UInt32), + `mandate_id` Nullable(String), + `browser_info` Nullable(String), + `error_code` Nullable(String), + `connector_metadata` Nullable(String), + `payment_experience` Nullable(String), + `created_at` DateTime CODEC(T64, LZ4), + `last_synced` Nullable(DateTime) CODEC(T64, LZ4), + `modified_at` DateTime CODEC(T64, LZ4), + `sign_flag` Int8 +) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', +kafka_topic_list = 'hyperswitch-payment-attempt-events', +kafka_group_name = 'hyper-c1', +kafka_format = 'JSONEachRow', +kafka_handle_error_mode = 'stream'; + + +CREATE TABLE hyperswitch.payment_attempt_dist on cluster '{cluster}' ( + `payment_id` String, + `merchant_id` String, + `attempt_id` String, + `status` LowCardinality(String), + `amount` Nullable(UInt32), + `currency` LowCardinality(Nullable(String)), + `connector` LowCardinality(Nullable(String)), + `save_to_locker` Nullable(Bool), + `error_message` Nullable(String), + `offer_amount` Nullable(UInt32), + `surcharge_amount` Nullable(UInt32), + `tax_amount` Nullable(UInt32), + `payment_method_id` Nullable(String), + `payment_method` LowCardinality(Nullable(String)), + `payment_method_type` LowCardinality(Nullable(String)), + `connector_transaction_id` Nullable(String), + `capture_method` Nullable(String), + `capture_on` Nullable(DateTime) CODEC(T64, LZ4), + `confirm` Bool, + `authentication_type` LowCardinality(Nullable(String)), + `cancellation_reason` Nullable(String), + `amount_to_capture` Nullable(UInt32), + `mandate_id` Nullable(String), + `browser_info` Nullable(String), + `error_code` Nullable(String), + `connector_metadata` Nullable(String), + `payment_experience` Nullable(String), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `last_synced` Nullable(DateTime) CODEC(T64, LZ4), + `modified_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `sign_flag` Int8 +) ENGINE = Distributed('{cluster}', 'hyperswitch', 'payment_attempt_clustered', cityHash64(attempt_id)); + + + +CREATE MATERIALIZED VIEW hyperswitch.payment_attempt_mv on cluster '{cluster}' TO hyperswitch.payment_attempt_dist ( + `payment_id` String, + `merchant_id` String, + `attempt_id` String, + `status` LowCardinality(String), + `amount` Nullable(UInt32), + `currency` LowCardinality(Nullable(String)), + `connector` LowCardinality(Nullable(String)), + `save_to_locker` Nullable(Bool), + `error_message` Nullable(String), + `offer_amount` Nullable(UInt32), + `surcharge_amount` Nullable(UInt32), + `tax_amount` Nullable(UInt32), + `payment_method_id` Nullable(String), + `payment_method` LowCardinality(Nullable(String)), + `payment_method_type` LowCardinality(Nullable(String)), + `connector_transaction_id` Nullable(String), + `capture_method` Nullable(String), + `confirm` Bool, + `authentication_type` LowCardinality(Nullable(String)), + `cancellation_reason` Nullable(String), + `amount_to_capture` Nullable(UInt32), + `mandate_id` Nullable(String), + `browser_info` Nullable(String), + `error_code` Nullable(String), + `connector_metadata` Nullable(String), + `payment_experience` Nullable(String), + `created_at` DateTime64(3), + `capture_on` Nullable(DateTime64(3)), + `last_synced` Nullable(DateTime64(3)), + `modified_at` DateTime64(3), + `inserted_at` DateTime64(3), + `sign_flag` Int8 +) AS +SELECT + payment_id, + merchant_id, + attempt_id, + status, + amount, + currency, + connector, + save_to_locker, + error_message, + offer_amount, + surcharge_amount, + tax_amount, + payment_method_id, + payment_method, + payment_method_type, + connector_transaction_id, + capture_method, + confirm, + authentication_type, + cancellation_reason, + amount_to_capture, + mandate_id, + browser_info, + error_code, + connector_metadata, + payment_experience, + created_at, + capture_on, + last_synced, + modified_at, + now() as inserted_at, + sign_flag +FROM + hyperswitch.payment_attempt_queue +WHERE length(_error) = 0; + + +CREATE TABLE hyperswitch.payment_attempt_clustered on cluster '{cluster}' ( + `payment_id` String, + `merchant_id` String, + `attempt_id` String, + `status` LowCardinality(String), + `amount` Nullable(UInt32), + `currency` LowCardinality(Nullable(String)), + `connector` LowCardinality(Nullable(String)), + `save_to_locker` Nullable(Bool), + `error_message` Nullable(String), + `offer_amount` Nullable(UInt32), + `surcharge_amount` Nullable(UInt32), + `tax_amount` Nullable(UInt32), + `payment_method_id` Nullable(String), + `payment_method` LowCardinality(Nullable(String)), + `payment_method_type` LowCardinality(Nullable(String)), + `connector_transaction_id` Nullable(String), + `capture_method` Nullable(String), + `capture_on` Nullable(DateTime) CODEC(T64, LZ4), + `confirm` Bool, + `authentication_type` LowCardinality(Nullable(String)), + `cancellation_reason` Nullable(String), + `amount_to_capture` Nullable(UInt32), + `mandate_id` Nullable(String), + `browser_info` Nullable(String), + `error_code` Nullable(String), + `connector_metadata` Nullable(String), + `payment_experience` Nullable(String), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `last_synced` Nullable(DateTime) CODEC(T64, LZ4), + `modified_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `sign_flag` Int8, + INDEX connectorIndex connector TYPE bloom_filter GRANULARITY 1, + INDEX paymentMethodIndex payment_method TYPE bloom_filter GRANULARITY 1, + INDEX authenticationTypeIndex authentication_type TYPE bloom_filter GRANULARITY 1, + INDEX currencyIndex currency TYPE bloom_filter GRANULARITY 1, + INDEX statusIndex status TYPE bloom_filter GRANULARITY 1 +) ENGINE = ReplicatedCollapsingMergeTree( + '/clickhouse/{installation}/{cluster}/tables/{shard}/hyperswitch/payment_attempt_clustered', + '{replica}', + sign_flag +) +PARTITION BY toStartOfDay(created_at) +ORDER BY + (created_at, merchant_id, attempt_id) +TTL created_at + toIntervalMonth(6) +; + +CREATE MATERIALIZED VIEW hyperswitch.payment_attempt_parse_errors on cluster '{cluster}' +( + `topic` String, + `partition` Int64, + `offset` Int64, + `raw` String, + `error` String +) +ENGINE = MergeTree +ORDER BY (topic, partition, offset) +SETTINGS index_granularity = 8192 AS +SELECT + _topic AS topic, + _partition AS partition, + _offset AS offset, + _raw_message AS raw, + _error AS error +FROM hyperswitch.payment_attempt_queue +WHERE length(_error) > 0 +; \ No newline at end of file diff --git a/crates/analytics/docs/clickhouse/cluster_setup/scripts/payment_intents.sql b/crates/analytics/docs/clickhouse/cluster_setup/scripts/payment_intents.sql new file mode 100644 index 000000000000..eb2d83140e92 --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/scripts/payment_intents.sql @@ -0,0 +1,165 @@ +CREATE TABLE hyperswitch.payment_intents_queue on cluster '{cluster}' ( + `payment_id` String, + `merchant_id` String, + `status` LowCardinality(String), + `amount` UInt32, + `currency` LowCardinality(Nullable(String)), + `amount_captured` Nullable(UInt32), + `customer_id` Nullable(String), + `description` Nullable(String), + `return_url` Nullable(String), + `connector_id` LowCardinality(Nullable(String)), + `statement_descriptor_name` Nullable(String), + `statement_descriptor_suffix` Nullable(String), + `setup_future_usage` LowCardinality(Nullable(String)), + `off_session` Nullable(Bool), + `client_secret` Nullable(String), + `active_attempt_id` String, + `business_country` String, + `business_label` String, + `modified_at` DateTime, + `created_at` DateTime, + `last_synced` Nullable(DateTime) CODEC(T64, LZ4), + `sign_flag` Int8 +) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', +kafka_topic_list = 'hyperswitch-payment-intent-events', +kafka_group_name = 'hyper-c1', +kafka_format = 'JSONEachRow', +kafka_handle_error_mode = 'stream'; + +CREATE TABLE hyperswitch.payment_intents_dist on cluster '{cluster}' ( + `payment_id` String, + `merchant_id` String, + `status` LowCardinality(String), + `amount` UInt32, + `currency` LowCardinality(Nullable(String)), + `amount_captured` Nullable(UInt32), + `customer_id` Nullable(String), + `description` Nullable(String), + `return_url` Nullable(String), + `connector_id` LowCardinality(Nullable(String)), + `statement_descriptor_name` Nullable(String), + `statement_descriptor_suffix` Nullable(String), + `setup_future_usage` LowCardinality(Nullable(String)), + `off_session` Nullable(Bool), + `client_secret` Nullable(String), + `active_attempt_id` String, + `business_country` LowCardinality(String), + `business_label` String, + `modified_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `last_synced` Nullable(DateTime) CODEC(T64, LZ4), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `sign_flag` Int8 +) ENGINE = Distributed('{cluster}', 'hyperswitch', 'payment_intents_clustered', cityHash64(payment_id)); + +CREATE TABLE hyperswitch.payment_intents_clustered on cluster '{cluster}' ( + `payment_id` String, + `merchant_id` String, + `status` LowCardinality(String), + `amount` UInt32, + `currency` LowCardinality(Nullable(String)), + `amount_captured` Nullable(UInt32), + `customer_id` Nullable(String), + `description` Nullable(String), + `return_url` Nullable(String), + `connector_id` LowCardinality(Nullable(String)), + `statement_descriptor_name` Nullable(String), + `statement_descriptor_suffix` Nullable(String), + `setup_future_usage` LowCardinality(Nullable(String)), + `off_session` Nullable(Bool), + `client_secret` Nullable(String), + `active_attempt_id` String, + `business_country` LowCardinality(String), + `business_label` String, + `modified_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `last_synced` Nullable(DateTime) CODEC(T64, LZ4), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `sign_flag` Int8, + INDEX connectorIndex connector_id TYPE bloom_filter GRANULARITY 1, + INDEX currencyIndex currency TYPE bloom_filter GRANULARITY 1, + INDEX statusIndex status TYPE bloom_filter GRANULARITY 1 +) ENGINE = ReplicatedCollapsingMergeTree( + '/clickhouse/{installation}/{cluster}/tables/{shard}/hyperswitch/payment_intents_clustered', + '{replica}', + sign_flag +) +PARTITION BY toStartOfDay(created_at) +ORDER BY + (created_at, merchant_id, payment_id) +TTL created_at + toIntervalMonth(6) +; + +CREATE MATERIALIZED VIEW hyperswitch.payment_intent_mv on cluster '{cluster}' TO hyperswitch.payment_intents_dist ( + `payment_id` String, + `merchant_id` String, + `status` LowCardinality(String), + `amount` UInt32, + `currency` LowCardinality(Nullable(String)), + `amount_captured` Nullable(UInt32), + `customer_id` Nullable(String), + `description` Nullable(String), + `return_url` Nullable(String), + `connector_id` LowCardinality(Nullable(String)), + `statement_descriptor_name` Nullable(String), + `statement_descriptor_suffix` Nullable(String), + `setup_future_usage` LowCardinality(Nullable(String)), + `off_session` Nullable(Bool), + `client_secret` Nullable(String), + `active_attempt_id` String, + `business_country` LowCardinality(String), + `business_label` String, + `modified_at` DateTime64(3), + `created_at` DateTime64(3), + `last_synced` Nullable(DateTime64(3)), + `inserted_at` DateTime64(3), + `sign_flag` Int8 +) AS +SELECT + payment_id, + merchant_id, + status, + amount, + currency, + amount_captured, + customer_id, + description, + return_url, + connector_id, + statement_descriptor_name, + statement_descriptor_suffix, + setup_future_usage, + off_session, + client_secret, + active_attempt_id, + business_country, + business_label, + modified_at, + created_at, + last_synced, + now() as inserted_at, + sign_flag +FROM hyperswitch.payment_intents_queue +WHERE length(_error) = 0; + +CREATE MATERIALIZED VIEW hyperswitch.payment_intent_parse_errors on cluster '{cluster}' +( + `topic` String, + `partition` Int64, + `offset` Int64, + `raw` String, + `error` String +) +ENGINE = MergeTree +ORDER BY (topic, partition, offset) +SETTINGS index_granularity = 8192 AS +SELECT + _topic AS topic, + _partition AS partition, + _offset AS offset, + _raw_message AS raw, + _error AS error +FROM hyperswitch.payment_intents_queue +WHERE length(_error) > 0 +; diff --git a/crates/analytics/docs/clickhouse/cluster_setup/scripts/refund_analytics.sql b/crates/analytics/docs/clickhouse/cluster_setup/scripts/refund_analytics.sql new file mode 100644 index 000000000000..bf5f6e0e2405 --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/scripts/refund_analytics.sql @@ -0,0 +1,173 @@ +CREATE TABLE hyperswitch.refund_queue on cluster '{cluster}' ( + `internal_reference_id` String, + `refund_id` String, + `payment_id` String, + `merchant_id` String, + `connector_transaction_id` String, + `connector` LowCardinality(Nullable(String)), + `connector_refund_id` Nullable(String), + `external_reference_id` Nullable(String), + `refund_type` LowCardinality(String), + `total_amount` Nullable(UInt32), + `currency` LowCardinality(String), + `refund_amount` Nullable(UInt32), + `refund_status` LowCardinality(String), + `sent_to_gateway` Bool, + `refund_error_message` Nullable(String), + `refund_arn` Nullable(String), + `attempt_id` String, + `description` Nullable(String), + `refund_reason` Nullable(String), + `refund_error_code` Nullable(String), + `created_at` DateTime, + `modified_at` DateTime, + `sign_flag` Int8 +) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', +kafka_topic_list = 'hyperswitch-refund-events', +kafka_group_name = 'hyper-c1', +kafka_format = 'JSONEachRow', +kafka_handle_error_mode = 'stream'; + +CREATE TABLE hyperswitch.refund_dist on cluster '{cluster}' ( + `internal_reference_id` String, + `refund_id` String, + `payment_id` String, + `merchant_id` String, + `connector_transaction_id` String, + `connector` LowCardinality(Nullable(String)), + `connector_refund_id` Nullable(String), + `external_reference_id` Nullable(String), + `refund_type` LowCardinality(String), + `total_amount` Nullable(UInt32), + `currency` LowCardinality(String), + `refund_amount` Nullable(UInt32), + `refund_status` LowCardinality(String), + `sent_to_gateway` Bool, + `refund_error_message` Nullable(String), + `refund_arn` Nullable(String), + `attempt_id` String, + `description` Nullable(String), + `refund_reason` Nullable(String), + `refund_error_code` Nullable(String), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `modified_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `sign_flag` Int8 +) ENGINE = Distributed('{cluster}', 'hyperswitch', 'refund_clustered', cityHash64(refund_id)); + + + +CREATE TABLE hyperswitch.refund_clustered on cluster '{cluster}' ( + `internal_reference_id` String, + `refund_id` String, + `payment_id` String, + `merchant_id` String, + `connector_transaction_id` String, + `connector` LowCardinality(Nullable(String)), + `connector_refund_id` Nullable(String), + `external_reference_id` Nullable(String), + `refund_type` LowCardinality(String), + `total_amount` Nullable(UInt32), + `currency` LowCardinality(String), + `refund_amount` Nullable(UInt32), + `refund_status` LowCardinality(String), + `sent_to_gateway` Bool, + `refund_error_message` Nullable(String), + `refund_arn` Nullable(String), + `attempt_id` String, + `description` Nullable(String), + `refund_reason` Nullable(String), + `refund_error_code` Nullable(String), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `modified_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `sign_flag` Int8, + INDEX connectorIndex connector TYPE bloom_filter GRANULARITY 1, + INDEX refundTypeIndex refund_type TYPE bloom_filter GRANULARITY 1, + INDEX currencyIndex currency TYPE bloom_filter GRANULARITY 1, + INDEX statusIndex refund_status TYPE bloom_filter GRANULARITY 1 +) ENGINE = ReplicatedCollapsingMergeTree( + '/clickhouse/{installation}/{cluster}/tables/{shard}/hyperswitch/refund_clustered', + '{replica}', + sign_flag +) +PARTITION BY toStartOfDay(created_at) +ORDER BY + (created_at, merchant_id, refund_id) +TTL created_at + toIntervalMonth(6) +; + +CREATE MATERIALIZED VIEW hyperswitch.kafka_parse_refund on cluster '{cluster}' TO hyperswitch.refund_dist ( + `internal_reference_id` String, + `refund_id` String, + `payment_id` String, + `merchant_id` String, + `connector_transaction_id` String, + `connector` LowCardinality(Nullable(String)), + `connector_refund_id` Nullable(String), + `external_reference_id` Nullable(String), + `refund_type` LowCardinality(String), + `total_amount` Nullable(UInt32), + `currency` LowCardinality(String), + `refund_amount` Nullable(UInt32), + `refund_status` LowCardinality(String), + `sent_to_gateway` Bool, + `refund_error_message` Nullable(String), + `refund_arn` Nullable(String), + `attempt_id` String, + `description` Nullable(String), + `refund_reason` Nullable(String), + `refund_error_code` Nullable(String), + `created_at` DateTime64(3), + `modified_at` DateTime64(3), + `inserted_at` DateTime64(3), + `sign_flag` Int8 +) AS +SELECT + internal_reference_id, + refund_id, + payment_id, + merchant_id, + connector_transaction_id, + connector, + connector_refund_id, + external_reference_id, + refund_type, + total_amount, + currency, + refund_amount, + refund_status, + sent_to_gateway, + refund_error_message, + refund_arn, + attempt_id, + description, + refund_reason, + refund_error_code, + created_at, + modified_at, + now() as inserted_at, + sign_flag +FROM hyperswitch.refund_queue +WHERE length(_error) = 0; + +CREATE MATERIALIZED VIEW hyperswitch.refund_parse_errors on cluster '{cluster}' +( + `topic` String, + `partition` Int64, + `offset` Int64, + `raw` String, + `error` String +) +ENGINE = MergeTree +ORDER BY (topic, partition, offset) +SETTINGS index_granularity = 8192 AS +SELECT + _topic AS topic, + _partition AS partition, + _offset AS offset, + _raw_message AS raw, + _error AS error +FROM hyperswitch.refund_queue +WHERE length(_error) > 0 +; \ No newline at end of file diff --git a/crates/analytics/docs/clickhouse/cluster_setup/scripts/sdk_events.sql b/crates/analytics/docs/clickhouse/cluster_setup/scripts/sdk_events.sql new file mode 100644 index 000000000000..37766392bc70 --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/scripts/sdk_events.sql @@ -0,0 +1,156 @@ +CREATE TABLE hyperswitch.sdk_events_queue on cluster '{cluster}' ( + `payment_id` Nullable(String), + `merchant_id` String, + `remote_ip` Nullable(String), + `log_type` LowCardinality(Nullable(String)), + `event_name` LowCardinality(Nullable(String)), + `first_event` LowCardinality(Nullable(String)), + `latency` Nullable(UInt32), + `timestamp` String, + `browser_name` LowCardinality(Nullable(String)), + `browser_version` Nullable(String), + `platform` LowCardinality(Nullable(String)), + `source` LowCardinality(Nullable(String)), + `category` LowCardinality(Nullable(String)), + `version` LowCardinality(Nullable(String)), + `value` Nullable(String), + `component` LowCardinality(Nullable(String)), + `payment_method` LowCardinality(Nullable(String)), + `payment_experience` LowCardinality(Nullable(String)) +) ENGINE = Kafka SETTINGS + kafka_broker_list = 'hyper-c1-kafka-brokers.kafka-cluster.svc.cluster.local:9092', + kafka_topic_list = 'hyper-sdk-logs', + kafka_group_name = 'hyper-c1', + kafka_format = 'JSONEachRow', + kafka_handle_error_mode = 'stream'; + +CREATE TABLE hyperswitch.sdk_events_clustered on cluster '{cluster}' ( + `payment_id` Nullable(String), + `merchant_id` String, + `remote_ip` Nullable(String), + `log_type` LowCardinality(Nullable(String)), + `event_name` LowCardinality(Nullable(String)), + `first_event` Bool DEFAULT 1, + `browser_name` LowCardinality(Nullable(String)), + `browser_version` Nullable(String), + `platform` LowCardinality(Nullable(String)), + `source` LowCardinality(Nullable(String)), + `category` LowCardinality(Nullable(String)), + `version` LowCardinality(Nullable(String)), + `value` Nullable(String), + `component` LowCardinality(Nullable(String)), + `payment_method` LowCardinality(Nullable(String)), + `payment_experience` LowCardinality(Nullable(String)) DEFAULT '', + `created_at` DateTime64(3) DEFAULT now64() CODEC(T64, LZ4), + `inserted_at` DateTime64(3) DEFAULT now64() CODEC(T64, LZ4), + `latency` Nullable(UInt32) DEFAULT 0, + INDEX paymentMethodIndex payment_method TYPE bloom_filter GRANULARITY 1, + INDEX eventIndex event_name TYPE bloom_filter GRANULARITY 1, + INDEX platformIndex platform TYPE bloom_filter GRANULARITY 1, + INDEX logTypeIndex log_type TYPE bloom_filter GRANULARITY 1, + INDEX categoryIndex category TYPE bloom_filter GRANULARITY 1, + INDEX sourceIndex source TYPE bloom_filter GRANULARITY 1, + INDEX componentIndex component TYPE bloom_filter GRANULARITY 1, + INDEX firstEventIndex first_event TYPE bloom_filter GRANULARITY 1 +) ENGINE = ReplicatedMergeTree( + '/clickhouse/{installation}/{cluster}/tables/{shard}/hyperswitch/sdk_events_clustered', '{replica}' +) +PARTITION BY + toStartOfDay(created_at) +ORDER BY + (created_at, merchant_id) +TTL + toDateTime(created_at) + toIntervalMonth(6) +SETTINGS + index_granularity = 8192 +; + +CREATE TABLE hyperswitch.sdk_events_dist on cluster '{cluster}' ( + `payment_id` Nullable(String), + `merchant_id` String, + `remote_ip` Nullable(String), + `log_type` LowCardinality(Nullable(String)), + `event_name` LowCardinality(Nullable(String)), + `first_event` Bool DEFAULT 1, + `browser_name` LowCardinality(Nullable(String)), + `browser_version` Nullable(String), + `platform` LowCardinality(Nullable(String)), + `source` LowCardinality(Nullable(String)), + `category` LowCardinality(Nullable(String)), + `version` LowCardinality(Nullable(String)), + `value` Nullable(String), + `component` LowCardinality(Nullable(String)), + `payment_method` LowCardinality(Nullable(String)), + `payment_experience` LowCardinality(Nullable(String)) DEFAULT '', + `created_at` DateTime64(3) DEFAULT now64() CODEC(T64, LZ4), + `inserted_at` DateTime64(3) DEFAULT now64() CODEC(T64, LZ4), + `latency` Nullable(UInt32) DEFAULT 0 +) ENGINE = Distributed( + '{cluster}', 'hyperswitch', 'sdk_events_clustered', rand() +); + +CREATE MATERIALIZED VIEW hyperswitch.sdk_events_mv on cluster '{cluster}' TO hyperswitch.sdk_events_dist ( + `payment_id` Nullable(String), + `merchant_id` String, + `remote_ip` Nullable(String), + `log_type` LowCardinality(Nullable(String)), + `event_name` LowCardinality(Nullable(String)), + `first_event` Bool, + `latency` Nullable(UInt32), + `browser_name` LowCardinality(Nullable(String)), + `browser_version` Nullable(String), + `platform` LowCardinality(Nullable(String)), + `source` LowCardinality(Nullable(String)), + `category` LowCardinality(Nullable(String)), + `version` LowCardinality(Nullable(String)), + `value` Nullable(String), + `component` LowCardinality(Nullable(String)), + `payment_method` LowCardinality(Nullable(String)), + `payment_experience` LowCardinality(Nullable(String)), + `created_at` DateTime64(3) +) AS +SELECT + payment_id, + merchant_id, + remote_ip, + log_type, + event_name, + multiIf(first_event = 'true', 1, 0) AS first_event, + latency, + browser_name, + browser_version, + platform, + source, + category, + version, + value, + component, + payment_method, + payment_experience, + toDateTime64(timestamp, 3) AS created_at +FROM + hyperswitch.sdk_events_queue +WHERE length(_error) = 0 +; + +CREATE MATERIALIZED VIEW hyperswitch.sdk_parse_errors on cluster '{cluster}' ( + `topic` String, + `partition` Int64, + `offset` Int64, + `raw` String, + `error` String +) ENGINE = MergeTree + ORDER BY (topic, partition, offset) +SETTINGS + index_granularity = 8192 AS +SELECT + _topic AS topic, + _partition AS partition, + _offset AS offset, + _raw_message AS raw, + _error AS error +FROM + hyperswitch.sdk_events_queue +WHERE + length(_error) > 0 +; diff --git a/crates/analytics/docs/clickhouse/cluster_setup/scripts/seed_scripts.sql b/crates/analytics/docs/clickhouse/cluster_setup/scripts/seed_scripts.sql new file mode 100644 index 000000000000..202b94ac6040 --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/scripts/seed_scripts.sql @@ -0,0 +1 @@ +create database hyperswitch on cluster '{cluster}'; \ No newline at end of file diff --git a/crates/analytics/docs/clickhouse/scripts/api_events_v2.sql b/crates/analytics/docs/clickhouse/scripts/api_events_v2.sql new file mode 100644 index 000000000000..b41a75fe67e5 --- /dev/null +++ b/crates/analytics/docs/clickhouse/scripts/api_events_v2.sql @@ -0,0 +1,134 @@ +CREATE TABLE api_events_v2_queue ( + `merchant_id` String, + `payment_id` Nullable(String), + `refund_id` Nullable(String), + `payment_method_id` Nullable(String), + `payment_method` Nullable(String), + `payment_method_type` Nullable(String), + `customer_id` Nullable(String), + `user_id` Nullable(String), + `connector` Nullable(String), + `request_id` String, + `flow_type` LowCardinality(String), + `api_flow` LowCardinality(String), + `api_auth_type` LowCardinality(String), + `request` String, + `response` Nullable(String), + `authentication_data` Nullable(String), + `status_code` UInt32, + `created_at` DateTime CODEC(T64, LZ4), + `latency` UInt128, + `user_agent` String, + `ip_addr` String, +) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', +kafka_topic_list = 'hyperswitch-api-log-events', +kafka_group_name = 'hyper-c1', +kafka_format = 'JSONEachRow', +kafka_handle_error_mode = 'stream'; + + +CREATE TABLE api_events_v2_dist ( + `merchant_id` String, + `payment_id` Nullable(String), + `refund_id` Nullable(String), + `payment_method_id` Nullable(String), + `payment_method` Nullable(String), + `payment_method_type` Nullable(String), + `customer_id` Nullable(String), + `user_id` Nullable(String), + `connector` Nullable(String), + `request_id` String, + `flow_type` LowCardinality(String), + `api_flow` LowCardinality(String), + `api_auth_type` LowCardinality(String), + `request` String, + `response` Nullable(String), + `authentication_data` Nullable(String), + `status_code` UInt32, + `created_at` DateTime CODEC(T64, LZ4), + `inserted_at` DateTime CODEC(T64, LZ4), + `latency` UInt128, + `user_agent` String, + `ip_addr` String, + INDEX flowIndex flow_type TYPE bloom_filter GRANULARITY 1, + INDEX apiIndex api_flow TYPE bloom_filter GRANULARITY 1, + INDEX statusIndex status_code TYPE bloom_filter GRANULARITY 1 +) ENGINE = MergeTree +PARTITION BY toStartOfDay(created_at) +ORDER BY + (created_at, merchant_id, flow_type, status_code, api_flow) +TTL created_at + toIntervalMonth(6) +; + +CREATE MATERIALIZED VIEW api_events_v2_mv TO api_events_v2_dist ( + `merchant_id` String, + `payment_id` Nullable(String), + `refund_id` Nullable(String), + `payment_method_id` Nullable(String), + `payment_method` Nullable(String), + `payment_method_type` Nullable(String), + `customer_id` Nullable(String), + `user_id` Nullable(String), + `connector` Nullable(String), + `request_id` String, + `flow_type` LowCardinality(String), + `api_flow` LowCardinality(String), + `api_auth_type` LowCardinality(String), + `request` String, + `response` Nullable(String), + `authentication_data` Nullable(String), + `status_code` UInt32, + `created_at` DateTime CODEC(T64, LZ4), + `inserted_at` DateTime CODEC(T64, LZ4), + `latency` UInt128, + `user_agent` String, + `ip_addr` String +) AS +SELECT + merchant_id, + payment_id, + refund_id, + payment_method_id, + payment_method, + payment_method_type, + customer_id, + user_id, + connector, + request_id, + flow_type, + api_flow, + api_auth_type, + request, + response, + authentication_data, + status_code, + created_at, + now() as inserted_at, + latency, + user_agent, + ip_addr +FROM + api_events_v2_queue +where length(_error) = 0; + + +CREATE MATERIALIZED VIEW api_events_parse_errors +( + `topic` String, + `partition` Int64, + `offset` Int64, + `raw` String, + `error` String +) +ENGINE = MergeTree +ORDER BY (topic, partition, offset) +SETTINGS index_granularity = 8192 AS +SELECT + _topic AS topic, + _partition AS partition, + _offset AS offset, + _raw_message AS raw, + _error AS error +FROM api_events_v2_queue +WHERE length(_error) > 0 +; diff --git a/crates/analytics/docs/clickhouse/scripts/payment_attempts.sql b/crates/analytics/docs/clickhouse/scripts/payment_attempts.sql new file mode 100644 index 000000000000..276e311e57a9 --- /dev/null +++ b/crates/analytics/docs/clickhouse/scripts/payment_attempts.sql @@ -0,0 +1,156 @@ +CREATE TABLE payment_attempts_queue ( + `payment_id` String, + `merchant_id` String, + `attempt_id` String, + `status` LowCardinality(String), + `amount` Nullable(UInt32), + `currency` LowCardinality(Nullable(String)), + `connector` LowCardinality(Nullable(String)), + `save_to_locker` Nullable(Bool), + `error_message` Nullable(String), + `offer_amount` Nullable(UInt32), + `surcharge_amount` Nullable(UInt32), + `tax_amount` Nullable(UInt32), + `payment_method_id` Nullable(String), + `payment_method` LowCardinality(Nullable(String)), + `payment_method_type` LowCardinality(Nullable(String)), + `connector_transaction_id` Nullable(String), + `capture_method` LowCardinality(Nullable(String)), + `capture_on` Nullable(DateTime) CODEC(T64, LZ4), + `confirm` Bool, + `authentication_type` LowCardinality(Nullable(String)), + `cancellation_reason` Nullable(String), + `amount_to_capture` Nullable(UInt32), + `mandate_id` Nullable(String), + `browser_info` Nullable(String), + `error_code` Nullable(String), + `connector_metadata` Nullable(String), + `payment_experience` Nullable(String), + `created_at` DateTime CODEC(T64, LZ4), + `last_synced` Nullable(DateTime) CODEC(T64, LZ4), + `modified_at` DateTime CODEC(T64, LZ4), + `sign_flag` Int8 +) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', +kafka_topic_list = 'hyperswitch-payment-attempt-events', +kafka_group_name = 'hyper-c1', +kafka_format = 'JSONEachRow', +kafka_handle_error_mode = 'stream'; + +CREATE TABLE payment_attempt_dist ( + `payment_id` String, + `merchant_id` String, + `attempt_id` String, + `status` LowCardinality(String), + `amount` Nullable(UInt32), + `currency` LowCardinality(Nullable(String)), + `connector` LowCardinality(Nullable(String)), + `save_to_locker` Nullable(Bool), + `error_message` Nullable(String), + `offer_amount` Nullable(UInt32), + `surcharge_amount` Nullable(UInt32), + `tax_amount` Nullable(UInt32), + `payment_method_id` Nullable(String), + `payment_method` LowCardinality(Nullable(String)), + `payment_method_type` LowCardinality(Nullable(String)), + `connector_transaction_id` Nullable(String), + `capture_method` Nullable(String), + `capture_on` Nullable(DateTime) CODEC(T64, LZ4), + `confirm` Bool, + `authentication_type` LowCardinality(Nullable(String)), + `cancellation_reason` Nullable(String), + `amount_to_capture` Nullable(UInt32), + `mandate_id` Nullable(String), + `browser_info` Nullable(String), + `error_code` Nullable(String), + `connector_metadata` Nullable(String), + `payment_experience` Nullable(String), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `last_synced` Nullable(DateTime) CODEC(T64, LZ4), + `modified_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `sign_flag` Int8, + INDEX connectorIndex connector TYPE bloom_filter GRANULARITY 1, + INDEX paymentMethodIndex payment_method TYPE bloom_filter GRANULARITY 1, + INDEX authenticationTypeIndex authentication_type TYPE bloom_filter GRANULARITY 1, + INDEX currencyIndex currency TYPE bloom_filter GRANULARITY 1, + INDEX statusIndex status TYPE bloom_filter GRANULARITY 1 +) ENGINE = CollapsingMergeTree( + sign_flag +) +PARTITION BY toStartOfDay(created_at) +ORDER BY + (created_at, merchant_id, attempt_id) +TTL created_at + toIntervalMonth(6) +; + + +CREATE MATERIALIZED VIEW kafka_parse_pa TO payment_attempt_dist ( + `payment_id` String, + `merchant_id` String, + `attempt_id` String, + `status` LowCardinality(String), + `amount` Nullable(UInt32), + `currency` LowCardinality(Nullable(String)), + `connector` LowCardinality(Nullable(String)), + `save_to_locker` Nullable(Bool), + `error_message` Nullable(String), + `offer_amount` Nullable(UInt32), + `surcharge_amount` Nullable(UInt32), + `tax_amount` Nullable(UInt32), + `payment_method_id` Nullable(String), + `payment_method` LowCardinality(Nullable(String)), + `payment_method_type` LowCardinality(Nullable(String)), + `connector_transaction_id` Nullable(String), + `capture_method` Nullable(String), + `confirm` Bool, + `authentication_type` LowCardinality(Nullable(String)), + `cancellation_reason` Nullable(String), + `amount_to_capture` Nullable(UInt32), + `mandate_id` Nullable(String), + `browser_info` Nullable(String), + `error_code` Nullable(String), + `connector_metadata` Nullable(String), + `payment_experience` Nullable(String), + `created_at` DateTime64(3), + `capture_on` Nullable(DateTime64(3)), + `last_synced` Nullable(DateTime64(3)), + `modified_at` DateTime64(3), + `inserted_at` DateTime64(3), + `sign_flag` Int8 +) AS +SELECT + payment_id, + merchant_id, + attempt_id, + status, + amount, + currency, + connector, + save_to_locker, + error_message, + offer_amount, + surcharge_amount, + tax_amount, + payment_method_id, + payment_method, + payment_method_type, + connector_transaction_id, + capture_method, + confirm, + authentication_type, + cancellation_reason, + amount_to_capture, + mandate_id, + browser_info, + error_code, + connector_metadata, + payment_experience, + created_at, + capture_on, + last_synced, + modified_at, + now() as inserted_at, + sign_flag +FROM + payment_attempts_queue; + diff --git a/crates/analytics/docs/clickhouse/scripts/payment_intents.sql b/crates/analytics/docs/clickhouse/scripts/payment_intents.sql new file mode 100644 index 000000000000..8cd487f364b4 --- /dev/null +++ b/crates/analytics/docs/clickhouse/scripts/payment_intents.sql @@ -0,0 +1,116 @@ +CREATE TABLE payment_intents_queue ( + `payment_id` String, + `merchant_id` String, + `status` LowCardinality(String), + `amount` UInt32, + `currency` LowCardinality(Nullable(String)), + `amount_captured` Nullable(UInt32), + `customer_id` Nullable(String), + `description` Nullable(String), + `return_url` Nullable(String), + `connector_id` LowCardinality(Nullable(String)), + `statement_descriptor_name` Nullable(String), + `statement_descriptor_suffix` Nullable(String), + `setup_future_usage` LowCardinality(Nullable(String)), + `off_session` Nullable(Bool), + `client_secret` Nullable(String), + `active_attempt_id` String, + `business_country` String, + `business_label` String, + `modified_at` DateTime CODEC(T64, LZ4), + `created_at` DateTime CODEC(T64, LZ4), + `last_synced` Nullable(DateTime) CODEC(T64, LZ4), + `sign_flag` Int8 +) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', +kafka_topic_list = 'hyperswitch-payment-intent-events', +kafka_group_name = 'hyper-c1', +kafka_format = 'JSONEachRow', +kafka_handle_error_mode = 'stream'; + + +CREATE TABLE payment_intents_dist ( + `payment_id` String, + `merchant_id` String, + `status` LowCardinality(String), + `amount` UInt32, + `currency` LowCardinality(Nullable(String)), + `amount_captured` Nullable(UInt32), + `customer_id` Nullable(String), + `description` Nullable(String), + `return_url` Nullable(String), + `connector_id` LowCardinality(Nullable(String)), + `statement_descriptor_name` Nullable(String), + `statement_descriptor_suffix` Nullable(String), + `setup_future_usage` LowCardinality(Nullable(String)), + `off_session` Nullable(Bool), + `client_secret` Nullable(String), + `active_attempt_id` String, + `business_country` LowCardinality(String), + `business_label` String, + `modified_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `last_synced` Nullable(DateTime) CODEC(T64, LZ4), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `sign_flag` Int8, + INDEX connectorIndex connector_id TYPE bloom_filter GRANULARITY 1, + INDEX currencyIndex currency TYPE bloom_filter GRANULARITY 1, + INDEX statusIndex status TYPE bloom_filter GRANULARITY 1 +) ENGINE = CollapsingMergeTree( + sign_flag +) +PARTITION BY toStartOfDay(created_at) +ORDER BY + (created_at, merchant_id, payment_id) +TTL created_at + toIntervalMonth(6) +; + +CREATE MATERIALIZED VIEW kafka_parse_payment_intent TO payment_intents_dist ( + `payment_id` String, + `merchant_id` String, + `status` LowCardinality(String), + `amount` UInt32, + `currency` LowCardinality(Nullable(String)), + `amount_captured` Nullable(UInt32), + `customer_id` Nullable(String), + `description` Nullable(String), + `return_url` Nullable(String), + `connector_id` LowCardinality(Nullable(String)), + `statement_descriptor_name` Nullable(String), + `statement_descriptor_suffix` Nullable(String), + `setup_future_usage` LowCardinality(Nullable(String)), + `off_session` Nullable(Bool), + `client_secret` Nullable(String), + `active_attempt_id` String, + `business_country` LowCardinality(String), + `business_label` String, + `modified_at` DateTime64(3), + `created_at` DateTime64(3), + `last_synced` Nullable(DateTime64(3)), + `inserted_at` DateTime64(3), + `sign_flag` Int8 +) AS +SELECT + payment_id, + merchant_id, + status, + amount, + currency, + amount_captured, + customer_id, + description, + return_url, + connector_id, + statement_descriptor_name, + statement_descriptor_suffix, + setup_future_usage, + off_session, + client_secret, + active_attempt_id, + business_country, + business_label, + modified_at, + created_at, + last_synced, + now() as inserted_at, + sign_flag +FROM payment_intents_queue; diff --git a/crates/analytics/docs/clickhouse/scripts/refunds.sql b/crates/analytics/docs/clickhouse/scripts/refunds.sql new file mode 100644 index 000000000000..a131270c1326 --- /dev/null +++ b/crates/analytics/docs/clickhouse/scripts/refunds.sql @@ -0,0 +1,121 @@ +CREATE TABLE refund_queue ( + `internal_reference_id` String, + `refund_id` String, + `payment_id` String, + `merchant_id` String, + `connector_transaction_id` String, + `connector` LowCardinality(Nullable(String)), + `connector_refund_id` Nullable(String), + `external_reference_id` Nullable(String), + `refund_type` LowCardinality(String), + `total_amount` Nullable(UInt32), + `currency` LowCardinality(String), + `refund_amount` Nullable(UInt32), + `refund_status` LowCardinality(String), + `sent_to_gateway` Bool, + `refund_error_message` Nullable(String), + `refund_arn` Nullable(String), + `attempt_id` String, + `description` Nullable(String), + `refund_reason` Nullable(String), + `refund_error_code` Nullable(String), + `created_at` DateTime CODEC(T64, LZ4), + `modified_at` DateTime CODEC(T64, LZ4), + `sign_flag` Int8 +) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', +kafka_topic_list = 'hyperswitch-refund-events', +kafka_group_name = 'hyper-c1', +kafka_format = 'JSONEachRow', +kafka_handle_error_mode = 'stream'; + + +CREATE TABLE refund_dist ( + `internal_reference_id` String, + `refund_id` String, + `payment_id` String, + `merchant_id` String, + `connector_transaction_id` String, + `connector` LowCardinality(Nullable(String)), + `connector_refund_id` Nullable(String), + `external_reference_id` Nullable(String), + `refund_type` LowCardinality(String), + `total_amount` Nullable(UInt32), + `currency` LowCardinality(String), + `refund_amount` Nullable(UInt32), + `refund_status` LowCardinality(String), + `sent_to_gateway` Bool, + `refund_error_message` Nullable(String), + `refund_arn` Nullable(String), + `attempt_id` String, + `description` Nullable(String), + `refund_reason` Nullable(String), + `refund_error_code` Nullable(String), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `modified_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `sign_flag` Int8, + INDEX connectorIndex connector TYPE bloom_filter GRANULARITY 1, + INDEX refundTypeIndex refund_type TYPE bloom_filter GRANULARITY 1, + INDEX currencyIndex currency TYPE bloom_filter GRANULARITY 1, + INDEX statusIndex refund_status TYPE bloom_filter GRANULARITY 1 +) ENGINE = CollapsingMergeTree( + sign_flag +) +PARTITION BY toStartOfDay(created_at) +ORDER BY + (created_at, merchant_id, refund_id) +TTL created_at + toIntervalMonth(6) +; + +CREATE MATERIALIZED VIEW kafka_parse_refund TO refund_dist ( + `internal_reference_id` String, + `refund_id` String, + `payment_id` String, + `merchant_id` String, + `connector_transaction_id` String, + `connector` LowCardinality(Nullable(String)), + `connector_refund_id` Nullable(String), + `external_reference_id` Nullable(String), + `refund_type` LowCardinality(String), + `total_amount` Nullable(UInt32), + `currency` LowCardinality(String), + `refund_amount` Nullable(UInt32), + `refund_status` LowCardinality(String), + `sent_to_gateway` Bool, + `refund_error_message` Nullable(String), + `refund_arn` Nullable(String), + `attempt_id` String, + `description` Nullable(String), + `refund_reason` Nullable(String), + `refund_error_code` Nullable(String), + `created_at` DateTime64(3), + `modified_at` DateTime64(3), + `inserted_at` DateTime64(3), + `sign_flag` Int8 +) AS +SELECT + internal_reference_id, + refund_id, + payment_id, + merchant_id, + connector_transaction_id, + connector, + connector_refund_id, + external_reference_id, + refund_type, + total_amount, + currency, + refund_amount, + refund_status, + sent_to_gateway, + refund_error_message, + refund_arn, + attempt_id, + description, + refund_reason, + refund_error_code, + created_at, + modified_at, + now() as inserted_at, + sign_flag +FROM refund_queue; diff --git a/crates/analytics/src/api_event.rs b/crates/analytics/src/api_event.rs new file mode 100644 index 000000000000..113344d47254 --- /dev/null +++ b/crates/analytics/src/api_event.rs @@ -0,0 +1,9 @@ +mod core; +pub mod events; +pub mod filters; +pub mod metrics; +pub mod types; + +pub trait APIEventAnalytics: events::ApiLogsFilterAnalytics {} + +pub use self::core::{api_events_core, get_api_event_metrics, get_filters}; diff --git a/crates/analytics/src/api_event/core.rs b/crates/analytics/src/api_event/core.rs new file mode 100644 index 000000000000..b368d6374f75 --- /dev/null +++ b/crates/analytics/src/api_event/core.rs @@ -0,0 +1,176 @@ +use std::collections::HashMap; + +use api_models::analytics::{ + api_event::{ + ApiEventMetricsBucketIdentifier, ApiEventMetricsBucketValue, ApiLogsRequest, + ApiMetricsBucketResponse, + }, + AnalyticsMetadata, ApiEventFiltersResponse, GetApiEventFiltersRequest, + GetApiEventMetricRequest, MetricsResponse, +}; +use error_stack::{IntoReport, ResultExt}; +use router_env::{ + instrument, logger, + tracing::{self, Instrument}, +}; + +use super::{ + events::{get_api_event, ApiLogsResult}, + metrics::ApiEventMetricRow, +}; +use crate::{ + errors::{AnalyticsError, AnalyticsResult}, + metrics, + types::FiltersError, + AnalyticsProvider, +}; + +#[instrument(skip_all)] +pub async fn api_events_core( + pool: &AnalyticsProvider, + req: ApiLogsRequest, + merchant_id: String, +) -> AnalyticsResult> { + let data = match pool { + AnalyticsProvider::Sqlx(_) => Err(FiltersError::NotImplemented) + .into_report() + .attach_printable("SQL Analytics is not implemented for API Events"), + AnalyticsProvider::Clickhouse(pool) => get_api_event(&merchant_id, req, pool).await, + AnalyticsProvider::CombinedSqlx(_sqlx_pool, ckh_pool) + | AnalyticsProvider::CombinedCkh(_sqlx_pool, ckh_pool) => { + get_api_event(&merchant_id, req, ckh_pool).await + } + } + .change_context(AnalyticsError::UnknownError)?; + Ok(data) +} + +pub async fn get_filters( + pool: &AnalyticsProvider, + req: GetApiEventFiltersRequest, + merchant_id: String, +) -> AnalyticsResult { + use api_models::analytics::{api_event::ApiEventDimensions, ApiEventFilterValue}; + + use super::filters::get_api_event_filter_for_dimension; + use crate::api_event::filters::ApiEventFilter; + + let mut res = ApiEventFiltersResponse::default(); + for dim in req.group_by_names { + let values = match pool { + AnalyticsProvider::Sqlx(_pool) => Err(FiltersError::NotImplemented) + .into_report() + .attach_printable("SQL Analytics is not implemented for API Events"), + AnalyticsProvider::Clickhouse(ckh_pool) + | AnalyticsProvider::CombinedSqlx(_, ckh_pool) + | AnalyticsProvider::CombinedCkh(_, ckh_pool) => { + get_api_event_filter_for_dimension(dim, &merchant_id, &req.time_range, ckh_pool) + .await + } + } + .change_context(AnalyticsError::UnknownError)? + .into_iter() + .filter_map(|fil: ApiEventFilter| match dim { + ApiEventDimensions::StatusCode => fil.status_code.map(|i| i.to_string()), + ApiEventDimensions::FlowType => fil.flow_type, + ApiEventDimensions::ApiFlow => fil.api_flow, + }) + .collect::>(); + res.query_data.push(ApiEventFilterValue { + dimension: dim, + values, + }) + } + + Ok(res) +} + +#[instrument(skip_all)] +pub async fn get_api_event_metrics( + pool: &AnalyticsProvider, + merchant_id: &str, + req: GetApiEventMetricRequest, +) -> AnalyticsResult> { + let mut metrics_accumulator: HashMap = + HashMap::new(); + + let mut set = tokio::task::JoinSet::new(); + for metric_type in req.metrics.iter().cloned() { + let req = req.clone(); + let pool = pool.clone(); + let task_span = tracing::debug_span!( + "analytics_api_metrics_query", + api_event_metric = metric_type.as_ref() + ); + + // TODO: lifetime issues with joinset, + // can be optimized away if joinset lifetime requirements are relaxed + let merchant_id_scoped = merchant_id.to_owned(); + set.spawn( + async move { + let data = pool + .get_api_event_metrics( + &metric_type, + &req.group_by_names.clone(), + &merchant_id_scoped, + &req.filters, + &req.time_series.map(|t| t.granularity), + &req.time_range, + ) + .await + .change_context(AnalyticsError::UnknownError); + (metric_type, data) + } + .instrument(task_span), + ); + } + + while let Some((metric, data)) = set + .join_next() + .await + .transpose() + .into_report() + .change_context(AnalyticsError::UnknownError)? + { + let data = data?; + let attributes = &[ + metrics::request::add_attributes("metric_type", metric.to_string()), + metrics::request::add_attributes("source", pool.to_string()), + ]; + + let value = u64::try_from(data.len()); + if let Ok(val) = value { + metrics::BUCKETS_FETCHED.record(&metrics::CONTEXT, val, attributes); + logger::debug!("Attributes: {:?}, Buckets fetched: {}", attributes, val); + } + for (id, value) in data { + metrics_accumulator + .entry(id) + .and_modify(|data| { + data.api_count = data.api_count.or(value.api_count); + data.status_code_count = data.status_code_count.or(value.status_code_count); + data.latency = data.latency.or(value.latency); + }) + .or_insert(value); + } + } + + let query_data: Vec = metrics_accumulator + .into_iter() + .map(|(id, val)| ApiMetricsBucketResponse { + values: ApiEventMetricsBucketValue { + latency: val.latency, + api_count: val.api_count, + status_code_count: val.status_code_count, + }, + dimensions: id, + }) + .collect(); + + Ok(MetricsResponse { + query_data, + meta_data: [AnalyticsMetadata { + current_time_range: req.time_range, + }], + }) +} diff --git a/crates/analytics/src/api_event/events.rs b/crates/analytics/src/api_event/events.rs new file mode 100644 index 000000000000..73b3fb9cbad2 --- /dev/null +++ b/crates/analytics/src/api_event/events.rs @@ -0,0 +1,105 @@ +use api_models::analytics::{ + api_event::{ApiLogsRequest, QueryType}, + Granularity, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use router_env::Flow; +use time::PrimitiveDateTime; + +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, FiltersError, FiltersResult, LoadRow}, +}; +pub trait ApiLogsFilterAnalytics: LoadRow {} + +pub async fn get_api_event( + merchant_id: &String, + query_param: ApiLogsRequest, + pool: &T, +) -> FiltersResult> +where + T: AnalyticsDataSource + ApiLogsFilterAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::ApiEvents); + query_builder.add_select_column("*").switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + match query_param.query_param { + QueryType::Payment { payment_id } => query_builder + .add_filter_clause("payment_id", payment_id) + .switch()?, + QueryType::Refund { + payment_id, + refund_id, + } => { + query_builder + .add_filter_clause("payment_id", payment_id) + .switch()?; + query_builder + .add_filter_clause("refund_id", refund_id) + .switch()?; + } + } + if let Some(list_api_name) = query_param.api_name_filter { + query_builder + .add_filter_in_range_clause("api_flow", &list_api_name) + .switch()?; + } else { + query_builder + .add_filter_in_range_clause( + "api_flow", + &[ + Flow::PaymentsCancel, + Flow::PaymentsCapture, + Flow::PaymentsConfirm, + Flow::PaymentsCreate, + Flow::PaymentsStart, + Flow::PaymentsUpdate, + Flow::RefundsCreate, + Flow::IncomingWebhookReceive, + ], + ) + .switch()?; + } + //TODO!: update the execute_query function to return reports instead of plain errors... + query_builder + .execute_query::(pool) + .await + .change_context(FiltersError::QueryBuildingError)? + .change_context(FiltersError::QueryExecutionFailure) +} +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct ApiLogsResult { + pub merchant_id: String, + pub payment_id: Option, + pub refund_id: Option, + pub payment_method_id: Option, + pub payment_method: Option, + pub payment_method_type: Option, + pub customer_id: Option, + pub user_id: Option, + pub connector: Option, + pub request_id: Option, + pub flow_type: String, + pub api_flow: String, + pub api_auth_type: Option, + pub request: String, + pub response: Option, + pub error: Option, + pub authentication_data: Option, + pub status_code: u16, + pub latency: Option, + pub user_agent: Option, + pub hs_latency: Option, + pub ip_addr: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: PrimitiveDateTime, +} diff --git a/crates/analytics/src/api_event/filters.rs b/crates/analytics/src/api_event/filters.rs new file mode 100644 index 000000000000..87414ebad4ba --- /dev/null +++ b/crates/analytics/src/api_event/filters.rs @@ -0,0 +1,53 @@ +use api_models::analytics::{api_event::ApiEventDimensions, Granularity, TimeRange}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, FiltersError, FiltersResult, LoadRow}, +}; + +pub trait ApiEventFilterAnalytics: LoadRow {} + +pub async fn get_api_event_filter_for_dimension( + dimension: ApiEventDimensions, + merchant_id: &String, + time_range: &TimeRange, + pool: &T, +) -> FiltersResult> +where + T: AnalyticsDataSource + ApiEventFilterAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::ApiEvents); + + query_builder.add_select_column(dimension).switch()?; + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + query_builder.set_distinct(); + + query_builder + .execute_query::(pool) + .await + .change_context(FiltersError::QueryBuildingError)? + .change_context(FiltersError::QueryExecutionFailure) +} + +#[derive(Debug, serde::Serialize, Eq, PartialEq, serde::Deserialize)] +pub struct ApiEventFilter { + pub status_code: Option, + pub flow_type: Option, + pub api_flow: Option, +} diff --git a/crates/analytics/src/api_event/metrics.rs b/crates/analytics/src/api_event/metrics.rs new file mode 100644 index 000000000000..16f2d7a2f5ab --- /dev/null +++ b/crates/analytics/src/api_event/metrics.rs @@ -0,0 +1,110 @@ +use api_models::analytics::{ + api_event::{ + ApiEventDimensions, ApiEventFilters, ApiEventMetrics, ApiEventMetricsBucketIdentifier, + }, + Granularity, TimeRange, +}; +use time::PrimitiveDateTime; + +use crate::{ + query::{Aggregate, GroupByClause, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, LoadRow, MetricsResult}, +}; + +mod api_count; +pub mod latency; +mod status_code_count; +use api_count::ApiCount; +use latency::MaxLatency; +use status_code_count::StatusCodeCount; + +use self::latency::LatencyAvg; + +#[derive(Debug, PartialEq, Eq, serde::Deserialize)] +pub struct ApiEventMetricRow { + pub latency: Option, + pub api_count: Option, + pub status_code_count: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] + pub start_bucket: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] + pub end_bucket: Option, +} + +pub trait ApiEventMetricAnalytics: LoadRow + LoadRow {} + +#[async_trait::async_trait] +pub trait ApiEventMetric +where + T: AnalyticsDataSource + ApiEventMetricAnalytics, +{ + async fn load_metrics( + &self, + dimensions: &[ApiEventDimensions], + merchant_id: &str, + filters: &ApiEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult>; +} + +#[async_trait::async_trait] +impl ApiEventMetric for ApiEventMetrics +where + T: AnalyticsDataSource + ApiEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[ApiEventDimensions], + merchant_id: &str, + filters: &ApiEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + match self { + Self::Latency => { + MaxLatency + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::ApiCount => { + ApiCount + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::StatusCodeCount => { + StatusCodeCount + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + } + } +} diff --git a/crates/analytics/src/api_event/metrics/api_count.rs b/crates/analytics/src/api_event/metrics/api_count.rs new file mode 100644 index 000000000000..7f5f291aa53e --- /dev/null +++ b/crates/analytics/src/api_event/metrics/api_count.rs @@ -0,0 +1,106 @@ +use api_models::analytics::{ + api_event::{ApiEventDimensions, ApiEventFilters, ApiEventMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::ApiEventMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct ApiCount; + +#[async_trait::async_trait] +impl super::ApiEventMetric for ApiCount +where + T: AnalyticsDataSource + super::ApiEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + _dimensions: &[ApiEventDimensions], + merchant_id: &str, + filters: &ApiEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::ApiEvents); + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("api_count"), + }) + .switch()?; + if !filters.flow_type.is_empty() { + query_builder + .add_filter_in_range_clause(ApiEventDimensions::FlowType, &filters.flow_type) + .attach_printable("Error adding flow_type filter") + .switch()?; + } + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + ApiEventMetricsBucketIdentifier::new(TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/api_event/metrics/latency.rs b/crates/analytics/src/api_event/metrics/latency.rs new file mode 100644 index 000000000000..379b39fbeb9e --- /dev/null +++ b/crates/analytics/src/api_event/metrics/latency.rs @@ -0,0 +1,138 @@ +use api_models::analytics::{ + api_event::{ApiEventDimensions, ApiEventFilters, ApiEventMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::ApiEventMetricRow; +use crate::{ + query::{ + Aggregate, FilterTypes, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, + Window, + }, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct MaxLatency; + +#[async_trait::async_trait] +impl super::ApiEventMetric for MaxLatency +where + T: AnalyticsDataSource + super::ApiEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + _dimensions: &[ApiEventDimensions], + merchant_id: &str, + filters: &ApiEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::ApiEvents); + + query_builder + .add_select_column(Aggregate::Sum { + field: "latency", + alias: Some("latency_sum"), + }) + .switch()?; + + query_builder + .add_select_column(Aggregate::Count { + field: Some("latency"), + alias: Some("latency_count"), + }) + .switch()?; + + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + query_builder + .add_custom_filter_clause("request", "10.63.134.6", FilterTypes::NotLike) + .attach_printable("Error filtering out locker IP") + .switch()?; + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + ApiEventMetricsBucketIdentifier::new(TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }), + ApiEventMetricRow { + latency: if i.latency_count != 0 { + Some(i.latency_sum.unwrap_or(0) / i.latency_count) + } else { + None + }, + api_count: None, + status_code_count: None, + start_bucket: i.start_bucket, + end_bucket: i.end_bucket, + }, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} + +#[derive(Debug, PartialEq, Eq, serde::Deserialize)] +pub struct LatencyAvg { + latency_sum: Option, + latency_count: u64, + #[serde(with = "common_utils::custom_serde::iso8601::option")] + pub start_bucket: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] + pub end_bucket: Option, +} diff --git a/crates/analytics/src/api_event/metrics/status_code_count.rs b/crates/analytics/src/api_event/metrics/status_code_count.rs new file mode 100644 index 000000000000..5c652fd8e0c9 --- /dev/null +++ b/crates/analytics/src/api_event/metrics/status_code_count.rs @@ -0,0 +1,103 @@ +use api_models::analytics::{ + api_event::{ApiEventDimensions, ApiEventFilters, ApiEventMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::ApiEventMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct StatusCodeCount; + +#[async_trait::async_trait] +impl super::ApiEventMetric for StatusCodeCount +where + T: AnalyticsDataSource + super::ApiEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + _dimensions: &[ApiEventDimensions], + merchant_id: &str, + filters: &ApiEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::ApiEvents); + + query_builder + .add_select_column(Aggregate::Count { + field: Some("status_code"), + alias: Some("status_code_count"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + ApiEventMetricsBucketIdentifier::new(TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/api_event/types.rs b/crates/analytics/src/api_event/types.rs new file mode 100644 index 000000000000..72205fc72abf --- /dev/null +++ b/crates/analytics/src/api_event/types.rs @@ -0,0 +1,33 @@ +use api_models::analytics::api_event::{ApiEventDimensions, ApiEventFilters}; +use error_stack::ResultExt; + +use crate::{ + query::{QueryBuilder, QueryFilter, QueryResult, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource}, +}; + +impl QueryFilter for ApiEventFilters +where + T: AnalyticsDataSource, + AnalyticsCollection: ToSql, +{ + fn set_filter_clause(&self, builder: &mut QueryBuilder) -> QueryResult<()> { + if !self.status_code.is_empty() { + builder + .add_filter_in_range_clause(ApiEventDimensions::StatusCode, &self.status_code) + .attach_printable("Error adding status_code filter")?; + } + if !self.flow_type.is_empty() { + builder + .add_filter_in_range_clause(ApiEventDimensions::FlowType, &self.flow_type) + .attach_printable("Error adding flow_type filter")?; + } + if !self.api_flow.is_empty() { + builder + .add_filter_in_range_clause(ApiEventDimensions::ApiFlow, &self.api_flow) + .attach_printable("Error adding api_name filter")?; + } + + Ok(()) + } +} diff --git a/crates/analytics/src/clickhouse.rs b/crates/analytics/src/clickhouse.rs new file mode 100644 index 000000000000..964486c93649 --- /dev/null +++ b/crates/analytics/src/clickhouse.rs @@ -0,0 +1,458 @@ +use std::sync::Arc; + +use actix_web::http::StatusCode; +use common_utils::errors::ParsingError; +use error_stack::{IntoReport, Report, ResultExt}; +use router_env::logger; +use time::PrimitiveDateTime; + +use super::{ + payments::{ + distribution::PaymentDistributionRow, filters::FilterRow, metrics::PaymentMetricRow, + }, + query::{Aggregate, ToSql, Window}, + refunds::{filters::RefundFilterRow, metrics::RefundMetricRow}, + sdk_events::{filters::SdkEventFilter, metrics::SdkEventMetricRow}, + types::{AnalyticsCollection, AnalyticsDataSource, LoadRow, QueryExecutionError}, +}; +use crate::{ + api_event::{ + events::ApiLogsResult, + filters::ApiEventFilter, + metrics::{latency::LatencyAvg, ApiEventMetricRow}, + }, + sdk_events::events::SdkEventsResult, + types::TableEngine, +}; + +pub type ClickhouseResult = error_stack::Result; + +#[derive(Clone, Debug)] +pub struct ClickhouseClient { + pub config: Arc, +} + +#[derive(Clone, Debug, serde::Deserialize)] +pub struct ClickhouseConfig { + username: String, + password: Option, + host: String, + database_name: String, +} + +impl Default for ClickhouseConfig { + fn default() -> Self { + Self { + username: "default".to_string(), + password: None, + host: "http://localhost:8123".to_string(), + database_name: "default".to_string(), + } + } +} + +impl ClickhouseClient { + async fn execute_query(&self, query: &str) -> ClickhouseResult> { + logger::debug!("Executing query: {query}"); + let client = reqwest::Client::new(); + let params = CkhQuery { + date_time_output_format: String::from("iso"), + output_format_json_quote_64bit_integers: 0, + database: self.config.database_name.clone(), + }; + let response = client + .post(&self.config.host) + .query(¶ms) + .basic_auth(self.config.username.clone(), self.config.password.clone()) + .body(format!("{query}\nFORMAT JSON")) + .send() + .await + .into_report() + .change_context(ClickhouseError::ConnectionError)?; + + logger::debug!(clickhouse_response=?response, query=?query, "Clickhouse response"); + if response.status() != StatusCode::OK { + response.text().await.map_or_else( + |er| { + Err(ClickhouseError::ResponseError) + .into_report() + .attach_printable_lazy(|| format!("Error: {er:?}")) + }, + |t| Err(ClickhouseError::ResponseNotOK(t)).into_report(), + ) + } else { + Ok(response + .json::>() + .await + .into_report() + .change_context(ClickhouseError::ResponseError)? + .data) + } + } +} + +#[async_trait::async_trait] +impl AnalyticsDataSource for ClickhouseClient { + type Row = serde_json::Value; + + async fn load_results( + &self, + query: &str, + ) -> common_utils::errors::CustomResult, QueryExecutionError> + where + Self: LoadRow, + { + self.execute_query(query) + .await + .change_context(QueryExecutionError::DatabaseError)? + .into_iter() + .map(Self::load_row) + .collect::, _>>() + .change_context(QueryExecutionError::RowExtractionFailure) + } + + fn get_table_engine(table: AnalyticsCollection) -> TableEngine { + match table { + AnalyticsCollection::Payment + | AnalyticsCollection::Refund + | AnalyticsCollection::PaymentIntent => { + TableEngine::CollapsingMergeTree { sign: "sign_flag" } + } + AnalyticsCollection::SdkEvents => TableEngine::BasicTree, + AnalyticsCollection::ApiEvents => TableEngine::BasicTree, + } + } +} + +impl LoadRow for ClickhouseClient +where + Self::Row: TryInto>, +{ + fn load_row(row: Self::Row) -> common_utils::errors::CustomResult { + row.try_into() + .change_context(QueryExecutionError::RowExtractionFailure) + } +} + +impl super::payments::filters::PaymentFilterAnalytics for ClickhouseClient {} +impl super::payments::metrics::PaymentMetricAnalytics for ClickhouseClient {} +impl super::payments::distribution::PaymentDistributionAnalytics for ClickhouseClient {} +impl super::refunds::metrics::RefundMetricAnalytics for ClickhouseClient {} +impl super::refunds::filters::RefundFilterAnalytics for ClickhouseClient {} +impl super::sdk_events::filters::SdkEventFilterAnalytics for ClickhouseClient {} +impl super::sdk_events::metrics::SdkEventMetricAnalytics for ClickhouseClient {} +impl super::sdk_events::events::SdkEventsFilterAnalytics for ClickhouseClient {} +impl super::api_event::events::ApiLogsFilterAnalytics for ClickhouseClient {} +impl super::api_event::filters::ApiEventFilterAnalytics for ClickhouseClient {} +impl super::api_event::metrics::ApiEventMetricAnalytics for ClickhouseClient {} + +#[derive(Debug, serde::Serialize)] +struct CkhQuery { + date_time_output_format: String, + output_format_json_quote_64bit_integers: u8, + database: String, +} + +#[derive(Debug, serde::Deserialize)] +struct CkhOutput { + data: Vec, +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse ApiLogsResult in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse SdkEventsResult in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse PaymentMetricRow in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse PaymentDistributionRow in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse FilterRow in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse RefundMetricRow in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse RefundFilterRow in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse ApiEventMetricRow in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse LatencyAvg in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse SdkEventMetricRow in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse SdkEventFilter in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse ApiEventFilter in clickhouse results", + )) + } +} + +impl ToSql for PrimitiveDateTime { + fn to_sql(&self, _table_engine: &TableEngine) -> error_stack::Result { + let format = + time::format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]") + .into_report() + .change_context(ParsingError::DateTimeParsingError) + .attach_printable("Failed to parse format description")?; + self.format(&format) + .into_report() + .change_context(ParsingError::EncodeError( + "failed to encode to clickhouse date-time format", + )) + .attach_printable("Failed to format date time") + } +} + +impl ToSql for AnalyticsCollection { + fn to_sql(&self, _table_engine: &TableEngine) -> error_stack::Result { + match self { + Self::Payment => Ok("payment_attempt_dist".to_string()), + Self::Refund => Ok("refund_dist".to_string()), + Self::SdkEvents => Ok("sdk_events_dist".to_string()), + Self::ApiEvents => Ok("api_audit_log".to_string()), + Self::PaymentIntent => Ok("payment_intents_dist".to_string()), + } + } +} + +impl ToSql for Aggregate +where + T: ToSql, +{ + fn to_sql(&self, table_engine: &TableEngine) -> error_stack::Result { + Ok(match self { + Self::Count { field: _, alias } => { + let query = match table_engine { + TableEngine::CollapsingMergeTree { sign } => format!("sum({sign})"), + TableEngine::BasicTree => "count(*)".to_string(), + }; + format!( + "{query}{}", + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + Self::Sum { field, alias } => { + let query = match table_engine { + TableEngine::CollapsingMergeTree { sign } => format!( + "sum({sign} * {})", + field + .to_sql(table_engine) + .attach_printable("Failed to sum aggregate")? + ), + TableEngine::BasicTree => format!( + "sum({})", + field + .to_sql(table_engine) + .attach_printable("Failed to sum aggregate")? + ), + }; + format!( + "{query}{}", + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + Self::Min { field, alias } => { + format!( + "min({}){}", + field + .to_sql(table_engine) + .attach_printable("Failed to min aggregate")?, + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + Self::Max { field, alias } => { + format!( + "max({}){}", + field + .to_sql(table_engine) + .attach_printable("Failed to max aggregate")?, + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + }) + } +} + +impl ToSql for Window +where + T: ToSql, +{ + fn to_sql(&self, table_engine: &TableEngine) -> error_stack::Result { + Ok(match self { + Self::Sum { + field, + partition_by, + order_by, + alias, + } => { + format!( + "sum({}) over ({}{}){}", + field + .to_sql(table_engine) + .attach_printable("Failed to sum window")?, + partition_by.as_ref().map_or_else( + || "".to_owned(), + |partition_by| format!("partition by {}", partition_by.to_owned()) + ), + order_by.as_ref().map_or_else( + || "".to_owned(), + |(order_column, order)| format!( + " order by {} {}", + order_column.to_owned(), + order.to_string() + ) + ), + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + Self::RowNumber { + field: _, + partition_by, + order_by, + alias, + } => { + format!( + "row_number() over ({}{}){}", + partition_by.as_ref().map_or_else( + || "".to_owned(), + |partition_by| format!("partition by {}", partition_by.to_owned()) + ), + order_by.as_ref().map_or_else( + || "".to_owned(), + |(order_column, order)| format!( + " order by {} {}", + order_column.to_owned(), + order.to_string() + ) + ), + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + }) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum ClickhouseError { + #[error("Clickhouse connection error")] + ConnectionError, + #[error("Clickhouse NON-200 response content: '{0}'")] + ResponseNotOK(String), + #[error("Clickhouse response error")] + ResponseError, +} diff --git a/crates/analytics/src/core.rs b/crates/analytics/src/core.rs new file mode 100644 index 000000000000..354e1e2f1766 --- /dev/null +++ b/crates/analytics/src/core.rs @@ -0,0 +1,31 @@ +use api_models::analytics::GetInfoResponse; + +use crate::{types::AnalyticsDomain, utils}; + +pub async fn get_domain_info( + domain: AnalyticsDomain, +) -> crate::errors::AnalyticsResult { + let info = match domain { + AnalyticsDomain::Payments => GetInfoResponse { + metrics: utils::get_payment_metrics_info(), + download_dimensions: None, + dimensions: utils::get_payment_dimensions(), + }, + AnalyticsDomain::Refunds => GetInfoResponse { + metrics: utils::get_refund_metrics_info(), + download_dimensions: None, + dimensions: utils::get_refund_dimensions(), + }, + AnalyticsDomain::SdkEvents => GetInfoResponse { + metrics: utils::get_sdk_event_metrics_info(), + download_dimensions: None, + dimensions: utils::get_sdk_event_dimensions(), + }, + AnalyticsDomain::ApiEvents => GetInfoResponse { + metrics: utils::get_api_event_metrics_info(), + download_dimensions: None, + dimensions: utils::get_api_event_dimensions(), + }, + }; + Ok(info) +} diff --git a/crates/router/src/analytics/errors.rs b/crates/analytics/src/errors.rs similarity index 100% rename from crates/router/src/analytics/errors.rs rename to crates/analytics/src/errors.rs diff --git a/crates/analytics/src/lambda_utils.rs b/crates/analytics/src/lambda_utils.rs new file mode 100644 index 000000000000..f9446a402b4e --- /dev/null +++ b/crates/analytics/src/lambda_utils.rs @@ -0,0 +1,36 @@ +use aws_config::{self, meta::region::RegionProviderChain}; +use aws_sdk_lambda::{config::Region, types::InvocationType::Event, Client}; +use aws_smithy_types::Blob; +use common_utils::errors::CustomResult; +use error_stack::{IntoReport, ResultExt}; + +use crate::errors::AnalyticsError; + +async fn get_aws_client(region: String) -> Client { + let region_provider = RegionProviderChain::first_try(Region::new(region)); + let sdk_config = aws_config::from_env().region(region_provider).load().await; + Client::new(&sdk_config) +} + +pub async fn invoke_lambda( + function_name: &str, + region: &str, + json_bytes: &[u8], +) -> CustomResult<(), AnalyticsError> { + get_aws_client(region.to_string()) + .await + .invoke() + .function_name(function_name) + .invocation_type(Event) + .payload(Blob::new(json_bytes.to_owned())) + .send() + .await + .into_report() + .map_err(|er| { + let er_rep = format!("{er:?}"); + er.attach_printable(er_rep) + }) + .change_context(AnalyticsError::UnknownError) + .attach_printable("Lambda invocation failed")?; + Ok(()) +} diff --git a/crates/analytics/src/lib.rs b/crates/analytics/src/lib.rs new file mode 100644 index 000000000000..24da77f84f2b --- /dev/null +++ b/crates/analytics/src/lib.rs @@ -0,0 +1,509 @@ +mod clickhouse; +pub mod core; +pub mod errors; +pub mod metrics; +pub mod payments; +mod query; +pub mod refunds; + +pub mod api_event; +pub mod sdk_events; +mod sqlx; +mod types; +use api_event::metrics::{ApiEventMetric, ApiEventMetricRow}; +pub use types::AnalyticsDomain; +pub mod lambda_utils; +pub mod utils; + +use std::sync::Arc; + +use api_models::analytics::{ + api_event::{ + ApiEventDimensions, ApiEventFilters, ApiEventMetrics, ApiEventMetricsBucketIdentifier, + }, + payments::{PaymentDimensions, PaymentFilters, PaymentMetrics, PaymentMetricsBucketIdentifier}, + refunds::{RefundDimensions, RefundFilters, RefundMetrics, RefundMetricsBucketIdentifier}, + sdk_events::{ + SdkEventDimensions, SdkEventFilters, SdkEventMetrics, SdkEventMetricsBucketIdentifier, + }, + Distribution, Granularity, TimeRange, +}; +use clickhouse::ClickhouseClient; +pub use clickhouse::ClickhouseConfig; +use error_stack::IntoReport; +use router_env::{ + logger, + tracing::{self, instrument}, +}; +use storage_impl::config::Database; + +use self::{ + payments::{ + distribution::{PaymentDistribution, PaymentDistributionRow}, + metrics::{PaymentMetric, PaymentMetricRow}, + }, + refunds::metrics::{RefundMetric, RefundMetricRow}, + sdk_events::metrics::{SdkEventMetric, SdkEventMetricRow}, + sqlx::SqlxClient, + types::MetricsError, +}; + +#[derive(Clone, Debug)] +pub enum AnalyticsProvider { + Sqlx(SqlxClient), + Clickhouse(ClickhouseClient), + CombinedCkh(SqlxClient, ClickhouseClient), + CombinedSqlx(SqlxClient, ClickhouseClient), +} + +impl Default for AnalyticsProvider { + fn default() -> Self { + Self::Sqlx(SqlxClient::default()) + } +} + +impl ToString for AnalyticsProvider { + fn to_string(&self) -> String { + String::from(match self { + Self::Clickhouse(_) => "Clickhouse", + Self::Sqlx(_) => "Sqlx", + Self::CombinedCkh(_, _) => "CombinedCkh", + Self::CombinedSqlx(_, _) => "CombinedSqlx", + }) + } +} + +impl AnalyticsProvider { + #[instrument(skip_all)] + pub async fn get_payment_metrics( + &self, + metric: &PaymentMetrics, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + ) -> types::MetricsResult> { + // Metrics to get the fetch time for each payment metric + metrics::request::record_operation_time( + async { + match self { + Self::Sqlx(pool) => { + metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::Clickhouse(pool) => { + metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::CombinedCkh(sqlx_pool, ckh_pool) => { + let (ckh_result, sqlx_result) = tokio::join!(metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + ckh_pool, + ), + metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + sqlx_pool, + )); + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + router_env::logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres payments analytics metrics") + }, + _ => {} + + }; + + ckh_result + } + Self::CombinedSqlx(sqlx_pool, ckh_pool) => { + let (ckh_result, sqlx_result) = tokio::join!(metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + ckh_pool, + ), + metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + sqlx_pool, + )); + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + router_env::logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres payments analytics metrics") + }, + _ => {} + + }; + + sqlx_result + } + } + }, + &metrics::METRIC_FETCH_TIME, + metric, + self, + ) + .await + } + + pub async fn get_payment_distribution( + &self, + distribution: &Distribution, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + ) -> types::MetricsResult> { + // Metrics to get the fetch time for each payment metric + metrics::request::record_operation_time( + async { + match self { + Self::Sqlx(pool) => { + distribution.distribution_for + .load_distribution( + distribution, + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::Clickhouse(pool) => { + distribution.distribution_for + .load_distribution( + distribution, + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::CombinedCkh(sqlx_pool, ckh_pool) => { + let (ckh_result, sqlx_result) = tokio::join!(distribution.distribution_for + .load_distribution( + distribution, + dimensions, + merchant_id, + filters, + granularity, + time_range, + ckh_pool, + ), + distribution.distribution_for + .load_distribution( + distribution, + dimensions, + merchant_id, + filters, + granularity, + time_range, + sqlx_pool, + )); + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + router_env::logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres payments analytics distribution") + }, + _ => {} + + }; + + ckh_result + } + Self::CombinedSqlx(sqlx_pool, ckh_pool) => { + let (ckh_result, sqlx_result) = tokio::join!(distribution.distribution_for + .load_distribution( + distribution, + dimensions, + merchant_id, + filters, + granularity, + time_range, + ckh_pool, + ), + distribution.distribution_for + .load_distribution( + distribution, + dimensions, + merchant_id, + filters, + granularity, + time_range, + sqlx_pool, + )); + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + router_env::logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres payments analytics distribution") + }, + _ => {} + + }; + + sqlx_result + } + } + }, + &metrics::METRIC_FETCH_TIME, + &distribution.distribution_for, + self, + ) + .await + } + + pub async fn get_refund_metrics( + &self, + metric: &RefundMetrics, + dimensions: &[RefundDimensions], + merchant_id: &str, + filters: &RefundFilters, + granularity: &Option, + time_range: &TimeRange, + ) -> types::MetricsResult> { + // Metrics to get the fetch time for each refund metric + metrics::request::record_operation_time( + async { + match self { + Self::Sqlx(pool) => { + metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::Clickhouse(pool) => { + metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::CombinedCkh(sqlx_pool, ckh_pool) => { + let (ckh_result, sqlx_result) = tokio::join!( + metric.load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + ckh_pool, + ), + metric.load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + sqlx_pool, + ) + ); + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres refunds analytics metrics") + } + _ => {} + }; + ckh_result + } + Self::CombinedSqlx(sqlx_pool, ckh_pool) => { + let (ckh_result, sqlx_result) = tokio::join!( + metric.load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + ckh_pool, + ), + metric.load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + sqlx_pool, + ) + ); + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres refunds analytics metrics") + } + _ => {} + }; + sqlx_result + } + } + }, + &metrics::METRIC_FETCH_TIME, + metric, + self, + ) + .await + } + + pub async fn get_sdk_event_metrics( + &self, + metric: &SdkEventMetrics, + dimensions: &[SdkEventDimensions], + pub_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + ) -> types::MetricsResult> { + match self { + Self::Sqlx(_pool) => Err(MetricsError::NotImplemented).into_report(), + Self::Clickhouse(pool) => { + metric + .load_metrics(dimensions, pub_key, filters, granularity, time_range, pool) + .await + } + Self::CombinedCkh(_sqlx_pool, ckh_pool) | Self::CombinedSqlx(_sqlx_pool, ckh_pool) => { + metric + .load_metrics( + dimensions, + pub_key, + filters, + granularity, + // Since SDK events are ckh only use ckh here + time_range, + ckh_pool, + ) + .await + } + } + } + + pub async fn get_api_event_metrics( + &self, + metric: &ApiEventMetrics, + dimensions: &[ApiEventDimensions], + pub_key: &str, + filters: &ApiEventFilters, + granularity: &Option, + time_range: &TimeRange, + ) -> types::MetricsResult> { + match self { + Self::Sqlx(_pool) => Err(MetricsError::NotImplemented).into_report(), + Self::Clickhouse(ckh_pool) + | Self::CombinedCkh(_, ckh_pool) + | Self::CombinedSqlx(_, ckh_pool) => { + // Since API events are ckh only use ckh here + metric + .load_metrics( + dimensions, + pub_key, + filters, + granularity, + time_range, + ckh_pool, + ) + .await + } + } + } + + pub async fn from_conf(config: &AnalyticsConfig) -> Self { + match config { + AnalyticsConfig::Sqlx { sqlx } => Self::Sqlx(SqlxClient::from_conf(sqlx).await), + AnalyticsConfig::Clickhouse { clickhouse } => Self::Clickhouse(ClickhouseClient { + config: Arc::new(clickhouse.clone()), + }), + AnalyticsConfig::CombinedCkh { sqlx, clickhouse } => Self::CombinedCkh( + SqlxClient::from_conf(sqlx).await, + ClickhouseClient { + config: Arc::new(clickhouse.clone()), + }, + ), + AnalyticsConfig::CombinedSqlx { sqlx, clickhouse } => Self::CombinedSqlx( + SqlxClient::from_conf(sqlx).await, + ClickhouseClient { + config: Arc::new(clickhouse.clone()), + }, + ), + } + } +} + +#[derive(Clone, Debug, serde::Deserialize)] +#[serde(tag = "source")] +#[serde(rename_all = "lowercase")] +pub enum AnalyticsConfig { + Sqlx { + sqlx: Database, + }, + Clickhouse { + clickhouse: ClickhouseConfig, + }, + CombinedCkh { + sqlx: Database, + clickhouse: ClickhouseConfig, + }, + CombinedSqlx { + sqlx: Database, + clickhouse: ClickhouseConfig, + }, +} + +impl Default for AnalyticsConfig { + fn default() -> Self { + Self::Sqlx { + sqlx: Database::default(), + } + } +} + +#[derive(Clone, Debug, serde::Deserialize, Default, serde::Serialize)] +pub struct ReportConfig { + pub payment_function: String, + pub refund_function: String, + pub dispute_function: String, + pub region: String, +} diff --git a/crates/analytics/src/main.rs b/crates/analytics/src/main.rs new file mode 100644 index 000000000000..5bf256ea9783 --- /dev/null +++ b/crates/analytics/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello world"); +} diff --git a/crates/router/src/analytics/metrics.rs b/crates/analytics/src/metrics.rs similarity index 100% rename from crates/router/src/analytics/metrics.rs rename to crates/analytics/src/metrics.rs diff --git a/crates/router/src/analytics/metrics/request.rs b/crates/analytics/src/metrics/request.rs similarity index 51% rename from crates/router/src/analytics/metrics/request.rs rename to crates/analytics/src/metrics/request.rs index b7c202f2db25..3d1a78808f34 100644 --- a/crates/router/src/analytics/metrics/request.rs +++ b/crates/analytics/src/metrics/request.rs @@ -6,24 +6,20 @@ pub fn add_attributes>( } #[inline] -pub async fn record_operation_time( +pub async fn record_operation_time( future: F, metric: &once_cell::sync::Lazy>, - metric_name: &api_models::analytics::payments::PaymentMetrics, - source: &crate::analytics::AnalyticsProvider, + metric_name: &T, + source: &crate::AnalyticsProvider, ) -> R where F: futures::Future, + T: ToString, { let (result, time) = time_future(future).await; let attributes = &[ add_attributes("metric_name", metric_name.to_string()), - add_attributes( - "source", - match source { - crate::analytics::AnalyticsProvider::Sqlx(_) => "Sqlx", - }, - ), + add_attributes("source", source.to_string()), ]; let value = time.as_secs_f64(); metric.record(&super::CONTEXT, value, attributes); @@ -44,17 +40,3 @@ where let time_spent = start.elapsed(); (result, time_spent) } - -#[macro_export] -macro_rules! histogram_metric { - ($name:ident, $meter:ident) => { - pub(crate) static $name: once_cell::sync::Lazy< - $crate::opentelemetry::metrics::Histogram, - > = once_cell::sync::Lazy::new(|| $meter.u64_histogram(stringify!($name)).init()); - }; - ($name:ident, $meter:ident, $description:literal) => { - pub(crate) static $name: once_cell::sync::Lazy< - $crate::opentelemetry::metrics::Histogram, - > = once_cell::sync::Lazy::new(|| $meter.u64_histogram($description).init()); - }; -} diff --git a/crates/analytics/src/payments.rs b/crates/analytics/src/payments.rs new file mode 100644 index 000000000000..984647172c5b --- /dev/null +++ b/crates/analytics/src/payments.rs @@ -0,0 +1,16 @@ +pub mod accumulator; +mod core; +pub mod distribution; +pub mod filters; +pub mod metrics; +pub mod types; +pub use accumulator::{ + PaymentDistributionAccumulator, PaymentMetricAccumulator, PaymentMetricsAccumulator, +}; + +pub trait PaymentAnalytics: + metrics::PaymentMetricAnalytics + filters::PaymentFilterAnalytics +{ +} + +pub use self::core::{get_filters, get_metrics}; diff --git a/crates/router/src/analytics/payments/accumulator.rs b/crates/analytics/src/payments/accumulator.rs similarity index 62% rename from crates/router/src/analytics/payments/accumulator.rs rename to crates/analytics/src/payments/accumulator.rs index 5eebd0974693..c340f2888f8b 100644 --- a/crates/router/src/analytics/payments/accumulator.rs +++ b/crates/analytics/src/payments/accumulator.rs @@ -1,8 +1,9 @@ -use api_models::analytics::payments::PaymentMetricsBucketValue; -use common_enums::enums as storage_enums; +use api_models::analytics::payments::{ErrorResult, PaymentMetricsBucketValue}; +use bigdecimal::ToPrimitive; +use diesel_models::enums as storage_enums; use router_env::logger; -use super::metrics::PaymentMetricRow; +use super::{distribution::PaymentDistributionRow, metrics::PaymentMetricRow}; #[derive(Debug, Default)] pub struct PaymentMetricsAccumulator { @@ -11,6 +12,22 @@ pub struct PaymentMetricsAccumulator { pub payment_success: CountAccumulator, pub processed_amount: SumAccumulator, pub avg_ticket_size: AverageAccumulator, + pub payment_error_message: ErrorDistributionAccumulator, + pub retries_count: CountAccumulator, + pub retries_amount_processed: SumAccumulator, + pub connector_success_rate: SuccessRateAccumulator, +} + +#[derive(Debug, Default)] +pub struct ErrorDistributionRow { + pub count: i64, + pub total: i64, + pub error_message: String, +} + +#[derive(Debug, Default)] +pub struct ErrorDistributionAccumulator { + pub error_vec: Vec, } #[derive(Debug, Default)] @@ -45,6 +62,51 @@ pub trait PaymentMetricAccumulator { fn collect(self) -> Self::MetricOutput; } +pub trait PaymentDistributionAccumulator { + type DistributionOutput; + + fn add_distribution_bucket(&mut self, distribution: &PaymentDistributionRow); + + fn collect(self) -> Self::DistributionOutput; +} + +impl PaymentDistributionAccumulator for ErrorDistributionAccumulator { + type DistributionOutput = Option>; + + fn add_distribution_bucket(&mut self, distribution: &PaymentDistributionRow) { + self.error_vec.push(ErrorDistributionRow { + count: distribution.count.unwrap_or_default(), + total: distribution + .total + .clone() + .map(|i| i.to_i64().unwrap_or_default()) + .unwrap_or_default(), + error_message: distribution.error_message.clone().unwrap_or("".to_string()), + }) + } + + fn collect(mut self) -> Self::DistributionOutput { + if self.error_vec.is_empty() { + None + } else { + self.error_vec.sort_by(|a, b| b.count.cmp(&a.count)); + let mut res: Vec = Vec::new(); + for val in self.error_vec.into_iter() { + let perc = f64::from(u32::try_from(val.count).ok()?) * 100.0 + / f64::from(u32::try_from(val.total).ok()?); + + res.push(ErrorResult { + reason: val.error_message, + count: val.count, + percentage: (perc * 100.0).round() / 100.0, + }) + } + + Some(res) + } + } +} + impl PaymentMetricAccumulator for SuccessRateAccumulator { type MetricOutput = Option; @@ -145,6 +207,10 @@ impl PaymentMetricsAccumulator { payment_success_count: self.payment_success.collect(), payment_processed_amount: self.processed_amount.collect(), avg_ticket_size: self.avg_ticket_size.collect(), + payment_error_message: self.payment_error_message.collect(), + retries_count: self.retries_count.collect(), + retries_amount_processed: self.retries_amount_processed.collect(), + connector_success_rate: self.connector_success_rate.collect(), } } } diff --git a/crates/analytics/src/payments/core.rs b/crates/analytics/src/payments/core.rs new file mode 100644 index 000000000000..138e88789327 --- /dev/null +++ b/crates/analytics/src/payments/core.rs @@ -0,0 +1,303 @@ +#![allow(dead_code)] +use std::collections::HashMap; + +use api_models::analytics::{ + payments::{ + MetricsBucketResponse, PaymentDimensions, PaymentDistributions, PaymentMetrics, + PaymentMetricsBucketIdentifier, + }, + AnalyticsMetadata, FilterValue, GetPaymentFiltersRequest, GetPaymentMetricRequest, + MetricsResponse, PaymentFiltersResponse, +}; +use common_utils::errors::CustomResult; +use error_stack::{IntoReport, ResultExt}; +use router_env::{ + instrument, logger, + tracing::{self, Instrument}, +}; + +use super::{ + distribution::PaymentDistributionRow, + filters::{get_payment_filter_for_dimension, FilterRow}, + metrics::PaymentMetricRow, + PaymentMetricsAccumulator, +}; +use crate::{ + errors::{AnalyticsError, AnalyticsResult}, + metrics, + payments::{PaymentDistributionAccumulator, PaymentMetricAccumulator}, + AnalyticsProvider, +}; + +#[derive(Debug)] +pub enum TaskType { + MetricTask( + PaymentMetrics, + CustomResult, AnalyticsError>, + ), + DistributionTask( + PaymentDistributions, + CustomResult, AnalyticsError>, + ), +} + +#[instrument(skip_all)] +pub async fn get_metrics( + pool: &AnalyticsProvider, + merchant_id: &str, + req: GetPaymentMetricRequest, +) -> AnalyticsResult> { + let mut metrics_accumulator: HashMap< + PaymentMetricsBucketIdentifier, + PaymentMetricsAccumulator, + > = HashMap::new(); + + let mut set = tokio::task::JoinSet::new(); + for metric_type in req.metrics.iter().cloned() { + let req = req.clone(); + let pool = pool.clone(); + let task_span = tracing::debug_span!( + "analytics_payments_metrics_query", + payment_metric = metric_type.as_ref() + ); + + // TODO: lifetime issues with joinset, + // can be optimized away if joinset lifetime requirements are relaxed + let merchant_id_scoped = merchant_id.to_owned(); + set.spawn( + async move { + let data = pool + .get_payment_metrics( + &metric_type, + &req.group_by_names.clone(), + &merchant_id_scoped, + &req.filters, + &req.time_series.map(|t| t.granularity), + &req.time_range, + ) + .await + .change_context(AnalyticsError::UnknownError); + TaskType::MetricTask(metric_type, data) + } + .instrument(task_span), + ); + } + + if let Some(distribution) = req.clone().distribution { + let req = req.clone(); + let pool = pool.clone(); + let task_span = tracing::debug_span!( + "analytics_payments_distribution_query", + payment_distribution = distribution.distribution_for.as_ref() + ); + + let merchant_id_scoped = merchant_id.to_owned(); + set.spawn( + async move { + let data = pool + .get_payment_distribution( + &distribution, + &req.group_by_names.clone(), + &merchant_id_scoped, + &req.filters, + &req.time_series.map(|t| t.granularity), + &req.time_range, + ) + .await + .change_context(AnalyticsError::UnknownError); + TaskType::DistributionTask(distribution.distribution_for, data) + } + .instrument(task_span), + ); + } + + while let Some(task_type) = set + .join_next() + .await + .transpose() + .into_report() + .change_context(AnalyticsError::UnknownError)? + { + match task_type { + TaskType::MetricTask(metric, data) => { + let data = data?; + let attributes = &[ + metrics::request::add_attributes("metric_type", metric.to_string()), + metrics::request::add_attributes("source", pool.to_string()), + ]; + + let value = u64::try_from(data.len()); + if let Ok(val) = value { + metrics::BUCKETS_FETCHED.record(&metrics::CONTEXT, val, attributes); + logger::debug!("Attributes: {:?}, Buckets fetched: {}", attributes, val); + } + + for (id, value) in data { + logger::debug!(bucket_id=?id, bucket_value=?value, "Bucket row for metric {metric}"); + let metrics_builder = metrics_accumulator.entry(id).or_default(); + match metric { + PaymentMetrics::PaymentSuccessRate => metrics_builder + .payment_success_rate + .add_metrics_bucket(&value), + PaymentMetrics::PaymentCount => { + metrics_builder.payment_count.add_metrics_bucket(&value) + } + PaymentMetrics::PaymentSuccessCount => { + metrics_builder.payment_success.add_metrics_bucket(&value) + } + PaymentMetrics::PaymentProcessedAmount => { + metrics_builder.processed_amount.add_metrics_bucket(&value) + } + PaymentMetrics::AvgTicketSize => { + metrics_builder.avg_ticket_size.add_metrics_bucket(&value) + } + PaymentMetrics::RetriesCount => { + metrics_builder.retries_count.add_metrics_bucket(&value); + metrics_builder + .retries_amount_processed + .add_metrics_bucket(&value) + } + PaymentMetrics::ConnectorSuccessRate => { + metrics_builder + .connector_success_rate + .add_metrics_bucket(&value); + } + } + } + + logger::debug!( + "Analytics Accumulated Results: metric: {}, results: {:#?}", + metric, + metrics_accumulator + ); + } + TaskType::DistributionTask(distribution, data) => { + let data = data?; + let attributes = &[ + metrics::request::add_attributes("distribution_type", distribution.to_string()), + metrics::request::add_attributes("source", pool.to_string()), + ]; + + let value = u64::try_from(data.len()); + if let Ok(val) = value { + metrics::BUCKETS_FETCHED.record(&metrics::CONTEXT, val, attributes); + logger::debug!("Attributes: {:?}, Buckets fetched: {}", attributes, val); + } + + for (id, value) in data { + logger::debug!(bucket_id=?id, bucket_value=?value, "Bucket row for distribution {distribution}"); + let metrics_accumulator = metrics_accumulator.entry(id).or_default(); + match distribution { + PaymentDistributions::PaymentErrorMessage => metrics_accumulator + .payment_error_message + .add_distribution_bucket(&value), + } + } + + logger::debug!( + "Analytics Accumulated Results: distribution: {}, results: {:#?}", + distribution, + metrics_accumulator + ); + } + } + } + + let query_data: Vec = metrics_accumulator + .into_iter() + .map(|(id, val)| MetricsBucketResponse { + values: val.collect(), + dimensions: id, + }) + .collect(); + + Ok(MetricsResponse { + query_data, + meta_data: [AnalyticsMetadata { + current_time_range: req.time_range, + }], + }) +} + +pub async fn get_filters( + pool: &AnalyticsProvider, + req: GetPaymentFiltersRequest, + merchant_id: &String, +) -> AnalyticsResult { + let mut res = PaymentFiltersResponse::default(); + + for dim in req.group_by_names { + let values = match pool { + AnalyticsProvider::Sqlx(pool) => { + get_payment_filter_for_dimension(dim, merchant_id, &req.time_range, pool) + .await + } + AnalyticsProvider::Clickhouse(pool) => { + get_payment_filter_for_dimension(dim, merchant_id, &req.time_range, pool) + .await + } + AnalyticsProvider::CombinedCkh(sqlx_poll, ckh_pool) => { + let ckh_result = get_payment_filter_for_dimension( + dim, + merchant_id, + &req.time_range, + ckh_pool, + ) + .await; + let sqlx_result = get_payment_filter_for_dimension( + dim, + merchant_id, + &req.time_range, + sqlx_poll, + ) + .await; + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + router_env::logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres payments analytics filters") + }, + _ => {} + }; + ckh_result + } + AnalyticsProvider::CombinedSqlx(sqlx_poll, ckh_pool) => { + let ckh_result = get_payment_filter_for_dimension( + dim, + merchant_id, + &req.time_range, + ckh_pool, + ) + .await; + let sqlx_result = get_payment_filter_for_dimension( + dim, + merchant_id, + &req.time_range, + sqlx_poll, + ) + .await; + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + router_env::logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres payments analytics filters") + }, + _ => {} + }; + sqlx_result + } + } + .change_context(AnalyticsError::UnknownError)? + .into_iter() + .filter_map(|fil: FilterRow| match dim { + PaymentDimensions::Currency => fil.currency.map(|i| i.as_ref().to_string()), + PaymentDimensions::PaymentStatus => fil.status.map(|i| i.as_ref().to_string()), + PaymentDimensions::Connector => fil.connector, + PaymentDimensions::AuthType => fil.authentication_type.map(|i| i.as_ref().to_string()), + PaymentDimensions::PaymentMethod => fil.payment_method, + PaymentDimensions::PaymentMethodType => fil.payment_method_type, + }) + .collect::>(); + res.query_data.push(FilterValue { + dimension: dim, + values, + }) + } + Ok(res) +} diff --git a/crates/analytics/src/payments/distribution.rs b/crates/analytics/src/payments/distribution.rs new file mode 100644 index 000000000000..cf18c26310a7 --- /dev/null +++ b/crates/analytics/src/payments/distribution.rs @@ -0,0 +1,92 @@ +use api_models::analytics::{ + payments::{ + PaymentDimensions, PaymentDistributions, PaymentFilters, PaymentMetricsBucketIdentifier, + }, + Distribution, Granularity, TimeRange, +}; +use diesel_models::enums as storage_enums; +use time::PrimitiveDateTime; + +use crate::{ + query::{Aggregate, GroupByClause, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, LoadRow, MetricsResult}, +}; + +mod payment_error_message; + +use payment_error_message::PaymentErrorMessage; + +#[derive(Debug, PartialEq, Eq, serde::Deserialize)] +pub struct PaymentDistributionRow { + pub currency: Option>, + pub status: Option>, + pub connector: Option, + pub authentication_type: Option>, + pub payment_method: Option, + pub payment_method_type: Option, + pub total: Option, + pub count: Option, + pub error_message: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] + pub start_bucket: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] + pub end_bucket: Option, +} + +pub trait PaymentDistributionAnalytics: LoadRow {} + +#[async_trait::async_trait] +pub trait PaymentDistribution +where + T: AnalyticsDataSource + PaymentDistributionAnalytics, +{ + #[allow(clippy::too_many_arguments)] + async fn load_distribution( + &self, + distribution: &Distribution, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult>; +} + +#[async_trait::async_trait] +impl PaymentDistribution for PaymentDistributions +where + T: AnalyticsDataSource + PaymentDistributionAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_distribution( + &self, + distribution: &Distribution, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + match self { + Self::PaymentErrorMessage => { + PaymentErrorMessage + .load_distribution( + distribution, + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + } + } +} diff --git a/crates/analytics/src/payments/distribution/payment_error_message.rs b/crates/analytics/src/payments/distribution/payment_error_message.rs new file mode 100644 index 000000000000..c70fc09aeac4 --- /dev/null +++ b/crates/analytics/src/payments/distribution/payment_error_message.rs @@ -0,0 +1,176 @@ +use api_models::analytics::{ + payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, + Distribution, Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use diesel_models::enums as storage_enums; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::{PaymentDistribution, PaymentDistributionRow}; +use crate::{ + query::{ + Aggregate, GroupByClause, Order, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window, + }, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct PaymentErrorMessage; + +#[async_trait::async_trait] +impl PaymentDistribution for PaymentErrorMessage +where + T: AnalyticsDataSource + super::PaymentDistributionAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_distribution( + &self, + distribution: &Distribution, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Payment); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(&distribution.distribution_for) + .switch()?; + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + query_builder + .add_group_by_clause(&distribution.distribution_for) + .attach_printable("Error grouping by distribution_for") + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .add_filter_clause( + PaymentDimensions::PaymentStatus, + storage_enums::AttemptStatus::Failure, + ) + .switch()?; + + for dim in dimensions.iter() { + query_builder.add_outer_select_column(dim).switch()?; + } + + query_builder + .add_outer_select_column(&distribution.distribution_for) + .switch()?; + query_builder.add_outer_select_column("count").switch()?; + query_builder + .add_outer_select_column("start_bucket") + .switch()?; + query_builder + .add_outer_select_column("end_bucket") + .switch()?; + let sql_dimensions = query_builder.transform_to_sql_values(dimensions).switch()?; + + query_builder + .add_outer_select_column(Window::Sum { + field: "count", + partition_by: Some(sql_dimensions), + order_by: None, + alias: Some("total"), + }) + .switch()?; + + query_builder + .add_top_n_clause( + dimensions, + distribution.distribution_cardinality.into(), + "count", + Order::Descending, + ) + .switch()?; + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + PaymentMetricsBucketIdentifier::new( + i.currency.as_ref().map(|i| i.0), + i.status.as_ref().map(|i| i.0), + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + i.payment_method_type.clone(), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/router/src/analytics/payments/filters.rs b/crates/analytics/src/payments/filters.rs similarity index 87% rename from crates/router/src/analytics/payments/filters.rs rename to crates/analytics/src/payments/filters.rs index f009aaa76329..6c165f78a8e4 100644 --- a/crates/router/src/analytics/payments/filters.rs +++ b/crates/analytics/src/payments/filters.rs @@ -1,11 +1,11 @@ use api_models::analytics::{payments::PaymentDimensions, Granularity, TimeRange}; -use common_enums::enums::{AttemptStatus, AuthenticationType, Currency}; use common_utils::errors::ReportSwitchExt; +use diesel_models::enums::{AttemptStatus, AuthenticationType, Currency}; use error_stack::ResultExt; use time::PrimitiveDateTime; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, types::{ AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, FiltersError, FiltersResult, LoadRow, @@ -26,6 +26,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Payment); @@ -48,11 +49,12 @@ where .change_context(FiltersError::QueryExecutionFailure) } -#[derive(Debug, serde::Serialize, Eq, PartialEq)] +#[derive(Debug, serde::Serialize, Eq, PartialEq, serde::Deserialize)] pub struct FilterRow { pub currency: Option>, pub status: Option>, pub connector: Option, pub authentication_type: Option>, pub payment_method: Option, + pub payment_method_type: Option, } diff --git a/crates/router/src/analytics/payments/metrics.rs b/crates/analytics/src/payments/metrics.rs similarity index 76% rename from crates/router/src/analytics/payments/metrics.rs rename to crates/analytics/src/payments/metrics.rs index f492e5bd4df9..6fe6b6260d48 100644 --- a/crates/router/src/analytics/payments/metrics.rs +++ b/crates/analytics/src/payments/metrics.rs @@ -2,36 +2,44 @@ use api_models::analytics::{ payments::{PaymentDimensions, PaymentFilters, PaymentMetrics, PaymentMetricsBucketIdentifier}, Granularity, TimeRange, }; -use common_enums::enums as storage_enums; +use diesel_models::enums as storage_enums; use time::PrimitiveDateTime; -use crate::analytics::{ - query::{Aggregate, GroupByClause, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, LoadRow, MetricsResult}, }; mod avg_ticket_size; +mod connector_success_rate; mod payment_count; mod payment_processed_amount; mod payment_success_count; +mod retries_count; mod success_rate; use avg_ticket_size::AvgTicketSize; +use connector_success_rate::ConnectorSuccessRate; use payment_count::PaymentCount; use payment_processed_amount::PaymentProcessedAmount; use payment_success_count::PaymentSuccessCount; use success_rate::PaymentSuccessRate; -#[derive(Debug, PartialEq, Eq)] +use self::retries_count::RetriesCount; + +#[derive(Debug, PartialEq, Eq, serde::Deserialize)] pub struct PaymentMetricRow { pub currency: Option>, pub status: Option>, pub connector: Option, pub authentication_type: Option>, pub payment_method: Option, + pub payment_method_type: Option, pub total: Option, pub count: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] pub start_bucket: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] pub end_bucket: Option, } @@ -61,6 +69,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -132,6 +141,30 @@ where ) .await } + Self::RetriesCount => { + RetriesCount + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::ConnectorSuccessRate => { + ConnectorSuccessRate + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } } } } diff --git a/crates/router/src/analytics/payments/metrics/avg_ticket_size.rs b/crates/analytics/src/payments/metrics/avg_ticket_size.rs similarity index 90% rename from crates/router/src/analytics/payments/metrics/avg_ticket_size.rs rename to crates/analytics/src/payments/metrics/avg_ticket_size.rs index 2230d870e68a..9475d5288a64 100644 --- a/crates/router/src/analytics/payments/metrics/avg_ticket_size.rs +++ b/crates/analytics/src/payments/metrics/avg_ticket_size.rs @@ -3,12 +3,13 @@ use api_models::analytics::{ Granularity, TimeRange, }; use common_utils::errors::ReportSwitchExt; +use diesel_models::enums as storage_enums; use error_stack::ResultExt; use time::PrimitiveDateTime; use super::{PaymentMetric, PaymentMetricRow}; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; @@ -23,6 +24,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -89,6 +91,13 @@ where .switch()?; } + query_builder + .add_filter_clause( + PaymentDimensions::PaymentStatus, + storage_enums::AttemptStatus::Charged, + ) + .switch()?; + query_builder .execute_query::(pool) .await @@ -103,6 +112,7 @@ where i.connector.clone(), i.authentication_type.as_ref().map(|i| i.0), i.payment_method.clone(), + i.payment_method_type.clone(), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, @@ -119,7 +129,7 @@ where }) .collect::, - crate::analytics::query::PostProcessingError, + crate::query::PostProcessingError, >>() .change_context(MetricsError::PostProcessingFailure) } diff --git a/crates/analytics/src/payments/metrics/connector_success_rate.rs b/crates/analytics/src/payments/metrics/connector_success_rate.rs new file mode 100644 index 000000000000..0c4d19b2e0ba --- /dev/null +++ b/crates/analytics/src/payments/metrics/connector_success_rate.rs @@ -0,0 +1,130 @@ +use api_models::analytics::{ + payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::PaymentMetricRow; +use crate::{ + query::{ + Aggregate, FilterTypes, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, + Window, + }, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct ConnectorSuccessRate; + +#[async_trait::async_trait] +impl super::PaymentMetric for ConnectorSuccessRate +where + T: AnalyticsDataSource + super::PaymentMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Payment); + let mut dimensions = dimensions.to_vec(); + + dimensions.push(PaymentDimensions::PaymentStatus); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + query_builder + .add_custom_filter_clause(PaymentDimensions::Connector, "NULL", FilterTypes::IsNotNull) + .switch()?; + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + PaymentMetricsBucketIdentifier::new( + i.currency.as_ref().map(|i| i.0), + None, + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + i.payment_method_type.clone(), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/router/src/analytics/payments/metrics/payment_count.rs b/crates/analytics/src/payments/metrics/payment_count.rs similarity index 94% rename from crates/router/src/analytics/payments/metrics/payment_count.rs rename to crates/analytics/src/payments/metrics/payment_count.rs index 661cec3dac36..34e71f3da6fb 100644 --- a/crates/router/src/analytics/payments/metrics/payment_count.rs +++ b/crates/analytics/src/payments/metrics/payment_count.rs @@ -7,8 +7,8 @@ use error_stack::ResultExt; use time::PrimitiveDateTime; use super::PaymentMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; @@ -23,6 +23,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -97,6 +98,7 @@ where i.connector.clone(), i.authentication_type.as_ref().map(|i| i.0), i.payment_method.clone(), + i.payment_method_type.clone(), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, @@ -111,7 +113,7 @@ where i, )) }) - .collect::, crate::analytics::query::PostProcessingError>>() + .collect::, crate::query::PostProcessingError>>() .change_context(MetricsError::PostProcessingFailure) } } diff --git a/crates/router/src/analytics/payments/metrics/payment_processed_amount.rs b/crates/analytics/src/payments/metrics/payment_processed_amount.rs similarity index 94% rename from crates/router/src/analytics/payments/metrics/payment_processed_amount.rs rename to crates/analytics/src/payments/metrics/payment_processed_amount.rs index 2ec0c6f18f9c..f2dbf97e0db9 100644 --- a/crates/router/src/analytics/payments/metrics/payment_processed_amount.rs +++ b/crates/analytics/src/payments/metrics/payment_processed_amount.rs @@ -2,14 +2,14 @@ use api_models::analytics::{ payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, Granularity, TimeRange, }; -use common_enums::enums as storage_enums; use common_utils::errors::ReportSwitchExt; +use diesel_models::enums as storage_enums; use error_stack::ResultExt; use time::PrimitiveDateTime; use super::PaymentMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; @@ -24,6 +24,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -105,6 +106,7 @@ where i.connector.clone(), i.authentication_type.as_ref().map(|i| i.0), i.payment_method.clone(), + i.payment_method_type.clone(), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, @@ -121,7 +123,7 @@ where }) .collect::, - crate::analytics::query::PostProcessingError, + crate::query::PostProcessingError, >>() .change_context(MetricsError::PostProcessingFailure) } diff --git a/crates/router/src/analytics/payments/metrics/payment_success_count.rs b/crates/analytics/src/payments/metrics/payment_success_count.rs similarity index 94% rename from crates/router/src/analytics/payments/metrics/payment_success_count.rs rename to crates/analytics/src/payments/metrics/payment_success_count.rs index 8245fe7aeb88..a6fb8ed2239d 100644 --- a/crates/router/src/analytics/payments/metrics/payment_success_count.rs +++ b/crates/analytics/src/payments/metrics/payment_success_count.rs @@ -2,14 +2,14 @@ use api_models::analytics::{ payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, Granularity, TimeRange, }; -use common_enums::enums as storage_enums; use common_utils::errors::ReportSwitchExt; +use diesel_models::enums as storage_enums; use error_stack::ResultExt; use time::PrimitiveDateTime; use super::PaymentMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; @@ -24,6 +24,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -104,6 +105,7 @@ where i.connector.clone(), i.authentication_type.as_ref().map(|i| i.0), i.payment_method.clone(), + i.payment_method_type.clone(), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, @@ -120,7 +122,7 @@ where }) .collect::, - crate::analytics::query::PostProcessingError, + crate::query::PostProcessingError, >>() .change_context(MetricsError::PostProcessingFailure) } diff --git a/crates/analytics/src/payments/metrics/retries_count.rs b/crates/analytics/src/payments/metrics/retries_count.rs new file mode 100644 index 000000000000..91952adb569a --- /dev/null +++ b/crates/analytics/src/payments/metrics/retries_count.rs @@ -0,0 +1,122 @@ +use api_models::analytics::{ + payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::PaymentMetricRow; +use crate::{ + query::{ + Aggregate, FilterTypes, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, + Window, + }, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct RetriesCount; + +#[async_trait::async_trait] +impl super::PaymentMetric for RetriesCount +where + T: AnalyticsDataSource + super::PaymentMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + _dimensions: &[PaymentDimensions], + merchant_id: &str, + _filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = + QueryBuilder::new(AnalyticsCollection::PaymentIntent); + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Sum { + field: "amount", + alias: Some("total"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + query_builder + .add_custom_filter_clause("attempt_count", "1", FilterTypes::Gt) + .switch()?; + query_builder + .add_custom_filter_clause("status", "succeeded", FilterTypes::Equal) + .switch()?; + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + PaymentMetricsBucketIdentifier::new( + i.currency.as_ref().map(|i| i.0), + None, + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + i.payment_method_type.clone(), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/router/src/analytics/payments/metrics/success_rate.rs b/crates/analytics/src/payments/metrics/success_rate.rs similarity index 95% rename from crates/router/src/analytics/payments/metrics/success_rate.rs rename to crates/analytics/src/payments/metrics/success_rate.rs index c63956d4b157..9e688240ddbf 100644 --- a/crates/router/src/analytics/payments/metrics/success_rate.rs +++ b/crates/analytics/src/payments/metrics/success_rate.rs @@ -7,8 +7,8 @@ use error_stack::ResultExt; use time::PrimitiveDateTime; use super::PaymentMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; @@ -23,6 +23,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -100,6 +101,7 @@ where i.connector.clone(), i.authentication_type.as_ref().map(|i| i.0), i.payment_method.clone(), + i.payment_method_type.clone(), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, @@ -116,7 +118,7 @@ where }) .collect::, - crate::analytics::query::PostProcessingError, + crate::query::PostProcessingError, >>() .change_context(MetricsError::PostProcessingFailure) } diff --git a/crates/router/src/analytics/payments/types.rs b/crates/analytics/src/payments/types.rs similarity index 82% rename from crates/router/src/analytics/payments/types.rs rename to crates/analytics/src/payments/types.rs index fdfbedef383d..d5d8eca13e58 100644 --- a/crates/router/src/analytics/payments/types.rs +++ b/crates/analytics/src/payments/types.rs @@ -1,7 +1,7 @@ use api_models::analytics::payments::{PaymentDimensions, PaymentFilters}; use error_stack::ResultExt; -use crate::analytics::{ +use crate::{ query::{QueryBuilder, QueryFilter, QueryResult, ToSql}, types::{AnalyticsCollection, AnalyticsDataSource}, }; @@ -41,6 +41,15 @@ where .add_filter_in_range_clause(PaymentDimensions::PaymentMethod, &self.payment_method) .attach_printable("Error adding payment method filter")?; } + + if !self.payment_method_type.is_empty() { + builder + .add_filter_in_range_clause( + PaymentDimensions::PaymentMethodType, + &self.payment_method_type, + ) + .attach_printable("Error adding payment method filter")?; + } Ok(()) } } diff --git a/crates/router/src/analytics/query.rs b/crates/analytics/src/query.rs similarity index 65% rename from crates/router/src/analytics/query.rs rename to crates/analytics/src/query.rs index b1f621d8153d..b924987f004c 100644 --- a/crates/router/src/analytics/query.rs +++ b/crates/analytics/src/query.rs @@ -1,26 +1,26 @@ -#![allow(dead_code)] use std::marker::PhantomData; use api_models::{ analytics::{ self as analytics_api, - payments::PaymentDimensions, + api_event::ApiEventDimensions, + payments::{PaymentDimensions, PaymentDistributions}, refunds::{RefundDimensions, RefundType}, + sdk_events::{SdkEventDimensions, SdkEventNames}, Granularity, }, - enums::Connector, + enums::{ + AttemptStatus, AuthenticationType, Connector, Currency, PaymentMethod, PaymentMethodType, + }, refunds::RefundStatus, }; -use common_enums::{ - enums as storage_enums, - enums::{AttemptStatus, AuthenticationType, Currency, PaymentMethod}, -}; use common_utils::errors::{CustomResult, ParsingError}; +use diesel_models::enums as storage_enums; use error_stack::{IntoReport, ResultExt}; -use router_env::logger; +use router_env::{logger, Flow}; -use super::types::{AnalyticsCollection, AnalyticsDataSource, LoadRow}; -use crate::analytics::types::QueryExecutionError; +use super::types::{AnalyticsCollection, AnalyticsDataSource, LoadRow, TableEngine}; +use crate::types::QueryExecutionError; pub type QueryResult = error_stack::Result; pub trait QueryFilter where @@ -89,12 +89,12 @@ impl GroupByClause for Granularity { let granularity_divisor = self.get_bucket_size(); builder - .add_group_by_clause(format!("DATE_TRUNC('{trunc_scale}', modified_at)")) + .add_group_by_clause(format!("DATE_TRUNC('{trunc_scale}', created_at)")) .attach_printable("Error adding time prune group by")?; if let Some(scale) = granularity_bucket_scale { builder .add_group_by_clause(format!( - "FLOOR(DATE_PART('{scale}', modified_at)/{granularity_divisor})" + "FLOOR(DATE_PART('{scale}', created_at)/{granularity_divisor})" )) .attach_printable("Error adding time binning group by")?; } @@ -102,6 +102,26 @@ impl GroupByClause for Granularity { } } +impl GroupByClause for Granularity { + fn set_group_by_clause( + &self, + builder: &mut QueryBuilder, + ) -> QueryResult<()> { + let interval = match self { + Self::OneMin => "toStartOfMinute(created_at)", + Self::FiveMin => "toStartOfFiveMinutes(created_at)", + Self::FifteenMin => "toStartOfFifteenMinutes(created_at)", + Self::ThirtyMin => "toStartOfInterval(created_at, INTERVAL 30 minute)", + Self::OneHour => "toStartOfHour(created_at)", + Self::OneDay => "toStartOfDay(created_at)", + }; + + builder + .add_group_by_clause(interval) + .attach_printable("Error adding interval group by") + } +} + #[derive(strum::Display)] #[strum(serialize_all = "lowercase")] pub enum TimeGranularityLevel { @@ -229,6 +249,76 @@ pub enum Aggregate { }, } +// Window functions in query +// --- +// Description - +// field: to_sql type value used as expr in aggregation +// partition_by: partition by fields in window +// order_by: order by fields and order (Ascending / Descending) in window +// alias: alias of window expr in query +// --- +// Usage - +// Window::Sum { +// field: "count", +// partition_by: Some(query_builder.transform_to_sql_values(&dimensions).switch()?), +// order_by: Some(("value", Descending)), +// alias: Some("total"), +// } +#[derive(Debug)] +pub enum Window { + Sum { + field: R, + partition_by: Option, + order_by: Option<(String, Order)>, + alias: Option<&'static str>, + }, + RowNumber { + field: R, + partition_by: Option, + order_by: Option<(String, Order)>, + alias: Option<&'static str>, + }, +} + +#[derive(Debug, Clone, Copy)] +pub enum Order { + Ascending, + Descending, +} + +impl ToString for Order { + fn to_string(&self) -> String { + String::from(match self { + Self::Ascending => "asc", + Self::Descending => "desc", + }) + } +} + +// Select TopN values for a group based on a metric +// --- +// Description - +// columns: Columns in group to select TopN values for +// count: N in TopN +// order_column: metric used to sort and limit TopN +// order: sort order of metric (Ascending / Descending) +// --- +// Usage - +// Use via add_top_n_clause fn of query_builder +// add_top_n_clause( +// &dimensions, +// distribution.distribution_cardinality.into(), +// "count", +// Order::Descending, +// ) +#[derive(Debug)] +pub struct TopN { + pub columns: String, + pub count: u64, + pub order_column: String, + pub order: Order, +} + #[derive(Debug)] pub struct QueryBuilder where @@ -239,13 +329,16 @@ where filters: Vec<(String, FilterTypes, String)>, group_by: Vec, having: Option>, + outer_select: Vec, + top_n: Option, table: AnalyticsCollection, distinct: bool, db_type: PhantomData, + table_engine: TableEngine, } pub trait ToSql { - fn to_sql(&self) -> error_stack::Result; + fn to_sql(&self, table_engine: &TableEngine) -> error_stack::Result; } /// Implement `ToSql` on arrays of types that impl `ToString`. @@ -253,7 +346,7 @@ macro_rules! impl_to_sql_for_to_string { ($($type:ty),+) => { $( impl ToSql for $type { - fn to_sql(&self) -> error_stack::Result { + fn to_sql(&self, _table_engine: &TableEngine) -> error_stack::Result { Ok(self.to_string()) } } @@ -267,8 +360,10 @@ impl_to_sql_for_to_string!( &PaymentDimensions, &RefundDimensions, PaymentDimensions, + &PaymentDistributions, RefundDimensions, PaymentMethod, + PaymentMethodType, AuthenticationType, Connector, AttemptStatus, @@ -276,12 +371,18 @@ impl_to_sql_for_to_string!( storage_enums::RefundStatus, Currency, RefundType, + Flow, &String, &bool, - &u64 + &u64, + u64, + Order ); -#[allow(dead_code)] +impl_to_sql_for_to_string!(&SdkEventDimensions, SdkEventDimensions, SdkEventNames); + +impl_to_sql_for_to_string!(&ApiEventDimensions, ApiEventDimensions); + #[derive(Debug)] pub enum FilterTypes { Equal, @@ -290,6 +391,23 @@ pub enum FilterTypes { Gte, Lte, Gt, + Like, + NotLike, + IsNotNull, +} + +pub fn filter_type_to_sql(l: &String, op: &FilterTypes, r: &String) -> String { + match op { + FilterTypes::EqualBool => format!("{l} = {r}"), + FilterTypes::Equal => format!("{l} = '{r}'"), + FilterTypes::In => format!("{l} IN ({r})"), + FilterTypes::Gte => format!("{l} >= '{r}'"), + FilterTypes::Gt => format!("{l} > {r}"), + FilterTypes::Lte => format!("{l} <= '{r}'"), + FilterTypes::Like => format!("{l} LIKE '%{r}%'"), + FilterTypes::NotLike => format!("{l} NOT LIKE '%{r}%'"), + FilterTypes::IsNotNull => format!("{l} IS NOT NULL"), + } } impl QueryBuilder @@ -303,22 +421,68 @@ where filters: Default::default(), group_by: Default::default(), having: Default::default(), + outer_select: Default::default(), + top_n: Default::default(), table, distinct: Default::default(), db_type: Default::default(), + table_engine: T::get_table_engine(table), } } pub fn add_select_column(&mut self, column: impl ToSql) -> QueryResult<()> { self.columns.push( column - .to_sql() + .to_sql(&self.table_engine) .change_context(QueryBuildingError::SqlSerializeError) .attach_printable("Error serializing select column")?, ); Ok(()) } + pub fn transform_to_sql_values(&mut self, values: &[impl ToSql]) -> QueryResult { + let res = values + .iter() + .map(|i| i.to_sql(&self.table_engine)) + .collect::, ParsingError>>() + .change_context(QueryBuildingError::SqlSerializeError) + .attach_printable("Error serializing range filter value")? + .join(", "); + Ok(res) + } + + pub fn add_top_n_clause( + &mut self, + columns: &[impl ToSql], + count: u64, + order_column: impl ToSql, + order: Order, + ) -> QueryResult<()> + where + Window<&'static str>: ToSql, + { + let partition_by_columns = self.transform_to_sql_values(columns)?; + let order_by_column = order_column + .to_sql(&self.table_engine) + .change_context(QueryBuildingError::SqlSerializeError) + .attach_printable("Error serializing select column")?; + + self.add_outer_select_column(Window::RowNumber { + field: "", + partition_by: Some(partition_by_columns.clone()), + order_by: Some((order_by_column.clone(), order)), + alias: Some("top_n"), + })?; + + self.top_n = Some(TopN { + columns: partition_by_columns, + count, + order_column: order_by_column, + order, + }); + Ok(()) + } + pub fn set_distinct(&mut self) { self.distinct = true } @@ -346,11 +510,11 @@ where comparison: FilterTypes, ) -> QueryResult<()> { self.filters.push(( - lhs.to_sql() + lhs.to_sql(&self.table_engine) .change_context(QueryBuildingError::SqlSerializeError) .attach_printable("Error serializing filter key")?, comparison, - rhs.to_sql() + rhs.to_sql(&self.table_engine) .change_context(QueryBuildingError::SqlSerializeError) .attach_printable("Error serializing filter value")?, )); @@ -366,7 +530,7 @@ where .iter() .map(|i| { // trimming whitespaces from the filter values received in request, to prevent a possibility of an SQL injection - i.to_sql().map(|s| { + i.to_sql(&self.table_engine).map(|s| { let trimmed_str = s.replace(' ', ""); format!("'{trimmed_str}'") }) @@ -381,7 +545,7 @@ where pub fn add_group_by_clause(&mut self, column: impl ToSql) -> QueryResult<()> { self.group_by.push( column - .to_sql() + .to_sql(&self.table_engine) .change_context(QueryBuildingError::SqlSerializeError) .attach_printable("Error serializing group by field")?, ); @@ -406,14 +570,7 @@ where fn get_filter_clause(&self) -> String { self.filters .iter() - .map(|(l, op, r)| match op { - FilterTypes::EqualBool => format!("{l} = {r}"), - FilterTypes::Equal => format!("{l} = '{r}'"), - FilterTypes::In => format!("{l} IN ({r})"), - FilterTypes::Gte => format!("{l} >= '{r}'"), - FilterTypes::Gt => format!("{l} > {r}"), - FilterTypes::Lte => format!("{l} <= '{r}'"), - }) + .map(|(l, op, r)| filter_type_to_sql(l, op, r)) .collect::>() .join(" AND ") } @@ -426,7 +583,10 @@ where self.group_by.join(", ") } - #[allow(dead_code)] + fn get_outer_select_clause(&self) -> String { + self.outer_select.join(", ") + } + pub fn add_having_clause( &mut self, aggregate: Aggregate, @@ -437,11 +597,11 @@ where Aggregate: ToSql, { let aggregate = aggregate - .to_sql() + .to_sql(&self.table_engine) .change_context(QueryBuildingError::SqlSerializeError) .attach_printable("Error serializing having aggregate")?; let value = value - .to_sql() + .to_sql(&self.table_engine) .change_context(QueryBuildingError::SqlSerializeError) .attach_printable("Error serializing having value")?; let entry = (aggregate, filter_type, value); @@ -453,16 +613,20 @@ where Ok(()) } + pub fn add_outer_select_column(&mut self, column: impl ToSql) -> QueryResult<()> { + self.outer_select.push( + column + .to_sql(&self.table_engine) + .change_context(QueryBuildingError::SqlSerializeError) + .attach_printable("Error serializing outer select column")?, + ); + Ok(()) + } + pub fn get_filter_type_clause(&self) -> Option { self.having.as_ref().map(|vec| { vec.iter() - .map(|(l, op, r)| match op { - FilterTypes::Equal | FilterTypes::EqualBool => format!("{l} = {r}"), - FilterTypes::In => format!("{l} IN ({r})"), - FilterTypes::Gte => format!("{l} >= {r}"), - FilterTypes::Lte => format!("{l} < {r}"), - FilterTypes::Gt => format!("{l} > {r}"), - }) + .map(|(l, op, r)| filter_type_to_sql(l, op, r)) .collect::>() .join(" AND ") }) @@ -471,6 +635,7 @@ where pub fn build_query(&mut self) -> QueryResult where Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { if self.columns.is_empty() { Err(QueryBuildingError::InvalidQuery( @@ -491,7 +656,7 @@ where query.push_str( &self .table - .to_sql() + .to_sql(&self.table_engine) .change_context(QueryBuildingError::SqlSerializeError) .attach_printable("Error serializing table value")?, ); @@ -504,6 +669,16 @@ where if !self.group_by.is_empty() { query.push_str(" GROUP BY "); query.push_str(&self.get_group_by_clause()); + if let TableEngine::CollapsingMergeTree { sign } = self.table_engine { + self.add_having_clause( + Aggregate::Count { + field: Some(sign), + alias: None, + }, + FilterTypes::Gte, + "1", + )?; + } } if self.having.is_some() { @@ -512,6 +687,22 @@ where query.push_str(condition.as_str()); } } + + if !self.outer_select.is_empty() { + query.insert_str( + 0, + format!("SELECT {} FROM (", &self.get_outer_select_clause()).as_str(), + ); + query.push_str(") _"); + } + + if let Some(top_n) = &self.top_n { + query.insert_str(0, "SELECT * FROM ("); + query.push_str(format!(") _ WHERE top_n <= {}", top_n.count).as_str()); + } + + println!("{}", query); + Ok(query) } @@ -522,6 +713,7 @@ where where P: LoadRow, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { let query = self .build_query() diff --git a/crates/router/src/analytics/refunds.rs b/crates/analytics/src/refunds.rs similarity index 81% rename from crates/router/src/analytics/refunds.rs rename to crates/analytics/src/refunds.rs index a8b52effe76d..53481e232817 100644 --- a/crates/router/src/analytics/refunds.rs +++ b/crates/analytics/src/refunds.rs @@ -7,4 +7,4 @@ pub mod types; pub use accumulator::{RefundMetricAccumulator, RefundMetricsAccumulator}; pub trait RefundAnalytics: metrics::RefundMetricAnalytics {} -pub use self::core::get_metrics; +pub use self::core::{get_filters, get_metrics}; diff --git a/crates/router/src/analytics/refunds/accumulator.rs b/crates/analytics/src/refunds/accumulator.rs similarity index 98% rename from crates/router/src/analytics/refunds/accumulator.rs rename to crates/analytics/src/refunds/accumulator.rs index 3d0c0e659f6c..9c51defdcf91 100644 --- a/crates/router/src/analytics/refunds/accumulator.rs +++ b/crates/analytics/src/refunds/accumulator.rs @@ -1,5 +1,5 @@ use api_models::analytics::refunds::RefundMetricsBucketValue; -use common_enums::enums as storage_enums; +use diesel_models::enums as storage_enums; use super::metrics::RefundMetricRow; #[derive(Debug, Default)] @@ -15,13 +15,11 @@ pub struct SuccessRateAccumulator { pub success: i64, pub total: i64, } - #[derive(Debug, Default)] #[repr(transparent)] pub struct CountAccumulator { pub count: Option, } - #[derive(Debug, Default)] #[repr(transparent)] pub struct SumAccumulator { diff --git a/crates/analytics/src/refunds/core.rs b/crates/analytics/src/refunds/core.rs new file mode 100644 index 000000000000..25a1e228f567 --- /dev/null +++ b/crates/analytics/src/refunds/core.rs @@ -0,0 +1,203 @@ +#![allow(dead_code)] +use std::collections::HashMap; + +use api_models::analytics::{ + refunds::{ + RefundDimensions, RefundMetrics, RefundMetricsBucketIdentifier, RefundMetricsBucketResponse, + }, + AnalyticsMetadata, GetRefundFilterRequest, GetRefundMetricRequest, MetricsResponse, + RefundFilterValue, RefundFiltersResponse, +}; +use error_stack::{IntoReport, ResultExt}; +use router_env::{ + logger, + tracing::{self, Instrument}, +}; + +use super::{ + filters::{get_refund_filter_for_dimension, RefundFilterRow}, + RefundMetricsAccumulator, +}; +use crate::{ + errors::{AnalyticsError, AnalyticsResult}, + metrics, + refunds::RefundMetricAccumulator, + AnalyticsProvider, +}; + +pub async fn get_metrics( + pool: &AnalyticsProvider, + merchant_id: &String, + req: GetRefundMetricRequest, +) -> AnalyticsResult> { + let mut metrics_accumulator: HashMap = + HashMap::new(); + let mut set = tokio::task::JoinSet::new(); + for metric_type in req.metrics.iter().cloned() { + let req = req.clone(); + let pool = pool.clone(); + let task_span = tracing::debug_span!( + "analytics_refund_query", + refund_metric = metric_type.as_ref() + ); + // Currently JoinSet works with only static lifetime references even if the task pool does not outlive the given reference + // We can optimize away this clone once that is fixed + let merchant_id_scoped = merchant_id.to_owned(); + set.spawn( + async move { + let data = pool + .get_refund_metrics( + &metric_type, + &req.group_by_names.clone(), + &merchant_id_scoped, + &req.filters, + &req.time_series.map(|t| t.granularity), + &req.time_range, + ) + .await + .change_context(AnalyticsError::UnknownError); + (metric_type, data) + } + .instrument(task_span), + ); + } + + while let Some((metric, data)) = set + .join_next() + .await + .transpose() + .into_report() + .change_context(AnalyticsError::UnknownError)? + { + let data = data?; + let attributes = &[ + metrics::request::add_attributes("metric_type", metric.to_string()), + metrics::request::add_attributes("source", pool.to_string()), + ]; + + let value = u64::try_from(data.len()); + if let Ok(val) = value { + metrics::BUCKETS_FETCHED.record(&metrics::CONTEXT, val, attributes); + logger::debug!("Attributes: {:?}, Buckets fetched: {}", attributes, val); + } + + for (id, value) in data { + logger::debug!(bucket_id=?id, bucket_value=?value, "Bucket row for metric {metric}"); + let metrics_builder = metrics_accumulator.entry(id).or_default(); + match metric { + RefundMetrics::RefundSuccessRate => metrics_builder + .refund_success_rate + .add_metrics_bucket(&value), + RefundMetrics::RefundCount => { + metrics_builder.refund_count.add_metrics_bucket(&value) + } + RefundMetrics::RefundSuccessCount => { + metrics_builder.refund_success.add_metrics_bucket(&value) + } + RefundMetrics::RefundProcessedAmount => { + metrics_builder.processed_amount.add_metrics_bucket(&value) + } + } + } + + logger::debug!( + "Analytics Accumulated Results: metric: {}, results: {:#?}", + metric, + metrics_accumulator + ); + } + let query_data: Vec = metrics_accumulator + .into_iter() + .map(|(id, val)| RefundMetricsBucketResponse { + values: val.collect(), + dimensions: id, + }) + .collect(); + + Ok(MetricsResponse { + query_data, + meta_data: [AnalyticsMetadata { + current_time_range: req.time_range, + }], + }) +} + +pub async fn get_filters( + pool: &AnalyticsProvider, + req: GetRefundFilterRequest, + merchant_id: &String, +) -> AnalyticsResult { + let mut res = RefundFiltersResponse::default(); + for dim in req.group_by_names { + let values = match pool { + AnalyticsProvider::Sqlx(pool) => { + get_refund_filter_for_dimension(dim, merchant_id, &req.time_range, pool) + .await + } + AnalyticsProvider::Clickhouse(pool) => { + get_refund_filter_for_dimension(dim, merchant_id, &req.time_range, pool) + .await + } + AnalyticsProvider::CombinedCkh(sqlx_pool, ckh_pool) => { + let ckh_result = get_refund_filter_for_dimension( + dim, + merchant_id, + &req.time_range, + ckh_pool, + ) + .await; + let sqlx_result = get_refund_filter_for_dimension( + dim, + merchant_id, + &req.time_range, + sqlx_pool, + ) + .await; + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + router_env::logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres refunds analytics filters") + }, + _ => {} + }; + ckh_result + } + AnalyticsProvider::CombinedSqlx(sqlx_pool, ckh_pool) => { + let ckh_result = get_refund_filter_for_dimension( + dim, + merchant_id, + &req.time_range, + ckh_pool, + ) + .await; + let sqlx_result = get_refund_filter_for_dimension( + dim, + merchant_id, + &req.time_range, + sqlx_pool, + ) + .await; + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + router_env::logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres refunds analytics filters") + }, + _ => {} + }; + sqlx_result + } + } + .change_context(AnalyticsError::UnknownError)? + .into_iter() + .filter_map(|fil: RefundFilterRow| match dim { + RefundDimensions::Currency => fil.currency.map(|i| i.as_ref().to_string()), + RefundDimensions::RefundStatus => fil.refund_status.map(|i| i.as_ref().to_string()), + RefundDimensions::Connector => fil.connector, + RefundDimensions::RefundType => fil.refund_type.map(|i| i.as_ref().to_string()), + }) + .collect::>(); + res.query_data.push(RefundFilterValue { + dimension: dim, + values, + }) + } + Ok(res) +} diff --git a/crates/router/src/analytics/refunds/filters.rs b/crates/analytics/src/refunds/filters.rs similarity index 90% rename from crates/router/src/analytics/refunds/filters.rs rename to crates/analytics/src/refunds/filters.rs index 6b45e9194fad..29375483eb9a 100644 --- a/crates/router/src/analytics/refunds/filters.rs +++ b/crates/analytics/src/refunds/filters.rs @@ -2,13 +2,13 @@ use api_models::analytics::{ refunds::{RefundDimensions, RefundType}, Granularity, TimeRange, }; -use common_enums::enums::{Currency, RefundStatus}; use common_utils::errors::ReportSwitchExt; +use diesel_models::enums::{Currency, RefundStatus}; use error_stack::ResultExt; use time::PrimitiveDateTime; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, types::{ AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, FiltersError, FiltersResult, LoadRow, @@ -28,6 +28,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Refund); @@ -49,8 +50,7 @@ where .change_context(FiltersError::QueryBuildingError)? .change_context(FiltersError::QueryExecutionFailure) } - -#[derive(Debug, serde::Serialize, Eq, PartialEq)] +#[derive(Debug, serde::Serialize, Eq, PartialEq, serde::Deserialize)] pub struct RefundFilterRow { pub currency: Option>, pub refund_status: Option>, diff --git a/crates/router/src/analytics/refunds/metrics.rs b/crates/analytics/src/refunds/metrics.rs similarity index 91% rename from crates/router/src/analytics/refunds/metrics.rs rename to crates/analytics/src/refunds/metrics.rs index d4f509b4a1e3..10cd03546772 100644 --- a/crates/router/src/analytics/refunds/metrics.rs +++ b/crates/analytics/src/refunds/metrics.rs @@ -4,7 +4,7 @@ use api_models::analytics::{ }, Granularity, TimeRange, }; -use common_enums::enums as storage_enums; +use diesel_models::enums as storage_enums; use time::PrimitiveDateTime; mod refund_count; mod refund_processed_amount; @@ -15,12 +15,11 @@ use refund_processed_amount::RefundProcessedAmount; use refund_success_count::RefundSuccessCount; use refund_success_rate::RefundSuccessRate; -use crate::analytics::{ - query::{Aggregate, GroupByClause, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, LoadRow, MetricsResult}, }; - -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq, serde::Deserialize)] pub struct RefundMetricRow { pub currency: Option>, pub refund_status: Option>, @@ -28,7 +27,9 @@ pub struct RefundMetricRow { pub refund_type: Option>, pub total: Option, pub count: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] pub start_bucket: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] pub end_bucket: Option, } @@ -42,6 +43,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -62,6 +64,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, diff --git a/crates/router/src/analytics/refunds/metrics/refund_count.rs b/crates/analytics/src/refunds/metrics/refund_count.rs similarity index 94% rename from crates/router/src/analytics/refunds/metrics/refund_count.rs rename to crates/analytics/src/refunds/metrics/refund_count.rs index 471327235073..cf3c7a509278 100644 --- a/crates/router/src/analytics/refunds/metrics/refund_count.rs +++ b/crates/analytics/src/refunds/metrics/refund_count.rs @@ -7,8 +7,8 @@ use error_stack::ResultExt; use time::PrimitiveDateTime; use super::RefundMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; @@ -23,6 +23,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -93,7 +94,7 @@ where Ok(( RefundMetricsBucketIdentifier::new( i.currency.as_ref().map(|i| i.0), - i.refund_status.as_ref().map(|i| i.0), + i.refund_status.as_ref().map(|i| i.0.to_string()), i.connector.clone(), i.refund_type.as_ref().map(|i| i.0.to_string()), TimeRange { @@ -110,7 +111,7 @@ where i, )) }) - .collect::, crate::analytics::query::PostProcessingError>>() + .collect::, crate::query::PostProcessingError>>() .change_context(MetricsError::PostProcessingFailure) } } diff --git a/crates/router/src/analytics/refunds/metrics/refund_processed_amount.rs b/crates/analytics/src/refunds/metrics/refund_processed_amount.rs similarity index 95% rename from crates/router/src/analytics/refunds/metrics/refund_processed_amount.rs rename to crates/analytics/src/refunds/metrics/refund_processed_amount.rs index c5f3a706aaef..661fca57b282 100644 --- a/crates/router/src/analytics/refunds/metrics/refund_processed_amount.rs +++ b/crates/analytics/src/refunds/metrics/refund_processed_amount.rs @@ -2,14 +2,14 @@ use api_models::analytics::{ refunds::{RefundDimensions, RefundFilters, RefundMetricsBucketIdentifier}, Granularity, TimeRange, }; -use common_enums::enums as storage_enums; use common_utils::errors::ReportSwitchExt; +use diesel_models::enums as storage_enums; use error_stack::ResultExt; use time::PrimitiveDateTime; use super::RefundMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; #[derive(Default)] @@ -23,6 +23,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -116,7 +117,7 @@ where i, )) }) - .collect::, crate::analytics::query::PostProcessingError>>() + .collect::, crate::query::PostProcessingError>>() .change_context(MetricsError::PostProcessingFailure) } } diff --git a/crates/router/src/analytics/refunds/metrics/refund_success_count.rs b/crates/analytics/src/refunds/metrics/refund_success_count.rs similarity index 95% rename from crates/router/src/analytics/refunds/metrics/refund_success_count.rs rename to crates/analytics/src/refunds/metrics/refund_success_count.rs index 0c8032908fd7..bc09d8b7ab64 100644 --- a/crates/router/src/analytics/refunds/metrics/refund_success_count.rs +++ b/crates/analytics/src/refunds/metrics/refund_success_count.rs @@ -2,14 +2,14 @@ use api_models::analytics::{ refunds::{RefundDimensions, RefundFilters, RefundMetricsBucketIdentifier}, Granularity, TimeRange, }; -use common_enums::enums as storage_enums; use common_utils::errors::ReportSwitchExt; +use diesel_models::enums as storage_enums; use error_stack::ResultExt; use time::PrimitiveDateTime; use super::RefundMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; @@ -24,6 +24,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -115,7 +116,7 @@ where }) .collect::, - crate::analytics::query::PostProcessingError, + crate::query::PostProcessingError, >>() .change_context(MetricsError::PostProcessingFailure) } diff --git a/crates/router/src/analytics/refunds/metrics/refund_success_rate.rs b/crates/analytics/src/refunds/metrics/refund_success_rate.rs similarity index 96% rename from crates/router/src/analytics/refunds/metrics/refund_success_rate.rs rename to crates/analytics/src/refunds/metrics/refund_success_rate.rs index 42f9ccf8d3c0..29b73b885d8e 100644 --- a/crates/router/src/analytics/refunds/metrics/refund_success_rate.rs +++ b/crates/analytics/src/refunds/metrics/refund_success_rate.rs @@ -7,8 +7,8 @@ use error_stack::ResultExt; use time::PrimitiveDateTime; use super::RefundMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; #[derive(Default)] @@ -22,6 +22,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -110,7 +111,7 @@ where }) .collect::, - crate::analytics::query::PostProcessingError, + crate::query::PostProcessingError, >>() .change_context(MetricsError::PostProcessingFailure) } diff --git a/crates/router/src/analytics/refunds/types.rs b/crates/analytics/src/refunds/types.rs similarity index 98% rename from crates/router/src/analytics/refunds/types.rs rename to crates/analytics/src/refunds/types.rs index fbfd69972671..d7d739e1aba7 100644 --- a/crates/router/src/analytics/refunds/types.rs +++ b/crates/analytics/src/refunds/types.rs @@ -1,7 +1,7 @@ use api_models::analytics::refunds::{RefundDimensions, RefundFilters}; use error_stack::ResultExt; -use crate::analytics::{ +use crate::{ query::{QueryBuilder, QueryFilter, QueryResult, ToSql}, types::{AnalyticsCollection, AnalyticsDataSource}, }; diff --git a/crates/analytics/src/sdk_events.rs b/crates/analytics/src/sdk_events.rs new file mode 100644 index 000000000000..fe8af7cfe2df --- /dev/null +++ b/crates/analytics/src/sdk_events.rs @@ -0,0 +1,14 @@ +pub mod accumulator; +mod core; +pub mod events; +pub mod filters; +pub mod metrics; +pub mod types; +pub use accumulator::{SdkEventMetricAccumulator, SdkEventMetricsAccumulator}; +pub trait SDKEventAnalytics: events::SdkEventsFilterAnalytics {} +pub trait SdkEventAnalytics: + metrics::SdkEventMetricAnalytics + filters::SdkEventFilterAnalytics +{ +} + +pub use self::core::{get_filters, get_metrics, sdk_events_core}; diff --git a/crates/analytics/src/sdk_events/accumulator.rs b/crates/analytics/src/sdk_events/accumulator.rs new file mode 100644 index 000000000000..ab9e9309434f --- /dev/null +++ b/crates/analytics/src/sdk_events/accumulator.rs @@ -0,0 +1,98 @@ +use api_models::analytics::sdk_events::SdkEventMetricsBucketValue; +use router_env::logger; + +use super::metrics::SdkEventMetricRow; + +#[derive(Debug, Default)] +pub struct SdkEventMetricsAccumulator { + pub payment_attempts: CountAccumulator, + pub payment_success: CountAccumulator, + pub payment_methods_call_count: CountAccumulator, + pub average_payment_time: AverageAccumulator, + pub sdk_initiated_count: CountAccumulator, + pub sdk_rendered_count: CountAccumulator, + pub payment_method_selected_count: CountAccumulator, + pub payment_data_filled_count: CountAccumulator, +} + +#[derive(Debug, Default)] +#[repr(transparent)] +pub struct CountAccumulator { + pub count: Option, +} + +#[derive(Debug, Default)] +pub struct AverageAccumulator { + pub total: u32, + pub count: u32, +} + +pub trait SdkEventMetricAccumulator { + type MetricOutput; + + fn add_metrics_bucket(&mut self, metrics: &SdkEventMetricRow); + + fn collect(self) -> Self::MetricOutput; +} + +impl SdkEventMetricAccumulator for CountAccumulator { + type MetricOutput = Option; + #[inline] + fn add_metrics_bucket(&mut self, metrics: &SdkEventMetricRow) { + self.count = match (self.count, metrics.count) { + (None, None) => None, + (None, i @ Some(_)) | (i @ Some(_), None) => i, + (Some(a), Some(b)) => Some(a + b), + } + } + #[inline] + fn collect(self) -> Self::MetricOutput { + self.count.and_then(|i| u64::try_from(i).ok()) + } +} + +impl SdkEventMetricAccumulator for AverageAccumulator { + type MetricOutput = Option; + + fn add_metrics_bucket(&mut self, metrics: &SdkEventMetricRow) { + let total = metrics + .total + .as_ref() + .and_then(bigdecimal::ToPrimitive::to_u32); + let count = metrics.count.and_then(|total| u32::try_from(total).ok()); + + match (total, count) { + (Some(total), Some(count)) => { + self.total += total; + self.count += count; + } + _ => { + logger::error!(message="Dropping metrics for average accumulator", metric=?metrics); + } + } + } + + fn collect(self) -> Self::MetricOutput { + if self.count == 0 { + None + } else { + Some(f64::from(self.total) / f64::from(self.count)) + } + } +} + +impl SdkEventMetricsAccumulator { + #[allow(dead_code)] + pub fn collect(self) -> SdkEventMetricsBucketValue { + SdkEventMetricsBucketValue { + payment_attempts: self.payment_attempts.collect(), + payment_success_count: self.payment_success.collect(), + payment_methods_call_count: self.payment_methods_call_count.collect(), + average_payment_time: self.average_payment_time.collect(), + sdk_initiated_count: self.sdk_initiated_count.collect(), + sdk_rendered_count: self.sdk_rendered_count.collect(), + payment_method_selected_count: self.payment_method_selected_count.collect(), + payment_data_filled_count: self.payment_data_filled_count.collect(), + } + } +} diff --git a/crates/analytics/src/sdk_events/core.rs b/crates/analytics/src/sdk_events/core.rs new file mode 100644 index 000000000000..34f23c745b05 --- /dev/null +++ b/crates/analytics/src/sdk_events/core.rs @@ -0,0 +1,201 @@ +use std::collections::HashMap; + +use api_models::analytics::{ + sdk_events::{ + MetricsBucketResponse, SdkEventMetrics, SdkEventMetricsBucketIdentifier, SdkEventsRequest, + }, + AnalyticsMetadata, GetSdkEventFiltersRequest, GetSdkEventMetricRequest, MetricsResponse, + SdkEventFiltersResponse, +}; +use error_stack::{IntoReport, ResultExt}; +use router_env::{instrument, logger, tracing}; + +use super::{ + events::{get_sdk_event, SdkEventsResult}, + SdkEventMetricsAccumulator, +}; +use crate::{ + errors::{AnalyticsError, AnalyticsResult}, + sdk_events::SdkEventMetricAccumulator, + types::FiltersError, + AnalyticsProvider, +}; + +#[instrument(skip_all)] +pub async fn sdk_events_core( + pool: &AnalyticsProvider, + req: SdkEventsRequest, + publishable_key: String, +) -> AnalyticsResult> { + match pool { + AnalyticsProvider::Sqlx(_) => Err(FiltersError::NotImplemented) + .into_report() + .attach_printable("SQL Analytics is not implemented for Sdk Events"), + AnalyticsProvider::Clickhouse(pool) => get_sdk_event(&publishable_key, req, pool).await, + AnalyticsProvider::CombinedSqlx(_sqlx_pool, ckh_pool) + | AnalyticsProvider::CombinedCkh(_sqlx_pool, ckh_pool) => { + get_sdk_event(&publishable_key, req, ckh_pool).await + } + } + .change_context(AnalyticsError::UnknownError) +} + +#[instrument(skip_all)] +pub async fn get_metrics( + pool: &AnalyticsProvider, + publishable_key: Option<&String>, + req: GetSdkEventMetricRequest, +) -> AnalyticsResult> { + let mut metrics_accumulator: HashMap< + SdkEventMetricsBucketIdentifier, + SdkEventMetricsAccumulator, + > = HashMap::new(); + + if let Some(publishable_key) = publishable_key { + let mut set = tokio::task::JoinSet::new(); + for metric_type in req.metrics.iter().cloned() { + let req = req.clone(); + let publishable_key_scoped = publishable_key.to_owned(); + let pool = pool.clone(); + set.spawn(async move { + let data = pool + .get_sdk_event_metrics( + &metric_type, + &req.group_by_names.clone(), + &publishable_key_scoped, + &req.filters, + &req.time_series.map(|t| t.granularity), + &req.time_range, + ) + .await + .change_context(AnalyticsError::UnknownError); + (metric_type, data) + }); + } + + while let Some((metric, data)) = set + .join_next() + .await + .transpose() + .into_report() + .change_context(AnalyticsError::UnknownError)? + { + logger::info!("Logging Result {:?}", data); + for (id, value) in data? { + let metrics_builder = metrics_accumulator.entry(id).or_default(); + match metric { + SdkEventMetrics::PaymentAttempts => { + metrics_builder.payment_attempts.add_metrics_bucket(&value) + } + SdkEventMetrics::PaymentSuccessCount => { + metrics_builder.payment_success.add_metrics_bucket(&value) + } + SdkEventMetrics::PaymentMethodsCallCount => metrics_builder + .payment_methods_call_count + .add_metrics_bucket(&value), + SdkEventMetrics::SdkRenderedCount => metrics_builder + .sdk_rendered_count + .add_metrics_bucket(&value), + SdkEventMetrics::SdkInitiatedCount => metrics_builder + .sdk_initiated_count + .add_metrics_bucket(&value), + SdkEventMetrics::PaymentMethodSelectedCount => metrics_builder + .payment_method_selected_count + .add_metrics_bucket(&value), + SdkEventMetrics::PaymentDataFilledCount => metrics_builder + .payment_data_filled_count + .add_metrics_bucket(&value), + SdkEventMetrics::AveragePaymentTime => metrics_builder + .average_payment_time + .add_metrics_bucket(&value), + } + } + + logger::debug!( + "Analytics Accumulated Results: metric: {}, results: {:#?}", + metric, + metrics_accumulator + ); + } + + let query_data: Vec = metrics_accumulator + .into_iter() + .map(|(id, val)| MetricsBucketResponse { + values: val.collect(), + dimensions: id, + }) + .collect(); + + Ok(MetricsResponse { + query_data, + meta_data: [AnalyticsMetadata { + current_time_range: req.time_range, + }], + }) + } else { + logger::error!("Publishable key not present for merchant ID"); + Ok(MetricsResponse { + query_data: vec![], + meta_data: [AnalyticsMetadata { + current_time_range: req.time_range, + }], + }) + } +} + +#[allow(dead_code)] +pub async fn get_filters( + pool: &AnalyticsProvider, + req: GetSdkEventFiltersRequest, + publishable_key: Option<&String>, +) -> AnalyticsResult { + use api_models::analytics::{sdk_events::SdkEventDimensions, SdkEventFilterValue}; + + use super::filters::get_sdk_event_filter_for_dimension; + use crate::sdk_events::filters::SdkEventFilter; + + let mut res = SdkEventFiltersResponse::default(); + + if let Some(publishable_key) = publishable_key { + for dim in req.group_by_names { + let values = match pool { + AnalyticsProvider::Sqlx(_pool) => Err(FiltersError::NotImplemented) + .into_report() + .attach_printable("SQL Analytics is not implemented for SDK Events"), + AnalyticsProvider::Clickhouse(pool) => { + get_sdk_event_filter_for_dimension(dim, publishable_key, &req.time_range, pool) + .await + } + AnalyticsProvider::CombinedSqlx(_sqlx_pool, ckh_pool) + | AnalyticsProvider::CombinedCkh(_sqlx_pool, ckh_pool) => { + get_sdk_event_filter_for_dimension( + dim, + publishable_key, + &req.time_range, + ckh_pool, + ) + .await + } + } + .change_context(AnalyticsError::UnknownError)? + .into_iter() + .filter_map(|fil: SdkEventFilter| match dim { + SdkEventDimensions::PaymentMethod => fil.payment_method, + SdkEventDimensions::Platform => fil.platform, + SdkEventDimensions::BrowserName => fil.browser_name, + SdkEventDimensions::Source => fil.source, + SdkEventDimensions::Component => fil.component, + SdkEventDimensions::PaymentExperience => fil.payment_experience, + }) + .collect::>(); + res.query_data.push(SdkEventFilterValue { + dimension: dim, + values, + }) + } + } else { + router_env::logger::error!("Publishable key not found for merchant"); + } + + Ok(res) +} diff --git a/crates/analytics/src/sdk_events/events.rs b/crates/analytics/src/sdk_events/events.rs new file mode 100644 index 000000000000..a4d044267e94 --- /dev/null +++ b/crates/analytics/src/sdk_events/events.rs @@ -0,0 +1,80 @@ +use api_models::analytics::{ + sdk_events::{SdkEventNames, SdkEventsRequest}, + Granularity, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use strum::IntoEnumIterator; +use time::PrimitiveDateTime; + +use crate::{ + query::{Aggregate, FilterTypes, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, FiltersError, FiltersResult, LoadRow}, +}; +pub trait SdkEventsFilterAnalytics: LoadRow {} + +pub async fn get_sdk_event( + merchant_id: &str, + request: SdkEventsRequest, + pool: &T, +) -> FiltersResult> +where + T: AnalyticsDataSource + SdkEventsFilterAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + let static_event_list = SdkEventNames::iter() + .map(|i| format!("'{}'", i.as_ref())) + .collect::>() + .join(","); + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); + query_builder.add_select_column("*").switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + query_builder + .add_filter_clause("payment_id", request.payment_id) + .switch()?; + query_builder + .add_custom_filter_clause("event_name", static_event_list, FilterTypes::In) + .switch()?; + let _ = &request + .time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + //TODO!: update the execute_query function to return reports instead of plain errors... + query_builder + .execute_query::(pool) + .await + .change_context(FiltersError::QueryBuildingError)? + .change_context(FiltersError::QueryExecutionFailure) +} +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct SdkEventsResult { + pub merchant_id: String, + pub payment_id: String, + pub event_name: Option, + pub log_type: Option, + pub first_event: bool, + pub browser_name: Option, + pub browser_version: Option, + pub source: Option, + pub category: Option, + pub version: Option, + pub value: Option, + pub platform: Option, + pub component: Option, + pub payment_method: Option, + pub payment_experience: Option, + pub latency: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at_precise: PrimitiveDateTime, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: PrimitiveDateTime, +} diff --git a/crates/analytics/src/sdk_events/filters.rs b/crates/analytics/src/sdk_events/filters.rs new file mode 100644 index 000000000000..9963f51ef947 --- /dev/null +++ b/crates/analytics/src/sdk_events/filters.rs @@ -0,0 +1,56 @@ +use api_models::analytics::{sdk_events::SdkEventDimensions, Granularity, TimeRange}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, FiltersError, FiltersResult, LoadRow}, +}; + +pub trait SdkEventFilterAnalytics: LoadRow {} + +pub async fn get_sdk_event_filter_for_dimension( + dimension: SdkEventDimensions, + publishable_key: &String, + time_range: &TimeRange, + pool: &T, +) -> FiltersResult> +where + T: AnalyticsDataSource + SdkEventFilterAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); + + query_builder.add_select_column(dimension).switch()?; + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + query_builder + .add_filter_clause("merchant_id", publishable_key) + .switch()?; + + query_builder.set_distinct(); + + query_builder + .execute_query::(pool) + .await + .change_context(FiltersError::QueryBuildingError)? + .change_context(FiltersError::QueryExecutionFailure) +} + +#[derive(Debug, serde::Serialize, Eq, PartialEq, serde::Deserialize)] +pub struct SdkEventFilter { + pub payment_method: Option, + pub platform: Option, + pub browser_name: Option, + pub source: Option, + pub component: Option, + pub payment_experience: Option, +} diff --git a/crates/analytics/src/sdk_events/metrics.rs b/crates/analytics/src/sdk_events/metrics.rs new file mode 100644 index 000000000000..354d2270d18a --- /dev/null +++ b/crates/analytics/src/sdk_events/metrics.rs @@ -0,0 +1,181 @@ +use api_models::analytics::{ + sdk_events::{ + SdkEventDimensions, SdkEventFilters, SdkEventMetrics, SdkEventMetricsBucketIdentifier, + }, + Granularity, TimeRange, +}; +use time::PrimitiveDateTime; + +use crate::{ + query::{Aggregate, GroupByClause, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, LoadRow, MetricsResult}, +}; + +mod average_payment_time; +mod payment_attempts; +mod payment_data_filled_count; +mod payment_method_selected_count; +mod payment_methods_call_count; +mod payment_success_count; +mod sdk_initiated_count; +mod sdk_rendered_count; + +use average_payment_time::AveragePaymentTime; +use payment_attempts::PaymentAttempts; +use payment_data_filled_count::PaymentDataFilledCount; +use payment_method_selected_count::PaymentMethodSelectedCount; +use payment_methods_call_count::PaymentMethodsCallCount; +use payment_success_count::PaymentSuccessCount; +use sdk_initiated_count::SdkInitiatedCount; +use sdk_rendered_count::SdkRenderedCount; + +#[derive(Debug, PartialEq, Eq, serde::Deserialize)] +pub struct SdkEventMetricRow { + pub total: Option, + pub count: Option, + pub time_bucket: Option, + pub payment_method: Option, + pub platform: Option, + pub browser_name: Option, + pub source: Option, + pub component: Option, + pub payment_experience: Option, +} + +pub trait SdkEventMetricAnalytics: LoadRow {} + +#[async_trait::async_trait] +pub trait SdkEventMetric +where + T: AnalyticsDataSource + SdkEventMetricAnalytics, +{ + async fn load_metrics( + &self, + dimensions: &[SdkEventDimensions], + publishable_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult>; +} + +#[async_trait::async_trait] +impl SdkEventMetric for SdkEventMetrics +where + T: AnalyticsDataSource + SdkEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[SdkEventDimensions], + publishable_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + match self { + Self::PaymentAttempts => { + PaymentAttempts + .load_metrics( + dimensions, + publishable_key, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::PaymentSuccessCount => { + PaymentSuccessCount + .load_metrics( + dimensions, + publishable_key, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::PaymentMethodsCallCount => { + PaymentMethodsCallCount + .load_metrics( + dimensions, + publishable_key, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::SdkRenderedCount => { + SdkRenderedCount + .load_metrics( + dimensions, + publishable_key, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::SdkInitiatedCount => { + SdkInitiatedCount + .load_metrics( + dimensions, + publishable_key, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::PaymentMethodSelectedCount => { + PaymentMethodSelectedCount + .load_metrics( + dimensions, + publishable_key, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::PaymentDataFilledCount => { + PaymentDataFilledCount + .load_metrics( + dimensions, + publishable_key, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::AveragePaymentTime => { + AveragePaymentTime + .load_metrics( + dimensions, + publishable_key, + filters, + granularity, + time_range, + pool, + ) + .await + } + } + } +} diff --git a/crates/analytics/src/sdk_events/metrics/average_payment_time.rs b/crates/analytics/src/sdk_events/metrics/average_payment_time.rs new file mode 100644 index 000000000000..db7171524ae5 --- /dev/null +++ b/crates/analytics/src/sdk_events/metrics/average_payment_time.rs @@ -0,0 +1,129 @@ +use api_models::analytics::{ + sdk_events::{ + SdkEventDimensions, SdkEventFilters, SdkEventMetricsBucketIdentifier, SdkEventNames, + }, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::SdkEventMetricRow; +use crate::{ + query::{Aggregate, FilterTypes, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct AveragePaymentTime; + +#[async_trait::async_trait] +impl super::SdkEventMetric for AveragePaymentTime +where + T: AnalyticsDataSource + super::SdkEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[SdkEventDimensions], + publishable_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); + let dimensions = dimensions.to_vec(); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + + query_builder + .add_select_column(Aggregate::Sum { + field: "latency", + alias: Some("total"), + }) + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + query_builder + .add_granularity_in_mins(granularity) + .switch()?; + } + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", publishable_key) + .switch()?; + + query_builder + .add_bool_filter_clause("first_event", 1) + .switch()?; + + query_builder + .add_filter_clause("event_name", SdkEventNames::PaymentAttempt) + .switch()?; + + query_builder + .add_custom_filter_clause("latency", 0, FilterTypes::Gt) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(_granularity) = granularity.as_ref() { + query_builder + .add_group_by_clause("time_bucket") + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + SdkEventMetricsBucketIdentifier::new( + i.payment_method.clone(), + i.platform.clone(), + i.browser_name.clone(), + i.source.clone(), + i.component.clone(), + i.payment_experience.clone(), + i.time_bucket.clone(), + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/sdk_events/metrics/payment_attempts.rs b/crates/analytics/src/sdk_events/metrics/payment_attempts.rs new file mode 100644 index 000000000000..b2a78188c4f2 --- /dev/null +++ b/crates/analytics/src/sdk_events/metrics/payment_attempts.rs @@ -0,0 +1,118 @@ +use api_models::analytics::{ + sdk_events::{ + SdkEventDimensions, SdkEventFilters, SdkEventMetricsBucketIdentifier, SdkEventNames, + }, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::SdkEventMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct PaymentAttempts; + +#[async_trait::async_trait] +impl super::SdkEventMetric for PaymentAttempts +where + T: AnalyticsDataSource + super::SdkEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[SdkEventDimensions], + publishable_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); + let dimensions = dimensions.to_vec(); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + query_builder + .add_granularity_in_mins(granularity) + .switch()?; + } + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", publishable_key) + .switch()?; + + query_builder + .add_bool_filter_clause("first_event", 1) + .switch()?; + + query_builder + .add_filter_clause("event_name", SdkEventNames::PaymentAttempt) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(_granularity) = granularity.as_ref() { + query_builder + .add_group_by_clause("time_bucket") + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + SdkEventMetricsBucketIdentifier::new( + i.payment_method.clone(), + i.platform.clone(), + i.browser_name.clone(), + i.source.clone(), + i.component.clone(), + i.payment_experience.clone(), + i.time_bucket.clone(), + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/sdk_events/metrics/payment_data_filled_count.rs b/crates/analytics/src/sdk_events/metrics/payment_data_filled_count.rs new file mode 100644 index 000000000000..a3c94baeda26 --- /dev/null +++ b/crates/analytics/src/sdk_events/metrics/payment_data_filled_count.rs @@ -0,0 +1,118 @@ +use api_models::analytics::{ + sdk_events::{ + SdkEventDimensions, SdkEventFilters, SdkEventMetricsBucketIdentifier, SdkEventNames, + }, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::SdkEventMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct PaymentDataFilledCount; + +#[async_trait::async_trait] +impl super::SdkEventMetric for PaymentDataFilledCount +where + T: AnalyticsDataSource + super::SdkEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[SdkEventDimensions], + publishable_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); + let dimensions = dimensions.to_vec(); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + query_builder + .add_granularity_in_mins(granularity) + .switch()?; + } + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", publishable_key) + .switch()?; + + query_builder + .add_bool_filter_clause("first_event", 1) + .switch()?; + + query_builder + .add_filter_clause("event_name", SdkEventNames::PaymentDataFilled) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(_granularity) = granularity.as_ref() { + query_builder + .add_group_by_clause("time_bucket") + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + SdkEventMetricsBucketIdentifier::new( + i.payment_method.clone(), + i.platform.clone(), + i.browser_name.clone(), + i.source.clone(), + i.component.clone(), + i.payment_experience.clone(), + i.time_bucket.clone(), + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/sdk_events/metrics/payment_method_selected_count.rs b/crates/analytics/src/sdk_events/metrics/payment_method_selected_count.rs new file mode 100644 index 000000000000..11aeac5e6ff9 --- /dev/null +++ b/crates/analytics/src/sdk_events/metrics/payment_method_selected_count.rs @@ -0,0 +1,118 @@ +use api_models::analytics::{ + sdk_events::{ + SdkEventDimensions, SdkEventFilters, SdkEventMetricsBucketIdentifier, SdkEventNames, + }, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::SdkEventMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct PaymentMethodSelectedCount; + +#[async_trait::async_trait] +impl super::SdkEventMetric for PaymentMethodSelectedCount +where + T: AnalyticsDataSource + super::SdkEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[SdkEventDimensions], + publishable_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); + let dimensions = dimensions.to_vec(); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + query_builder + .add_granularity_in_mins(granularity) + .switch()?; + } + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", publishable_key) + .switch()?; + + query_builder + .add_bool_filter_clause("first_event", 1) + .switch()?; + + query_builder + .add_filter_clause("event_name", SdkEventNames::PaymentMethodChanged) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(_granularity) = granularity.as_ref() { + query_builder + .add_group_by_clause("time_bucket") + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + SdkEventMetricsBucketIdentifier::new( + i.payment_method.clone(), + i.platform.clone(), + i.browser_name.clone(), + i.source.clone(), + i.component.clone(), + i.payment_experience.clone(), + i.time_bucket.clone(), + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/sdk_events/metrics/payment_methods_call_count.rs b/crates/analytics/src/sdk_events/metrics/payment_methods_call_count.rs new file mode 100644 index 000000000000..7570f1292e5e --- /dev/null +++ b/crates/analytics/src/sdk_events/metrics/payment_methods_call_count.rs @@ -0,0 +1,126 @@ +use api_models::analytics::{ + sdk_events::{ + SdkEventDimensions, SdkEventFilters, SdkEventMetricsBucketIdentifier, SdkEventNames, + }, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::SdkEventMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct PaymentMethodsCallCount; + +#[async_trait::async_trait] +impl super::SdkEventMetric for PaymentMethodsCallCount +where + T: AnalyticsDataSource + super::SdkEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[SdkEventDimensions], + publishable_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); + let dimensions = dimensions.to_vec(); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + query_builder + .add_granularity_in_mins(granularity) + .switch()?; + } + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", publishable_key) + .switch()?; + + query_builder + .add_bool_filter_clause("first_event", 1) + .switch()?; + + query_builder + .add_filter_clause("event_name", SdkEventNames::PaymentMethodsCall) + .switch()?; + + query_builder + .add_filter_clause("log_type", "INFO") + .switch()?; + + query_builder + .add_filter_clause("category", "API") + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(_granularity) = granularity.as_ref() { + query_builder + .add_group_by_clause("time_bucket") + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + SdkEventMetricsBucketIdentifier::new( + i.payment_method.clone(), + i.platform.clone(), + i.browser_name.clone(), + i.source.clone(), + i.component.clone(), + i.payment_experience.clone(), + i.time_bucket.clone(), + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/sdk_events/metrics/payment_success_count.rs b/crates/analytics/src/sdk_events/metrics/payment_success_count.rs new file mode 100644 index 000000000000..3faf8213632f --- /dev/null +++ b/crates/analytics/src/sdk_events/metrics/payment_success_count.rs @@ -0,0 +1,118 @@ +use api_models::analytics::{ + sdk_events::{ + SdkEventDimensions, SdkEventFilters, SdkEventMetricsBucketIdentifier, SdkEventNames, + }, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::SdkEventMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct PaymentSuccessCount; + +#[async_trait::async_trait] +impl super::SdkEventMetric for PaymentSuccessCount +where + T: AnalyticsDataSource + super::SdkEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[SdkEventDimensions], + publishable_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); + let dimensions = dimensions.to_vec(); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + query_builder + .add_granularity_in_mins(granularity) + .switch()?; + } + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", publishable_key) + .switch()?; + + query_builder + .add_bool_filter_clause("first_event", 1) + .switch()?; + + query_builder + .add_filter_clause("event_name", SdkEventNames::PaymentSuccess) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(_granularity) = granularity.as_ref() { + query_builder + .add_group_by_clause("time_bucket") + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + SdkEventMetricsBucketIdentifier::new( + i.payment_method.clone(), + i.platform.clone(), + i.browser_name.clone(), + i.source.clone(), + i.component.clone(), + i.payment_experience.clone(), + i.time_bucket.clone(), + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/sdk_events/metrics/sdk_initiated_count.rs b/crates/analytics/src/sdk_events/metrics/sdk_initiated_count.rs new file mode 100644 index 000000000000..a525e7890b75 --- /dev/null +++ b/crates/analytics/src/sdk_events/metrics/sdk_initiated_count.rs @@ -0,0 +1,118 @@ +use api_models::analytics::{ + sdk_events::{ + SdkEventDimensions, SdkEventFilters, SdkEventMetricsBucketIdentifier, SdkEventNames, + }, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::SdkEventMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct SdkInitiatedCount; + +#[async_trait::async_trait] +impl super::SdkEventMetric for SdkInitiatedCount +where + T: AnalyticsDataSource + super::SdkEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[SdkEventDimensions], + publishable_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); + let dimensions = dimensions.to_vec(); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + query_builder + .add_granularity_in_mins(granularity) + .switch()?; + } + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", publishable_key) + .switch()?; + + query_builder + .add_bool_filter_clause("first_event", 1) + .switch()?; + + query_builder + .add_filter_clause("event_name", SdkEventNames::StripeElementsCalled) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(_granularity) = granularity.as_ref() { + query_builder + .add_group_by_clause("time_bucket") + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + SdkEventMetricsBucketIdentifier::new( + i.payment_method.clone(), + i.platform.clone(), + i.browser_name.clone(), + i.source.clone(), + i.component.clone(), + i.payment_experience.clone(), + i.time_bucket.clone(), + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/sdk_events/metrics/sdk_rendered_count.rs b/crates/analytics/src/sdk_events/metrics/sdk_rendered_count.rs new file mode 100644 index 000000000000..ed9e776423a8 --- /dev/null +++ b/crates/analytics/src/sdk_events/metrics/sdk_rendered_count.rs @@ -0,0 +1,118 @@ +use api_models::analytics::{ + sdk_events::{ + SdkEventDimensions, SdkEventFilters, SdkEventMetricsBucketIdentifier, SdkEventNames, + }, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::SdkEventMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct SdkRenderedCount; + +#[async_trait::async_trait] +impl super::SdkEventMetric for SdkRenderedCount +where + T: AnalyticsDataSource + super::SdkEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[SdkEventDimensions], + publishable_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); + let dimensions = dimensions.to_vec(); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + query_builder + .add_granularity_in_mins(granularity) + .switch()?; + } + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", publishable_key) + .switch()?; + + query_builder + .add_bool_filter_clause("first_event", 1) + .switch()?; + + query_builder + .add_filter_clause("event_name", SdkEventNames::AppRendered) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(_granularity) = granularity.as_ref() { + query_builder + .add_group_by_clause("time_bucket") + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + SdkEventMetricsBucketIdentifier::new( + i.payment_method.clone(), + i.platform.clone(), + i.browser_name.clone(), + i.source.clone(), + i.component.clone(), + i.payment_experience.clone(), + i.time_bucket.clone(), + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/sdk_events/types.rs b/crates/analytics/src/sdk_events/types.rs new file mode 100644 index 000000000000..d631b3158ed4 --- /dev/null +++ b/crates/analytics/src/sdk_events/types.rs @@ -0,0 +1,50 @@ +use api_models::analytics::sdk_events::{SdkEventDimensions, SdkEventFilters}; +use error_stack::ResultExt; + +use crate::{ + query::{QueryBuilder, QueryFilter, QueryResult, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource}, +}; + +impl QueryFilter for SdkEventFilters +where + T: AnalyticsDataSource, + AnalyticsCollection: ToSql, +{ + fn set_filter_clause(&self, builder: &mut QueryBuilder) -> QueryResult<()> { + if !self.payment_method.is_empty() { + builder + .add_filter_in_range_clause(SdkEventDimensions::PaymentMethod, &self.payment_method) + .attach_printable("Error adding payment method filter")?; + } + if !self.platform.is_empty() { + builder + .add_filter_in_range_clause(SdkEventDimensions::Platform, &self.platform) + .attach_printable("Error adding platform filter")?; + } + if !self.browser_name.is_empty() { + builder + .add_filter_in_range_clause(SdkEventDimensions::BrowserName, &self.browser_name) + .attach_printable("Error adding browser name filter")?; + } + if !self.source.is_empty() { + builder + .add_filter_in_range_clause(SdkEventDimensions::Source, &self.source) + .attach_printable("Error adding source filter")?; + } + if !self.component.is_empty() { + builder + .add_filter_in_range_clause(SdkEventDimensions::Component, &self.component) + .attach_printable("Error adding component filter")?; + } + if !self.payment_experience.is_empty() { + builder + .add_filter_in_range_clause( + SdkEventDimensions::PaymentExperience, + &self.payment_experience, + ) + .attach_printable("Error adding payment experience filter")?; + } + Ok(()) + } +} diff --git a/crates/router/src/analytics/sqlx.rs b/crates/analytics/src/sqlx.rs similarity index 64% rename from crates/router/src/analytics/sqlx.rs rename to crates/analytics/src/sqlx.rs index b88a2065f0b0..cdd2647e4e71 100644 --- a/crates/router/src/analytics/sqlx.rs +++ b/crates/analytics/src/sqlx.rs @@ -1,14 +1,11 @@ use std::{fmt::Display, str::FromStr}; use api_models::analytics::refunds::RefundType; -use common_enums::enums::{ +use common_utils::errors::{CustomResult, ParsingError}; +use diesel_models::enums::{ AttemptStatus, AuthenticationType, Currency, PaymentMethod, RefundStatus, }; -use common_utils::errors::{CustomResult, ParsingError}; use error_stack::{IntoReport, ResultExt}; -#[cfg(feature = "kms")] -use external_services::{kms, kms::decrypt::KmsDecrypt}; -#[cfg(not(feature = "kms"))] use masking::PeekInterface; use sqlx::{ postgres::{PgArgumentBuffer, PgPoolOptions, PgRow, PgTypeInfo, PgValueRef}, @@ -16,15 +13,16 @@ use sqlx::{ Error::ColumnNotFound, FromRow, Pool, Postgres, Row, }; +use storage_impl::config::Database; use time::PrimitiveDateTime; use super::{ - query::{Aggregate, ToSql}, + query::{Aggregate, ToSql, Window}, types::{ AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, LoadRow, QueryExecutionError, + TableEngine, }, }; -use crate::configs::settings::Database; #[derive(Debug, Clone)] pub struct SqlxClient { @@ -47,19 +45,7 @@ impl Default for SqlxClient { } impl SqlxClient { - pub async fn from_conf( - conf: &Database, - #[cfg(feature = "kms")] kms_client: &kms::KmsClient, - ) -> Self { - #[cfg(feature = "kms")] - #[allow(clippy::expect_used)] - let password = conf - .password - .decrypt_inner(kms_client) - .await - .expect("Failed to KMS decrypt database password"); - - #[cfg(not(feature = "kms"))] + pub async fn from_conf(conf: &Database) -> Self { let password = &conf.password.peek(); let database_url = format!( "postgres://{}:{}@{}:{}/{}", @@ -154,6 +140,7 @@ where impl super::payments::filters::PaymentFilterAnalytics for SqlxClient {} impl super::payments::metrics::PaymentMetricAnalytics for SqlxClient {} +impl super::payments::distribution::PaymentDistributionAnalytics for SqlxClient {} impl super::refunds::metrics::RefundMetricAnalytics for SqlxClient {} impl super::refunds::filters::RefundFilterAnalytics for SqlxClient {} @@ -207,7 +194,7 @@ impl<'a> FromRow<'a, PgRow> for super::refunds::metrics::RefundMetricRow { ColumnNotFound(_) => Ok(Default::default()), e => Err(e), })?; - + // Removing millisecond precision to get accurate diffs against clickhouse let start_bucket: Option = row .try_get::, _>("start_bucket")? .and_then(|dt| dt.replace_millisecond(0).ok()); @@ -253,6 +240,11 @@ impl<'a> FromRow<'a, PgRow> for super::payments::metrics::PaymentMetricRow { ColumnNotFound(_) => Ok(Default::default()), e => Err(e), })?; + let payment_method_type: Option = + row.try_get("payment_method_type").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; let total: Option = row.try_get("total").or_else(|e| match e { ColumnNotFound(_) => Ok(Default::default()), e => Err(e), @@ -261,7 +253,72 @@ impl<'a> FromRow<'a, PgRow> for super::payments::metrics::PaymentMetricRow { ColumnNotFound(_) => Ok(Default::default()), e => Err(e), })?; + // Removing millisecond precision to get accurate diffs against clickhouse + let start_bucket: Option = row + .try_get::, _>("start_bucket")? + .and_then(|dt| dt.replace_millisecond(0).ok()); + let end_bucket: Option = row + .try_get::, _>("end_bucket")? + .and_then(|dt| dt.replace_millisecond(0).ok()); + Ok(Self { + currency, + status, + connector, + authentication_type, + payment_method, + payment_method_type, + total, + count, + start_bucket, + end_bucket, + }) + } +} +impl<'a> FromRow<'a, PgRow> for super::payments::distribution::PaymentDistributionRow { + fn from_row(row: &'a PgRow) -> sqlx::Result { + let currency: Option> = + row.try_get("currency").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let status: Option> = + row.try_get("status").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let connector: Option = row.try_get("connector").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let authentication_type: Option> = + row.try_get("authentication_type").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let payment_method: Option = + row.try_get("payment_method").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let payment_method_type: Option = + row.try_get("payment_method_type").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let total: Option = row.try_get("total").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let count: Option = row.try_get("count").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let error_message: Option = row.try_get("error_message").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + // Removing millisecond precision to get accurate diffs against clickhouse let start_bucket: Option = row .try_get::, _>("start_bucket")? .and_then(|dt| dt.replace_millisecond(0).ok()); @@ -274,8 +331,10 @@ impl<'a> FromRow<'a, PgRow> for super::payments::metrics::PaymentMetricRow { connector, authentication_type, payment_method, + payment_method_type, total, count, + error_message, start_bucket, end_bucket, }) @@ -308,12 +367,18 @@ impl<'a> FromRow<'a, PgRow> for super::payments::filters::FilterRow { ColumnNotFound(_) => Ok(Default::default()), e => Err(e), })?; + let payment_method_type: Option = + row.try_get("payment_method_type").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; Ok(Self { currency, status, connector, authentication_type, payment_method, + payment_method_type, }) } } @@ -349,16 +414,21 @@ impl<'a> FromRow<'a, PgRow> for super::refunds::filters::RefundFilterRow { } impl ToSql for PrimitiveDateTime { - fn to_sql(&self) -> error_stack::Result { + fn to_sql(&self, _table_engine: &TableEngine) -> error_stack::Result { Ok(self.to_string()) } } impl ToSql for AnalyticsCollection { - fn to_sql(&self) -> error_stack::Result { + fn to_sql(&self, _table_engine: &TableEngine) -> error_stack::Result { match self { Self::Payment => Ok("payment_attempt".to_string()), Self::Refund => Ok("refund".to_string()), + Self::SdkEvents => Err(error_stack::report!(ParsingError::UnknownError) + .attach_printable("SdkEvents table is not implemented for Sqlx"))?, + Self::ApiEvents => Err(error_stack::report!(ParsingError::UnknownError) + .attach_printable("ApiEvents table is not implemented for Sqlx"))?, + Self::PaymentIntent => Ok("payment_intent".to_string()), } } } @@ -367,7 +437,7 @@ impl ToSql for Aggregate where T: ToSql, { - fn to_sql(&self) -> error_stack::Result { + fn to_sql(&self, table_engine: &TableEngine) -> error_stack::Result { Ok(match self { Self::Count { field: _, alias } => { format!( @@ -378,21 +448,86 @@ where Self::Sum { field, alias } => { format!( "sum({}){}", - field.to_sql().attach_printable("Failed to sum aggregate")?, + field + .to_sql(table_engine) + .attach_printable("Failed to sum aggregate")?, alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) ) } Self::Min { field, alias } => { format!( "min({}){}", - field.to_sql().attach_printable("Failed to min aggregate")?, + field + .to_sql(table_engine) + .attach_printable("Failed to min aggregate")?, alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) ) } Self::Max { field, alias } => { format!( "max({}){}", - field.to_sql().attach_printable("Failed to max aggregate")?, + field + .to_sql(table_engine) + .attach_printable("Failed to max aggregate")?, + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + }) + } +} + +impl ToSql for Window +where + T: ToSql, +{ + fn to_sql(&self, table_engine: &TableEngine) -> error_stack::Result { + Ok(match self { + Self::Sum { + field, + partition_by, + order_by, + alias, + } => { + format!( + "sum({}) over ({}{}){}", + field + .to_sql(table_engine) + .attach_printable("Failed to sum window")?, + partition_by.as_ref().map_or_else( + || "".to_owned(), + |partition_by| format!("partition by {}", partition_by.to_owned()) + ), + order_by.as_ref().map_or_else( + || "".to_owned(), + |(order_column, order)| format!( + " order by {} {}", + order_column.to_owned(), + order.to_string() + ) + ), + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + Self::RowNumber { + field: _, + partition_by, + order_by, + alias, + } => { + format!( + "row_number() over ({}{}){}", + partition_by.as_ref().map_or_else( + || "".to_owned(), + |partition_by| format!("partition by {}", partition_by.to_owned()) + ), + order_by.as_ref().map_or_else( + || "".to_owned(), + |(order_column, order)| format!( + " order by {} {}", + order_column.to_owned(), + order.to_string() + ) + ), alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) ) } diff --git a/crates/router/src/analytics/types.rs b/crates/analytics/src/types.rs similarity index 83% rename from crates/router/src/analytics/types.rs rename to crates/analytics/src/types.rs index fe20e812a9b8..16d342d3d2ee 100644 --- a/crates/router/src/analytics/types.rs +++ b/crates/analytics/src/types.rs @@ -2,25 +2,36 @@ use std::{fmt::Display, str::FromStr}; use common_utils::{ errors::{CustomResult, ErrorSwitch, ParsingError}, - events::ApiEventMetric, + events::{ApiEventMetric, ApiEventsType}, + impl_misc_api_event_type, }; use error_stack::{report, Report, ResultExt}; use super::query::QueryBuildingError; -#[derive(serde::Deserialize, Debug, masking::Serialize)] +#[derive(serde::Deserialize, Debug, serde::Serialize)] #[serde(rename_all = "snake_case")] pub enum AnalyticsDomain { Payments, Refunds, + SdkEvents, + ApiEvents, } -impl ApiEventMetric for AnalyticsDomain {} - #[derive(Debug, strum::AsRefStr, strum::Display, Clone, Copy)] pub enum AnalyticsCollection { Payment, Refund, + SdkEvents, + ApiEvents, + PaymentIntent, +} + +#[allow(dead_code)] +#[derive(Debug)] +pub enum TableEngine { + CollapsingMergeTree { sign: &'static str }, + BasicTree, } #[derive(Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq)] @@ -50,6 +61,7 @@ where // Analytics Framework pub trait RefundAnalytics {} +pub trait SdkEventAnalytics {} #[async_trait::async_trait] pub trait AnalyticsDataSource @@ -60,6 +72,10 @@ where async fn load_results(&self, query: &str) -> CustomResult, QueryExecutionError> where Self: LoadRow; + + fn get_table_engine(_table: AnalyticsCollection) -> TableEngine { + TableEngine::BasicTree + } } pub trait LoadRow @@ -117,3 +133,5 @@ impl ErrorSwitch for QueryBuildingError { FiltersError::QueryBuildingError } } + +impl_misc_api_event_type!(AnalyticsDomain); diff --git a/crates/router/src/analytics/utils.rs b/crates/analytics/src/utils.rs similarity index 52% rename from crates/router/src/analytics/utils.rs rename to crates/analytics/src/utils.rs index f7e6ea69dc37..6a0aa973a1e7 100644 --- a/crates/router/src/analytics/utils.rs +++ b/crates/analytics/src/utils.rs @@ -1,6 +1,8 @@ use api_models::analytics::{ + api_event::{ApiEventDimensions, ApiEventMetrics}, payments::{PaymentDimensions, PaymentMetrics}, refunds::{RefundDimensions, RefundMetrics}, + sdk_events::{SdkEventDimensions, SdkEventMetrics}, NameDescription, }; use strum::IntoEnumIterator; @@ -13,6 +15,14 @@ pub fn get_refund_dimensions() -> Vec { RefundDimensions::iter().map(Into::into).collect() } +pub fn get_sdk_event_dimensions() -> Vec { + SdkEventDimensions::iter().map(Into::into).collect() +} + +pub fn get_api_event_dimensions() -> Vec { + ApiEventDimensions::iter().map(Into::into).collect() +} + pub fn get_payment_metrics_info() -> Vec { PaymentMetrics::iter().map(Into::into).collect() } @@ -20,3 +30,11 @@ pub fn get_payment_metrics_info() -> Vec { pub fn get_refund_metrics_info() -> Vec { RefundMetrics::iter().map(Into::into).collect() } + +pub fn get_sdk_event_metrics_info() -> Vec { + SdkEventMetrics::iter().map(Into::into).collect() +} + +pub fn get_api_event_metrics_info() -> Vec { + ApiEventMetrics::iter().map(Into::into).collect() +} diff --git a/crates/api_models/src/analytics.rs b/crates/api_models/src/analytics.rs index 0358b6b313cf..0263427b0fde 100644 --- a/crates/api_models/src/analytics.rs +++ b/crates/api_models/src/analytics.rs @@ -1,15 +1,20 @@ use std::collections::HashSet; -use common_utils::events::ApiEventMetric; -use time::PrimitiveDateTime; +use common_utils::pii::EmailStrategy; +use masking::Secret; use self::{ - payments::{PaymentDimensions, PaymentMetrics}, + api_event::{ApiEventDimensions, ApiEventMetrics}, + payments::{PaymentDimensions, PaymentDistributions, PaymentMetrics}, refunds::{RefundDimensions, RefundMetrics}, + sdk_events::{SdkEventDimensions, SdkEventMetrics}, }; +pub use crate::payments::TimeRange; +pub mod api_event; pub mod payments; pub mod refunds; +pub mod sdk_events; #[derive(Debug, serde::Serialize)] pub struct NameDescription { @@ -25,23 +30,12 @@ pub struct GetInfoResponse { pub dimensions: Vec, } -impl ApiEventMetric for GetInfoResponse {} - -#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash)] -#[serde(rename_all = "camelCase")] -pub struct TimeRange { - #[serde(with = "common_utils::custom_serde::iso8601")] - pub start_time: PrimitiveDateTime, - #[serde(default, with = "common_utils::custom_serde::iso8601::option")] - pub end_time: Option, -} - -#[derive(Clone, Copy, Debug, serde::Deserialize, masking::Serialize)] +#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)] pub struct TimeSeries { pub granularity: Granularity, } -#[derive(Clone, Copy, Debug, serde::Deserialize, masking::Serialize)] +#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)] pub enum Granularity { #[serde(rename = "G_ONEMIN")] OneMin, @@ -57,7 +51,7 @@ pub enum Granularity { OneDay, } -#[derive(Clone, Debug, serde::Deserialize, masking::Serialize)] +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct GetPaymentMetricRequest { pub time_series: Option, @@ -67,13 +61,51 @@ pub struct GetPaymentMetricRequest { #[serde(default)] pub filters: payments::PaymentFilters, pub metrics: HashSet, + pub distribution: Option, #[serde(default)] pub delta: bool, } -impl ApiEventMetric for GetPaymentMetricRequest {} +#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)] +pub enum QueryLimit { + #[serde(rename = "TOP_5")] + Top5, + #[serde(rename = "TOP_10")] + Top10, +} + +#[allow(clippy::from_over_into)] +impl Into for QueryLimit { + fn into(self) -> u64 { + match self { + Self::Top5 => 5, + Self::Top10 => 10, + } + } +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Distribution { + pub distribution_for: PaymentDistributions, + pub distribution_cardinality: QueryLimit, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ReportRequest { + pub time_range: TimeRange, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GenerateReportRequest { + pub request: ReportRequest, + pub merchant_id: String, + pub email: Secret, +} -#[derive(Clone, Debug, serde::Deserialize, masking::Serialize)] +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct GetRefundMetricRequest { pub time_series: Option, @@ -87,14 +119,26 @@ pub struct GetRefundMetricRequest { pub delta: bool, } -impl ApiEventMetric for GetRefundMetricRequest {} +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetSdkEventMetricRequest { + pub time_series: Option, + pub time_range: TimeRange, + #[serde(default)] + pub group_by_names: Vec, + #[serde(default)] + pub filters: sdk_events::SdkEventFilters, + pub metrics: HashSet, + #[serde(default)] + pub delta: bool, +} #[derive(Debug, serde::Serialize)] pub struct AnalyticsMetadata { pub current_time_range: TimeRange, } -#[derive(Debug, serde::Deserialize, masking::Serialize)] +#[derive(Debug, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct GetPaymentFiltersRequest { pub time_range: TimeRange, @@ -102,16 +146,12 @@ pub struct GetPaymentFiltersRequest { pub group_by_names: Vec, } -impl ApiEventMetric for GetPaymentFiltersRequest {} - #[derive(Debug, Default, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct PaymentFiltersResponse { pub query_data: Vec, } -impl ApiEventMetric for PaymentFiltersResponse {} - #[derive(Debug, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct FilterValue { @@ -119,34 +159,88 @@ pub struct FilterValue { pub values: Vec, } -#[derive(Debug, serde::Deserialize, masking::Serialize)] +#[derive(Debug, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] + pub struct GetRefundFilterRequest { pub time_range: TimeRange, #[serde(default)] pub group_by_names: Vec, } -impl ApiEventMetric for GetRefundFilterRequest {} - #[derive(Debug, Default, serde::Serialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct RefundFiltersResponse { pub query_data: Vec, } -impl ApiEventMetric for RefundFiltersResponse {} - #[derive(Debug, serde::Serialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] + pub struct RefundFilterValue { pub dimension: RefundDimensions, pub values: Vec, } +#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetSdkEventFiltersRequest { + pub time_range: TimeRange, + #[serde(default)] + pub group_by_names: Vec, +} + +#[derive(Debug, Default, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SdkEventFiltersResponse { + pub query_data: Vec, +} + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SdkEventFilterValue { + pub dimension: SdkEventDimensions, + pub values: Vec, +} + #[derive(Debug, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct MetricsResponse { pub query_data: Vec, pub meta_data: [AnalyticsMetadata; 1], } + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetApiEventFiltersRequest { + pub time_range: TimeRange, + #[serde(default)] + pub group_by_names: Vec, +} + +#[derive(Debug, Default, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiEventFiltersResponse { + pub query_data: Vec, +} + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiEventFilterValue { + pub dimension: ApiEventDimensions, + pub values: Vec, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetApiEventMetricRequest { + pub time_series: Option, + pub time_range: TimeRange, + #[serde(default)] + pub group_by_names: Vec, + #[serde(default)] + pub filters: api_event::ApiEventFilters, + pub metrics: HashSet, + #[serde(default)] + pub delta: bool, +} diff --git a/crates/api_models/src/analytics/api_event.rs b/crates/api_models/src/analytics/api_event.rs new file mode 100644 index 000000000000..62fe829f01b9 --- /dev/null +++ b/crates/api_models/src/analytics/api_event.rs @@ -0,0 +1,148 @@ +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, +}; + +use super::{NameDescription, TimeRange}; +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct ApiLogsRequest { + #[serde(flatten)] + pub query_param: QueryType, + pub api_name_filter: Option>, +} + +pub enum FilterType { + ApiCountFilter, + LatencyFilter, + StatusCodeFilter, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(tag = "type")] +pub enum QueryType { + Payment { + payment_id: String, + }, + Refund { + payment_id: String, + refund_id: String, + }, +} + +#[derive( + Debug, + serde::Serialize, + serde::Deserialize, + strum::AsRefStr, + PartialEq, + PartialOrd, + Eq, + Ord, + strum::Display, + strum::EnumIter, + Clone, + Copy, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum ApiEventDimensions { + // Do not change the order of these enums + // Consult the Dashboard FE folks since these also affects the order of metrics on FE + StatusCode, + FlowType, + ApiFlow, +} + +impl From for NameDescription { + fn from(value: ApiEventDimensions) -> Self { + Self { + name: value.to_string(), + desc: String::new(), + } + } +} +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +pub struct ApiEventFilters { + pub status_code: Vec, + pub flow_type: Vec, + pub api_flow: Vec, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumIter, + strum::AsRefStr, +)] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum ApiEventMetrics { + Latency, + ApiCount, + StatusCodeCount, +} + +impl From for NameDescription { + fn from(value: ApiEventMetrics) -> Self { + Self { + name: value.to_string(), + desc: String::new(), + } + } +} + +#[derive(Debug, serde::Serialize, Eq)] +pub struct ApiEventMetricsBucketIdentifier { + #[serde(rename = "time_range")] + pub time_bucket: TimeRange, + // Coz FE sucks + #[serde(rename = "time_bucket")] + #[serde(with = "common_utils::custom_serde::iso8601custom")] + pub start_time: time::PrimitiveDateTime, +} + +impl ApiEventMetricsBucketIdentifier { + pub fn new(normalized_time_range: TimeRange) -> Self { + Self { + time_bucket: normalized_time_range, + start_time: normalized_time_range.start_time, + } + } +} + +impl Hash for ApiEventMetricsBucketIdentifier { + fn hash(&self, state: &mut H) { + self.time_bucket.hash(state); + } +} + +impl PartialEq for ApiEventMetricsBucketIdentifier { + fn eq(&self, other: &Self) -> bool { + let mut left = DefaultHasher::new(); + self.hash(&mut left); + let mut right = DefaultHasher::new(); + other.hash(&mut right); + left.finish() == right.finish() + } +} + +#[derive(Debug, serde::Serialize)] +pub struct ApiEventMetricsBucketValue { + pub latency: Option, + pub api_count: Option, + pub status_code_count: Option, +} + +#[derive(Debug, serde::Serialize)] +pub struct ApiMetricsBucketResponse { + #[serde(flatten)] + pub values: ApiEventMetricsBucketValue, + #[serde(flatten)] + pub dimensions: ApiEventMetricsBucketIdentifier, +} diff --git a/crates/api_models/src/analytics/payments.rs b/crates/api_models/src/analytics/payments.rs index b5e5852d6283..2d7ae262f489 100644 --- a/crates/api_models/src/analytics/payments.rs +++ b/crates/api_models/src/analytics/payments.rs @@ -3,13 +3,12 @@ use std::{ hash::{Hash, Hasher}, }; -use common_enums::enums::{AttemptStatus, AuthenticationType, Currency, PaymentMethod}; -use common_utils::events::ApiEventMetric; - use super::{NameDescription, TimeRange}; -use crate::{analytics::MetricsResponse, enums::Connector}; +use crate::enums::{ + AttemptStatus, AuthenticationType, Connector, Currency, PaymentMethod, PaymentMethodType, +}; -#[derive(Clone, Debug, Default, serde::Deserialize, masking::Serialize)] +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] pub struct PaymentFilters { #[serde(default)] pub currency: Vec, @@ -21,6 +20,8 @@ pub struct PaymentFilters { pub auth_type: Vec, #[serde(default)] pub payment_method: Vec, + #[serde(default)] + pub payment_method_type: Vec, } #[derive( @@ -44,6 +45,7 @@ pub enum PaymentDimensions { // Consult the Dashboard FE folks since these also affects the order of metrics on FE Connector, PaymentMethod, + PaymentMethodType, Currency, #[strum(serialize = "authentication_type")] #[serde(rename = "authentication_type")] @@ -73,6 +75,35 @@ pub enum PaymentMetrics { PaymentSuccessCount, PaymentProcessedAmount, AvgTicketSize, + RetriesCount, + ConnectorSuccessRate, +} + +#[derive(Debug, Default, serde::Serialize)] +pub struct ErrorResult { + pub reason: String, + pub count: i64, + pub percentage: f64, +} + +#[derive( + Clone, + Copy, + Debug, + Hash, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumIter, + strum::AsRefStr, +)] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum PaymentDistributions { + #[strum(serialize = "error_message")] + PaymentErrorMessage, } pub mod metric_behaviour { @@ -109,6 +140,7 @@ pub struct PaymentMetricsBucketIdentifier { #[serde(rename = "authentication_type")] pub auth_type: Option, pub payment_method: Option, + pub payment_method_type: Option, #[serde(rename = "time_range")] pub time_bucket: TimeRange, // Coz FE sucks @@ -124,6 +156,7 @@ impl PaymentMetricsBucketIdentifier { connector: Option, auth_type: Option, payment_method: Option, + payment_method_type: Option, normalized_time_range: TimeRange, ) -> Self { Self { @@ -132,6 +165,7 @@ impl PaymentMetricsBucketIdentifier { connector, auth_type, payment_method, + payment_method_type, time_bucket: normalized_time_range, start_time: normalized_time_range.start_time, } @@ -145,6 +179,7 @@ impl Hash for PaymentMetricsBucketIdentifier { self.connector.hash(state); self.auth_type.map(|i| i.to_string()).hash(state); self.payment_method.hash(state); + self.payment_method_type.hash(state); self.time_bucket.hash(state); } } @@ -166,6 +201,10 @@ pub struct PaymentMetricsBucketValue { pub payment_success_count: Option, pub payment_processed_amount: Option, pub avg_ticket_size: Option, + pub payment_error_message: Option>, + pub retries_count: Option, + pub retries_amount_processed: Option, + pub connector_success_rate: Option, } #[derive(Debug, serde::Serialize)] @@ -175,6 +214,3 @@ pub struct MetricsBucketResponse { #[serde(flatten)] pub dimensions: PaymentMetricsBucketIdentifier, } - -impl ApiEventMetric for MetricsBucketResponse {} -impl ApiEventMetric for MetricsResponse {} diff --git a/crates/api_models/src/analytics/refunds.rs b/crates/api_models/src/analytics/refunds.rs index c5d444338d38..5ecdf1cecb3f 100644 --- a/crates/api_models/src/analytics/refunds.rs +++ b/crates/api_models/src/analytics/refunds.rs @@ -3,10 +3,7 @@ use std::{ hash::{Hash, Hasher}, }; -use common_enums::enums::{Currency, RefundStatus}; -use common_utils::events::ApiEventMetric; - -use crate::analytics::MetricsResponse; +use crate::{enums::Currency, refunds::RefundStatus}; #[derive( Clone, @@ -20,7 +17,7 @@ use crate::analytics::MetricsResponse; strum::Display, strum::EnumString, )] -// TODO RefundType common_enums need to mapped to storage_model +// TODO RefundType api_models_oss need to mapped to storage_model #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum RefundType { @@ -31,7 +28,7 @@ pub enum RefundType { } use super::{NameDescription, TimeRange}; -#[derive(Clone, Debug, Default, serde::Deserialize, masking::Serialize)] +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] pub struct RefundFilters { #[serde(default)] pub currency: Vec, @@ -115,8 +112,9 @@ impl From for NameDescription { #[derive(Debug, serde::Serialize, Eq)] pub struct RefundMetricsBucketIdentifier { pub currency: Option, - pub refund_status: Option, + pub refund_status: Option, pub connector: Option, + pub refund_type: Option, #[serde(rename = "time_range")] pub time_bucket: TimeRange, @@ -128,7 +126,7 @@ pub struct RefundMetricsBucketIdentifier { impl Hash for RefundMetricsBucketIdentifier { fn hash(&self, state: &mut H) { self.currency.hash(state); - self.refund_status.map(|i| i.to_string()).hash(state); + self.refund_status.hash(state); self.connector.hash(state); self.refund_type.hash(state); self.time_bucket.hash(state); @@ -147,7 +145,7 @@ impl PartialEq for RefundMetricsBucketIdentifier { impl RefundMetricsBucketIdentifier { pub fn new( currency: Option, - refund_status: Option, + refund_status: Option, connector: Option, refund_type: Option, normalized_time_range: TimeRange, @@ -162,7 +160,6 @@ impl RefundMetricsBucketIdentifier { } } } - #[derive(Debug, serde::Serialize)] pub struct RefundMetricsBucketValue { pub refund_success_rate: Option, @@ -170,7 +167,6 @@ pub struct RefundMetricsBucketValue { pub refund_success_count: Option, pub refund_processed_amount: Option, } - #[derive(Debug, serde::Serialize)] pub struct RefundMetricsBucketResponse { #[serde(flatten)] @@ -178,6 +174,3 @@ pub struct RefundMetricsBucketResponse { #[serde(flatten)] pub dimensions: RefundMetricsBucketIdentifier, } - -impl ApiEventMetric for RefundMetricsBucketResponse {} -impl ApiEventMetric for MetricsResponse {} diff --git a/crates/api_models/src/analytics/sdk_events.rs b/crates/api_models/src/analytics/sdk_events.rs new file mode 100644 index 000000000000..76ccb29867f2 --- /dev/null +++ b/crates/api_models/src/analytics/sdk_events.rs @@ -0,0 +1,215 @@ +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, +}; + +use super::{NameDescription, TimeRange}; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SdkEventsRequest { + pub payment_id: String, + pub time_range: TimeRange, +} + +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +pub struct SdkEventFilters { + #[serde(default)] + pub payment_method: Vec, + #[serde(default)] + pub platform: Vec, + #[serde(default)] + pub browser_name: Vec, + #[serde(default)] + pub source: Vec, + #[serde(default)] + pub component: Vec, + #[serde(default)] + pub payment_experience: Vec, +} + +#[derive( + Debug, + serde::Serialize, + serde::Deserialize, + strum::AsRefStr, + PartialEq, + PartialOrd, + Eq, + Ord, + strum::Display, + strum::EnumIter, + Clone, + Copy, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum SdkEventDimensions { + // Do not change the order of these enums + // Consult the Dashboard FE folks since these also affects the order of metrics on FE + PaymentMethod, + Platform, + BrowserName, + Source, + Component, + PaymentExperience, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumIter, + strum::AsRefStr, +)] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum SdkEventMetrics { + PaymentAttempts, + PaymentSuccessCount, + PaymentMethodsCallCount, + SdkRenderedCount, + SdkInitiatedCount, + PaymentMethodSelectedCount, + PaymentDataFilledCount, + AveragePaymentTime, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumIter, + strum::AsRefStr, +)] +#[strum(serialize_all = "SCREAMING_SNAKE_CASE")] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum SdkEventNames { + StripeElementsCalled, + AppRendered, + PaymentMethodChanged, + PaymentDataFilled, + PaymentAttempt, + PaymentSuccess, + PaymentMethodsCall, + ConfirmCall, + SessionsCall, + CustomerPaymentMethodsCall, + RedirectingUser, + DisplayBankTransferInfoPage, + DisplayQrCodeInfoPage, +} + +pub mod metric_behaviour { + pub struct PaymentAttempts; + pub struct PaymentSuccessCount; + pub struct PaymentMethodsCallCount; + pub struct SdkRenderedCount; + pub struct SdkInitiatedCount; + pub struct PaymentMethodSelectedCount; + pub struct PaymentDataFilledCount; + pub struct AveragePaymentTime; +} + +impl From for NameDescription { + fn from(value: SdkEventMetrics) -> Self { + Self { + name: value.to_string(), + desc: String::new(), + } + } +} + +impl From for NameDescription { + fn from(value: SdkEventDimensions) -> Self { + Self { + name: value.to_string(), + desc: String::new(), + } + } +} + +#[derive(Debug, serde::Serialize, Eq)] +pub struct SdkEventMetricsBucketIdentifier { + pub payment_method: Option, + pub platform: Option, + pub browser_name: Option, + pub source: Option, + pub component: Option, + pub payment_experience: Option, + pub time_bucket: Option, +} + +impl SdkEventMetricsBucketIdentifier { + pub fn new( + payment_method: Option, + platform: Option, + browser_name: Option, + source: Option, + component: Option, + payment_experience: Option, + time_bucket: Option, + ) -> Self { + Self { + payment_method, + platform, + browser_name, + source, + component, + payment_experience, + time_bucket, + } + } +} + +impl Hash for SdkEventMetricsBucketIdentifier { + fn hash(&self, state: &mut H) { + self.payment_method.hash(state); + self.platform.hash(state); + self.browser_name.hash(state); + self.source.hash(state); + self.component.hash(state); + self.payment_experience.hash(state); + self.time_bucket.hash(state); + } +} + +impl PartialEq for SdkEventMetricsBucketIdentifier { + fn eq(&self, other: &Self) -> bool { + let mut left = DefaultHasher::new(); + self.hash(&mut left); + let mut right = DefaultHasher::new(); + other.hash(&mut right); + left.finish() == right.finish() + } +} + +#[derive(Debug, serde::Serialize)] +pub struct SdkEventMetricsBucketValue { + pub payment_attempts: Option, + pub payment_success_count: Option, + pub payment_methods_call_count: Option, + pub average_payment_time: Option, + pub sdk_rendered_count: Option, + pub sdk_initiated_count: Option, + pub payment_method_selected_count: Option, + pub payment_data_filled_count: Option, +} + +#[derive(Debug, serde::Serialize)] +pub struct MetricsBucketResponse { + #[serde(flatten)] + pub values: SdkEventMetricsBucketValue, + #[serde(flatten)] + pub dimensions: SdkEventMetricsBucketIdentifier, +} diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index 782c02be7a3a..345f827daeac 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -14,8 +14,16 @@ use common_utils::{ }; use crate::{ - admin::*, api_keys::*, cards_info::*, disputes::*, files::*, mandates::*, payment_methods::*, - payments::*, verifications::*, + admin::*, + analytics::{api_event::*, sdk_events::*, *}, + api_keys::*, + cards_info::*, + disputes::*, + files::*, + mandates::*, + payment_methods::*, + payments::*, + verifications::*, }; impl ApiEventMetric for TimeRange {} @@ -63,7 +71,23 @@ impl_misc_api_event_type!( ApplepayMerchantVerificationRequest, ApplepayMerchantResponse, ApplepayVerifiedDomainsResponse, - UpdateApiKeyRequest + UpdateApiKeyRequest, + GetApiEventFiltersRequest, + ApiEventFiltersResponse, + GetInfoResponse, + GetPaymentMetricRequest, + GetRefundMetricRequest, + GetSdkEventMetricRequest, + GetPaymentFiltersRequest, + PaymentFiltersResponse, + GetRefundFilterRequest, + RefundFiltersResponse, + GetSdkEventFiltersRequest, + SdkEventFiltersResponse, + ApiLogsRequest, + GetApiEventMetricRequest, + SdkEventsRequest, + ReportRequest ); #[cfg(feature = "stripe")] @@ -76,3 +100,9 @@ impl_misc_api_event_type!( CustomerPaymentMethodListResponse, CreateCustomerResponse ); + +impl ApiEventMetric for MetricsResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Miscellaneous) + } +} diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index acb9bbdd6cd4..bd4c59211e24 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -2339,9 +2339,11 @@ pub struct PaymentListFilters { pub struct TimeRange { /// The start time to filter payments list or to get list of filters. To get list of filters start time is needed to be passed #[serde(with = "common_utils::custom_serde::iso8601")] + #[serde(alias = "startTime")] pub start_time: PrimitiveDateTime, /// The end time to filter payments list or to get list of filters. If not passed the default time is now #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + #[serde(alias = "endTime")] pub end_time: Option, } diff --git a/crates/data_models/Cargo.toml b/crates/data_models/Cargo.toml index 57ae1ec1ec87..857d53b6999e 100644 --- a/crates/data_models/Cargo.toml +++ b/crates/data_models/Cargo.toml @@ -18,7 +18,6 @@ common_enums = { version = "0.1.0", path = "../common_enums" } common_utils = { version = "0.1.0", path = "../common_utils" } masking = { version = "0.1.0", path = "../masking" } - # Third party deps async-trait = "0.1.68" error-stack = "0.3.1" diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index b51dc045b20d..f508460574dd 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -16,7 +16,7 @@ email = ["external_services/email", "dep:aws-config", "olap"] basilisk = ["kms"] stripe = ["dep:serde_qs"] release = ["kms", "stripe", "basilisk", "s3", "email", "business_profile_routing", "accounts_cache", "kv_store", "profile_specific_fallback_routing"] -olap = ["data_models/olap", "storage_impl/olap", "scheduler/olap"] +olap = ["data_models/olap", "storage_impl/olap", "scheduler/olap", "dep:analytics"] oltp = ["storage_impl/oltp"] kv_store = ["scheduler/kv_store"] accounts_cache = [] @@ -102,6 +102,7 @@ tracing-futures = { version = "0.2.5", features = ["tokio"] } # First party crates api_models = { version = "0.1.0", path = "../api_models", features = ["errors"] } +analytics = { version = "0.1.0", path = "../analytics", optional = true } cards = { version = "0.1.0", path = "../cards" } common_enums = { version = "0.1.0", path = "../common_enums" } common_utils = { version = "0.1.0", path = "../common_utils", features = ["signals", "async_ext", "logs"] } @@ -118,6 +119,7 @@ router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra scheduler = { version = "0.1.0", path = "../scheduler", default-features = false } storage_impl = { version = "0.1.0", path = "../storage_impl", default-features = false } erased-serde = "0.3.31" +rdkafka = "0.36.0" [build-dependencies] router_env = { version = "0.1.0", path = "../router_env", default-features = false } diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs index d57403d92989..f31e908e0dc3 100644 --- a/crates/router/src/analytics.rs +++ b/crates/router/src/analytics.rs @@ -1,129 +1,560 @@ -mod core; -mod errors; -pub mod metrics; -mod payments; -mod query; -mod refunds; -pub mod routes; - -mod sqlx; -mod types; -mod utils; - -use api_models::analytics::{ - payments::{PaymentDimensions, PaymentFilters, PaymentMetrics, PaymentMetricsBucketIdentifier}, - refunds::{RefundDimensions, RefundFilters, RefundMetrics, RefundMetricsBucketIdentifier}, - Granularity, TimeRange, -}; -use router_env::{instrument, tracing}; - -use self::{ - payments::metrics::{PaymentMetric, PaymentMetricRow}, - refunds::metrics::{RefundMetric, RefundMetricRow}, - sqlx::SqlxClient, -}; -use crate::configs::settings::Database; - -#[derive(Clone, Debug)] -pub enum AnalyticsProvider { - Sqlx(SqlxClient), -} +pub use analytics::*; + +pub mod routes { + use actix_web::{web, Responder, Scope}; + use analytics::{ + api_event::api_events_core, errors::AnalyticsError, lambda_utils::invoke_lambda, + sdk_events::sdk_events_core, + }; + use api_models::analytics::{ + GenerateReportRequest, GetApiEventFiltersRequest, GetApiEventMetricRequest, + GetPaymentFiltersRequest, GetPaymentMetricRequest, GetRefundFilterRequest, + GetRefundMetricRequest, GetSdkEventFiltersRequest, GetSdkEventMetricRequest, ReportRequest, + }; + use error_stack::ResultExt; + use router_env::AnalyticsFlow; + + use crate::{ + core::api_locking, + db::user::UserInterface, + routes::AppState, + services::{ + api, + authentication::{self as auth, AuthToken, AuthenticationData}, + authorization::permissions::Permission, + ApplicationResponse, + }, + types::domain::UserEmail, + }; + + pub struct Analytics; + + impl Analytics { + pub fn server(state: AppState) -> Scope { + let mut route = web::scope("/analytics/v1").app_data(web::Data::new(state)); + { + route = route + .service( + web::resource("metrics/payments") + .route(web::post().to(get_payment_metrics)), + ) + .service( + web::resource("metrics/refunds").route(web::post().to(get_refunds_metrics)), + ) + .service( + web::resource("filters/payments") + .route(web::post().to(get_payment_filters)), + ) + .service( + web::resource("filters/refunds").route(web::post().to(get_refund_filters)), + ) + .service(web::resource("{domain}/info").route(web::get().to(get_info))) + .service( + web::resource("report/dispute") + .route(web::post().to(generate_dispute_report)), + ) + .service( + web::resource("report/refunds") + .route(web::post().to(generate_refund_report)), + ) + .service( + web::resource("report/payments") + .route(web::post().to(generate_payment_report)), + ) + .service( + web::resource("metrics/sdk_events") + .route(web::post().to(get_sdk_event_metrics)), + ) + .service( + web::resource("filters/sdk_events") + .route(web::post().to(get_sdk_event_filters)), + ) + .service(web::resource("api_event_logs").route(web::get().to(get_api_events))) + .service(web::resource("sdk_event_logs").route(web::post().to(get_sdk_events))) + .service( + web::resource("filters/api_events") + .route(web::post().to(get_api_event_filters)), + ) + .service( + web::resource("metrics/api_events") + .route(web::post().to(get_api_events_metrics)), + ) + } + route + } + } -impl Default for AnalyticsProvider { - fn default() -> Self { - Self::Sqlx(SqlxClient::default()) + pub async fn get_info( + state: web::Data, + req: actix_web::HttpRequest, + domain: actix_web::web::Path, + ) -> impl Responder { + let flow = AnalyticsFlow::GetInfo; + Box::pin(api::server_wrap( + flow, + state, + &req, + domain.into_inner(), + |_, _, domain| async { + analytics::core::get_domain_info(domain) + .await + .map(ApplicationResponse::Json) + }, + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await } -} -impl AnalyticsProvider { - #[instrument(skip_all)] + /// # Panics + /// + /// Panics if `json_payload` array does not contain one `GetPaymentMetricRequest` element. pub async fn get_payment_metrics( - &self, - metric: &PaymentMetrics, - dimensions: &[PaymentDimensions], - merchant_id: &str, - filters: &PaymentFilters, - granularity: &Option, - time_range: &TimeRange, - ) -> types::MetricsResult> { - // Metrics to get the fetch time for each payment metric - metrics::request::record_operation_time( - async { - match self { - Self::Sqlx(pool) => { - metric - .load_metrics( - dimensions, - merchant_id, - filters, - granularity, - time_range, - pool, - ) - .await - } - } + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json<[GetPaymentMetricRequest; 1]>, + ) -> impl Responder { + // safety: This shouldn't panic owing to the data type + #[allow(clippy::expect_used)] + let payload = json_payload + .into_inner() + .to_vec() + .pop() + .expect("Couldn't get GetPaymentMetricRequest"); + let flow = AnalyticsFlow::GetPaymentMetrics; + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth: AuthenticationData, req| async move { + analytics::payments::get_metrics( + &state.pool, + &auth.merchant_account.merchant_id, + req, + ) + .await + .map(ApplicationResponse::Json) }, - &metrics::METRIC_FETCH_TIME, - metric, - self, - ) + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) .await } - pub async fn get_refund_metrics( - &self, - metric: &RefundMetrics, - dimensions: &[RefundDimensions], - merchant_id: &str, - filters: &RefundFilters, - granularity: &Option, - time_range: &TimeRange, - ) -> types::MetricsResult> { - match self { - Self::Sqlx(pool) => { - metric - .load_metrics( - dimensions, - merchant_id, - filters, - granularity, - time_range, - pool, - ) + /// # Panics + /// + /// Panics if `json_payload` array does not contain one `GetRefundMetricRequest` element. + pub async fn get_refunds_metrics( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json<[GetRefundMetricRequest; 1]>, + ) -> impl Responder { + #[allow(clippy::expect_used)] + // safety: This shouldn't panic owing to the data type + let payload = json_payload + .into_inner() + .to_vec() + .pop() + .expect("Couldn't get GetRefundMetricRequest"); + let flow = AnalyticsFlow::GetRefundsMetrics; + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth: AuthenticationData, req| async move { + analytics::refunds::get_metrics( + &state.pool, + &auth.merchant_account.merchant_id, + req, + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + + /// # Panics + /// + /// Panics if `json_payload` array does not contain one `GetSdkEventMetricRequest` element. + pub async fn get_sdk_event_metrics( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json<[GetSdkEventMetricRequest; 1]>, + ) -> impl Responder { + // safety: This shouldn't panic owing to the data type + #[allow(clippy::expect_used)] + let payload = json_payload + .into_inner() + .to_vec() + .pop() + .expect("Couldn't get GetSdkEventMetricRequest"); + let flow = AnalyticsFlow::GetSdkMetrics; + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth: AuthenticationData, req| async move { + analytics::sdk_events::get_metrics( + &state.pool, + auth.merchant_account.publishable_key.as_ref(), + req, + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + + pub async fn get_payment_filters( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + ) -> impl Responder { + let flow = AnalyticsFlow::GetPaymentFilters; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, req| async move { + analytics::payments::get_filters( + &state.pool, + req, + &auth.merchant_account.merchant_id, + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + + pub async fn get_refund_filters( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + ) -> impl Responder { + let flow = AnalyticsFlow::GetRefundFilters; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, req: GetRefundFilterRequest| async move { + analytics::refunds::get_filters( + &state.pool, + req, + &auth.merchant_account.merchant_id, + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + + pub async fn get_sdk_event_filters( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + ) -> impl Responder { + let flow = AnalyticsFlow::GetSdkEventFilters; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, req| async move { + analytics::sdk_events::get_filters( + &state.pool, + req, + auth.merchant_account.publishable_key.as_ref(), + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + + pub async fn get_api_events( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Query, + ) -> impl Responder { + let flow = AnalyticsFlow::GetApiEvents; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, req| async move { + api_events_core(&state.pool, req, auth.merchant_account.merchant_id) .await - } - } + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await } - pub async fn from_conf( - config: &AnalyticsConfig, - #[cfg(feature = "kms")] kms_client: &external_services::kms::KmsClient, - ) -> Self { - match config { - AnalyticsConfig::Sqlx { sqlx } => Self::Sqlx( - SqlxClient::from_conf( - sqlx, - #[cfg(feature = "kms")] - kms_client, + pub async fn get_sdk_events( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + ) -> impl Responder { + let flow = AnalyticsFlow::GetSdkEvents; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, req| async move { + sdk_events_core( + &state.pool, + req, + auth.merchant_account.publishable_key.unwrap_or_default(), ) - .await, - ), - } + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await } -} -#[derive(Clone, Debug, serde::Deserialize)] -#[serde(tag = "source")] -#[serde(rename_all = "lowercase")] -pub enum AnalyticsConfig { - Sqlx { sqlx: Database }, -} + pub async fn generate_refund_report( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + ) -> impl Responder { + let state_ref = &state; + let req_headers = &req.headers(); -impl Default for AnalyticsConfig { - fn default() -> Self { - Self::Sqlx { - sqlx: Database::default(), - } + let flow = AnalyticsFlow::GenerateRefundReport; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, payload| async move { + let jwt_payload = + auth::parse_jwt_payload::(req_headers, state_ref).await; + + let user_id = jwt_payload + .change_context(AnalyticsError::UnknownError)? + .user_id; + + let user = UserInterface::find_user_by_id(&*state.store, &user_id) + .await + .change_context(AnalyticsError::UnknownError)?; + + let user_email = UserEmail::from_pii_email(user.email) + .change_context(AnalyticsError::UnknownError)? + .get_secret(); + + let lambda_req = GenerateReportRequest { + request: payload, + merchant_id: auth.merchant_account.merchant_id.to_string(), + email: user_email, + }; + + let json_bytes = + serde_json::to_vec(&lambda_req).map_err(|_| AnalyticsError::UnknownError)?; + invoke_lambda( + &state.conf.report_download_config.refund_function, + &state.conf.report_download_config.region, + &json_bytes, + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + + pub async fn generate_dispute_report( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + ) -> impl Responder { + let state_ref = &state; + let req_headers = &req.headers(); + + let flow = AnalyticsFlow::GenerateDisputeReport; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, payload| async move { + let jwt_payload = + auth::parse_jwt_payload::(req_headers, state_ref).await; + + let user_id = jwt_payload + .change_context(AnalyticsError::UnknownError)? + .user_id; + + let user = UserInterface::find_user_by_id(&*state.store, &user_id) + .await + .change_context(AnalyticsError::UnknownError)?; + + let user_email = UserEmail::from_pii_email(user.email) + .change_context(AnalyticsError::UnknownError)? + .get_secret(); + + let lambda_req = GenerateReportRequest { + request: payload, + merchant_id: auth.merchant_account.merchant_id.to_string(), + email: user_email, + }; + + let json_bytes = + serde_json::to_vec(&lambda_req).map_err(|_| AnalyticsError::UnknownError)?; + invoke_lambda( + &state.conf.report_download_config.dispute_function, + &state.conf.report_download_config.region, + &json_bytes, + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + + pub async fn generate_payment_report( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + ) -> impl Responder { + let state_ref = &state; + let req_headers = &req.headers(); + + let flow = AnalyticsFlow::GeneratePaymentReport; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, payload| async move { + let jwt_payload = + auth::parse_jwt_payload::(req_headers, state_ref).await; + + let user_id = jwt_payload + .change_context(AnalyticsError::UnknownError)? + .user_id; + + let user = UserInterface::find_user_by_id(&*state.store, &user_id) + .await + .change_context(AnalyticsError::UnknownError)?; + + let user_email = UserEmail::from_pii_email(user.email) + .change_context(AnalyticsError::UnknownError)? + .get_secret(); + + let lambda_req = GenerateReportRequest { + request: payload, + merchant_id: auth.merchant_account.merchant_id.to_string(), + email: user_email, + }; + + let json_bytes = + serde_json::to_vec(&lambda_req).map_err(|_| AnalyticsError::UnknownError)?; + invoke_lambda( + &state.conf.report_download_config.payment_function, + &state.conf.report_download_config.region, + &json_bytes, + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + + /// # Panics + /// + /// Panics if `json_payload` array does not contain one `GetApiEventMetricRequest` element. + pub async fn get_api_events_metrics( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json<[GetApiEventMetricRequest; 1]>, + ) -> impl Responder { + // safety: This shouldn't panic owing to the data type + #[allow(clippy::expect_used)] + let payload = json_payload + .into_inner() + .to_vec() + .pop() + .expect("Couldn't get GetApiEventMetricRequest"); + let flow = AnalyticsFlow::GetApiEventMetrics; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + payload, + |state, auth: AuthenticationData, req| async move { + analytics::api_event::get_api_event_metrics( + &state.pool, + &auth.merchant_account.merchant_id, + req, + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + + pub async fn get_api_event_filters( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + ) -> impl Responder { + let flow = AnalyticsFlow::GetApiEventFilters; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, req| async move { + analytics::api_event::get_filters( + &state.pool, + req, + auth.merchant_account.merchant_id, + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await } } diff --git a/crates/router/src/analytics/core.rs b/crates/router/src/analytics/core.rs deleted file mode 100644 index bf124a6c0e85..000000000000 --- a/crates/router/src/analytics/core.rs +++ /dev/null @@ -1,96 +0,0 @@ -use api_models::analytics::{ - payments::PaymentDimensions, refunds::RefundDimensions, FilterValue, GetInfoResponse, - GetPaymentFiltersRequest, GetRefundFilterRequest, PaymentFiltersResponse, RefundFilterValue, - RefundFiltersResponse, -}; -use error_stack::ResultExt; - -use super::{ - errors::{self, AnalyticsError}, - payments::filters::{get_payment_filter_for_dimension, FilterRow}, - refunds::filters::{get_refund_filter_for_dimension, RefundFilterRow}, - types::AnalyticsDomain, - utils, AnalyticsProvider, -}; -use crate::{services::ApplicationResponse, types::domain}; - -pub type AnalyticsApiResponse = errors::AnalyticsResult>; - -pub async fn get_domain_info(domain: AnalyticsDomain) -> AnalyticsApiResponse { - let info = match domain { - AnalyticsDomain::Payments => GetInfoResponse { - metrics: utils::get_payment_metrics_info(), - download_dimensions: None, - dimensions: utils::get_payment_dimensions(), - }, - AnalyticsDomain::Refunds => GetInfoResponse { - metrics: utils::get_refund_metrics_info(), - download_dimensions: None, - dimensions: utils::get_refund_dimensions(), - }, - }; - Ok(ApplicationResponse::Json(info)) -} - -pub async fn payment_filters_core( - pool: AnalyticsProvider, - req: GetPaymentFiltersRequest, - merchant: domain::MerchantAccount, -) -> AnalyticsApiResponse { - let mut res = PaymentFiltersResponse::default(); - - for dim in req.group_by_names { - let values = match pool.clone() { - AnalyticsProvider::Sqlx(pool) => { - get_payment_filter_for_dimension(dim, &merchant.merchant_id, &req.time_range, &pool) - .await - } - } - .change_context(AnalyticsError::UnknownError)? - .into_iter() - .filter_map(|fil: FilterRow| match dim { - PaymentDimensions::Currency => fil.currency.map(|i| i.as_ref().to_string()), - PaymentDimensions::PaymentStatus => fil.status.map(|i| i.as_ref().to_string()), - PaymentDimensions::Connector => fil.connector, - PaymentDimensions::AuthType => fil.authentication_type.map(|i| i.as_ref().to_string()), - PaymentDimensions::PaymentMethod => fil.payment_method, - }) - .collect::>(); - res.query_data.push(FilterValue { - dimension: dim, - values, - }) - } - - Ok(ApplicationResponse::Json(res)) -} - -pub async fn refund_filter_core( - pool: AnalyticsProvider, - req: GetRefundFilterRequest, - merchant: domain::MerchantAccount, -) -> AnalyticsApiResponse { - let mut res = RefundFiltersResponse::default(); - for dim in req.group_by_names { - let values = match pool.clone() { - AnalyticsProvider::Sqlx(pool) => { - get_refund_filter_for_dimension(dim, &merchant.merchant_id, &req.time_range, &pool) - .await - } - } - .change_context(AnalyticsError::UnknownError)? - .into_iter() - .filter_map(|fil: RefundFilterRow| match dim { - RefundDimensions::Currency => fil.currency.map(|i| i.as_ref().to_string()), - RefundDimensions::RefundStatus => fil.refund_status.map(|i| i.as_ref().to_string()), - RefundDimensions::Connector => fil.connector, - RefundDimensions::RefundType => fil.refund_type.map(|i| i.as_ref().to_string()), - }) - .collect::>(); - res.query_data.push(RefundFilterValue { - dimension: dim, - values, - }) - } - Ok(ApplicationResponse::Json(res)) -} diff --git a/crates/router/src/analytics/payments.rs b/crates/router/src/analytics/payments.rs deleted file mode 100644 index 527bf75a3c72..000000000000 --- a/crates/router/src/analytics/payments.rs +++ /dev/null @@ -1,13 +0,0 @@ -pub mod accumulator; -mod core; -pub mod filters; -pub mod metrics; -pub mod types; -pub use accumulator::{PaymentMetricAccumulator, PaymentMetricsAccumulator}; - -pub trait PaymentAnalytics: - metrics::PaymentMetricAnalytics + filters::PaymentFilterAnalytics -{ -} - -pub use self::core::get_metrics; diff --git a/crates/router/src/analytics/payments/core.rs b/crates/router/src/analytics/payments/core.rs deleted file mode 100644 index 23eca8879a70..000000000000 --- a/crates/router/src/analytics/payments/core.rs +++ /dev/null @@ -1,129 +0,0 @@ -use std::collections::HashMap; - -use api_models::analytics::{ - payments::{MetricsBucketResponse, PaymentMetrics, PaymentMetricsBucketIdentifier}, - AnalyticsMetadata, GetPaymentMetricRequest, MetricsResponse, -}; -use error_stack::{IntoReport, ResultExt}; -use router_env::{ - instrument, logger, - tracing::{self, Instrument}, -}; - -use super::PaymentMetricsAccumulator; -use crate::{ - analytics::{ - core::AnalyticsApiResponse, errors::AnalyticsError, metrics, - payments::PaymentMetricAccumulator, AnalyticsProvider, - }, - services::ApplicationResponse, - types::domain, -}; - -#[instrument(skip_all)] -pub async fn get_metrics( - pool: AnalyticsProvider, - merchant_account: domain::MerchantAccount, - req: GetPaymentMetricRequest, -) -> AnalyticsApiResponse> { - let mut metrics_accumulator: HashMap< - PaymentMetricsBucketIdentifier, - PaymentMetricsAccumulator, - > = HashMap::new(); - - let mut set = tokio::task::JoinSet::new(); - for metric_type in req.metrics.iter().cloned() { - let req = req.clone(); - let merchant_id = merchant_account.merchant_id.clone(); - let pool = pool.clone(); - let task_span = tracing::debug_span!( - "analytics_payments_query", - payment_metric = metric_type.as_ref() - ); - set.spawn( - async move { - let data = pool - .get_payment_metrics( - &metric_type, - &req.group_by_names.clone(), - &merchant_id, - &req.filters, - &req.time_series.map(|t| t.granularity), - &req.time_range, - ) - .await - .change_context(AnalyticsError::UnknownError); - (metric_type, data) - } - .instrument(task_span), - ); - } - - while let Some((metric, data)) = set - .join_next() - .await - .transpose() - .into_report() - .change_context(AnalyticsError::UnknownError)? - { - let data = data?; - let attributes = &[ - metrics::request::add_attributes("metric_type", metric.to_string()), - metrics::request::add_attributes( - "source", - match pool { - crate::analytics::AnalyticsProvider::Sqlx(_) => "Sqlx", - }, - ), - ]; - - let value = u64::try_from(data.len()); - if let Ok(val) = value { - metrics::BUCKETS_FETCHED.record(&metrics::CONTEXT, val, attributes); - logger::debug!("Attributes: {:?}, Buckets fetched: {}", attributes, val); - } - - for (id, value) in data { - logger::debug!(bucket_id=?id, bucket_value=?value, "Bucket row for metric {metric}"); - let metrics_builder = metrics_accumulator.entry(id).or_default(); - match metric { - PaymentMetrics::PaymentSuccessRate => metrics_builder - .payment_success_rate - .add_metrics_bucket(&value), - PaymentMetrics::PaymentCount => { - metrics_builder.payment_count.add_metrics_bucket(&value) - } - PaymentMetrics::PaymentSuccessCount => { - metrics_builder.payment_success.add_metrics_bucket(&value) - } - PaymentMetrics::PaymentProcessedAmount => { - metrics_builder.processed_amount.add_metrics_bucket(&value) - } - PaymentMetrics::AvgTicketSize => { - metrics_builder.avg_ticket_size.add_metrics_bucket(&value) - } - } - } - - logger::debug!( - "Analytics Accumulated Results: metric: {}, results: {:#?}", - metric, - metrics_accumulator - ); - } - - let query_data: Vec = metrics_accumulator - .into_iter() - .map(|(id, val)| MetricsBucketResponse { - values: val.collect(), - dimensions: id, - }) - .collect(); - - Ok(ApplicationResponse::Json(MetricsResponse { - query_data, - meta_data: [AnalyticsMetadata { - current_time_range: req.time_range, - }], - })) -} diff --git a/crates/router/src/analytics/refunds/core.rs b/crates/router/src/analytics/refunds/core.rs deleted file mode 100644 index 4c2d2c394181..000000000000 --- a/crates/router/src/analytics/refunds/core.rs +++ /dev/null @@ -1,104 +0,0 @@ -use std::collections::HashMap; - -use api_models::analytics::{ - refunds::{RefundMetrics, RefundMetricsBucketIdentifier, RefundMetricsBucketResponse}, - AnalyticsMetadata, GetRefundMetricRequest, MetricsResponse, -}; -use error_stack::{IntoReport, ResultExt}; -use router_env::{ - logger, - tracing::{self, Instrument}, -}; - -use super::RefundMetricsAccumulator; -use crate::{ - analytics::{ - core::AnalyticsApiResponse, errors::AnalyticsError, refunds::RefundMetricAccumulator, - AnalyticsProvider, - }, - services::ApplicationResponse, - types::domain, -}; - -pub async fn get_metrics( - pool: AnalyticsProvider, - merchant_account: domain::MerchantAccount, - req: GetRefundMetricRequest, -) -> AnalyticsApiResponse> { - let mut metrics_accumulator: HashMap = - HashMap::new(); - let mut set = tokio::task::JoinSet::new(); - for metric_type in req.metrics.iter().cloned() { - let req = req.clone(); - let merchant_id = merchant_account.merchant_id.clone(); - let pool = pool.clone(); - let task_span = tracing::debug_span!( - "analytics_refund_query", - refund_metric = metric_type.as_ref() - ); - set.spawn( - async move { - let data = pool - .get_refund_metrics( - &metric_type, - &req.group_by_names.clone(), - &merchant_id, - &req.filters, - &req.time_series.map(|t| t.granularity), - &req.time_range, - ) - .await - .change_context(AnalyticsError::UnknownError); - (metric_type, data) - } - .instrument(task_span), - ); - } - - while let Some((metric, data)) = set - .join_next() - .await - .transpose() - .into_report() - .change_context(AnalyticsError::UnknownError)? - { - for (id, value) in data? { - logger::debug!(bucket_id=?id, bucket_value=?value, "Bucket row for metric {metric}"); - let metrics_builder = metrics_accumulator.entry(id).or_default(); - match metric { - RefundMetrics::RefundSuccessRate => metrics_builder - .refund_success_rate - .add_metrics_bucket(&value), - RefundMetrics::RefundCount => { - metrics_builder.refund_count.add_metrics_bucket(&value) - } - RefundMetrics::RefundSuccessCount => { - metrics_builder.refund_success.add_metrics_bucket(&value) - } - RefundMetrics::RefundProcessedAmount => { - metrics_builder.processed_amount.add_metrics_bucket(&value) - } - } - } - - logger::debug!( - "Analytics Accumulated Results: metric: {}, results: {:#?}", - metric, - metrics_accumulator - ); - } - let query_data: Vec = metrics_accumulator - .into_iter() - .map(|(id, val)| RefundMetricsBucketResponse { - values: val.collect(), - dimensions: id, - }) - .collect(); - - Ok(ApplicationResponse::Json(MetricsResponse { - query_data, - meta_data: [AnalyticsMetadata { - current_time_range: req.time_range, - }], - })) -} diff --git a/crates/router/src/analytics/routes.rs b/crates/router/src/analytics/routes.rs deleted file mode 100644 index 113312cdf10f..000000000000 --- a/crates/router/src/analytics/routes.rs +++ /dev/null @@ -1,164 +0,0 @@ -use actix_web::{web, Responder, Scope}; -use api_models::analytics::{ - GetPaymentFiltersRequest, GetPaymentMetricRequest, GetRefundFilterRequest, - GetRefundMetricRequest, -}; -use router_env::AnalyticsFlow; - -use super::{core::*, payments, refunds, types::AnalyticsDomain}; -use crate::{ - core::api_locking, - services::{ - api, authentication as auth, authentication::AuthenticationData, - authorization::permissions::Permission, - }, - AppState, -}; - -pub struct Analytics; - -impl Analytics { - pub fn server(state: AppState) -> Scope { - let route = web::scope("/analytics/v1").app_data(web::Data::new(state)); - route - .service(web::resource("metrics/payments").route(web::post().to(get_payment_metrics))) - .service(web::resource("metrics/refunds").route(web::post().to(get_refunds_metrics))) - .service(web::resource("filters/payments").route(web::post().to(get_payment_filters))) - .service(web::resource("filters/refunds").route(web::post().to(get_refund_filters))) - .service(web::resource("{domain}/info").route(web::get().to(get_info))) - } -} - -pub async fn get_info( - state: web::Data, - req: actix_web::HttpRequest, - domain: actix_web::web::Path, -) -> impl Responder { - let flow = AnalyticsFlow::GetInfo; - api::server_wrap( - flow, - state, - &req, - domain.into_inner(), - |_, _, domain| get_domain_info(domain), - &auth::NoAuth, - api_locking::LockAction::NotApplicable, - ) - .await -} - -/// # Panics -/// -/// Panics if `json_payload` array does not contain one `GetPaymentMetricRequest` element. -pub async fn get_payment_metrics( - state: web::Data, - req: actix_web::HttpRequest, - json_payload: web::Json<[GetPaymentMetricRequest; 1]>, -) -> impl Responder { - // safety: This shouldn't panic owing to the data type - #[allow(clippy::expect_used)] - let payload = json_payload - .into_inner() - .to_vec() - .pop() - .expect("Couldn't get GetPaymentMetricRequest"); - let flow = AnalyticsFlow::GetPaymentMetrics; - api::server_wrap( - flow, - state, - &req, - payload, - |state, auth: AuthenticationData, req| { - payments::get_metrics(state.pool.clone(), auth.merchant_account, req) - }, - auth::auth_type( - &auth::ApiKeyAuth, - &auth::JWTAuth(Permission::Analytics), - req.headers(), - ), - api_locking::LockAction::NotApplicable, - ) - .await -} - -/// # Panics -/// -/// Panics if `json_payload` array does not contain one `GetRefundMetricRequest` element. -pub async fn get_refunds_metrics( - state: web::Data, - req: actix_web::HttpRequest, - json_payload: web::Json<[GetRefundMetricRequest; 1]>, -) -> impl Responder { - #[allow(clippy::expect_used)] - // safety: This shouldn't panic owing to the data type - let payload = json_payload - .into_inner() - .to_vec() - .pop() - .expect("Couldn't get GetRefundMetricRequest"); - let flow = AnalyticsFlow::GetRefundsMetrics; - api::server_wrap( - flow, - state, - &req, - payload, - |state, auth: AuthenticationData, req| { - refunds::get_metrics(state.pool.clone(), auth.merchant_account, req) - }, - auth::auth_type( - &auth::ApiKeyAuth, - &auth::JWTAuth(Permission::Analytics), - req.headers(), - ), - api_locking::LockAction::NotApplicable, - ) - .await -} - -pub async fn get_payment_filters( - state: web::Data, - req: actix_web::HttpRequest, - json_payload: web::Json, -) -> impl Responder { - let flow = AnalyticsFlow::GetPaymentFilters; - api::server_wrap( - flow, - state, - &req, - json_payload.into_inner(), - |state, auth: AuthenticationData, req| { - payment_filters_core(state.pool.clone(), req, auth.merchant_account) - }, - auth::auth_type( - &auth::ApiKeyAuth, - &auth::JWTAuth(Permission::Analytics), - req.headers(), - ), - api_locking::LockAction::NotApplicable, - ) - .await -} - -pub async fn get_refund_filters( - state: web::Data, - req: actix_web::HttpRequest, - json_payload: web::Json, -) -> impl Responder { - let flow = AnalyticsFlow::GetRefundFilters; - api::server_wrap( - flow, - state, - &req, - json_payload.into_inner(), - |state, auth: AuthenticationData, req: GetRefundFilterRequest| { - refund_filter_core(state.pool.clone(), req, auth.merchant_account) - }, - auth::auth_type( - &auth::ApiKeyAuth, - &auth::JWTAuth(Permission::Analytics), - req.headers(), - ), - api_locking::LockAction::NotApplicable, - ) - .await -} diff --git a/crates/router/src/bin/scheduler.rs b/crates/router/src/bin/scheduler.rs index 4c19408582bc..32e9cfc6ca29 100644 --- a/crates/router/src/bin/scheduler.rs +++ b/crates/router/src/bin/scheduler.rs @@ -20,7 +20,6 @@ use strum::EnumString; use tokio::sync::{mpsc, oneshot}; const SCHEDULER_FLOW: &str = "SCHEDULER_FLOW"; - #[tokio::main] async fn main() -> CustomResult<(), ProcessTrackerError> { // console_subscriber::init(); @@ -30,7 +29,6 @@ async fn main() -> CustomResult<(), ProcessTrackerError> { #[allow(clippy::expect_used)] let conf = Settings::with_config_path(cmd_line.config_path) .expect("Unable to construct application configuration"); - let api_client = Box::new( services::ProxyClient::new( conf.proxy.clone(), diff --git a/crates/router/src/configs/kms.rs b/crates/router/src/configs/kms.rs index c2f159d16cf1..37f2d15774a5 100644 --- a/crates/router/src/configs/kms.rs +++ b/crates/router/src/configs/kms.rs @@ -63,7 +63,7 @@ impl KmsDecrypt for settings::Database { password: self.password.decrypt_inner(kms_client).await?.into(), pool_size: self.pool_size, connection_timeout: self.connection_timeout, - queue_strategy: self.queue_strategy.into(), + queue_strategy: self.queue_strategy, min_idle: self.min_idle, max_lifetime: self.max_lifetime, }) diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 918ae6647eef..f2d962b0abee 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -4,6 +4,8 @@ use std::{ str::FromStr, }; +#[cfg(feature = "olap")] +use analytics::ReportConfig; use api_models::{enums, payment_methods::RequiredFieldInfo}; use common_utils::ext_traits::ConfigExt; use config::{Environment, File}; @@ -16,12 +18,14 @@ pub use router_env::config::{Log, LogConsole, LogFile, LogTelemetry}; use rust_decimal::Decimal; use scheduler::SchedulerSettings; use serde::{de::Error, Deserialize, Deserializer}; +use storage_impl::config::QueueStrategy; #[cfg(feature = "olap")] use crate::analytics::AnalyticsConfig; use crate::{ core::errors::{ApplicationError, ApplicationResult}, env::{self, logger, Env}, + events::EventsConfig, }; #[cfg(feature = "kms")] pub type Password = kms::KmsValue; @@ -109,6 +113,9 @@ pub struct Settings { pub analytics: AnalyticsConfig, #[cfg(feature = "kv_store")] pub kv_config: KvConfig, + #[cfg(feature = "olap")] + pub report_download_config: ReportConfig, + pub events: EventsConfig, } #[derive(Debug, Deserialize, Clone)] @@ -521,23 +528,6 @@ pub struct Database { pub max_lifetime: Option, } -#[derive(Debug, Deserialize, Clone, Default)] -#[serde(rename_all = "PascalCase")] -pub enum QueueStrategy { - #[default] - Fifo, - Lifo, -} - -impl From for bb8::QueueStrategy { - fn from(value: QueueStrategy) -> Self { - match value { - QueueStrategy::Fifo => Self::Fifo, - QueueStrategy::Lifo => Self::Lifo, - } - } -} - #[cfg(not(feature = "kms"))] impl From for storage_impl::config::Database { fn from(val: Database) -> Self { @@ -837,6 +827,7 @@ impl Settings { #[cfg(feature = "s3")] self.file_upload_config.validate()?; self.lock_settings.validate()?; + self.events.validate()?; Ok(()) } } diff --git a/crates/router/src/core/refunds.rs b/crates/router/src/core/refunds.rs index 2d572cee9513..33435bb0ad96 100644 --- a/crates/router/src/core/refunds.rs +++ b/crates/router/src/core/refunds.rs @@ -927,7 +927,9 @@ pub async fn start_refund_workflow( ) -> Result<(), errors::ProcessTrackerError> { match refund_tracker.name.as_deref() { Some("EXECUTE_REFUND") => trigger_refund_execute_workflow(state, refund_tracker).await, - Some("SYNC_REFUND") => sync_refund_with_gateway_workflow(state, refund_tracker).await, + Some("SYNC_REFUND") => { + Box::pin(sync_refund_with_gateway_workflow(state, refund_tracker)).await + } _ => Err(errors::ProcessTrackerError::JobNotFound), } } diff --git a/crates/router/src/core/webhooks.rs b/crates/router/src/core/webhooks.rs index 67154ae33aef..be8d118a47c2 100644 --- a/crates/router/src/core/webhooks.rs +++ b/crates/router/src/core/webhooks.rs @@ -905,6 +905,7 @@ pub async fn webhooks_wrapper { diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs index 9687f7f97c92..549bda78eda8 100644 --- a/crates/router/src/db.rs +++ b/crates/router/src/db.rs @@ -12,6 +12,7 @@ pub mod events; pub mod file; pub mod fraud_check; pub mod gsm; +mod kafka_store; pub mod locker_mock_up; pub mod mandate; pub mod merchant_account; @@ -31,11 +32,24 @@ pub mod user_role; use data_models::payments::{ payment_attempt::PaymentAttemptInterface, payment_intent::PaymentIntentInterface, }; +use diesel_models::{ + fraud_check::{FraudCheck, FraudCheckNew, FraudCheckUpdate}, + organization::{Organization, OrganizationNew, OrganizationUpdate}, +}; +use error_stack::ResultExt; use masking::PeekInterface; use redis_interface::errors::RedisError; -use storage_impl::{redis::kv_store::RedisConnInterface, MockDb}; - -use crate::{errors::CustomResult, services::Store}; +use storage_impl::{errors::StorageError, redis::kv_store::RedisConnInterface, MockDb}; + +pub use self::kafka_store::KafkaStore; +use self::{fraud_check::FraudCheckInterface, organization::OrganizationInterface}; +pub use crate::{ + errors::CustomResult, + services::{ + kafka::{KafkaError, KafkaProducer, MQResult}, + Store, + }, +}; #[derive(PartialEq, Eq)] pub enum StorageImpl { @@ -58,7 +72,7 @@ pub trait StorageInterface: + ephemeral_key::EphemeralKeyInterface + events::EventInterface + file::FileMetadataInterface - + fraud_check::FraudCheckInterface + + FraudCheckInterface + locker_mock_up::LockerMockUpInterface + mandate::MandateInterface + merchant_account::MerchantAccountInterface @@ -79,7 +93,7 @@ pub trait StorageInterface: + RedisConnInterface + RequestIdStore + business_profile::BusinessProfileInterface - + organization::OrganizationInterface + + OrganizationInterface + routing_algorithm::RoutingAlgorithmInterface + gsm::GsmInterface + user::UserInterface @@ -151,7 +165,6 @@ where T: serde::de::DeserializeOwned, { use common_utils::ext_traits::ByteSliceExt; - use error_stack::ResultExt; let bytes = db.get_key(key).await?; bytes @@ -160,3 +173,72 @@ where } dyn_clone::clone_trait_object!(StorageInterface); + +impl RequestIdStore for KafkaStore { + fn add_request_id(&mut self, request_id: String) { + self.diesel_store.add_request_id(request_id) + } +} + +#[async_trait::async_trait] +impl FraudCheckInterface for KafkaStore { + async fn insert_fraud_check_response( + &self, + new: FraudCheckNew, + ) -> CustomResult { + self.diesel_store.insert_fraud_check_response(new).await + } + async fn update_fraud_check_response_with_attempt_id( + &self, + fraud_check: FraudCheck, + fraud_check_update: FraudCheckUpdate, + ) -> CustomResult { + self.diesel_store + .update_fraud_check_response_with_attempt_id(fraud_check, fraud_check_update) + .await + } + async fn find_fraud_check_by_payment_id( + &self, + payment_id: String, + merchant_id: String, + ) -> CustomResult { + self.diesel_store + .find_fraud_check_by_payment_id(payment_id, merchant_id) + .await + } + async fn find_fraud_check_by_payment_id_if_present( + &self, + payment_id: String, + merchant_id: String, + ) -> CustomResult, StorageError> { + self.diesel_store + .find_fraud_check_by_payment_id_if_present(payment_id, merchant_id) + .await + } +} + +#[async_trait::async_trait] +impl OrganizationInterface for KafkaStore { + async fn insert_organization( + &self, + organization: OrganizationNew, + ) -> CustomResult { + self.diesel_store.insert_organization(organization).await + } + async fn find_organization_by_org_id( + &self, + org_id: &str, + ) -> CustomResult { + self.diesel_store.find_organization_by_org_id(org_id).await + } + + async fn update_organization_by_org_id( + &self, + org_id: &str, + update: OrganizationUpdate, + ) -> CustomResult { + self.diesel_store + .update_organization_by_org_id(org_id, update) + .await + } +} diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs new file mode 100644 index 000000000000..9cf1a7b80b8b --- /dev/null +++ b/crates/router/src/db/kafka_store.rs @@ -0,0 +1,1917 @@ +use std::sync::Arc; + +use common_enums::enums::MerchantStorageScheme; +use common_utils::errors::CustomResult; +use data_models::payments::{ + payment_attempt::PaymentAttemptInterface, payment_intent::PaymentIntentInterface, +}; +use diesel_models::{ + enums::ProcessTrackerStatus, + ephemeral_key::{EphemeralKey, EphemeralKeyNew}, + reverse_lookup::{ReverseLookup, ReverseLookupNew}, + user_role as user_storage, +}; +use masking::Secret; +use redis_interface::{errors::RedisError, RedisConnectionPool, RedisEntryId}; +use router_env::logger; +use scheduler::{ + db::{process_tracker::ProcessTrackerInterface, queue::QueueInterface}, + SchedulerInterface, +}; +use storage_impl::redis::kv_store::RedisConnInterface; +use time::PrimitiveDateTime; + +use super::{user::UserInterface, user_role::UserRoleInterface}; +use crate::{ + core::errors::{self, ProcessTrackerError}, + db::{ + address::AddressInterface, + api_keys::ApiKeyInterface, + business_profile::BusinessProfileInterface, + capture::CaptureInterface, + cards_info::CardsInfoInterface, + configs::ConfigInterface, + customers::CustomerInterface, + dispute::DisputeInterface, + ephemeral_key::EphemeralKeyInterface, + events::EventInterface, + file::FileMetadataInterface, + gsm::GsmInterface, + locker_mock_up::LockerMockUpInterface, + mandate::MandateInterface, + merchant_account::MerchantAccountInterface, + merchant_connector_account::{ConnectorAccessToken, MerchantConnectorAccountInterface}, + merchant_key_store::MerchantKeyStoreInterface, + payment_link::PaymentLinkInterface, + payment_method::PaymentMethodInterface, + payout_attempt::PayoutAttemptInterface, + payouts::PayoutsInterface, + refund::RefundInterface, + reverse_lookup::ReverseLookupInterface, + routing_algorithm::RoutingAlgorithmInterface, + MasterKeyInterface, StorageInterface, + }, + services::{authentication, kafka::KafkaProducer, Store}, + types::{ + domain, + storage::{self, business_profile}, + AccessToken, + }, +}; + +#[derive(Clone)] +pub struct KafkaStore { + kafka_producer: KafkaProducer, + pub diesel_store: Store, +} + +impl KafkaStore { + pub async fn new(store: Store, kafka_producer: KafkaProducer) -> Self { + Self { + kafka_producer, + diesel_store: store, + } + } +} + +#[async_trait::async_trait] +impl AddressInterface for KafkaStore { + async fn find_address_by_address_id( + &self, + address_id: &str, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .find_address_by_address_id(address_id, key_store) + .await + } + + async fn update_address( + &self, + address_id: String, + address: storage::AddressUpdate, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .update_address(address_id, address, key_store) + .await + } + + async fn update_address_for_payments( + &self, + this: domain::Address, + address: domain::AddressUpdate, + payment_id: String, + key_store: &domain::MerchantKeyStore, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .update_address_for_payments(this, address, payment_id, key_store, storage_scheme) + .await + } + + async fn insert_address_for_payments( + &self, + payment_id: &str, + address: domain::Address, + key_store: &domain::MerchantKeyStore, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .insert_address_for_payments(payment_id, address, key_store, storage_scheme) + .await + } + + async fn find_address_by_merchant_id_payment_id_address_id( + &self, + merchant_id: &str, + payment_id: &str, + address_id: &str, + key_store: &domain::MerchantKeyStore, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_address_by_merchant_id_payment_id_address_id( + merchant_id, + payment_id, + address_id, + key_store, + storage_scheme, + ) + .await + } + + async fn insert_address_for_customers( + &self, + address: domain::Address, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .insert_address_for_customers(address, key_store) + .await + } + + async fn update_address_by_merchant_id_customer_id( + &self, + customer_id: &str, + merchant_id: &str, + address: storage::AddressUpdate, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .update_address_by_merchant_id_customer_id(customer_id, merchant_id, address, key_store) + .await + } +} + +#[async_trait::async_trait] +impl ApiKeyInterface for KafkaStore { + async fn insert_api_key( + &self, + api_key: storage::ApiKeyNew, + ) -> CustomResult { + self.diesel_store.insert_api_key(api_key).await + } + + async fn update_api_key( + &self, + merchant_id: String, + key_id: String, + api_key: storage::ApiKeyUpdate, + ) -> CustomResult { + self.diesel_store + .update_api_key(merchant_id, key_id, api_key) + .await + } + + async fn revoke_api_key( + &self, + merchant_id: &str, + key_id: &str, + ) -> CustomResult { + self.diesel_store.revoke_api_key(merchant_id, key_id).await + } + + async fn find_api_key_by_merchant_id_key_id_optional( + &self, + merchant_id: &str, + key_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_api_key_by_merchant_id_key_id_optional(merchant_id, key_id) + .await + } + + async fn find_api_key_by_hash_optional( + &self, + hashed_api_key: storage::HashedApiKey, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_api_key_by_hash_optional(hashed_api_key) + .await + } + + async fn list_api_keys_by_merchant_id( + &self, + merchant_id: &str, + limit: Option, + offset: Option, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .list_api_keys_by_merchant_id(merchant_id, limit, offset) + .await + } +} + +#[async_trait::async_trait] +impl CardsInfoInterface for KafkaStore { + async fn get_card_info( + &self, + card_iin: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store.get_card_info(card_iin).await + } +} + +#[async_trait::async_trait] +impl ConfigInterface for KafkaStore { + async fn insert_config( + &self, + config: storage::ConfigNew, + ) -> CustomResult { + self.diesel_store.insert_config(config).await + } + + async fn find_config_by_key( + &self, + key: &str, + ) -> CustomResult { + self.diesel_store.find_config_by_key(key).await + } + + async fn find_config_by_key_from_db( + &self, + key: &str, + ) -> CustomResult { + self.diesel_store.find_config_by_key_from_db(key).await + } + + async fn update_config_in_database( + &self, + key: &str, + config_update: storage::ConfigUpdate, + ) -> CustomResult { + self.diesel_store + .update_config_in_database(key, config_update) + .await + } + + async fn update_config_by_key( + &self, + key: &str, + config_update: storage::ConfigUpdate, + ) -> CustomResult { + self.diesel_store + .update_config_by_key(key, config_update) + .await + } + + async fn delete_config_by_key(&self, key: &str) -> CustomResult { + self.diesel_store.delete_config_by_key(key).await + } + + async fn find_config_by_key_unwrap_or( + &self, + key: &str, + default_config: Option, + ) -> CustomResult { + self.diesel_store + .find_config_by_key_unwrap_or(key, default_config) + .await + } +} + +#[async_trait::async_trait] +impl CustomerInterface for KafkaStore { + async fn delete_customer_by_customer_id_merchant_id( + &self, + customer_id: &str, + merchant_id: &str, + ) -> CustomResult { + self.diesel_store + .delete_customer_by_customer_id_merchant_id(customer_id, merchant_id) + .await + } + + async fn find_customer_optional_by_customer_id_merchant_id( + &self, + customer_id: &str, + merchant_id: &str, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_customer_optional_by_customer_id_merchant_id(customer_id, merchant_id, key_store) + .await + } + + async fn update_customer_by_customer_id_merchant_id( + &self, + customer_id: String, + merchant_id: String, + customer: storage::CustomerUpdate, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .update_customer_by_customer_id_merchant_id( + customer_id, + merchant_id, + customer, + key_store, + ) + .await + } + + async fn list_customers_by_merchant_id( + &self, + merchant_id: &str, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .list_customers_by_merchant_id(merchant_id, key_store) + .await + } + + async fn find_customer_by_customer_id_merchant_id( + &self, + customer_id: &str, + merchant_id: &str, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .find_customer_by_customer_id_merchant_id(customer_id, merchant_id, key_store) + .await + } + + async fn insert_customer( + &self, + customer_data: domain::Customer, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .insert_customer(customer_data, key_store) + .await + } +} + +#[async_trait::async_trait] +impl DisputeInterface for KafkaStore { + async fn insert_dispute( + &self, + dispute: storage::DisputeNew, + ) -> CustomResult { + self.diesel_store.insert_dispute(dispute).await + } + + async fn find_by_merchant_id_payment_id_connector_dispute_id( + &self, + merchant_id: &str, + payment_id: &str, + connector_dispute_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_by_merchant_id_payment_id_connector_dispute_id( + merchant_id, + payment_id, + connector_dispute_id, + ) + .await + } + + async fn find_dispute_by_merchant_id_dispute_id( + &self, + merchant_id: &str, + dispute_id: &str, + ) -> CustomResult { + self.diesel_store + .find_dispute_by_merchant_id_dispute_id(merchant_id, dispute_id) + .await + } + + async fn find_disputes_by_merchant_id( + &self, + merchant_id: &str, + dispute_constraints: api_models::disputes::DisputeListConstraints, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_disputes_by_merchant_id(merchant_id, dispute_constraints) + .await + } + + async fn update_dispute( + &self, + this: storage::Dispute, + dispute: storage::DisputeUpdate, + ) -> CustomResult { + self.diesel_store.update_dispute(this, dispute).await + } + + async fn find_disputes_by_merchant_id_payment_id( + &self, + merchant_id: &str, + payment_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_disputes_by_merchant_id_payment_id(merchant_id, payment_id) + .await + } +} + +#[async_trait::async_trait] +impl EphemeralKeyInterface for KafkaStore { + async fn create_ephemeral_key( + &self, + ek: EphemeralKeyNew, + validity: i64, + ) -> CustomResult { + self.diesel_store.create_ephemeral_key(ek, validity).await + } + async fn get_ephemeral_key( + &self, + key: &str, + ) -> CustomResult { + self.diesel_store.get_ephemeral_key(key).await + } + async fn delete_ephemeral_key( + &self, + id: &str, + ) -> CustomResult { + self.diesel_store.delete_ephemeral_key(id).await + } +} + +#[async_trait::async_trait] +impl EventInterface for KafkaStore { + async fn insert_event( + &self, + event: storage::EventNew, + ) -> CustomResult { + self.diesel_store.insert_event(event).await + } + + async fn update_event( + &self, + event_id: String, + event: storage::EventUpdate, + ) -> CustomResult { + self.diesel_store.update_event(event_id, event).await + } +} + +#[async_trait::async_trait] +impl LockerMockUpInterface for KafkaStore { + async fn find_locker_by_card_id( + &self, + card_id: &str, + ) -> CustomResult { + self.diesel_store.find_locker_by_card_id(card_id).await + } + + async fn insert_locker_mock_up( + &self, + new: storage::LockerMockUpNew, + ) -> CustomResult { + self.diesel_store.insert_locker_mock_up(new).await + } + + async fn delete_locker_mock_up( + &self, + card_id: &str, + ) -> CustomResult { + self.diesel_store.delete_locker_mock_up(card_id).await + } +} + +#[async_trait::async_trait] +impl MandateInterface for KafkaStore { + async fn find_mandate_by_merchant_id_mandate_id( + &self, + merchant_id: &str, + mandate_id: &str, + ) -> CustomResult { + self.diesel_store + .find_mandate_by_merchant_id_mandate_id(merchant_id, mandate_id) + .await + } + + async fn find_mandate_by_merchant_id_connector_mandate_id( + &self, + merchant_id: &str, + connector_mandate_id: &str, + ) -> CustomResult { + self.diesel_store + .find_mandate_by_merchant_id_connector_mandate_id(merchant_id, connector_mandate_id) + .await + } + + async fn find_mandate_by_merchant_id_customer_id( + &self, + merchant_id: &str, + customer_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_mandate_by_merchant_id_customer_id(merchant_id, customer_id) + .await + } + + async fn update_mandate_by_merchant_id_mandate_id( + &self, + merchant_id: &str, + mandate_id: &str, + mandate: storage::MandateUpdate, + ) -> CustomResult { + self.diesel_store + .update_mandate_by_merchant_id_mandate_id(merchant_id, mandate_id, mandate) + .await + } + + async fn find_mandates_by_merchant_id( + &self, + merchant_id: &str, + mandate_constraints: api_models::mandates::MandateListConstraints, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_mandates_by_merchant_id(merchant_id, mandate_constraints) + .await + } + + async fn insert_mandate( + &self, + mandate: storage::MandateNew, + ) -> CustomResult { + self.diesel_store.insert_mandate(mandate).await + } +} + +#[async_trait::async_trait] +impl PaymentLinkInterface for KafkaStore { + async fn find_payment_link_by_payment_link_id( + &self, + payment_link_id: &str, + ) -> CustomResult { + self.diesel_store + .find_payment_link_by_payment_link_id(payment_link_id) + .await + } + + async fn insert_payment_link( + &self, + payment_link_object: storage::PaymentLinkNew, + ) -> CustomResult { + self.diesel_store + .insert_payment_link(payment_link_object) + .await + } + + async fn list_payment_link_by_merchant_id( + &self, + merchant_id: &str, + payment_link_constraints: api_models::payments::PaymentLinkListConstraints, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .list_payment_link_by_merchant_id(merchant_id, payment_link_constraints) + .await + } +} + +#[async_trait::async_trait] +impl MerchantAccountInterface for KafkaStore { + async fn insert_merchant( + &self, + merchant_account: domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .insert_merchant(merchant_account, key_store) + .await + } + + async fn find_merchant_account_by_merchant_id( + &self, + merchant_id: &str, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .find_merchant_account_by_merchant_id(merchant_id, key_store) + .await + } + + async fn update_merchant( + &self, + this: domain::MerchantAccount, + merchant_account: storage::MerchantAccountUpdate, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .update_merchant(this, merchant_account, key_store) + .await + } + + async fn update_specific_fields_in_merchant( + &self, + merchant_id: &str, + merchant_account: storage::MerchantAccountUpdate, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .update_specific_fields_in_merchant(merchant_id, merchant_account, key_store) + .await + } + + async fn find_merchant_account_by_publishable_key( + &self, + publishable_key: &str, + ) -> CustomResult { + self.diesel_store + .find_merchant_account_by_publishable_key(publishable_key) + .await + } + + #[cfg(feature = "olap")] + async fn list_merchant_accounts_by_organization_id( + &self, + organization_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .list_merchant_accounts_by_organization_id(organization_id) + .await + } + + async fn delete_merchant_account_by_merchant_id( + &self, + merchant_id: &str, + ) -> CustomResult { + self.diesel_store + .delete_merchant_account_by_merchant_id(merchant_id) + .await + } +} + +#[async_trait::async_trait] +impl ConnectorAccessToken for KafkaStore { + async fn get_access_token( + &self, + merchant_id: &str, + connector_name: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .get_access_token(merchant_id, connector_name) + .await + } + + async fn set_access_token( + &self, + merchant_id: &str, + connector_name: &str, + access_token: AccessToken, + ) -> CustomResult<(), errors::StorageError> { + self.diesel_store + .set_access_token(merchant_id, connector_name, access_token) + .await + } +} + +#[async_trait::async_trait] +impl FileMetadataInterface for KafkaStore { + async fn insert_file_metadata( + &self, + file: storage::FileMetadataNew, + ) -> CustomResult { + self.diesel_store.insert_file_metadata(file).await + } + + async fn find_file_metadata_by_merchant_id_file_id( + &self, + merchant_id: &str, + file_id: &str, + ) -> CustomResult { + self.diesel_store + .find_file_metadata_by_merchant_id_file_id(merchant_id, file_id) + .await + } + + async fn delete_file_metadata_by_merchant_id_file_id( + &self, + merchant_id: &str, + file_id: &str, + ) -> CustomResult { + self.diesel_store + .delete_file_metadata_by_merchant_id_file_id(merchant_id, file_id) + .await + } + + async fn update_file_metadata( + &self, + this: storage::FileMetadata, + file_metadata: storage::FileMetadataUpdate, + ) -> CustomResult { + self.diesel_store + .update_file_metadata(this, file_metadata) + .await + } +} + +#[async_trait::async_trait] +impl MerchantConnectorAccountInterface for KafkaStore { + async fn find_merchant_connector_account_by_merchant_id_connector_label( + &self, + merchant_id: &str, + connector: &str, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .find_merchant_connector_account_by_merchant_id_connector_label( + merchant_id, + connector, + key_store, + ) + .await + } + + async fn find_merchant_connector_account_by_merchant_id_connector_name( + &self, + merchant_id: &str, + connector_name: &str, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_merchant_connector_account_by_merchant_id_connector_name( + merchant_id, + connector_name, + key_store, + ) + .await + } + + async fn find_merchant_connector_account_by_profile_id_connector_name( + &self, + profile_id: &str, + connector_name: &str, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .find_merchant_connector_account_by_profile_id_connector_name( + profile_id, + connector_name, + key_store, + ) + .await + } + + async fn insert_merchant_connector_account( + &self, + t: domain::MerchantConnectorAccount, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .insert_merchant_connector_account(t, key_store) + .await + } + + async fn find_by_merchant_connector_account_merchant_id_merchant_connector_id( + &self, + merchant_id: &str, + merchant_connector_id: &str, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .find_by_merchant_connector_account_merchant_id_merchant_connector_id( + merchant_id, + merchant_connector_id, + key_store, + ) + .await + } + + async fn find_merchant_connector_account_by_merchant_id_and_disabled_list( + &self, + merchant_id: &str, + get_disabled: bool, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_merchant_connector_account_by_merchant_id_and_disabled_list( + merchant_id, + get_disabled, + key_store, + ) + .await + } + + async fn update_merchant_connector_account( + &self, + this: domain::MerchantConnectorAccount, + merchant_connector_account: storage::MerchantConnectorAccountUpdateInternal, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .update_merchant_connector_account(this, merchant_connector_account, key_store) + .await + } + + async fn delete_merchant_connector_account_by_merchant_id_merchant_connector_id( + &self, + merchant_id: &str, + merchant_connector_id: &str, + ) -> CustomResult { + self.diesel_store + .delete_merchant_connector_account_by_merchant_id_merchant_connector_id( + merchant_id, + merchant_connector_id, + ) + .await + } +} + +#[async_trait::async_trait] +impl QueueInterface for KafkaStore { + async fn fetch_consumer_tasks( + &self, + stream_name: &str, + group_name: &str, + consumer_name: &str, + ) -> CustomResult, ProcessTrackerError> { + self.diesel_store + .fetch_consumer_tasks(stream_name, group_name, consumer_name) + .await + } + + async fn consumer_group_create( + &self, + stream: &str, + group: &str, + id: &RedisEntryId, + ) -> CustomResult<(), RedisError> { + self.diesel_store + .consumer_group_create(stream, group, id) + .await + } + + async fn acquire_pt_lock( + &self, + tag: &str, + lock_key: &str, + lock_val: &str, + ttl: i64, + ) -> CustomResult { + self.diesel_store + .acquire_pt_lock(tag, lock_key, lock_val, ttl) + .await + } + + async fn release_pt_lock(&self, tag: &str, lock_key: &str) -> CustomResult { + self.diesel_store.release_pt_lock(tag, lock_key).await + } + + async fn stream_append_entry( + &self, + stream: &str, + entry_id: &RedisEntryId, + fields: Vec<(&str, String)>, + ) -> CustomResult<(), RedisError> { + self.diesel_store + .stream_append_entry(stream, entry_id, fields) + .await + } + + async fn get_key(&self, key: &str) -> CustomResult, RedisError> { + self.diesel_store.get_key(key).await + } +} + +#[async_trait::async_trait] +impl PaymentAttemptInterface for KafkaStore { + async fn insert_payment_attempt( + &self, + payment_attempt: storage::PaymentAttemptNew, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + let attempt = self + .diesel_store + .insert_payment_attempt(payment_attempt, storage_scheme) + .await?; + + if let Err(er) = self + .kafka_producer + .log_payment_attempt(&attempt, None) + .await + { + logger::error!(message="Failed to log analytics event for payment attempt {attempt:?}", error_message=?er) + } + + Ok(attempt) + } + + async fn update_payment_attempt_with_attempt_id( + &self, + this: storage::PaymentAttempt, + payment_attempt: storage::PaymentAttemptUpdate, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + let attempt = self + .diesel_store + .update_payment_attempt_with_attempt_id(this.clone(), payment_attempt, storage_scheme) + .await?; + + if let Err(er) = self + .kafka_producer + .log_payment_attempt(&attempt, Some(this)) + .await + { + logger::error!(message="Failed to log analytics event for payment attempt {attempt:?}", error_message=?er) + } + + Ok(attempt) + } + + async fn find_payment_attempt_by_connector_transaction_id_payment_id_merchant_id( + &self, + connector_transaction_id: &str, + payment_id: &str, + merchant_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_payment_attempt_by_connector_transaction_id_payment_id_merchant_id( + connector_transaction_id, + payment_id, + merchant_id, + storage_scheme, + ) + .await + } + + async fn find_payment_attempt_by_merchant_id_connector_txn_id( + &self, + merchant_id: &str, + connector_txn_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_payment_attempt_by_merchant_id_connector_txn_id( + merchant_id, + connector_txn_id, + storage_scheme, + ) + .await + } + + async fn find_payment_attempt_by_payment_id_merchant_id_attempt_id( + &self, + payment_id: &str, + merchant_id: &str, + attempt_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_payment_attempt_by_payment_id_merchant_id_attempt_id( + payment_id, + merchant_id, + attempt_id, + storage_scheme, + ) + .await + } + + async fn find_payment_attempt_by_attempt_id_merchant_id( + &self, + attempt_id: &str, + merchant_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_payment_attempt_by_attempt_id_merchant_id(attempt_id, merchant_id, storage_scheme) + .await + } + + async fn find_payment_attempt_last_successful_attempt_by_payment_id_merchant_id( + &self, + payment_id: &str, + merchant_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_payment_attempt_last_successful_attempt_by_payment_id_merchant_id( + payment_id, + merchant_id, + storage_scheme, + ) + .await + } + + async fn find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + &self, + payment_id: &str, + merchant_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + payment_id, + merchant_id, + storage_scheme, + ) + .await + } + + async fn find_payment_attempt_by_preprocessing_id_merchant_id( + &self, + preprocessing_id: &str, + merchant_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_payment_attempt_by_preprocessing_id_merchant_id( + preprocessing_id, + merchant_id, + storage_scheme, + ) + .await + } + + async fn get_filters_for_payments( + &self, + pi: &[data_models::payments::PaymentIntent], + merchant_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult< + data_models::payments::payment_attempt::PaymentListFilters, + errors::DataStorageError, + > { + self.diesel_store + .get_filters_for_payments(pi, merchant_id, storage_scheme) + .await + } + + async fn get_total_count_of_filtered_payment_attempts( + &self, + merchant_id: &str, + active_attempt_ids: &[String], + connector: Option>, + payment_method: Option>, + payment_method_type: Option>, + authentication_type: Option>, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .get_total_count_of_filtered_payment_attempts( + merchant_id, + active_attempt_ids, + connector, + payment_method, + payment_method_type, + authentication_type, + storage_scheme, + ) + .await + } + + async fn find_attempts_by_merchant_id_payment_id( + &self, + merchant_id: &str, + payment_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult, errors::DataStorageError> { + self.diesel_store + .find_attempts_by_merchant_id_payment_id(merchant_id, payment_id, storage_scheme) + .await + } +} + +#[async_trait::async_trait] +impl PaymentIntentInterface for KafkaStore { + async fn update_payment_intent( + &self, + this: storage::PaymentIntent, + payment_intent: storage::PaymentIntentUpdate, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + let intent = self + .diesel_store + .update_payment_intent(this.clone(), payment_intent, storage_scheme) + .await?; + + if let Err(er) = self + .kafka_producer + .log_payment_intent(&intent, Some(this)) + .await + { + logger::error!(message="Failed to add analytics entry for Payment Intent {intent:?}", error_message=?er); + }; + + Ok(intent) + } + + async fn insert_payment_intent( + &self, + new: storage::PaymentIntentNew, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + logger::debug!("Inserting PaymentIntent Via KafkaStore"); + let intent = self + .diesel_store + .insert_payment_intent(new, storage_scheme) + .await?; + + if let Err(er) = self.kafka_producer.log_payment_intent(&intent, None).await { + logger::error!(message="Failed to add analytics entry for Payment Intent {intent:?}", error_message=?er); + }; + + Ok(intent) + } + + async fn find_payment_intent_by_payment_id_merchant_id( + &self, + payment_id: &str, + merchant_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_payment_intent_by_payment_id_merchant_id(payment_id, merchant_id, storage_scheme) + .await + } + + #[cfg(feature = "olap")] + async fn filter_payment_intent_by_constraints( + &self, + merchant_id: &str, + filters: &data_models::payments::payment_intent::PaymentIntentFetchConstraints, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult, errors::DataStorageError> { + self.diesel_store + .filter_payment_intent_by_constraints(merchant_id, filters, storage_scheme) + .await + } + + #[cfg(feature = "olap")] + async fn filter_payment_intents_by_time_range_constraints( + &self, + merchant_id: &str, + time_range: &api_models::payments::TimeRange, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult, errors::DataStorageError> { + self.diesel_store + .filter_payment_intents_by_time_range_constraints( + merchant_id, + time_range, + storage_scheme, + ) + .await + } + + #[cfg(feature = "olap")] + async fn get_filtered_payment_intents_attempt( + &self, + merchant_id: &str, + constraints: &data_models::payments::payment_intent::PaymentIntentFetchConstraints, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult< + Vec<( + data_models::payments::PaymentIntent, + data_models::payments::payment_attempt::PaymentAttempt, + )>, + errors::DataStorageError, + > { + self.diesel_store + .get_filtered_payment_intents_attempt(merchant_id, constraints, storage_scheme) + .await + } + + #[cfg(feature = "olap")] + async fn get_filtered_active_attempt_ids_for_total_count( + &self, + merchant_id: &str, + constraints: &data_models::payments::payment_intent::PaymentIntentFetchConstraints, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult, errors::DataStorageError> { + self.diesel_store + .get_filtered_active_attempt_ids_for_total_count( + merchant_id, + constraints, + storage_scheme, + ) + .await + } + + async fn get_active_payment_attempt( + &self, + payment: &mut storage::PaymentIntent, + storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result { + self.diesel_store + .get_active_payment_attempt(payment, storage_scheme) + .await + } +} + +#[async_trait::async_trait] +impl PaymentMethodInterface for KafkaStore { + async fn find_payment_method( + &self, + payment_method_id: &str, + ) -> CustomResult { + self.diesel_store + .find_payment_method(payment_method_id) + .await + } + + async fn find_payment_method_by_customer_id_merchant_id_list( + &self, + customer_id: &str, + merchant_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_payment_method_by_customer_id_merchant_id_list(customer_id, merchant_id) + .await + } + + async fn insert_payment_method( + &self, + m: storage::PaymentMethodNew, + ) -> CustomResult { + self.diesel_store.insert_payment_method(m).await + } + + async fn update_payment_method( + &self, + payment_method: storage::PaymentMethod, + payment_method_update: storage::PaymentMethodUpdate, + ) -> CustomResult { + self.diesel_store + .update_payment_method(payment_method, payment_method_update) + .await + } + + async fn delete_payment_method_by_merchant_id_payment_method_id( + &self, + merchant_id: &str, + payment_method_id: &str, + ) -> CustomResult { + self.diesel_store + .delete_payment_method_by_merchant_id_payment_method_id(merchant_id, payment_method_id) + .await + } +} + +#[async_trait::async_trait] +impl PayoutAttemptInterface for KafkaStore { + async fn find_payout_attempt_by_merchant_id_payout_id( + &self, + merchant_id: &str, + payout_id: &str, + ) -> CustomResult { + self.diesel_store + .find_payout_attempt_by_merchant_id_payout_id(merchant_id, payout_id) + .await + } + + async fn update_payout_attempt_by_merchant_id_payout_id( + &self, + merchant_id: &str, + payout_id: &str, + payout: storage::PayoutAttemptUpdate, + ) -> CustomResult { + self.diesel_store + .update_payout_attempt_by_merchant_id_payout_id(merchant_id, payout_id, payout) + .await + } + + async fn insert_payout_attempt( + &self, + payout: storage::PayoutAttemptNew, + ) -> CustomResult { + self.diesel_store.insert_payout_attempt(payout).await + } +} + +#[async_trait::async_trait] +impl PayoutsInterface for KafkaStore { + async fn find_payout_by_merchant_id_payout_id( + &self, + merchant_id: &str, + payout_id: &str, + ) -> CustomResult { + self.diesel_store + .find_payout_by_merchant_id_payout_id(merchant_id, payout_id) + .await + } + + async fn update_payout_by_merchant_id_payout_id( + &self, + merchant_id: &str, + payout_id: &str, + payout: storage::PayoutsUpdate, + ) -> CustomResult { + self.diesel_store + .update_payout_by_merchant_id_payout_id(merchant_id, payout_id, payout) + .await + } + + async fn insert_payout( + &self, + payout: storage::PayoutsNew, + ) -> CustomResult { + self.diesel_store.insert_payout(payout).await + } +} + +#[async_trait::async_trait] +impl ProcessTrackerInterface for KafkaStore { + async fn reinitialize_limbo_processes( + &self, + ids: Vec, + schedule_time: PrimitiveDateTime, + ) -> CustomResult { + self.diesel_store + .reinitialize_limbo_processes(ids, schedule_time) + .await + } + + async fn find_process_by_id( + &self, + id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store.find_process_by_id(id).await + } + + async fn update_process( + &self, + this: storage::ProcessTracker, + process: storage::ProcessTrackerUpdate, + ) -> CustomResult { + self.diesel_store.update_process(this, process).await + } + + async fn process_tracker_update_process_status_by_ids( + &self, + task_ids: Vec, + task_update: storage::ProcessTrackerUpdate, + ) -> CustomResult { + self.diesel_store + .process_tracker_update_process_status_by_ids(task_ids, task_update) + .await + } + async fn update_process_tracker( + &self, + this: storage::ProcessTracker, + process: storage::ProcessTrackerUpdate, + ) -> CustomResult { + self.diesel_store + .update_process_tracker(this, process) + .await + } + + async fn insert_process( + &self, + new: storage::ProcessTrackerNew, + ) -> CustomResult { + self.diesel_store.insert_process(new).await + } + + async fn find_processes_by_time_status( + &self, + time_lower_limit: PrimitiveDateTime, + time_upper_limit: PrimitiveDateTime, + status: ProcessTrackerStatus, + limit: Option, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_processes_by_time_status(time_lower_limit, time_upper_limit, status, limit) + .await + } +} + +#[async_trait::async_trait] +impl CaptureInterface for KafkaStore { + async fn insert_capture( + &self, + capture: storage::CaptureNew, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .insert_capture(capture, storage_scheme) + .await + } + + async fn update_capture_with_capture_id( + &self, + this: storage::Capture, + capture: storage::CaptureUpdate, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .update_capture_with_capture_id(this, capture, storage_scheme) + .await + } + + async fn find_all_captures_by_merchant_id_payment_id_authorized_attempt_id( + &self, + merchant_id: &str, + payment_id: &str, + authorized_attempt_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_all_captures_by_merchant_id_payment_id_authorized_attempt_id( + merchant_id, + payment_id, + authorized_attempt_id, + storage_scheme, + ) + .await + } +} + +#[async_trait::async_trait] +impl RefundInterface for KafkaStore { + async fn find_refund_by_internal_reference_id_merchant_id( + &self, + internal_reference_id: &str, + merchant_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_refund_by_internal_reference_id_merchant_id( + internal_reference_id, + merchant_id, + storage_scheme, + ) + .await + } + + async fn find_refund_by_payment_id_merchant_id( + &self, + payment_id: &str, + merchant_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_refund_by_payment_id_merchant_id(payment_id, merchant_id, storage_scheme) + .await + } + + async fn find_refund_by_merchant_id_refund_id( + &self, + merchant_id: &str, + refund_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_refund_by_merchant_id_refund_id(merchant_id, refund_id, storage_scheme) + .await + } + + async fn find_refund_by_merchant_id_connector_refund_id_connector( + &self, + merchant_id: &str, + connector_refund_id: &str, + connector: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_refund_by_merchant_id_connector_refund_id_connector( + merchant_id, + connector_refund_id, + connector, + storage_scheme, + ) + .await + } + + async fn update_refund( + &self, + this: storage::Refund, + refund: storage::RefundUpdate, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + let refund = self + .diesel_store + .update_refund(this.clone(), refund, storage_scheme) + .await?; + + if let Err(er) = self.kafka_producer.log_refund(&refund, Some(this)).await { + logger::error!(message="Failed to insert analytics event for Refund Update {refund?}", error_message=?er); + } + Ok(refund) + } + + async fn find_refund_by_merchant_id_connector_transaction_id( + &self, + merchant_id: &str, + connector_transaction_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_refund_by_merchant_id_connector_transaction_id( + merchant_id, + connector_transaction_id, + storage_scheme, + ) + .await + } + + async fn insert_refund( + &self, + new: storage::RefundNew, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + let refund = self.diesel_store.insert_refund(new, storage_scheme).await?; + + if let Err(er) = self.kafka_producer.log_refund(&refund, None).await { + logger::error!(message="Failed to insert analytics event for Refund Create {refund?}", error_message=?er); + } + Ok(refund) + } + + #[cfg(feature = "olap")] + async fn filter_refund_by_constraints( + &self, + merchant_id: &str, + refund_details: &api_models::refunds::RefundListRequest, + storage_scheme: MerchantStorageScheme, + limit: i64, + offset: i64, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .filter_refund_by_constraints( + merchant_id, + refund_details, + storage_scheme, + limit, + offset, + ) + .await + } + + #[cfg(feature = "olap")] + async fn filter_refund_by_meta_constraints( + &self, + merchant_id: &str, + refund_details: &api_models::payments::TimeRange, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .filter_refund_by_meta_constraints(merchant_id, refund_details, storage_scheme) + .await + } + + #[cfg(feature = "olap")] + async fn get_total_count_of_refunds( + &self, + merchant_id: &str, + refund_details: &api_models::refunds::RefundListRequest, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .get_total_count_of_refunds(merchant_id, refund_details, storage_scheme) + .await + } +} + +#[async_trait::async_trait] +impl MerchantKeyStoreInterface for KafkaStore { + async fn insert_merchant_key_store( + &self, + merchant_key_store: domain::MerchantKeyStore, + key: &Secret>, + ) -> CustomResult { + self.diesel_store + .insert_merchant_key_store(merchant_key_store, key) + .await + } + + async fn get_merchant_key_store_by_merchant_id( + &self, + merchant_id: &str, + key: &Secret>, + ) -> CustomResult { + self.diesel_store + .get_merchant_key_store_by_merchant_id(merchant_id, key) + .await + } + + async fn delete_merchant_key_store_by_merchant_id( + &self, + merchant_id: &str, + ) -> CustomResult { + self.diesel_store + .delete_merchant_key_store_by_merchant_id(merchant_id) + .await + } +} + +#[async_trait::async_trait] +impl BusinessProfileInterface for KafkaStore { + async fn insert_business_profile( + &self, + business_profile: business_profile::BusinessProfileNew, + ) -> CustomResult { + self.diesel_store + .insert_business_profile(business_profile) + .await + } + + async fn find_business_profile_by_profile_id( + &self, + profile_id: &str, + ) -> CustomResult { + self.diesel_store + .find_business_profile_by_profile_id(profile_id) + .await + } + + async fn update_business_profile_by_profile_id( + &self, + current_state: business_profile::BusinessProfile, + business_profile_update: business_profile::BusinessProfileUpdateInternal, + ) -> CustomResult { + self.diesel_store + .update_business_profile_by_profile_id(current_state, business_profile_update) + .await + } + + async fn delete_business_profile_by_profile_id_merchant_id( + &self, + profile_id: &str, + merchant_id: &str, + ) -> CustomResult { + self.diesel_store + .delete_business_profile_by_profile_id_merchant_id(profile_id, merchant_id) + .await + } + + async fn list_business_profile_by_merchant_id( + &self, + merchant_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .list_business_profile_by_merchant_id(merchant_id) + .await + } + + async fn find_business_profile_by_profile_name_merchant_id( + &self, + profile_name: &str, + merchant_id: &str, + ) -> CustomResult { + self.diesel_store + .find_business_profile_by_profile_name_merchant_id(profile_name, merchant_id) + .await + } +} + +#[async_trait::async_trait] +impl ReverseLookupInterface for KafkaStore { + async fn insert_reverse_lookup( + &self, + new: ReverseLookupNew, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .insert_reverse_lookup(new, storage_scheme) + .await + } + + async fn get_lookup_by_lookup_id( + &self, + id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .get_lookup_by_lookup_id(id, storage_scheme) + .await + } +} + +#[async_trait::async_trait] +impl RoutingAlgorithmInterface for KafkaStore { + async fn insert_routing_algorithm( + &self, + routing_algorithm: storage::RoutingAlgorithm, + ) -> CustomResult { + self.diesel_store + .insert_routing_algorithm(routing_algorithm) + .await + } + + async fn find_routing_algorithm_by_profile_id_algorithm_id( + &self, + profile_id: &str, + algorithm_id: &str, + ) -> CustomResult { + self.diesel_store + .find_routing_algorithm_by_profile_id_algorithm_id(profile_id, algorithm_id) + .await + } + + async fn find_routing_algorithm_by_algorithm_id_merchant_id( + &self, + algorithm_id: &str, + merchant_id: &str, + ) -> CustomResult { + self.diesel_store + .find_routing_algorithm_by_algorithm_id_merchant_id(algorithm_id, merchant_id) + .await + } + + async fn find_routing_algorithm_metadata_by_algorithm_id_profile_id( + &self, + algorithm_id: &str, + profile_id: &str, + ) -> CustomResult { + self.diesel_store + .find_routing_algorithm_metadata_by_algorithm_id_profile_id(algorithm_id, profile_id) + .await + } + + async fn list_routing_algorithm_metadata_by_profile_id( + &self, + profile_id: &str, + limit: i64, + offset: i64, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .list_routing_algorithm_metadata_by_profile_id(profile_id, limit, offset) + .await + } + + async fn list_routing_algorithm_metadata_by_merchant_id( + &self, + merchant_id: &str, + limit: i64, + offset: i64, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .list_routing_algorithm_metadata_by_merchant_id(merchant_id, limit, offset) + .await + } +} + +#[async_trait::async_trait] +impl GsmInterface for KafkaStore { + async fn add_gsm_rule( + &self, + rule: storage::GatewayStatusMappingNew, + ) -> CustomResult { + self.diesel_store.add_gsm_rule(rule).await + } + + async fn find_gsm_decision( + &self, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + ) -> CustomResult { + self.diesel_store + .find_gsm_decision(connector, flow, sub_flow, code, message) + .await + } + + async fn find_gsm_rule( + &self, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + ) -> CustomResult { + self.diesel_store + .find_gsm_rule(connector, flow, sub_flow, code, message) + .await + } + + async fn update_gsm_rule( + &self, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + data: storage::GatewayStatusMappingUpdate, + ) -> CustomResult { + self.diesel_store + .update_gsm_rule(connector, flow, sub_flow, code, message, data) + .await + } + + async fn delete_gsm_rule( + &self, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + ) -> CustomResult { + self.diesel_store + .delete_gsm_rule(connector, flow, sub_flow, code, message) + .await + } +} + +#[async_trait::async_trait] +impl StorageInterface for KafkaStore { + fn get_scheduler_db(&self) -> Box { + Box::new(self.clone()) + } +} + +#[async_trait::async_trait] +impl SchedulerInterface for KafkaStore {} + +impl MasterKeyInterface for KafkaStore { + fn get_master_key(&self) -> &[u8] { + self.diesel_store.get_master_key() + } +} +#[async_trait::async_trait] +impl UserInterface for KafkaStore { + async fn insert_user( + &self, + user_data: storage::UserNew, + ) -> CustomResult { + self.diesel_store.insert_user(user_data).await + } + + async fn find_user_by_email( + &self, + user_email: &str, + ) -> CustomResult { + self.diesel_store.find_user_by_email(user_email).await + } + + async fn find_user_by_id( + &self, + user_id: &str, + ) -> CustomResult { + self.diesel_store.find_user_by_id(user_id).await + } + + async fn update_user_by_user_id( + &self, + user_id: &str, + user: storage::UserUpdate, + ) -> CustomResult { + self.diesel_store + .update_user_by_user_id(user_id, user) + .await + } + + async fn delete_user_by_user_id( + &self, + user_id: &str, + ) -> CustomResult { + self.diesel_store.delete_user_by_user_id(user_id).await + } +} + +impl RedisConnInterface for KafkaStore { + fn get_redis_conn(&self) -> CustomResult, RedisError> { + self.diesel_store.get_redis_conn() + } +} + +#[async_trait::async_trait] +impl UserRoleInterface for KafkaStore { + async fn insert_user_role( + &self, + user_role: user_storage::UserRoleNew, + ) -> CustomResult { + self.diesel_store.insert_user_role(user_role).await + } + async fn find_user_role_by_user_id( + &self, + user_id: &str, + ) -> CustomResult { + self.diesel_store.find_user_role_by_user_id(user_id).await + } + async fn update_user_role_by_user_id_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + update: user_storage::UserRoleUpdate, + ) -> CustomResult { + self.diesel_store + .update_user_role_by_user_id_merchant_id(user_id, merchant_id, update) + .await + } + async fn delete_user_role(&self, user_id: &str) -> CustomResult { + self.diesel_store.delete_user_role(user_id).await + } + async fn list_user_roles_by_user_id( + &self, + user_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store.list_user_roles_by_user_id(user_id).await + } +} diff --git a/crates/router/src/events.rs b/crates/router/src/events.rs index 39a8543a68c4..8f980fee504a 100644 --- a/crates/router/src/events.rs +++ b/crates/router/src/events.rs @@ -1,15 +1,21 @@ -use serde::Serialize; +use data_models::errors::{StorageError, StorageResult}; +use error_stack::ResultExt; +use serde::{Deserialize, Serialize}; +use storage_impl::errors::ApplicationError; + +use crate::{db::KafkaProducer, services::kafka::KafkaSettings}; pub mod api_logs; pub mod event_logger; +pub mod kafka_handler; -pub trait EventHandler: Sync + Send + dyn_clone::DynClone { +pub(super) trait EventHandler: Sync + Send + dyn_clone::DynClone { fn log_event(&self, event: RawEvent); } dyn_clone::clone_trait_object!(EventHandler); -#[derive(Debug)] +#[derive(Debug, Serialize)] pub struct RawEvent { pub event_type: EventType, pub key: String, @@ -24,3 +30,55 @@ pub enum EventType { Refund, ApiLogs, } + +#[derive(Debug, Default, Deserialize, Clone)] +#[serde(tag = "source")] +#[serde(rename_all = "lowercase")] +pub enum EventsConfig { + Kafka { + kafka: KafkaSettings, + }, + #[default] + Logs, +} + +#[derive(Debug, Clone)] +pub enum EventsHandler { + Kafka(KafkaProducer), + Logs(event_logger::EventLogger), +} + +impl Default for EventsHandler { + fn default() -> Self { + Self::Logs(event_logger::EventLogger {}) + } +} + +impl EventsConfig { + pub async fn get_event_handler(&self) -> StorageResult { + Ok(match self { + Self::Kafka { kafka } => EventsHandler::Kafka( + KafkaProducer::create(kafka) + .await + .change_context(StorageError::InitializationError)?, + ), + Self::Logs => EventsHandler::Logs(event_logger::EventLogger::default()), + }) + } + + pub fn validate(&self) -> Result<(), ApplicationError> { + match self { + Self::Kafka { kafka } => kafka.validate(), + Self::Logs => Ok(()), + } + } +} + +impl EventsHandler { + pub fn log_event(&self, event: RawEvent) { + match self { + Self::Kafka(kafka) => kafka.log_event(event), + Self::Logs(logger) => logger.log_event(event), + } + } +} diff --git a/crates/router/src/events/api_logs.rs b/crates/router/src/events/api_logs.rs index 3f598e88394b..bfc10f722c1f 100644 --- a/crates/router/src/events/api_logs.rs +++ b/crates/router/src/events/api_logs.rs @@ -24,6 +24,7 @@ use crate::{ #[derive(Clone, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] pub struct ApiEvent { + merchant_id: Option, api_flow: String, created_at_timestamp: i128, request_id: String, @@ -40,11 +41,13 @@ pub struct ApiEvent { #[serde(flatten)] event_type: ApiEventsType, hs_latency: Option, + http_method: Option, } impl ApiEvent { #[allow(clippy::too_many_arguments)] pub fn new( + merchant_id: Option, api_flow: &impl FlowMetric, request_id: &RequestId, latency: u128, @@ -56,8 +59,10 @@ impl ApiEvent { error: Option, event_type: ApiEventsType, http_req: &HttpRequest, + http_method: Option, ) -> Self { Self { + merchant_id, api_flow: api_flow.to_string(), created_at_timestamp: OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000_000, request_id: request_id.as_hyphenated().to_string(), @@ -78,6 +83,7 @@ impl ApiEvent { url_path: http_req.path().to_string(), event_type, hs_latency, + http_method, } } } diff --git a/crates/router/src/events/event_logger.rs b/crates/router/src/events/event_logger.rs index fda9b1a036ae..1bd75341be4a 100644 --- a/crates/router/src/events/event_logger.rs +++ b/crates/router/src/events/event_logger.rs @@ -7,6 +7,6 @@ pub struct EventLogger {} impl EventHandler for EventLogger { #[track_caller] fn log_event(&self, event: RawEvent) { - logger::info!(event = ?serde_json::to_string(&event.payload).unwrap_or(r#"{ "error": "Serialization failed" }"#.to_string()), event_type =? event.event_type, event_id =? event.key, log_type = "event"); + logger::info!(event = ?event.payload.to_string(), event_type =? event.event_type, event_id =? event.key, log_type = "event"); } } diff --git a/crates/router/src/events/kafka_handler.rs b/crates/router/src/events/kafka_handler.rs new file mode 100644 index 000000000000..d55847e6e8e7 --- /dev/null +++ b/crates/router/src/events/kafka_handler.rs @@ -0,0 +1,29 @@ +use error_stack::{IntoReport, ResultExt}; +use router_env::tracing; + +use super::{EventHandler, RawEvent}; +use crate::{ + db::MQResult, + services::kafka::{KafkaError, KafkaMessage, KafkaProducer}, +}; +impl EventHandler for KafkaProducer { + fn log_event(&self, event: RawEvent) { + let topic = self.get_topic(event.event_type); + if let Err(er) = self.log_kafka_event(topic, &event) { + tracing::error!("Failed to log event to kafka: {:?}", er); + } + } +} + +impl KafkaMessage for RawEvent { + fn key(&self) -> String { + self.key.clone() + } + + fn value(&self) -> MQResult> { + // Add better error logging here + serde_json::to_vec(&self.payload) + .into_report() + .change_context(KafkaError::GenericError) + } +} diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 2b1f9c692d86..035314f71dfb 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -1,8 +1,6 @@ #![forbid(unsafe_code)] #![recursion_limit = "256"] -#[cfg(feature = "olap")] -pub mod analytics; #[cfg(feature = "stripe")] pub mod compatibility; pub mod configs; @@ -17,6 +15,8 @@ pub(crate) mod macros; pub mod routes; pub mod workflows; +#[cfg(feature = "olap")] +pub mod analytics; pub mod events; pub mod middleware; pub mod openapi; @@ -35,10 +35,7 @@ use storage_impl::errors::ApplicationResult; use tokio::sync::{mpsc, oneshot}; pub use self::env::logger; -use crate::{ - configs::settings, - core::errors::{self}, -}; +use crate::{configs::settings, core::errors}; #[cfg(feature = "mimalloc")] #[global_allocator] diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 1a6f36363d1d..80993429c4e2 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -33,7 +33,7 @@ use super::{ephemeral_key::*, payment_methods::*, webhooks::*}; pub use crate::{ configs::settings, db::{StorageImpl, StorageInterface}, - events::{event_logger::EventLogger, EventHandler}, + events::EventsHandler, routes::cards_info::card_iin_info, services::get_store, }; @@ -43,7 +43,7 @@ pub struct AppState { pub flow_name: String, pub store: Box, pub conf: Arc, - pub event_handler: Box, + pub event_handler: EventsHandler, #[cfg(feature = "email")] pub email_client: Arc, #[cfg(feature = "kms")] @@ -62,7 +62,7 @@ impl scheduler::SchedulerAppState for AppState { pub trait AppStateInfo { fn conf(&self) -> settings::Settings; fn store(&self) -> Box; - fn event_handler(&self) -> Box; + fn event_handler(&self) -> EventsHandler; #[cfg(feature = "email")] fn email_client(&self) -> Arc; fn add_request_id(&mut self, request_id: RequestId); @@ -82,8 +82,8 @@ impl AppStateInfo for AppState { fn email_client(&self) -> Arc { self.email_client.to_owned() } - fn event_handler(&self) -> Box { - self.event_handler.to_owned() + fn event_handler(&self) -> EventsHandler { + self.event_handler.clone() } fn add_request_id(&mut self, request_id: RequestId) { self.api_client.add_request_id(request_id); @@ -130,13 +130,31 @@ impl AppState { #[cfg(feature = "kms")] let kms_client = kms::get_kms_client(&conf.kms).await; let testable = storage_impl == StorageImpl::PostgresqlTest; + #[allow(clippy::expect_used)] + let event_handler = conf + .events + .get_event_handler() + .await + .expect("Failed to create event handler"); let store: Box = match storage_impl { - StorageImpl::Postgresql | StorageImpl::PostgresqlTest => Box::new( - #[allow(clippy::expect_used)] - get_store(&conf, shut_down_signal, testable) - .await - .expect("Failed to create store"), - ), + StorageImpl::Postgresql | StorageImpl::PostgresqlTest => match &event_handler { + EventsHandler::Kafka(kafka_client) => Box::new( + crate::db::KafkaStore::new( + #[allow(clippy::expect_used)] + get_store(&conf.clone(), shut_down_signal, testable) + .await + .expect("Failed to create store"), + kafka_client.clone(), + ) + .await, + ), + EventsHandler::Logs(_) => Box::new( + #[allow(clippy::expect_used)] + get_store(&conf, shut_down_signal, testable) + .await + .expect("Failed to create store"), + ), + }, #[allow(clippy::expect_used)] StorageImpl::Mock => Box::new( MockDb::new(&conf.redis) @@ -146,12 +164,7 @@ impl AppState { }; #[cfg(feature = "olap")] - let pool = crate::analytics::AnalyticsProvider::from_conf( - &conf.analytics, - #[cfg(feature = "kms")] - kms_client, - ) - .await; + let pool = crate::analytics::AnalyticsProvider::from_conf(&conf.analytics).await; #[cfg(feature = "kms")] #[allow(clippy::expect_used)] @@ -174,7 +187,7 @@ impl AppState { #[cfg(feature = "kms")] kms_secrets: Arc::new(kms_secrets), api_client, - event_handler: Box::::default(), + event_handler, #[cfg(feature = "olap")] pool, } diff --git a/crates/router/src/services.rs b/crates/router/src/services.rs index faea707f2a14..e46612b95dfc 100644 --- a/crates/router/src/services.rs +++ b/crates/router/src/services.rs @@ -4,6 +4,7 @@ pub mod authorization; pub mod encryption; #[cfg(feature = "olap")] pub mod jwt; +pub mod kafka; pub mod logger; #[cfg(feature = "email")] diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 5481d5c5cf9d..1ff46474db59 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -873,6 +873,7 @@ where }; let api_event = ApiEvent::new( + Some(merchant_id.clone()), flow, &request_id, request_duration, @@ -884,6 +885,7 @@ where error, event_type.unwrap_or(ApiEventsType::Miscellaneous), request, + Some(request.method().to_string()), ); match api_event.clone().try_into() { Ok(event) => { diff --git a/crates/router/src/services/kafka.rs b/crates/router/src/services/kafka.rs new file mode 100644 index 000000000000..497ac16721b5 --- /dev/null +++ b/crates/router/src/services/kafka.rs @@ -0,0 +1,314 @@ +use std::sync::Arc; + +use common_utils::errors::CustomResult; +use error_stack::{report, IntoReport, ResultExt}; +use rdkafka::{ + config::FromClientConfig, + producer::{BaseRecord, DefaultProducerContext, Producer, ThreadedProducer}, +}; + +use crate::events::EventType; +mod api_event; +pub mod outgoing_request; +mod payment_attempt; +mod payment_intent; +mod refund; +pub use api_event::{ApiCallEventType, ApiEvents, ApiEventsType}; +use data_models::payments::{payment_attempt::PaymentAttempt, PaymentIntent}; +use diesel_models::refund::Refund; +use serde::Serialize; +use time::OffsetDateTime; + +use self::{ + payment_attempt::KafkaPaymentAttempt, payment_intent::KafkaPaymentIntent, refund::KafkaRefund, +}; +// Using message queue result here to avoid confusion with Kafka result provided by library +pub type MQResult = CustomResult; + +pub trait KafkaMessage +where + Self: Serialize, +{ + fn value(&self) -> MQResult> { + // Add better error logging here + serde_json::to_vec(&self) + .into_report() + .change_context(KafkaError::GenericError) + } + + fn key(&self) -> String; + + fn creation_timestamp(&self) -> Option { + None + } +} + +#[derive(serde::Serialize, Debug)] +struct KafkaEvent<'a, T: KafkaMessage> { + #[serde(flatten)] + event: &'a T, + sign_flag: i32, +} + +impl<'a, T: KafkaMessage> KafkaEvent<'a, T> { + fn new(event: &'a T) -> Self { + Self { + event, + sign_flag: 1, + } + } + fn old(event: &'a T) -> Self { + Self { + event, + sign_flag: -1, + } + } +} + +impl<'a, T: KafkaMessage> KafkaMessage for KafkaEvent<'a, T> { + fn key(&self) -> String { + self.event.key() + } + + fn creation_timestamp(&self) -> Option { + self.event.creation_timestamp() + } +} + +#[derive(Debug, serde::Deserialize, Clone, Default)] +#[serde(default)] +pub struct KafkaSettings { + brokers: Vec, + intent_analytics_topic: String, + attempt_analytics_topic: String, + refund_analytics_topic: String, + api_logs_topic: String, +} + +impl KafkaSettings { + pub fn validate(&self) -> Result<(), crate::core::errors::ApplicationError> { + use common_utils::ext_traits::ConfigExt; + + use crate::core::errors::ApplicationError; + + common_utils::fp_utils::when(self.brokers.is_empty(), || { + Err(ApplicationError::InvalidConfigurationValueError( + "Kafka brokers must not be empty".into(), + )) + })?; + + common_utils::fp_utils::when(self.intent_analytics_topic.is_default_or_empty(), || { + Err(ApplicationError::InvalidConfigurationValueError( + "Kafka Intent Analytics topic must not be empty".into(), + )) + })?; + + common_utils::fp_utils::when(self.attempt_analytics_topic.is_default_or_empty(), || { + Err(ApplicationError::InvalidConfigurationValueError( + "Kafka Attempt Analytics topic must not be empty".into(), + )) + })?; + + common_utils::fp_utils::when(self.refund_analytics_topic.is_default_or_empty(), || { + Err(ApplicationError::InvalidConfigurationValueError( + "Kafka Refund Analytics topic must not be empty".into(), + )) + })?; + + common_utils::fp_utils::when(self.api_logs_topic.is_default_or_empty(), || { + Err(ApplicationError::InvalidConfigurationValueError( + "Kafka API event Analytics topic must not be empty".into(), + )) + }) + } +} + +#[derive(Clone, Debug)] +pub struct KafkaProducer { + producer: Arc, + intent_analytics_topic: String, + attempt_analytics_topic: String, + refund_analytics_topic: String, + api_logs_topic: String, +} + +struct RdKafkaProducer(ThreadedProducer); + +impl std::fmt::Debug for RdKafkaProducer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("RdKafkaProducer") + } +} + +#[derive(Debug, Clone, thiserror::Error)] +pub enum KafkaError { + #[error("Generic Kafka Error")] + GenericError, + #[error("Kafka not implemented")] + NotImplemented, + #[error("Kafka Initialization Error")] + InitializationError, +} + +#[allow(unused)] +impl KafkaProducer { + pub async fn create(conf: &KafkaSettings) -> MQResult { + Ok(Self { + producer: Arc::new(RdKafkaProducer( + ThreadedProducer::from_config( + rdkafka::ClientConfig::new().set("bootstrap.servers", conf.brokers.join(",")), + ) + .into_report() + .change_context(KafkaError::InitializationError)?, + )), + + intent_analytics_topic: conf.intent_analytics_topic.clone(), + attempt_analytics_topic: conf.attempt_analytics_topic.clone(), + refund_analytics_topic: conf.refund_analytics_topic.clone(), + api_logs_topic: conf.api_logs_topic.clone(), + }) + } + + pub fn log_kafka_event( + &self, + topic: &str, + event: &T, + ) -> MQResult<()> { + router_env::logger::debug!("Logging Kafka Event {event:?}"); + self.producer + .0 + .send( + BaseRecord::to(topic) + .key(&event.key()) + .payload(&event.value()?) + .timestamp( + event + .creation_timestamp() + .unwrap_or_else(|| OffsetDateTime::now_utc().unix_timestamp()), + ), + ) + .map_err(|(error, record)| report!(error).attach_printable(format!("{record:?}"))) + .change_context(KafkaError::GenericError) + } + + pub async fn log_payment_attempt( + &self, + attempt: &PaymentAttempt, + old_attempt: Option, + ) -> MQResult<()> { + if let Some(negative_event) = old_attempt { + self.log_kafka_event( + &self.attempt_analytics_topic, + &KafkaEvent::old(&KafkaPaymentAttempt::from_storage(&negative_event)), + ) + .attach_printable_lazy(|| { + format!("Failed to add negative attempt event {negative_event:?}") + })?; + }; + self.log_kafka_event( + &self.attempt_analytics_topic, + &KafkaEvent::new(&KafkaPaymentAttempt::from_storage(attempt)), + ) + .attach_printable_lazy(|| format!("Failed to add positive attempt event {attempt:?}")) + } + + pub async fn log_payment_attempt_delete( + &self, + delete_old_attempt: &PaymentAttempt, + ) -> MQResult<()> { + self.log_kafka_event( + &self.attempt_analytics_topic, + &KafkaEvent::old(&KafkaPaymentAttempt::from_storage(delete_old_attempt)), + ) + .attach_printable_lazy(|| { + format!("Failed to add negative attempt event {delete_old_attempt:?}") + }) + } + + pub async fn log_payment_intent( + &self, + intent: &PaymentIntent, + old_intent: Option, + ) -> MQResult<()> { + if let Some(negative_event) = old_intent { + self.log_kafka_event( + &self.intent_analytics_topic, + &KafkaEvent::old(&KafkaPaymentIntent::from_storage(&negative_event)), + ) + .attach_printable_lazy(|| { + format!("Failed to add negative intent event {negative_event:?}") + })?; + }; + self.log_kafka_event( + &self.intent_analytics_topic, + &KafkaEvent::new(&KafkaPaymentIntent::from_storage(intent)), + ) + .attach_printable_lazy(|| format!("Failed to add positive intent event {intent:?}")) + } + + pub async fn log_payment_intent_delete( + &self, + delete_old_intent: &PaymentIntent, + ) -> MQResult<()> { + self.log_kafka_event( + &self.intent_analytics_topic, + &KafkaEvent::old(&KafkaPaymentIntent::from_storage(delete_old_intent)), + ) + .attach_printable_lazy(|| { + format!("Failed to add negative intent event {delete_old_intent:?}") + }) + } + + pub async fn log_refund(&self, refund: &Refund, old_refund: Option) -> MQResult<()> { + if let Some(negative_event) = old_refund { + self.log_kafka_event( + &self.refund_analytics_topic, + &KafkaEvent::old(&KafkaRefund::from_storage(&negative_event)), + ) + .attach_printable_lazy(|| { + format!("Failed to add negative refund event {negative_event:?}") + })?; + }; + self.log_kafka_event( + &self.refund_analytics_topic, + &KafkaEvent::new(&KafkaRefund::from_storage(refund)), + ) + .attach_printable_lazy(|| format!("Failed to add positive refund event {refund:?}")) + } + + pub async fn log_refund_delete(&self, delete_old_refund: &Refund) -> MQResult<()> { + self.log_kafka_event( + &self.refund_analytics_topic, + &KafkaEvent::old(&KafkaRefund::from_storage(delete_old_refund)), + ) + .attach_printable_lazy(|| { + format!("Failed to add negative refund event {delete_old_refund:?}") + }) + } + + pub async fn log_api_event(&self, event: &ApiEvents) -> MQResult<()> { + self.log_kafka_event(&self.api_logs_topic, event) + .attach_printable_lazy(|| format!("Failed to add api log event {event:?}")) + } + + pub fn get_topic(&self, event: EventType) -> &str { + match event { + EventType::ApiLogs => &self.api_logs_topic, + EventType::PaymentAttempt => &self.attempt_analytics_topic, + EventType::PaymentIntent => &self.intent_analytics_topic, + EventType::Refund => &self.refund_analytics_topic, + } + } +} + +impl Drop for RdKafkaProducer { + fn drop(&mut self) { + // Flush the producer to send any pending messages + match self.0.flush(rdkafka::util::Timeout::After( + std::time::Duration::from_secs(5), + )) { + Ok(_) => router_env::logger::info!("Kafka events flush Successful"), + Err(error) => router_env::logger::error!("Failed to flush Kafka Events {error:?}"), + } + } +} diff --git a/crates/router/src/services/kafka/api_event.rs b/crates/router/src/services/kafka/api_event.rs new file mode 100644 index 000000000000..7de271915927 --- /dev/null +++ b/crates/router/src/services/kafka/api_event.rs @@ -0,0 +1,108 @@ +use api_models::enums as api_enums; +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(tag = "flow_type")] +pub enum ApiEventsType { + Payment { + payment_id: String, + }, + Refund { + payment_id: String, + refund_id: String, + }, + Default, + PaymentMethod { + payment_method_id: String, + payment_method: Option, + payment_method_type: Option, + }, + Customer { + customer_id: String, + }, + User { + //specified merchant_id will overridden on global defined + merchant_id: String, + user_id: String, + }, + Webhooks { + connector: String, + payment_id: Option, + }, + OutgoingEvent, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct ApiEvents { + pub api_name: String, + pub request_id: Option, + //It is require to solve ambiquity in case of event_type is User + #[serde(skip_serializing_if = "Option::is_none")] + pub merchant_id: Option, + pub request: String, + pub response: String, + pub status_code: u16, + #[serde(with = "time::serde::timestamp")] + pub created_at: OffsetDateTime, + pub latency: u128, + //conflicting fields underlying enums will be used + #[serde(flatten)] + pub event_type: ApiEventsType, + pub user_agent: Option, + pub ip_addr: Option, + pub url_path: Option, + pub api_event_type: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub enum ApiCallEventType { + IncomingApiEvent, + OutgoingApiEvent, +} + +impl super::KafkaMessage for ApiEvents { + fn key(&self) -> String { + match &self.event_type { + ApiEventsType::Payment { payment_id } => format!( + "{}_{}", + self.merchant_id + .as_ref() + .unwrap_or(&"default_merchant_id".to_string()), + payment_id + ), + ApiEventsType::Refund { + payment_id, + refund_id, + } => format!("{payment_id}_{refund_id}"), + ApiEventsType::Default => "key".to_string(), + ApiEventsType::PaymentMethod { + payment_method_id, + payment_method, + payment_method_type, + } => format!( + "{:?}_{:?}_{:?}", + payment_method_id.clone(), + payment_method.clone(), + payment_method_type.clone(), + ), + ApiEventsType::Customer { customer_id } => customer_id.to_string(), + ApiEventsType::User { + merchant_id, + user_id, + } => format!("{}_{}", merchant_id, user_id), + ApiEventsType::Webhooks { + connector, + payment_id, + } => format!( + "webhook_{}_{connector}", + payment_id.clone().unwrap_or_default() + ), + ApiEventsType::OutgoingEvent => "outgoing_event".to_string(), + } + } + + fn creation_timestamp(&self) -> Option { + Some(self.created_at.unix_timestamp()) + } +} diff --git a/crates/router/src/services/kafka/outgoing_request.rs b/crates/router/src/services/kafka/outgoing_request.rs new file mode 100644 index 000000000000..bb09fe91fe6d --- /dev/null +++ b/crates/router/src/services/kafka/outgoing_request.rs @@ -0,0 +1,19 @@ +use reqwest::Url; + +pub struct OutgoingRequest { + pub url: Url, + pub latency: u128, +} + +// impl super::KafkaMessage for OutgoingRequest { +// fn key(&self) -> String { +// format!( +// "{}_{}", + +// ) +// } + +// fn creation_timestamp(&self) -> Option { +// Some(self.created_at.unix_timestamp()) +// } +// } diff --git a/crates/router/src/services/kafka/payment_attempt.rs b/crates/router/src/services/kafka/payment_attempt.rs new file mode 100644 index 000000000000..ea0721f418e5 --- /dev/null +++ b/crates/router/src/services/kafka/payment_attempt.rs @@ -0,0 +1,92 @@ +use data_models::payments::payment_attempt::PaymentAttempt; +use diesel_models::enums as storage_enums; +use time::OffsetDateTime; + +#[derive(serde::Serialize, Debug)] +pub struct KafkaPaymentAttempt<'a> { + pub payment_id: &'a String, + pub merchant_id: &'a String, + pub attempt_id: &'a String, + pub status: storage_enums::AttemptStatus, + pub amount: i64, + pub currency: Option, + pub save_to_locker: Option, + pub connector: Option<&'a String>, + pub error_message: Option<&'a String>, + pub offer_amount: Option, + pub surcharge_amount: Option, + pub tax_amount: Option, + pub payment_method_id: Option<&'a String>, + pub payment_method: Option, + pub connector_transaction_id: Option<&'a String>, + pub capture_method: Option, + #[serde(default, with = "time::serde::timestamp::option")] + pub capture_on: Option, + pub confirm: bool, + pub authentication_type: Option, + #[serde(with = "time::serde::timestamp")] + pub created_at: OffsetDateTime, + #[serde(with = "time::serde::timestamp")] + pub modified_at: OffsetDateTime, + #[serde(default, with = "time::serde::timestamp::option")] + pub last_synced: Option, + pub cancellation_reason: Option<&'a String>, + pub amount_to_capture: Option, + pub mandate_id: Option<&'a String>, + pub browser_info: Option, + pub error_code: Option<&'a String>, + pub connector_metadata: Option, + // TODO: These types should implement copy ideally + pub payment_experience: Option<&'a storage_enums::PaymentExperience>, + pub payment_method_type: Option<&'a storage_enums::PaymentMethodType>, +} + +impl<'a> KafkaPaymentAttempt<'a> { + pub fn from_storage(attempt: &'a PaymentAttempt) -> Self { + Self { + payment_id: &attempt.payment_id, + merchant_id: &attempt.merchant_id, + attempt_id: &attempt.attempt_id, + status: attempt.status, + amount: attempt.amount, + currency: attempt.currency, + save_to_locker: attempt.save_to_locker, + connector: attempt.connector.as_ref(), + error_message: attempt.error_message.as_ref(), + offer_amount: attempt.offer_amount, + surcharge_amount: attempt.surcharge_amount, + tax_amount: attempt.tax_amount, + payment_method_id: attempt.payment_method_id.as_ref(), + payment_method: attempt.payment_method, + connector_transaction_id: attempt.connector_transaction_id.as_ref(), + capture_method: attempt.capture_method, + capture_on: attempt.capture_on.map(|i| i.assume_utc()), + confirm: attempt.confirm, + authentication_type: attempt.authentication_type, + created_at: attempt.created_at.assume_utc(), + modified_at: attempt.modified_at.assume_utc(), + last_synced: attempt.last_synced.map(|i| i.assume_utc()), + cancellation_reason: attempt.cancellation_reason.as_ref(), + amount_to_capture: attempt.amount_to_capture, + mandate_id: attempt.mandate_id.as_ref(), + browser_info: attempt.browser_info.as_ref().map(|v| v.to_string()), + error_code: attempt.error_code.as_ref(), + connector_metadata: attempt.connector_metadata.as_ref().map(|v| v.to_string()), + payment_experience: attempt.payment_experience.as_ref(), + payment_method_type: attempt.payment_method_type.as_ref(), + } + } +} + +impl<'a> super::KafkaMessage for KafkaPaymentAttempt<'a> { + fn key(&self) -> String { + format!( + "{}_{}_{}", + self.merchant_id, self.payment_id, self.attempt_id + ) + } + + fn creation_timestamp(&self) -> Option { + Some(self.modified_at.unix_timestamp()) + } +} diff --git a/crates/router/src/services/kafka/payment_intent.rs b/crates/router/src/services/kafka/payment_intent.rs new file mode 100644 index 000000000000..70980a6e8652 --- /dev/null +++ b/crates/router/src/services/kafka/payment_intent.rs @@ -0,0 +1,71 @@ +use data_models::payments::PaymentIntent; +use diesel_models::enums as storage_enums; +use time::OffsetDateTime; + +#[derive(serde::Serialize, Debug)] +pub struct KafkaPaymentIntent<'a> { + pub payment_id: &'a String, + pub merchant_id: &'a String, + pub status: storage_enums::IntentStatus, + pub amount: i64, + pub currency: Option, + pub amount_captured: Option, + pub customer_id: Option<&'a String>, + pub description: Option<&'a String>, + pub return_url: Option<&'a String>, + pub connector_id: Option<&'a String>, + pub statement_descriptor_name: Option<&'a String>, + pub statement_descriptor_suffix: Option<&'a String>, + #[serde(with = "time::serde::timestamp")] + pub created_at: OffsetDateTime, + #[serde(with = "time::serde::timestamp")] + pub modified_at: OffsetDateTime, + #[serde(default, with = "time::serde::timestamp::option")] + pub last_synced: Option, + pub setup_future_usage: Option, + pub off_session: Option, + pub client_secret: Option<&'a String>, + pub active_attempt_id: String, + pub business_country: Option, + pub business_label: Option<&'a String>, + pub attempt_count: i16, +} + +impl<'a> KafkaPaymentIntent<'a> { + pub fn from_storage(intent: &'a PaymentIntent) -> Self { + Self { + payment_id: &intent.payment_id, + merchant_id: &intent.merchant_id, + status: intent.status, + amount: intent.amount, + currency: intent.currency, + amount_captured: intent.amount_captured, + customer_id: intent.customer_id.as_ref(), + description: intent.description.as_ref(), + return_url: intent.return_url.as_ref(), + connector_id: intent.connector_id.as_ref(), + statement_descriptor_name: intent.statement_descriptor_name.as_ref(), + statement_descriptor_suffix: intent.statement_descriptor_suffix.as_ref(), + created_at: intent.created_at.assume_utc(), + modified_at: intent.modified_at.assume_utc(), + last_synced: intent.last_synced.map(|i| i.assume_utc()), + setup_future_usage: intent.setup_future_usage, + off_session: intent.off_session, + client_secret: intent.client_secret.as_ref(), + active_attempt_id: intent.active_attempt.get_id(), + business_country: intent.business_country, + business_label: intent.business_label.as_ref(), + attempt_count: intent.attempt_count, + } + } +} + +impl<'a> super::KafkaMessage for KafkaPaymentIntent<'a> { + fn key(&self) -> String { + format!("{}_{}", self.merchant_id, self.payment_id) + } + + fn creation_timestamp(&self) -> Option { + Some(self.modified_at.unix_timestamp()) + } +} diff --git a/crates/router/src/services/kafka/refund.rs b/crates/router/src/services/kafka/refund.rs new file mode 100644 index 000000000000..0cc4865e7512 --- /dev/null +++ b/crates/router/src/services/kafka/refund.rs @@ -0,0 +1,68 @@ +use diesel_models::{enums as storage_enums, refund::Refund}; +use time::OffsetDateTime; + +#[derive(serde::Serialize, Debug)] +pub struct KafkaRefund<'a> { + pub internal_reference_id: &'a String, + pub refund_id: &'a String, //merchant_reference id + pub payment_id: &'a String, + pub merchant_id: &'a String, + pub connector_transaction_id: &'a String, + pub connector: &'a String, + pub connector_refund_id: Option<&'a String>, + pub external_reference_id: Option<&'a String>, + pub refund_type: &'a storage_enums::RefundType, + pub total_amount: &'a i64, + pub currency: &'a storage_enums::Currency, + pub refund_amount: &'a i64, + pub refund_status: &'a storage_enums::RefundStatus, + pub sent_to_gateway: &'a bool, + pub refund_error_message: Option<&'a String>, + pub refund_arn: Option<&'a String>, + #[serde(default, with = "time::serde::timestamp")] + pub created_at: OffsetDateTime, + #[serde(default, with = "time::serde::timestamp")] + pub modified_at: OffsetDateTime, + pub description: Option<&'a String>, + pub attempt_id: &'a String, + pub refund_reason: Option<&'a String>, + pub refund_error_code: Option<&'a String>, +} + +impl<'a> KafkaRefund<'a> { + pub fn from_storage(refund: &'a Refund) -> Self { + Self { + internal_reference_id: &refund.internal_reference_id, + refund_id: &refund.refund_id, + payment_id: &refund.payment_id, + merchant_id: &refund.merchant_id, + connector_transaction_id: &refund.connector_transaction_id, + connector: &refund.connector, + connector_refund_id: refund.connector_refund_id.as_ref(), + external_reference_id: refund.external_reference_id.as_ref(), + refund_type: &refund.refund_type, + total_amount: &refund.total_amount, + currency: &refund.currency, + refund_amount: &refund.refund_amount, + refund_status: &refund.refund_status, + sent_to_gateway: &refund.sent_to_gateway, + refund_error_message: refund.refund_error_message.as_ref(), + refund_arn: refund.refund_arn.as_ref(), + created_at: refund.created_at.assume_utc(), + modified_at: refund.updated_at.assume_utc(), + description: refund.description.as_ref(), + attempt_id: &refund.attempt_id, + refund_reason: refund.refund_reason.as_ref(), + refund_error_code: refund.refund_error_code.as_ref(), + } + } +} + +impl<'a> super::KafkaMessage for KafkaRefund<'a> { + fn key(&self) -> String { + format!( + "{}_{}_{}_{}", + self.merchant_id, self.payment_id, self.attempt_id, self.refund_id + ) + } +} diff --git a/crates/router/src/types/storage/payment_attempt.rs b/crates/router/src/types/storage/payment_attempt.rs index f94d06997ca9..13b9f3dd5d5c 100644 --- a/crates/router/src/types/storage/payment_attempt.rs +++ b/crates/router/src/types/storage/payment_attempt.rs @@ -7,7 +7,6 @@ use error_stack::ResultExt; use crate::{ core::errors, errors::RouterResult, types::transformers::ForeignFrom, utils::OptionExt, }; - pub trait PaymentAttemptExt { fn make_new_capture( &self, @@ -134,9 +133,7 @@ mod tests { use crate::configs::settings::Settings; let conf = Settings::new().expect("invalid settings"); let tx: oneshot::Sender<()> = oneshot::channel().0; - let api_client = Box::new(services::MockApiClient); - let state = routes::AppState::with_storage(conf, StorageImpl::PostgresqlTest, tx, api_client).await; @@ -187,7 +184,6 @@ mod tests { let tx: oneshot::Sender<()> = oneshot::channel().0; let api_client = Box::new(services::MockApiClient); - let state = routes::AppState::with_storage(conf, StorageImpl::PostgresqlTest, tx, api_client).await; let current_time = common_utils::date_time::now(); diff --git a/crates/router/tests/connectors/aci.rs b/crates/router/tests/connectors/aci.rs index c9ee3a34f2ef..e12e27708f87 100644 --- a/crates/router/tests/connectors/aci.rs +++ b/crates/router/tests/connectors/aci.rs @@ -160,6 +160,7 @@ fn construct_refund_router_data() -> types::RefundsRouterData { async fn payments_create_success() { let conf = Settings::new().unwrap(); let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( conf, StorageImpl::PostgresqlTest, @@ -204,6 +205,7 @@ async fn payments_create_failure() { let conf = Settings::new().unwrap(); static CV: aci::Aci = aci::Aci; let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( conf, StorageImpl::PostgresqlTest, @@ -265,6 +267,7 @@ async fn refund_for_successful_payments() { merchant_connector_id: None, }; let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( conf, StorageImpl::PostgresqlTest, @@ -333,6 +336,7 @@ async fn refunds_create_failure() { merchant_connector_id: None, }; let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( conf, StorageImpl::PostgresqlTest, diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 67a0625968fb..f325370e737f 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -96,6 +96,7 @@ pub trait ConnectorActions: Connector { payment_info, ); let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( Settings::new().unwrap(), StorageImpl::PostgresqlTest, @@ -120,6 +121,7 @@ pub trait ConnectorActions: Connector { payment_info, ); let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( Settings::new().unwrap(), StorageImpl::PostgresqlTest, @@ -148,6 +150,7 @@ pub trait ConnectorActions: Connector { payment_info, ); let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( Settings::new().unwrap(), StorageImpl::PostgresqlTest, @@ -561,6 +564,7 @@ pub trait ConnectorActions: Connector { .get_connector_integration(); let mut request = self.get_payout_request(None, payout_type, payment_info); let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( Settings::new().unwrap(), StorageImpl::PostgresqlTest, @@ -601,6 +605,7 @@ pub trait ConnectorActions: Connector { .get_connector_integration(); let mut request = self.get_payout_request(connector_payout_id, payout_type, payment_info); let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( Settings::new().unwrap(), StorageImpl::PostgresqlTest, @@ -642,6 +647,7 @@ pub trait ConnectorActions: Connector { let mut request = self.get_payout_request(None, payout_type, payment_info); request.connector_customer = connector_customer; let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( Settings::new().unwrap(), StorageImpl::PostgresqlTest, @@ -683,6 +689,7 @@ pub trait ConnectorActions: Connector { let mut request = self.get_payout_request(Some(connector_payout_id), payout_type, payment_info); let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( Settings::new().unwrap(), StorageImpl::PostgresqlTest, @@ -770,6 +777,7 @@ pub trait ConnectorActions: Connector { .get_connector_integration(); let mut request = self.get_payout_request(None, payout_type, payment_info); let tx = oneshot::channel().0; + let state = routes::AppState::with_storage( Settings::new().unwrap(), StorageImpl::PostgresqlTest, @@ -802,6 +810,7 @@ async fn call_connector< ) -> Result, Report> { let conf = Settings::new().unwrap(); let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( conf, StorageImpl::PostgresqlTest, diff --git a/crates/router/tests/payments2.rs b/crates/router/tests/payments2.rs index 5d4ca844061f..42e5524a15d5 100644 --- a/crates/router/tests/payments2.rs +++ b/crates/router/tests/payments2.rs @@ -217,6 +217,7 @@ async fn payments_create_core_adyen_no_redirect() { use router::configs::settings::Settings; let conf = Settings::new().expect("invalid settings"); let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( conf, StorageImpl::PostgresqlTest, diff --git a/crates/router/tests/utils.rs b/crates/router/tests/utils.rs index 6cddbc043662..339eca6fa0fb 100644 --- a/crates/router/tests/utils.rs +++ b/crates/router/tests/utils.rs @@ -48,6 +48,7 @@ pub async fn mk_service( conf.connectors.stripe.base_url = url; } let tx: oneshot::Sender<()> = oneshot::channel().0; + let app_state = AppState::with_storage( conf, router::db::StorageImpl::Mock, diff --git a/crates/router_env/src/lib.rs b/crates/router_env/src/lib.rs index e75606aa1531..3c7ba8b93df7 100644 --- a/crates/router_env/src/lib.rs +++ b/crates/router_env/src/lib.rs @@ -39,10 +39,19 @@ use crate::types::FlowMetric; #[derive(Debug, Display, Clone, PartialEq, Eq)] pub enum AnalyticsFlow { GetInfo, + GetPaymentMetrics, + GetRefundsMetrics, + GetSdkMetrics, GetPaymentFilters, GetRefundFilters, - GetRefundsMetrics, - GetPaymentMetrics, + GetSdkEventFilters, + GetApiEvents, + GetSdkEvents, + GeneratePaymentReport, + GenerateDisputeReport, + GenerateRefundReport, + GetApiEventMetrics, + GetApiEventFilters, } impl FlowMetric for AnalyticsFlow {} diff --git a/crates/scheduler/Cargo.toml b/crates/scheduler/Cargo.toml index e0b68c709e8d..5e8674ab3814 100644 --- a/crates/scheduler/Cargo.toml +++ b/crates/scheduler/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [features] default = ["kv_store", "olap"] -olap = [] +olap = ["storage_impl/olap"] kv_store = [] [dependencies] diff --git a/crates/storage_impl/src/config.rs b/crates/storage_impl/src/config.rs index f53507831b11..fd95a6d315d6 100644 --- a/crates/storage_impl/src/config.rs +++ b/crates/storage_impl/src/config.rs @@ -1,6 +1,6 @@ use masking::Secret; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, serde::Deserialize)] pub struct Database { pub username: String, pub password: Secret, @@ -9,7 +9,41 @@ pub struct Database { pub dbname: String, pub pool_size: u32, pub connection_timeout: u64, - pub queue_strategy: bb8::QueueStrategy, + pub queue_strategy: QueueStrategy, pub min_idle: Option, pub max_lifetime: Option, } + +#[derive(Debug, serde::Deserialize, Clone, Copy, Default)] +#[serde(rename_all = "PascalCase")] +pub enum QueueStrategy { + #[default] + Fifo, + Lifo, +} + +impl From for bb8::QueueStrategy { + fn from(value: QueueStrategy) -> Self { + match value { + QueueStrategy::Fifo => Self::Fifo, + QueueStrategy::Lifo => Self::Lifo, + } + } +} + +impl Default for Database { + fn default() -> Self { + Self { + username: String::new(), + password: Secret::::default(), + host: "localhost".into(), + port: 5432, + dbname: String::new(), + pool_size: 5, + connection_timeout: 10, + queue_strategy: QueueStrategy::default(), + min_idle: None, + max_lifetime: None, + } + } +} diff --git a/crates/storage_impl/src/database/store.rs b/crates/storage_impl/src/database/store.rs index c36575e37c97..75c34af14ac1 100644 --- a/crates/storage_impl/src/database/store.rs +++ b/crates/storage_impl/src/database/store.rs @@ -89,7 +89,7 @@ pub async fn diesel_make_pg_pool( let mut pool = bb8::Pool::builder() .max_size(database.pool_size) .min_idle(database.min_idle) - .queue_strategy(database.queue_strategy) + .queue_strategy(database.queue_strategy.into()) .connection_timeout(std::time::Duration::from_secs(database.connection_timeout)) .max_lifetime(database.max_lifetime.map(std::time::Duration::from_secs)); diff --git a/docker-compose.yml b/docker-compose.yml index fd18906143f5..f51a47aee940 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -273,3 +273,66 @@ services: - "8001:8001" volumes: - redisinsight_store:/db + + kafka0: + image: confluentinc/cp-kafka:7.0.5 + hostname: kafka0 + networks: + - router_net + ports: + - 9092:9092 + - 9093 + - 9997 + - 29092 + environment: + KAFKA_BROKER_ID: 1 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092 + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_PROCESS_ROLES: 'broker,controller' + KAFKA_NODE_ID: 1 + KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka0:29093' + KAFKA_LISTENERS: 'PLAINTEXT://kafka0:29092,CONTROLLER://kafka0:29093,PLAINTEXT_HOST://0.0.0.0:9092' + KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER' + KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs' + JMX_PORT: 9997 + KAFKA_JMX_OPTS: -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=kafka0 -Dcom.sun.management.jmxremote.rmi.port=9997 + profiles: + - analytics + volumes: + - ./monitoring/kafka-script.sh:/tmp/update_run.sh + command: "bash -c 'if [ ! -f /tmp/update_run.sh ]; then echo \"ERROR: Did you forget the update_run.sh file that came with this docker-compose.yml file?\" && exit 1 ; else /tmp/update_run.sh && /etc/confluent/docker/run ; fi'" + + # Kafka UI for debugging kafka queues + kafka-ui: + image: provectuslabs/kafka-ui:latest + ports: + - 8090:8080 + networks: + - router_net + depends_on: + - kafka0 + profiles: + - analytics + environment: + KAFKA_CLUSTERS_0_NAME: local + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092 + KAFKA_CLUSTERS_0_JMXPORT: 9997 + + clickhouse-server: + image: clickhouse/clickhouse-server:23.5 + networks: + - router_net + ports: + - "9000" + - "8123:8123" + profiles: + - analytics + ulimits: + nofile: + soft: 262144 + hard: 262144 \ No newline at end of file From 70ba4ffe7bb9e685f3dc8afc26de241f2457e86c Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 29 Nov 2023 14:31:26 +0000 Subject: [PATCH 127/146] chore(version): v1.92.0 --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a63dcc2cae0..f2966b238bba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.92.0 (2023-11-29) + +### Features + +- **analytics:** Add Clickhouse based analytics ([#2988](https://github.com/juspay/hyperswitch/pull/2988)) ([`9df4e01`](https://github.com/juspay/hyperswitch/commit/9df4e0193ffeb6d1cc323bdebb7e2bdfb2a375e2)) +- **ses_email:** Add email services to hyperswitch ([#2977](https://github.com/juspay/hyperswitch/pull/2977)) ([`5f5e895`](https://github.com/juspay/hyperswitch/commit/5f5e895f638701a0e6ab3deea9101ef39033dd16)) + +### Bug Fixes + +- **router:** Make use of warning to log errors when apple pay metadata parsing fails ([#3010](https://github.com/juspay/hyperswitch/pull/3010)) ([`2e57745`](https://github.com/juspay/hyperswitch/commit/2e57745352c547323ac2df2554f6bc2dbd6da37f)) + +**Full Changelog:** [`v1.91.1...v1.92.0`](https://github.com/juspay/hyperswitch/compare/v1.91.1...v1.92.0) + +- - - + + ## 1.91.1 (2023-11-29) ### Bug Fixes From 6b7ada1a34450ea3a7fc019375ba462a14ddd6ab Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Wed, 29 Nov 2023 20:35:33 +0530 Subject: [PATCH 128/146] fix(core): Error message on Refund update for `Not Implemented` Case (#3011) --- crates/router/src/core/refunds.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/router/src/core/refunds.rs b/crates/router/src/core/refunds.rs index 33435bb0ad96..c43c00b7259c 100644 --- a/crates/router/src/core/refunds.rs +++ b/crates/router/src/core/refunds.rs @@ -211,7 +211,10 @@ pub async fn trigger_refund_to_gateway( errors::ConnectorError::NotImplemented(message) => { Some(storage::RefundUpdate::ErrorUpdate { refund_status: Some(enums::RefundStatus::Failure), - refund_error_message: Some(message.to_string()), + refund_error_message: Some( + errors::ConnectorError::NotImplemented(message.to_owned()) + .to_string(), + ), refund_error_code: Some("NOT_IMPLEMENTED".to_string()), updated_by: storage_scheme.to_string(), }) From c05432c0bd70f222c2f898ce2cbb47a46364a490 Mon Sep 17 00:00:00 2001 From: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> Date: Wed, 29 Nov 2023 21:01:36 +0530 Subject: [PATCH 129/146] fix(pm_list): [Trustpay] Update Cards, Bank_redirect - blik pm type required field info for Trustpay (#2999) Co-authored-by: Arjun Karthik --- crates/router/src/configs/defaults.rs | 172 ++++++++++++++++++++++++-- 1 file changed, 164 insertions(+), 8 deletions(-) diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index a92e63d67639..f5c3b46b27f2 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -1910,14 +1910,63 @@ impl Default for super::settings::RequiredFields { } ), ( - "payment_method_data.card.card_holder_name".to_string(), + "billing.address.first_name".to_string(), RequiredFieldInfo { - required_field: "payment_method_data.card.card_holder_name".to_string(), + required_field: "billing.address.first_name".to_string(), display_name: "card_holder_name".to_string(), - field_type: enums::FieldType::UserFullName, + field_type: enums::FieldType::UserBillingName, value: None, } - ) + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "card_holder_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "billing.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserAddressCity, + value: None, + } + ), + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), ] ), common: HashMap::new() @@ -3686,14 +3735,63 @@ impl Default for super::settings::RequiredFields { } ), ( - "payment_method_data.card.card_holder_name".to_string(), + "billing.address.first_name".to_string(), RequiredFieldInfo { - required_field: "payment_method_data.card.card_holder_name".to_string(), + required_field: "billing.address.first_name".to_string(), display_name: "card_holder_name".to_string(), - field_type: enums::FieldType::UserFullName, + field_type: enums::FieldType::UserBillingName, value: None, } - ) + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "card_holder_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "billing.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserAddressCity, + value: None, + } + ), + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), ] ), common: HashMap::new() @@ -4056,6 +4154,64 @@ impl Default for super::settings::RequiredFields { value: None, } ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.first_name".to_string(), + display_name: "card_holder_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "card_holder_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "billing.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserAddressCity, + value: None, + } + ), + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), ]), } ) From 8a4dabc61df3e6012e50f785d93808ca3349be65 Mon Sep 17 00:00:00 2001 From: Brian Silah <71752651+unpervertedkid@users.noreply.github.com> Date: Wed, 29 Nov 2023 21:07:59 +0300 Subject: [PATCH 130/146] refactor(connector): [Stax] change error message from NotSupported to NotImplemented (#2879) --- .../router/src/connector/stax/transformers.rs | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/crates/router/src/connector/stax/transformers.rs b/crates/router/src/connector/stax/transformers.rs index bb37bf1fc9e7..5aa0949a09cc 100644 --- a/crates/router/src/connector/stax/transformers.rs +++ b/crates/router/src/connector/stax/transformers.rs @@ -63,10 +63,9 @@ impl TryFrom<&StaxRouterData<&types::PaymentsAuthorizeRouterData>> for StaxPayme item: &StaxRouterData<&types::PaymentsAuthorizeRouterData>, ) -> Result { if item.router_data.request.currency != enums::Currency::USD { - Err(errors::ConnectorError::NotSupported { - message: item.router_data.request.currency.to_string(), - connector: "Stax", - })? + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Stax"), + ))? } let total = item.amount; @@ -119,10 +118,9 @@ impl TryFrom<&StaxRouterData<&types::PaymentsAuthorizeRouterData>> for StaxPayme | api::PaymentMethodData::GiftCard(_) | api::PaymentMethodData::CardRedirect(_) | api::PaymentMethodData::Upi(_) - | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { - message: "SELECTED_PAYMENT_METHOD".to_string(), - connector: "Stax", - })?, + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Stax"), + ))?, } } } @@ -270,10 +268,9 @@ impl TryFrom<&types::TokenizationRouterData> for StaxTokenRequest { | api::PaymentMethodData::GiftCard(_) | api::PaymentMethodData::CardRedirect(_) | api::PaymentMethodData::Upi(_) - | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { - message: "SELECTED_PAYMENT_METHOD".to_string(), - connector: "Stax", - })?, + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Stax"), + ))?, } } } From de8e31b70d9b3c11e268cd1deffa71918dc4270d Mon Sep 17 00:00:00 2001 From: Brian Silah <71752651+unpervertedkid@users.noreply.github.com> Date: Wed, 29 Nov 2023 21:08:30 +0300 Subject: [PATCH 131/146] refactor(connector): [Volt] change error message from NotSupported to NotImplemented (#2878) --- crates/router/src/connector/volt/transformers.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/router/src/connector/volt/transformers.rs b/crates/router/src/connector/volt/transformers.rs index efed7c797c76..6f4c67dce8a3 100644 --- a/crates/router/src/connector/volt/transformers.rs +++ b/crates/router/src/connector/volt/transformers.rs @@ -130,10 +130,9 @@ impl TryFrom<&VoltRouterData<&types::PaymentsAuthorizeRouterData>> for VoltPayme | api_models::payments::BankRedirectData::Trustly { .. } | api_models::payments::BankRedirectData::OnlineBankingFpx { .. } | api_models::payments::BankRedirectData::OnlineBankingThailand { .. } => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Volt", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Volt"), + ) .into()) } }, @@ -150,10 +149,9 @@ impl TryFrom<&VoltRouterData<&types::PaymentsAuthorizeRouterData>> for VoltPayme | api_models::payments::PaymentMethodData::Voucher(_) | api_models::payments::PaymentMethodData::GiftCard(_) | api_models::payments::PaymentMethodData::CardToken(_) => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Volt", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Volt"), + ) .into()) } } From ab3dac79b4f138cd1f60a9afc0635dcc137a4a05 Mon Sep 17 00:00:00 2001 From: Swangi Kumari <85639103+swangi-kumari@users.noreply.github.com> Date: Thu, 30 Nov 2023 12:24:23 +0530 Subject: [PATCH 132/146] refactor(connector): [Adyen] Change country and issuer type to Optional for OpenBankingUk (#2993) Co-authored-by: Arjun Karthik Co-authored-by: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> --- crates/api_models/src/payments.rs | 4 +- .../src/connector/adyen/transformers.rs | 145 ++++++++++++++++-- 2 files changed, 138 insertions(+), 11 deletions(-) diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index bd4c59211e24..5ecbf795ac56 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -1204,10 +1204,10 @@ pub enum BankRedirectData { OpenBankingUk { // Issuer banks #[schema(value_type = BankNames)] - issuer: api_enums::BankNames, + issuer: Option, /// The country for bank payment #[schema(value_type = CountryAlpha2, example = "US")] - country: api_enums::CountryAlpha2, + country: Option, }, Przelewy24 { //Issuer banks diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index cfa601112677..4b3fcc851323 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -879,7 +879,126 @@ impl TryFrom<&api_enums::BankNames> for OpenBankingUKIssuer { api::enums::BankNames::TsbBank => Ok(Self::TsbBank), api::enums::BankNames::TescoBank => Ok(Self::TescoBank), api::enums::BankNames::UlsterBank => Ok(Self::UlsterBank), - _ => Err(errors::ConnectorError::NotSupported { + enums::BankNames::AmericanExpress + | enums::BankNames::AffinBank + | enums::BankNames::AgroBank + | enums::BankNames::AllianceBank + | enums::BankNames::AmBank + | enums::BankNames::BankOfAmerica + | enums::BankNames::BankIslam + | enums::BankNames::BankMuamalat + | enums::BankNames::BankRakyat + | enums::BankNames::BankSimpananNasional + | enums::BankNames::BlikPSP + | enums::BankNames::CapitalOne + | enums::BankNames::Chase + | enums::BankNames::Citi + | enums::BankNames::CimbBank + | enums::BankNames::Discover + | enums::BankNames::NavyFederalCreditUnion + | enums::BankNames::PentagonFederalCreditUnion + | enums::BankNames::SynchronyBank + | enums::BankNames::WellsFargo + | enums::BankNames::AbnAmro + | enums::BankNames::AsnBank + | enums::BankNames::Bunq + | enums::BankNames::Handelsbanken + | enums::BankNames::HongLeongBank + | enums::BankNames::Ing + | enums::BankNames::Knab + | enums::BankNames::KuwaitFinanceHouse + | enums::BankNames::Moneyou + | enums::BankNames::Rabobank + | enums::BankNames::Regiobank + | enums::BankNames::SnsBank + | enums::BankNames::TriodosBank + | enums::BankNames::VanLanschot + | enums::BankNames::ArzteUndApothekerBank + | enums::BankNames::AustrianAnadiBankAg + | enums::BankNames::BankAustria + | enums::BankNames::Bank99Ag + | enums::BankNames::BankhausCarlSpangler + | enums::BankNames::BankhausSchelhammerUndSchatteraAg + | enums::BankNames::BankMillennium + | enums::BankNames::BankPEKAOSA + | enums::BankNames::BawagPskAg + | enums::BankNames::BksBankAg + | enums::BankNames::BrullKallmusBankAg + | enums::BankNames::BtvVierLanderBank + | enums::BankNames::CapitalBankGraweGruppeAg + | enums::BankNames::CeskaSporitelna + | enums::BankNames::Dolomitenbank + | enums::BankNames::EasybankAg + | enums::BankNames::EPlatbyVUB + | enums::BankNames::ErsteBankUndSparkassen + | enums::BankNames::FrieslandBank + | enums::BankNames::HypoAlpeadriabankInternationalAg + | enums::BankNames::HypoNoeLbFurNiederosterreichUWien + | enums::BankNames::HypoOberosterreichSalzburgSteiermark + | enums::BankNames::HypoTirolBankAg + | enums::BankNames::HypoVorarlbergBankAg + | enums::BankNames::HypoBankBurgenlandAktiengesellschaft + | enums::BankNames::KomercniBanka + | enums::BankNames::MBank + | enums::BankNames::MarchfelderBank + | enums::BankNames::Maybank + | enums::BankNames::OberbankAg + | enums::BankNames::OsterreichischeArzteUndApothekerbank + | enums::BankNames::OcbcBank + | enums::BankNames::PayWithING + | enums::BankNames::PlaceZIPKO + | enums::BankNames::PlatnoscOnlineKartaPlatnicza + | enums::BankNames::PosojilnicaBankEGen + | enums::BankNames::PostovaBanka + | enums::BankNames::PublicBank + | enums::BankNames::RaiffeisenBankengruppeOsterreich + | enums::BankNames::RhbBank + | enums::BankNames::SchelhammerCapitalBankAg + | enums::BankNames::StandardCharteredBank + | enums::BankNames::SchoellerbankAg + | enums::BankNames::SpardaBankWien + | enums::BankNames::SporoPay + | enums::BankNames::TatraPay + | enums::BankNames::Viamo + | enums::BankNames::VolksbankGruppe + | enums::BankNames::VolkskreditbankAg + | enums::BankNames::VrBankBraunau + | enums::BankNames::UobBank + | enums::BankNames::PayWithAliorBank + | enums::BankNames::BankiSpoldzielcze + | enums::BankNames::PayWithInteligo + | enums::BankNames::BNPParibasPoland + | enums::BankNames::BankNowySA + | enums::BankNames::CreditAgricole + | enums::BankNames::PayWithBOS + | enums::BankNames::PayWithCitiHandlowy + | enums::BankNames::PayWithPlusBank + | enums::BankNames::ToyotaBank + | enums::BankNames::VeloBank + | enums::BankNames::ETransferPocztowy24 + | enums::BankNames::PlusBank + | enums::BankNames::EtransferPocztowy24 + | enums::BankNames::BankiSpbdzielcze + | enums::BankNames::BankNowyBfgSa + | enums::BankNames::GetinBank + | enums::BankNames::Blik + | enums::BankNames::NoblePay + | enums::BankNames::IdeaBank + | enums::BankNames::EnveloBank + | enums::BankNames::NestPrzelew + | enums::BankNames::MbankMtransfer + | enums::BankNames::Inteligo + | enums::BankNames::PbacZIpko + | enums::BankNames::BnpParibas + | enums::BankNames::BankPekaoSa + | enums::BankNames::VolkswagenBank + | enums::BankNames::AliorBank + | enums::BankNames::Boz + | enums::BankNames::BangkokBank + | enums::BankNames::KrungsriBank + | enums::BankNames::KrungThaiBank + | enums::BankNames::TheSiamCommercialBank + | enums::BankNames::KasikornBank => Err(errors::ConnectorError::NotSupported { message: String::from("BankRedirect"), connector: "Adyen", })?, @@ -2102,7 +2221,12 @@ impl<'a> TryFrom<&api_models::payments::BankRedirectData> for AdyenPaymentMethod ), api_models::payments::BankRedirectData::OpenBankingUk { issuer, .. } => Ok( AdyenPaymentMethod::OpenBankingUK(Box::new(OpenBankingUKData { - issuer: OpenBankingUKIssuer::try_from(issuer)?, + issuer: match issuer { + Some(bank_name) => OpenBankingUKIssuer::try_from(bank_name)?, + None => Err(errors::ConnectorError::MissingRequiredField { + field_name: "issuer", + })?, + }, })), ), api_models::payments::BankRedirectData::Sofort { .. } => Ok(AdyenPaymentMethod::Sofort), @@ -2580,7 +2704,7 @@ impl<'a> let additional_data = get_additional_data(item.router_data); let return_url = item.router_data.request.get_return_url()?; let payment_method = AdyenPaymentMethod::try_from(bank_redirect_data)?; - let (shopper_locale, country) = get_redirect_extra_details(item.router_data); + let (shopper_locale, country) = get_redirect_extra_details(item.router_data)?; let line_items = Some(get_line_items(item)); Ok(AdyenPaymentRequest { @@ -2611,7 +2735,7 @@ impl<'a> fn get_redirect_extra_details( item: &types::PaymentsAuthorizeRouterData, -) -> (Option, Option) { +) -> Result<(Option, Option), errors::ConnectorError> { match item.request.payment_method_data { api_models::payments::PaymentMethodData::BankRedirect(ref redirect_data) => { match redirect_data { @@ -2619,17 +2743,20 @@ fn get_redirect_extra_details( country, preferred_language, .. - } => ( + } => Ok(( Some(preferred_language.to_string()), Some(country.to_owned()), - ), + )), api_models::payments::BankRedirectData::OpenBankingUk { country, .. } => { - (None, Some(country.to_owned())) + let country = country.ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "country", + })?; + Ok((None, Some(country))) } - _ => (None, None), + _ => Ok((None, None)), } } - _ => (None, None), + _ => Ok((None, None)), } } From 44b1f4949ea06d59480670ccfa02446fa7713d13 Mon Sep 17 00:00:00 2001 From: Sudheer konagalla <50401745+cb-sudheer@users.noreply.github.com> Date: Thu, 30 Nov 2023 12:57:38 +0530 Subject: [PATCH 133/146] fix(router): [Dlocal] connector transaction id fix (#2872) --- .../src/connector/dlocal/transformers.rs | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/crates/router/src/connector/dlocal/transformers.rs b/crates/router/src/connector/dlocal/transformers.rs index a9033e53d666..f7cfa6a868bd 100644 --- a/crates/router/src/connector/dlocal/transformers.rs +++ b/crates/router/src/connector/dlocal/transformers.rs @@ -303,7 +303,7 @@ pub struct DlocalPaymentsResponse { status: DlocalPaymentStatus, id: String, three_dsecure: Option, - order_id: String, + order_id: Option, } impl @@ -323,12 +323,12 @@ impl }); let response = types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(item.response.order_id.clone()), + resource_id: types::ResponseId::ConnectorTransactionId(item.response.id.clone()), redirection_data, mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: Some(item.response.order_id.clone()), + connector_response_reference_id: item.response.order_id.clone(), }; Ok(Self { status: enums::AttemptStatus::from(item.response.status), @@ -342,7 +342,7 @@ impl pub struct DlocalPaymentsSyncResponse { status: DlocalPaymentStatus, id: String, - order_id: String, + order_id: Option, } impl @@ -362,14 +362,12 @@ impl Ok(Self { status: enums::AttemptStatus::from(item.response.status), response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId( - item.response.order_id.clone(), - ), + resource_id: types::ResponseId::ConnectorTransactionId(item.response.id.clone()), redirection_data: None, mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: Some(item.response.order_id.clone()), + connector_response_reference_id: item.response.order_id.clone(), }), ..item.data }) @@ -380,7 +378,7 @@ impl pub struct DlocalPaymentsCaptureResponse { status: DlocalPaymentStatus, id: String, - order_id: String, + order_id: Option, } impl @@ -400,14 +398,12 @@ impl Ok(Self { status: enums::AttemptStatus::from(item.response.status), response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId( - item.response.order_id.clone(), - ), + resource_id: types::ResponseId::ConnectorTransactionId(item.response.id.clone()), redirection_data: None, mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: Some(item.response.order_id.clone()), + connector_response_reference_id: item.response.order_id.clone(), }), ..item.data }) From 39f255b4b209588dec35d780078c2ab7ceb37b10 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Thu, 30 Nov 2023 13:06:35 +0530 Subject: [PATCH 134/146] feat(core): Add ability to verify connector credentials before integrating the connector (#2986) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/admin.rs | 32 ++++ crates/api_models/src/lib.rs | 1 + crates/api_models/src/verify_connector.rs | 11 ++ crates/router/src/consts.rs | 5 + crates/router/src/core.rs | 2 + crates/router/src/core/verify_connector.rs | 63 ++++++ crates/router/src/routes.rs | 2 + crates/router/src/routes/app.rs | 6 + crates/router/src/routes/lock_utils.rs | 4 +- crates/router/src/routes/verify_connector.rs | 28 +++ crates/router/src/types.rs | 74 ++++++- crates/router/src/types/api.rs | 2 + .../router/src/types/api/verify_connector.rs | 181 ++++++++++++++++++ .../src/types/api/verify_connector/paypal.rs | 54 ++++++ .../src/types/api/verify_connector/stripe.rs | 36 ++++ crates/router/src/utils.rs | 2 + crates/router/src/utils/verify_connector.rs | 49 +++++ crates/router_env/src/logger/types.rs | 2 + 18 files changed, 552 insertions(+), 2 deletions(-) create mode 100644 crates/api_models/src/verify_connector.rs create mode 100644 crates/router/src/core/verify_connector.rs create mode 100644 crates/router/src/routes/verify_connector.rs create mode 100644 crates/router/src/types/api/verify_connector.rs create mode 100644 crates/router/src/types/api/verify_connector/paypal.rs create mode 100644 crates/router/src/types/api/verify_connector/stripe.rs create mode 100644 crates/router/src/utils/verify_connector.rs diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index efde4a048323..6bb4fd4afa0f 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use common_utils::{ crypto::{Encryptable, OptionalEncryptableName}, pii, @@ -614,6 +616,36 @@ pub struct MerchantConnectorCreate { pub status: Option, } +// Different patterns of authentication. +#[derive(Default, Debug, Clone, serde::Deserialize, serde::Serialize)] +#[serde(tag = "auth_type")] +pub enum ConnectorAuthType { + TemporaryAuth, + HeaderKey { + api_key: Secret, + }, + BodyKey { + api_key: Secret, + key1: Secret, + }, + SignatureKey { + api_key: Secret, + key1: Secret, + api_secret: Secret, + }, + MultiAuthKey { + api_key: Secret, + key1: Secret, + api_secret: Secret, + key2: Secret, + }, + CurrencyAuthKey { + auth_key_map: HashMap, + }, + #[default] + NoKey, +} + #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(deny_unknown_fields)] pub struct MerchantConnectorWebhookDetails { diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index ab40a96582bb..8ef40d319140 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -27,4 +27,5 @@ pub mod routing; pub mod surcharge_decision_configs; pub mod user; pub mod verifications; +pub mod verify_connector; pub mod webhooks; diff --git a/crates/api_models/src/verify_connector.rs b/crates/api_models/src/verify_connector.rs new file mode 100644 index 000000000000..1db5a19a030a --- /dev/null +++ b/crates/api_models/src/verify_connector.rs @@ -0,0 +1,11 @@ +use common_utils::events::{ApiEventMetric, ApiEventsType}; + +use crate::{admin, enums}; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct VerifyConnectorRequest { + pub connector_name: enums::Connector, + pub connector_account_details: admin::ConnectorAuthType, +} + +common_utils::impl_misc_api_event_type!(VerifyConnectorRequest); diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 61072d06221b..49c5cfacad1f 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -65,3 +65,8 @@ pub const JWT_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24 * 2; // 2 days #[cfg(feature = "email")] pub const EMAIL_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24; // 1 day pub const ROLE_ID_ORGANIZATION_ADMIN: &str = "org_admin"; + +#[cfg(feature = "olap")] +pub const VERIFY_CONNECTOR_ID_PREFIX: &str = "conn_verify"; +#[cfg(feature = "olap")] +pub const VERIFY_CONNECTOR_MERCHANT_ID: &str = "test_merchant"; diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index cff2dc8e58f1..30fe1a1ce8cb 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -28,4 +28,6 @@ pub mod user; pub mod utils; #[cfg(all(feature = "olap", feature = "kms"))] pub mod verification; +#[cfg(feature = "olap")] +pub mod verify_connector; pub mod webhooks; diff --git a/crates/router/src/core/verify_connector.rs b/crates/router/src/core/verify_connector.rs new file mode 100644 index 000000000000..e837e8b8b259 --- /dev/null +++ b/crates/router/src/core/verify_connector.rs @@ -0,0 +1,63 @@ +use api_models::{enums::Connector, verify_connector::VerifyConnectorRequest}; +use error_stack::{IntoReport, ResultExt}; + +use crate::{ + connector, + core::errors, + services, + types::{ + api, + api::verify_connector::{self as types, VerifyConnector}, + }, + utils::verify_connector as utils, + AppState, +}; + +pub async fn verify_connector_credentials( + state: AppState, + req: VerifyConnectorRequest, +) -> errors::RouterResponse<()> { + let boxed_connector = api::ConnectorData::get_connector_by_name( + &state.conf.connectors, + &req.connector_name.to_string(), + api::GetToken::Connector, + None, + ) + .change_context(errors::ApiErrorResponse::IncorrectConnectorNameGiven)?; + + let card_details = utils::get_test_card_details(req.connector_name)? + .ok_or(errors::ApiErrorResponse::FlowNotSupported { + flow: "Verify credentials".to_string(), + connector: req.connector_name.to_string(), + }) + .into_report()?; + + match req.connector_name { + Connector::Stripe => { + connector::Stripe::verify( + &state, + types::VerifyConnectorData { + connector: *boxed_connector.connector, + connector_auth: req.connector_account_details.into(), + card_details, + }, + ) + .await + } + Connector::Paypal => connector::Paypal::get_access_token( + &state, + types::VerifyConnectorData { + connector: *boxed_connector.connector, + connector_auth: req.connector_account_details.into(), + card_details, + }, + ) + .await + .map(|_| services::ApplicationResponse::StatusOk), + _ => Err(errors::ApiErrorResponse::FlowNotSupported { + flow: "Verify credentials".to_string(), + connector: req.connector_name.to_string(), + }) + .into_report(), + } +} diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 37cc1339e1a1..22c2610d3255 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -29,6 +29,8 @@ pub mod routing; pub mod user; #[cfg(all(feature = "olap", feature = "kms"))] pub mod verification; +#[cfg(feature = "olap")] +pub mod verify_connector; pub mod webhooks; pub mod locker_migration; diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 80993429c4e2..2a7e1ab61905 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -30,6 +30,8 @@ use super::{cache::*, health::*}; use super::{configs::*, customers::*, mandates::*, payments::*, refunds::*}; #[cfg(feature = "oltp")] use super::{ephemeral_key::*, payment_methods::*, webhooks::*}; +#[cfg(feature = "olap")] +use crate::routes::verify_connector::payment_connector_verify; pub use crate::{ configs::settings, db::{StorageImpl, StorageInterface}, @@ -548,6 +550,10 @@ impl MerchantConnectorAccount { use super::admin::*; route = route + .service( + web::resource("/connectors/verify") + .route(web::post().to(payment_connector_verify)), + ) .service( web::resource("/{merchant_id}/connectors") .route(web::post().to(payment_connector_create)) diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 5c2ad123749c..c7369b9e4d52 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -147,7 +147,9 @@ impl From for ApiIdentifier { | Flow::GsmRuleUpdate | Flow::GsmRuleDelete => Self::Gsm, - Flow::UserConnectAccount | Flow::ChangePassword => Self::User, + Flow::UserConnectAccount | Flow::ChangePassword | Flow::VerifyPaymentConnector => { + Self::User + } } } } diff --git a/crates/router/src/routes/verify_connector.rs b/crates/router/src/routes/verify_connector.rs new file mode 100644 index 000000000000..bfb1b781ada4 --- /dev/null +++ b/crates/router/src/routes/verify_connector.rs @@ -0,0 +1,28 @@ +use actix_web::{web, HttpRequest, HttpResponse}; +use api_models::verify_connector::VerifyConnectorRequest; +use router_env::{instrument, tracing, Flow}; + +use super::AppState; +use crate::{ + core::{api_locking, verify_connector}, + services::{self, authentication as auth, authorization::permissions::Permission}, +}; + +#[instrument(skip_all, fields(flow = ?Flow::VerifyPaymentConnector))] +pub async fn payment_connector_verify( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::VerifyPaymentConnector; + Box::pin(services::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, _: (), req| verify_connector::verify_connector_credentials(state, req), + &auth::JWTAuth(Permission::MerchantConnectorAccountWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index cd37fbb549d9..c3118f0c05be 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -33,7 +33,7 @@ use crate::{ payments::{PaymentData, RecurringMandatePaymentData}, }, services, - types::storage::payment_attempt::PaymentAttemptExt, + types::{storage::payment_attempt::PaymentAttemptExt, transformers::ForeignFrom}, utils::OptionExt, }; @@ -942,6 +942,78 @@ pub enum ConnectorAuthType { NoKey, } +impl From for ConnectorAuthType { + fn from(value: api_models::admin::ConnectorAuthType) -> Self { + match value { + api_models::admin::ConnectorAuthType::TemporaryAuth => Self::TemporaryAuth, + api_models::admin::ConnectorAuthType::HeaderKey { api_key } => { + Self::HeaderKey { api_key } + } + api_models::admin::ConnectorAuthType::BodyKey { api_key, key1 } => { + Self::BodyKey { api_key, key1 } + } + api_models::admin::ConnectorAuthType::SignatureKey { + api_key, + key1, + api_secret, + } => Self::SignatureKey { + api_key, + key1, + api_secret, + }, + api_models::admin::ConnectorAuthType::MultiAuthKey { + api_key, + key1, + api_secret, + key2, + } => Self::MultiAuthKey { + api_key, + key1, + api_secret, + key2, + }, + api_models::admin::ConnectorAuthType::CurrencyAuthKey { auth_key_map } => { + Self::CurrencyAuthKey { auth_key_map } + } + api_models::admin::ConnectorAuthType::NoKey => Self::NoKey, + } + } +} + +impl ForeignFrom for api_models::admin::ConnectorAuthType { + fn foreign_from(from: ConnectorAuthType) -> Self { + match from { + ConnectorAuthType::TemporaryAuth => Self::TemporaryAuth, + ConnectorAuthType::HeaderKey { api_key } => Self::HeaderKey { api_key }, + ConnectorAuthType::BodyKey { api_key, key1 } => Self::BodyKey { api_key, key1 }, + ConnectorAuthType::SignatureKey { + api_key, + key1, + api_secret, + } => Self::SignatureKey { + api_key, + key1, + api_secret, + }, + ConnectorAuthType::MultiAuthKey { + api_key, + key1, + api_secret, + key2, + } => Self::MultiAuthKey { + api_key, + key1, + api_secret, + key2, + }, + ConnectorAuthType::CurrencyAuthKey { auth_key_map } => { + Self::CurrencyAuthKey { auth_key_map } + } + ConnectorAuthType::NoKey => Self::NoKey, + } + } +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ConnectorsList { pub connectors: Vec, diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index bcb3a9add553..96bcaca3ed5d 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -13,6 +13,8 @@ pub mod payments; pub mod payouts; pub mod refunds; pub mod routing; +#[cfg(feature = "olap")] +pub mod verify_connector; pub mod webhooks; use std::{fmt::Debug, str::FromStr}; diff --git a/crates/router/src/types/api/verify_connector.rs b/crates/router/src/types/api/verify_connector.rs new file mode 100644 index 000000000000..3e3511ccb98f --- /dev/null +++ b/crates/router/src/types/api/verify_connector.rs @@ -0,0 +1,181 @@ +pub mod paypal; +pub mod stripe; + +use error_stack::{IntoReport, ResultExt}; + +use crate::{ + consts, + core::errors, + services, + services::ConnectorIntegration, + types::{self, api, storage::enums as storage_enums}, + AppState, +}; + +#[derive(Clone, Debug)] +pub struct VerifyConnectorData { + pub connector: &'static (dyn types::api::Connector + Sync), + pub connector_auth: types::ConnectorAuthType, + pub card_details: api::Card, +} + +impl VerifyConnectorData { + fn get_payment_authorize_data(&self) -> types::PaymentsAuthorizeData { + types::PaymentsAuthorizeData { + payment_method_data: api::PaymentMethodData::Card(self.card_details.clone()), + email: None, + amount: 1000, + confirm: true, + currency: storage_enums::Currency::USD, + mandate_id: None, + webhook_url: None, + customer_id: None, + off_session: None, + browser_info: None, + session_token: None, + order_details: None, + order_category: None, + capture_method: None, + enrolled_for_3ds: false, + router_return_url: None, + surcharge_details: None, + setup_future_usage: None, + payment_experience: None, + payment_method_type: None, + statement_descriptor: None, + setup_mandate_details: None, + complete_authorize_url: None, + related_transaction_id: None, + statement_descriptor_suffix: None, + } + } + + fn get_router_data( + &self, + request_data: R1, + access_token: Option, + ) -> types::RouterData { + let attempt_id = + common_utils::generate_id_with_default_len(consts::VERIFY_CONNECTOR_ID_PREFIX); + types::RouterData { + flow: std::marker::PhantomData, + status: storage_enums::AttemptStatus::Started, + request: request_data, + response: Err(errors::ApiErrorResponse::InternalServerError.into()), + connector: self.connector.id().to_string(), + auth_type: storage_enums::AuthenticationType::NoThreeDs, + test_mode: None, + return_url: None, + attempt_id: attempt_id.clone(), + description: None, + customer_id: None, + merchant_id: consts::VERIFY_CONNECTOR_MERCHANT_ID.to_string(), + reference_id: None, + access_token, + session_token: None, + payment_method: storage_enums::PaymentMethod::Card, + amount_captured: None, + preprocessing_id: None, + payment_method_id: None, + connector_customer: None, + connector_auth_type: self.connector_auth.clone(), + connector_meta_data: None, + payment_method_token: None, + connector_api_version: None, + recurring_mandate_payment_data: None, + connector_request_reference_id: attempt_id, + address: types::PaymentAddress { + shipping: None, + billing: None, + }, + payment_id: common_utils::generate_id_with_default_len( + consts::VERIFY_CONNECTOR_ID_PREFIX, + ), + #[cfg(feature = "payouts")] + payout_method_data: None, + #[cfg(feature = "payouts")] + quote_id: None, + payment_method_balance: None, + connector_http_status_code: None, + external_latency: None, + apple_pay_flow: None, + } + } +} + +#[async_trait::async_trait] +pub trait VerifyConnector { + async fn verify( + state: &AppState, + connector_data: VerifyConnectorData, + ) -> errors::RouterResponse<()> { + let authorize_data = connector_data.get_payment_authorize_data(); + let access_token = Self::get_access_token(state, connector_data.clone()).await?; + let router_data = connector_data.get_router_data(authorize_data, access_token); + + let request = connector_data + .connector + .build_request(&router_data, &state.conf.connectors) + .change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "Payment request cannot be built".to_string(), + })? + .ok_or(errors::ApiErrorResponse::InternalServerError)?; + + let response = services::call_connector_api(&state.to_owned(), request) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + match response { + Ok(_) => Ok(services::ApplicationResponse::StatusOk), + Err(error_response) => { + Self::handle_payment_error_response::< + api::Authorize, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >(connector_data.connector, error_response) + .await + } + } + } + + async fn get_access_token( + _state: &AppState, + _connector_data: VerifyConnectorData, + ) -> errors::CustomResult, errors::ApiErrorResponse> { + // AccessToken is None for the connectors without the AccessToken Flow. + // If a connector has that, then it should override this implementation. + Ok(None) + } + + async fn handle_payment_error_response( + connector: &(dyn types::api::Connector + Sync), + error_response: types::Response, + ) -> errors::RouterResponse<()> + where + dyn types::api::Connector + Sync: ConnectorIntegration, + { + let error = connector + .get_error_response(error_response) + .change_context(errors::ApiErrorResponse::InternalServerError)?; + Err(errors::ApiErrorResponse::InvalidRequestData { + message: error.reason.unwrap_or(error.message), + }) + .into_report() + } + + async fn handle_access_token_error_response( + connector: &(dyn types::api::Connector + Sync), + error_response: types::Response, + ) -> errors::RouterResult> + where + dyn types::api::Connector + Sync: ConnectorIntegration, + { + let error = connector + .get_error_response(error_response) + .change_context(errors::ApiErrorResponse::InternalServerError)?; + Err(errors::ApiErrorResponse::InvalidRequestData { + message: error.reason.unwrap_or(error.message), + }) + .into_report() + } +} diff --git a/crates/router/src/types/api/verify_connector/paypal.rs b/crates/router/src/types/api/verify_connector/paypal.rs new file mode 100644 index 000000000000..33e848f909df --- /dev/null +++ b/crates/router/src/types/api/verify_connector/paypal.rs @@ -0,0 +1,54 @@ +use error_stack::ResultExt; + +use super::{VerifyConnector, VerifyConnectorData}; +use crate::{ + connector, + core::errors, + routes::AppState, + services, + types::{self, api}, +}; + +#[async_trait::async_trait] +impl VerifyConnector for connector::Paypal { + async fn get_access_token( + state: &AppState, + connector_data: VerifyConnectorData, + ) -> errors::CustomResult, errors::ApiErrorResponse> { + let token_data: types::AccessTokenRequestData = + connector_data.connector_auth.clone().try_into()?; + let router_data = connector_data.get_router_data(token_data, None); + + let request = connector_data + .connector + .build_request(&router_data, &state.conf.connectors) + .change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "Payment request cannot be built".to_string(), + })? + .ok_or(errors::ApiErrorResponse::InternalServerError)?; + + let response = services::call_connector_api(&state.to_owned(), request) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + match response { + Ok(res) => Some( + connector_data + .connector + .handle_response(&router_data, res) + .change_context(errors::ApiErrorResponse::InternalServerError)? + .response + .map_err(|_| errors::ApiErrorResponse::InternalServerError.into()), + ) + .transpose(), + Err(response_data) => { + Self::handle_access_token_error_response::< + api::AccessTokenAuth, + types::AccessTokenRequestData, + types::AccessToken, + >(connector_data.connector, response_data) + .await + } + } + } +} diff --git a/crates/router/src/types/api/verify_connector/stripe.rs b/crates/router/src/types/api/verify_connector/stripe.rs new file mode 100644 index 000000000000..ece9fa15a1d9 --- /dev/null +++ b/crates/router/src/types/api/verify_connector/stripe.rs @@ -0,0 +1,36 @@ +use error_stack::{IntoReport, ResultExt}; +use router_env::env; + +use super::VerifyConnector; +use crate::{ + connector, + core::errors, + services::{self, ConnectorIntegration}, + types, +}; + +#[async_trait::async_trait] +impl VerifyConnector for connector::Stripe { + async fn handle_payment_error_response( + connector: &(dyn types::api::Connector + Sync), + error_response: types::Response, + ) -> errors::RouterResponse<()> + where + dyn types::api::Connector + Sync: ConnectorIntegration, + { + let error = connector + .get_error_response(error_response) + .change_context(errors::ApiErrorResponse::InternalServerError)?; + match (env::which(), error.code.as_str()) { + // In situations where an attempt is made to process a payment using a + // Stripe production key along with a test card (which verify_connector is using), + // Stripe will respond with a "card_declined" error. In production, + // when this scenario occurs we will send back an "Ok" response. + (env::Env::Production, "card_declined") => Ok(services::ApplicationResponse::StatusOk), + _ => Err(errors::ApiErrorResponse::InvalidRequestData { + message: error.reason.unwrap_or(error.message), + }) + .into_report(), + } + } +} diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index c936ee858c17..81968cd9b628 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -6,6 +6,8 @@ pub mod ext_traits; pub mod storage_partitioning; #[cfg(feature = "olap")] pub mod user; +#[cfg(feature = "olap")] +pub mod verify_connector; use std::fmt::Debug; diff --git a/crates/router/src/utils/verify_connector.rs b/crates/router/src/utils/verify_connector.rs new file mode 100644 index 000000000000..6ad683d63ba1 --- /dev/null +++ b/crates/router/src/utils/verify_connector.rs @@ -0,0 +1,49 @@ +use api_models::enums::Connector; +use error_stack::{IntoReport, ResultExt}; + +use crate::{core::errors, types::api}; + +pub fn generate_card_from_details( + card_number: String, + card_exp_year: String, + card_exp_month: String, + card_cvv: String, +) -> errors::RouterResult { + Ok(api::Card { + card_number: card_number + .parse() + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error while parsing card number")?, + card_issuer: None, + card_cvc: masking::Secret::new(card_cvv), + card_network: None, + card_exp_year: masking::Secret::new(card_exp_year), + card_exp_month: masking::Secret::new(card_exp_month), + card_holder_name: masking::Secret::new("HyperSwitch".to_string()), + nick_name: None, + card_type: None, + card_issuing_country: None, + bank_code: None, + }) +} + +pub fn get_test_card_details(connector_name: Connector) -> errors::RouterResult> { + match connector_name { + Connector::Stripe => Some(generate_card_from_details( + "4242424242424242".to_string(), + "2025".to_string(), + "12".to_string(), + "100".to_string(), + )) + .transpose(), + Connector::Paypal => Some(generate_card_from_details( + "4111111111111111".to_string(), + "2025".to_string(), + "02".to_string(), + "123".to_string(), + )) + .transpose(), + _ => Ok(None), + } +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 2a174f42eb63..c254f89b4eef 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -259,6 +259,8 @@ pub enum Flow { DecisionManagerRetrieveConfig, /// Change password flow ChangePassword, + /// Payment Connector Verify + VerifyPaymentConnector, } /// From 663754d629d59a17ba9d4985fe04f9404ceb16b7 Mon Sep 17 00:00:00 2001 From: SamraatBansal <55536657+SamraatBansal@users.noreply.github.com> Date: Thu, 30 Nov 2023 13:59:35 +0530 Subject: [PATCH 135/146] fix(connector): move authorised status to charged in setup mandate (#3017) --- crates/router/src/connector/cybersource.rs | 17 ++--- .../src/connector/cybersource/transformers.rs | 74 ++++++++++++++++++- 2 files changed, 80 insertions(+), 11 deletions(-) diff --git a/crates/router/src/connector/cybersource.rs b/crates/router/src/connector/cybersource.rs index 1868611184f9..1de107af086d 100644 --- a/crates/router/src/connector/cybersource.rs +++ b/crates/router/src/connector/cybersource.rs @@ -307,18 +307,15 @@ impl data: &types::SetupMandateRouterData, res: types::Response, ) -> CustomResult { - let response: cybersource::CybersourcePaymentsResponse = res + let response: cybersource::CybersourceSetupMandatesResponse = res .response - .parse_struct("CybersourceMandateResponse") + .parse_struct("CybersourceSetupMandatesResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - types::RouterData::try_from(( - types::ResponseRouterData { - response, - data: data.clone(), - http_code: res.status_code, - }, - false, - )) + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) } fn get_error_response( diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 656c45b6d6b6..81df29966725 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -499,6 +499,16 @@ pub struct CybersourcePaymentsResponse { token_information: Option, } +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceSetupMandatesResponse { + id: String, + status: CybersourcePaymentStatus, + error_information: Option, + client_reference_information: Option, + token_information: Option, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ClientReferenceInformation { @@ -553,7 +563,69 @@ impl reason: Some(error.reason), status_code: item.http_code, attempt_status: None, - connector_transaction_id: None, + connector_transaction_id: Some(item.response.id), + }), + _ => Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + item.response.id.clone(), + ), + redirection_data: None, + mandate_reference, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: item + .response + .client_reference_information + .map(|cref| cref.code) + .unwrap_or(Some(item.response.id)), + }), + }, + ..item.data + }) + } +} + +impl + TryFrom< + types::ResponseRouterData< + F, + CybersourceSetupMandatesResponse, + T, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + CybersourceSetupMandatesResponse, + T, + types::PaymentsResponseData, + >, + ) -> Result { + let mandate_reference = + item.response + .token_information + .map(|token_info| types::MandateReference { + connector_mandate_id: Some(token_info.instrument_identifier.id), + payment_method_id: None, + }); + let mut mandate_status: enums::AttemptStatus = item.response.status.into(); + if matches!(mandate_status, enums::AttemptStatus::Authorized) { + //In case of zero auth mandates we want to make the payment reach the terminal status so we are converting the authorized status to charged as well. + mandate_status = enums::AttemptStatus::Charged + } + Ok(Self { + status: mandate_status, + response: match item.response.error_information { + Some(error) => Err(types::ErrorResponse { + code: consts::NO_ERROR_CODE.to_string(), + message: error.message, + reason: Some(error.reason), + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: Some(item.response.id), }), _ => Ok(types::PaymentsResponseData::TransactionResponse { resource_id: types::ResponseId::ConnectorTransactionId( From b1fe76a82b4026d6eaa3baf4356378040880a458 Mon Sep 17 00:00:00 2001 From: Shanks Date: Thu, 30 Nov 2023 14:47:27 +0530 Subject: [PATCH 136/146] fix(router): use default value for the routing algorithm column during business profile creation (#2791) --- crates/router/src/types/api/admin.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/router/src/types/api/admin.rs b/crates/router/src/types/api/admin.rs index 6bbe9149f4d7..fe99d084223a 100644 --- a/crates/router/src/types/api/admin.rs +++ b/crates/router/src/types/api/admin.rs @@ -124,9 +124,10 @@ impl ForeignTryFrom<(domain::MerchantAccount, BusinessProfileCreate)> .unwrap_or(merchant_account.redirect_to_merchant_with_http_post), webhook_details: webhook_details.or(merchant_account.webhook_details), metadata: request.metadata, - routing_algorithm: request - .routing_algorithm - .or(merchant_account.routing_algorithm), + routing_algorithm: Some(serde_json::json!({ + "algorithm_id": null, + "timestamp": 0 + })), intent_fulfillment_time: request .intent_fulfillment_time .map(i64::from) From 6a2e4ab4169820f35e953a949bd2e82e7f098ed2 Mon Sep 17 00:00:00 2001 From: Apoorv Dixit <64925866+apoorvdixit88@users.noreply.github.com> Date: Thu, 30 Nov 2023 14:58:37 +0530 Subject: [PATCH 137/146] feat(user): add support for dashboard metadata (#3000) Co-authored-by: Rachit Naithani <81706961+racnan@users.noreply.github.com> Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Co-authored-by: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> Co-authored-by: Arjun Karthik Co-authored-by: Brian Silah <71752651+unpervertedkid@users.noreply.github.com> --- crates/api_models/src/events/user.rs | 15 +- crates/api_models/src/user.rs | 1 + .../api_models/src/user/dashboard_metadata.rs | 110 ++++ crates/diesel_models/src/enums.rs | 36 ++ crates/diesel_models/src/query.rs | 1 + .../src/query/dashboard_metadata.rs | 64 +++ crates/diesel_models/src/schema.rs | 25 + crates/diesel_models/src/user.rs | 2 + .../src/user/dashboard_metadata.rs | 35 ++ crates/router/src/core/errors/user.rs | 26 +- crates/router/src/core/user.rs | 2 + .../src/core/user/dashboard_metadata.rs | 537 ++++++++++++++++++ crates/router/src/db.rs | 2 + crates/router/src/db/dashboard_metadata.rs | 184 ++++++ crates/router/src/db/kafka_store.rs | 38 +- crates/router/src/routes/app.rs | 5 + crates/router/src/routes/lock_utils.rs | 8 +- crates/router/src/routes/user.rs | 57 +- crates/router/src/types/domain/user.rs | 2 + .../types/domain/user/dashboard_metadata.rs | 56 ++ crates/router/src/types/storage.rs | 11 +- .../src/types/storage/dashboard_metadata.rs | 1 + crates/router/src/utils/user.rs | 1 + .../src/utils/user/dashboard_metadata.rs | 162 ++++++ crates/router_env/src/logger/types.rs | 4 + crates/storage_impl/src/mock_db.rs | 2 + .../down.sql | 3 + .../up.sql | 15 + 28 files changed, 1389 insertions(+), 16 deletions(-) create mode 100644 crates/api_models/src/user/dashboard_metadata.rs create mode 100644 crates/diesel_models/src/query/dashboard_metadata.rs create mode 100644 crates/diesel_models/src/user/dashboard_metadata.rs create mode 100644 crates/router/src/core/user/dashboard_metadata.rs create mode 100644 crates/router/src/db/dashboard_metadata.rs create mode 100644 crates/router/src/types/domain/user/dashboard_metadata.rs create mode 100644 crates/router/src/types/storage/dashboard_metadata.rs create mode 100644 crates/router/src/utils/user/dashboard_metadata.rs create mode 100644 migrations/2023-11-23-100644_create_dashboard_metadata_table/down.sql create mode 100644 migrations/2023-11-23-100644_create_dashboard_metadata_table/up.sql diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index 4e9f2f284173..edfdcf1d6652 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -1,6 +1,11 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; -use crate::user::{ChangePasswordRequest, ConnectAccountRequest, ConnectAccountResponse}; +use crate::user::{ + dashboard_metadata::{ + GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest, + }, + ChangePasswordRequest, ConnectAccountRequest, ConnectAccountResponse, +}; impl ApiEventMetric for ConnectAccountResponse { fn get_api_event_type(&self) -> Option { @@ -13,4 +18,10 @@ impl ApiEventMetric for ConnectAccountResponse { impl ApiEventMetric for ConnectAccountRequest {} -common_utils::impl_misc_api_event_type!(ChangePasswordRequest); +common_utils::impl_misc_api_event_type!( + ChangePasswordRequest, + GetMultipleMetaDataPayload, + GetMetaDataResponse, + GetMetaDataRequest, + SetMetaDataRequest +); diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index 41ea9cc5193a..84659432aa6a 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -1,5 +1,6 @@ use common_utils::pii; use masking::Secret; +pub mod dashboard_metadata; #[derive(serde::Deserialize, Debug, Clone, serde::Serialize)] pub struct ConnectAccountRequest { diff --git a/crates/api_models/src/user/dashboard_metadata.rs b/crates/api_models/src/user/dashboard_metadata.rs new file mode 100644 index 000000000000..04cda3bd7075 --- /dev/null +++ b/crates/api_models/src/user/dashboard_metadata.rs @@ -0,0 +1,110 @@ +use masking::Secret; +use strum::EnumString; + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub enum SetMetaDataRequest { + ProductionAgreement(ProductionAgreementRequest), + SetupProcessor(SetupProcessor), + ConfigureEndpoint, + SetupComplete, + FirstProcessorConnected(ProcessorConnected), + SecondProcessorConnected(ProcessorConnected), + ConfiguredRouting(ConfiguredRouting), + TestPayment(TestPayment), + IntegrationMethod(IntegrationMethod), + IntegrationCompleted, + SPRoutingConfigured(ConfiguredRouting), + SPTestPayment, + DownloadWoocom, + ConfigureWoocom, + SetupWoocomWebhook, + IsMultipleConfiguration, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct ProductionAgreementRequest { + pub version: String, + #[serde(skip_deserializing)] + pub ip_address: Option>, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct SetupProcessor { + pub connector_id: String, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct ProcessorConnected { + pub processor_id: String, + pub processor_name: String, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct ConfiguredRouting { + pub routing_id: String, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct TestPayment { + pub payment_id: String, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct IntegrationMethod { + pub integration_type: String, +} + +#[derive(Debug, serde::Deserialize, EnumString, serde::Serialize)] +pub enum GetMetaDataRequest { + ProductionAgreement, + SetupProcessor, + ConfigureEndpoint, + SetupComplete, + FirstProcessorConnected, + SecondProcessorConnected, + ConfiguredRouting, + TestPayment, + IntegrationMethod, + IntegrationCompleted, + StripeConnected, + PaypalConnected, + SPRoutingConfigured, + SPTestPayment, + DownloadWoocom, + ConfigureWoocom, + SetupWoocomWebhook, + IsMultipleConfiguration, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[serde(transparent)] +pub struct GetMultipleMetaDataPayload { + pub results: Vec, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct GetMultipleMetaDataRequest { + pub keys: String, +} + +#[derive(Debug, serde::Serialize)] +pub enum GetMetaDataResponse { + ProductionAgreement(bool), + SetupProcessor(Option), + ConfigureEndpoint(bool), + SetupComplete(bool), + FirstProcessorConnected(Option), + SecondProcessorConnected(Option), + ConfiguredRouting(Option), + TestPayment(Option), + IntegrationMethod(Option), + IntegrationCompleted(bool), + StripeConnected(Option), + PaypalConnected(Option), + SPRoutingConfigured(Option), + SPTestPayment(bool), + DownloadWoocom(bool), + ConfigureWoocom(bool), + SetupWoocomWebhook(bool), + IsMultipleConfiguration(bool), +} diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index dc4a7614f587..3ddd85f37891 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -425,3 +425,39 @@ pub enum UserStatus { #[default] InvitationSent, } + +#[derive( + Clone, + Copy, + Debug, + Eq, + PartialEq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumString, + frunk::LabelledGeneric, +)] +#[router_derive::diesel_enum(storage_type = "text")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum DashboardMetadata { + ProductionAgreement, + SetupProcessor, + ConfigureEndpoint, + SetupComplete, + FirstProcessorConnected, + SecondProcessorConnected, + ConfiguredRouting, + TestPayment, + IntegrationMethod, + IntegrationCompleted, + StripeConnected, + PaypalConnected, + SpRoutingConfigured, + SpTestPayment, + DownloadWoocom, + ConfigureWoocom, + SetupWoocomWebhook, + IsMultipleConfiguration, +} diff --git a/crates/diesel_models/src/query.rs b/crates/diesel_models/src/query.rs index cf5a993c2686..b0537d0a287b 100644 --- a/crates/diesel_models/src/query.rs +++ b/crates/diesel_models/src/query.rs @@ -6,6 +6,7 @@ pub mod cards_info; pub mod configs; pub mod customers; +pub mod dashboard_metadata; pub mod dispute; pub mod events; pub mod file; diff --git a/crates/diesel_models/src/query/dashboard_metadata.rs b/crates/diesel_models/src/query/dashboard_metadata.rs new file mode 100644 index 000000000000..03e4a2dab38b --- /dev/null +++ b/crates/diesel_models/src/query/dashboard_metadata.rs @@ -0,0 +1,64 @@ +use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; +use router_env::tracing::{self, instrument}; + +use crate::{ + enums, + query::generics, + schema::dashboard_metadata::dsl, + user::dashboard_metadata::{DashboardMetadata, DashboardMetadataNew}, + PgPooledConn, StorageResult, +}; + +impl DashboardMetadataNew { + #[instrument(skip(conn))] + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } +} + +impl DashboardMetadata { + pub async fn find_user_scoped_dashboard_metadata( + conn: &PgPooledConn, + user_id: String, + merchant_id: String, + org_id: String, + data_types: Vec, + ) -> StorageResult> { + let predicate = dsl::user_id + .eq(user_id) + .and(dsl::merchant_id.eq(merchant_id)) + .and(dsl::org_id.eq(org_id)) + .and(dsl::data_key.eq_any(data_types)); + + generics::generic_filter::<::Table, _, _, _>( + conn, + predicate, + None, + None, + Some(dsl::last_modified_at.asc()), + ) + .await + } + + pub async fn find_merchant_scoped_dashboard_metadata( + conn: &PgPooledConn, + merchant_id: String, + org_id: String, + data_types: Vec, + ) -> StorageResult> { + let predicate = dsl::user_id + .is_null() + .and(dsl::merchant_id.eq(merchant_id)) + .and(dsl::org_id.eq(org_id)) + .and(dsl::data_key.eq_any(data_types)); + + generics::generic_filter::<::Table, _, _, _>( + conn, + predicate, + None, + None, + Some(dsl::last_modified_at.asc()), + ) + .await + } +} diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 33400635f052..6cab6d5730d0 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -183,6 +183,30 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + dashboard_metadata (id) { + id -> Int4, + #[max_length = 64] + user_id -> Nullable, + #[max_length = 64] + merchant_id -> Varchar, + #[max_length = 64] + org_id -> Varchar, + #[max_length = 64] + data_key -> Varchar, + data_value -> Json, + #[max_length = 64] + created_by -> Varchar, + created_at -> Timestamp, + #[max_length = 64] + last_modified_by -> Varchar, + last_modified_at -> Timestamp, + } +} + diesel::table! { use diesel::sql_types::*; use crate::enums::diesel_exports::*; @@ -965,6 +989,7 @@ diesel::allow_tables_to_appear_in_same_query!( cards_info, configs, customers, + dashboard_metadata, dispute, events, file_metadata, diff --git a/crates/diesel_models/src/user.rs b/crates/diesel_models/src/user.rs index 6a2e864b291c..4eec710ea185 100644 --- a/crates/diesel_models/src/user.rs +++ b/crates/diesel_models/src/user.rs @@ -5,6 +5,8 @@ use time::PrimitiveDateTime; use crate::schema::users; +pub mod dashboard_metadata; + #[derive(Clone, Debug, Identifiable, Queryable)] #[diesel(table_name = users)] pub struct User { diff --git a/crates/diesel_models/src/user/dashboard_metadata.rs b/crates/diesel_models/src/user/dashboard_metadata.rs new file mode 100644 index 000000000000..018808f1c0db --- /dev/null +++ b/crates/diesel_models/src/user/dashboard_metadata.rs @@ -0,0 +1,35 @@ +use diesel::{query_builder::AsChangeset, Identifiable, Insertable, Queryable}; +use time::PrimitiveDateTime; + +use crate::{enums, schema::dashboard_metadata}; + +#[derive(Clone, Debug, Identifiable, Queryable)] +#[diesel(table_name = dashboard_metadata)] +pub struct DashboardMetadata { + pub id: i32, + pub user_id: Option, + pub merchant_id: String, + pub org_id: String, + pub data_key: enums::DashboardMetadata, + pub data_value: serde_json::Value, + pub created_by: String, + pub created_at: PrimitiveDateTime, + pub last_modified_by: String, + pub last_modified_at: PrimitiveDateTime, +} + +#[derive( + router_derive::Setter, Clone, Debug, Insertable, router_derive::DebugAsDisplay, AsChangeset, +)] +#[diesel(table_name = dashboard_metadata)] +pub struct DashboardMetadataNew { + pub user_id: Option, + pub merchant_id: String, + pub org_id: String, + pub data_key: enums::DashboardMetadata, + pub data_value: serde_json::Value, + pub created_by: String, + pub created_at: PrimitiveDateTime, + pub last_modified_by: String, + pub last_modified_at: PrimitiveDateTime, +} diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs index b86c395b9814..f5c50e28ccc6 100644 --- a/crates/router/src/core/errors/user.rs +++ b/crates/router/src/core/errors/user.rs @@ -27,10 +27,16 @@ pub enum UserErrors { MerchantAccountCreationError(String), #[error("InvalidEmailError")] InvalidEmailError, - #[error("DuplicateOrganizationId")] - DuplicateOrganizationId, #[error("MerchantIdNotFound")] MerchantIdNotFound, + #[error("MetadataAlreadySet")] + MetadataAlreadySet, + #[error("DuplicateOrganizationId")] + DuplicateOrganizationId, + #[error("IpAddressParsingFailed")] + IpAddressParsingFailed, + #[error("InvalidMetadataRequest")] + InvalidMetadataRequest, } impl common_utils::errors::ErrorSwitch for UserErrors { @@ -77,15 +83,27 @@ impl common_utils::errors::ErrorSwitch { AER::BadRequest(ApiError::new(sub_code, 16, "Invalid Email", None)) } + Self::MerchantIdNotFound => { + AER::BadRequest(ApiError::new(sub_code, 18, "Invalid Merchant ID", None)) + } + Self::MetadataAlreadySet => { + AER::BadRequest(ApiError::new(sub_code, 19, "Metadata already set", None)) + } Self::DuplicateOrganizationId => AER::InternalServerError(ApiError::new( sub_code, 21, "An Organization with the id already exists", None, )), - Self::MerchantIdNotFound => { - AER::BadRequest(ApiError::new(sub_code, 18, "Invalid Merchant ID", None)) + Self::IpAddressParsingFailed => { + AER::InternalServerError(ApiError::new(sub_code, 24, "Something Went Wrong", None)) } + Self::InvalidMetadataRequest => AER::BadRequest(ApiError::new( + sub_code, + 26, + "Invalid Metadata Request", + None, + )), } } } diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 1dc0e2e1a112..9a199d09b8fd 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -13,6 +13,8 @@ use crate::{ types::domain, }; +pub mod dashboard_metadata; + pub async fn connect_account( state: AppState, request: api::ConnectAccountRequest, diff --git a/crates/router/src/core/user/dashboard_metadata.rs b/crates/router/src/core/user/dashboard_metadata.rs new file mode 100644 index 000000000000..de385fb8ed65 --- /dev/null +++ b/crates/router/src/core/user/dashboard_metadata.rs @@ -0,0 +1,537 @@ +use api_models::user::dashboard_metadata::{self as api, GetMultipleMetaDataPayload}; +use diesel_models::{ + enums::DashboardMetadata as DBEnum, user::dashboard_metadata::DashboardMetadata, +}; +use error_stack::ResultExt; + +use crate::{ + core::errors::{UserErrors, UserResponse, UserResult}, + routes::AppState, + services::{authentication::UserFromToken, ApplicationResponse}, + types::domain::{user::dashboard_metadata as types, MerchantKeyStore}, + utils::user::dashboard_metadata as utils, +}; + +pub async fn set_metadata( + state: AppState, + user: UserFromToken, + request: api::SetMetaDataRequest, +) -> UserResponse<()> { + let metadata_value = parse_set_request(request)?; + let metadata_key = DBEnum::from(&metadata_value); + + insert_metadata(&state, user, metadata_key, metadata_value).await?; + + Ok(ApplicationResponse::StatusOk) +} + +pub async fn get_multiple_metadata( + state: AppState, + user: UserFromToken, + request: GetMultipleMetaDataPayload, +) -> UserResponse> { + let metadata_keys: Vec = request.results.into_iter().map(parse_get_request).collect(); + + let metadata = fetch_metadata(&state, &user, metadata_keys.clone()).await?; + + let mut response = Vec::with_capacity(metadata_keys.len()); + for key in metadata_keys { + let data = metadata.iter().find(|ele| ele.data_key == key); + let resp; + if data.is_none() && utils::is_backfill_required(&key) { + let backfill_data = backfill_metadata(&state, &user, &key).await?; + resp = into_response(backfill_data.as_ref(), &key)?; + } else { + resp = into_response(data, &key)?; + } + response.push(resp); + } + + Ok(ApplicationResponse::Json(response)) +} + +fn parse_set_request(data_enum: api::SetMetaDataRequest) -> UserResult { + match data_enum { + api::SetMetaDataRequest::ProductionAgreement(req) => { + let ip_address = req + .ip_address + .ok_or(UserErrors::InternalServerError.into()) + .attach_printable("Error Getting Ip Address")?; + Ok(types::MetaData::ProductionAgreement( + types::ProductionAgreementValue { + version: req.version, + ip_address, + timestamp: common_utils::date_time::now(), + }, + )) + } + api::SetMetaDataRequest::SetupProcessor(req) => Ok(types::MetaData::SetupProcessor(req)), + api::SetMetaDataRequest::ConfigureEndpoint => Ok(types::MetaData::ConfigureEndpoint(true)), + api::SetMetaDataRequest::SetupComplete => Ok(types::MetaData::SetupComplete(true)), + api::SetMetaDataRequest::FirstProcessorConnected(req) => { + Ok(types::MetaData::FirstProcessorConnected(req)) + } + api::SetMetaDataRequest::SecondProcessorConnected(req) => { + Ok(types::MetaData::SecondProcessorConnected(req)) + } + api::SetMetaDataRequest::ConfiguredRouting(req) => { + Ok(types::MetaData::ConfiguredRouting(req)) + } + api::SetMetaDataRequest::TestPayment(req) => Ok(types::MetaData::TestPayment(req)), + api::SetMetaDataRequest::IntegrationMethod(req) => { + Ok(types::MetaData::IntegrationMethod(req)) + } + api::SetMetaDataRequest::IntegrationCompleted => { + Ok(types::MetaData::IntegrationCompleted(true)) + } + api::SetMetaDataRequest::SPRoutingConfigured(req) => { + Ok(types::MetaData::SPRoutingConfigured(req)) + } + api::SetMetaDataRequest::SPTestPayment => Ok(types::MetaData::SPTestPayment(true)), + api::SetMetaDataRequest::DownloadWoocom => Ok(types::MetaData::DownloadWoocom(true)), + api::SetMetaDataRequest::ConfigureWoocom => Ok(types::MetaData::ConfigureWoocom(true)), + api::SetMetaDataRequest::SetupWoocomWebhook => { + Ok(types::MetaData::SetupWoocomWebhook(true)) + } + api::SetMetaDataRequest::IsMultipleConfiguration => { + Ok(types::MetaData::IsMultipleConfiguration(true)) + } + } +} + +fn parse_get_request(data_enum: api::GetMetaDataRequest) -> DBEnum { + match data_enum { + api::GetMetaDataRequest::ProductionAgreement => DBEnum::ProductionAgreement, + api::GetMetaDataRequest::SetupProcessor => DBEnum::SetupProcessor, + api::GetMetaDataRequest::ConfigureEndpoint => DBEnum::ConfigureEndpoint, + api::GetMetaDataRequest::SetupComplete => DBEnum::SetupComplete, + api::GetMetaDataRequest::FirstProcessorConnected => DBEnum::FirstProcessorConnected, + api::GetMetaDataRequest::SecondProcessorConnected => DBEnum::SecondProcessorConnected, + api::GetMetaDataRequest::ConfiguredRouting => DBEnum::ConfiguredRouting, + api::GetMetaDataRequest::TestPayment => DBEnum::TestPayment, + api::GetMetaDataRequest::IntegrationMethod => DBEnum::IntegrationMethod, + api::GetMetaDataRequest::IntegrationCompleted => DBEnum::IntegrationCompleted, + api::GetMetaDataRequest::StripeConnected => DBEnum::StripeConnected, + api::GetMetaDataRequest::PaypalConnected => DBEnum::PaypalConnected, + api::GetMetaDataRequest::SPRoutingConfigured => DBEnum::SpRoutingConfigured, + api::GetMetaDataRequest::SPTestPayment => DBEnum::SpTestPayment, + api::GetMetaDataRequest::DownloadWoocom => DBEnum::DownloadWoocom, + api::GetMetaDataRequest::ConfigureWoocom => DBEnum::ConfigureWoocom, + api::GetMetaDataRequest::SetupWoocomWebhook => DBEnum::SetupWoocomWebhook, + api::GetMetaDataRequest::IsMultipleConfiguration => DBEnum::IsMultipleConfiguration, + } +} + +fn into_response( + data: Option<&DashboardMetadata>, + data_type: &DBEnum, +) -> UserResult { + match data_type { + DBEnum::ProductionAgreement => Ok(api::GetMetaDataResponse::ProductionAgreement( + data.is_some(), + )), + DBEnum::SetupProcessor => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::SetupProcessor(resp)) + } + DBEnum::ConfigureEndpoint => { + Ok(api::GetMetaDataResponse::ConfigureEndpoint(data.is_some())) + } + DBEnum::SetupComplete => Ok(api::GetMetaDataResponse::SetupComplete(data.is_some())), + DBEnum::FirstProcessorConnected => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::FirstProcessorConnected(resp)) + } + DBEnum::SecondProcessorConnected => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::SecondProcessorConnected(resp)) + } + DBEnum::ConfiguredRouting => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::ConfiguredRouting(resp)) + } + DBEnum::TestPayment => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::TestPayment(resp)) + } + DBEnum::IntegrationMethod => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::IntegrationMethod(resp)) + } + DBEnum::IntegrationCompleted => Ok(api::GetMetaDataResponse::IntegrationCompleted( + data.is_some(), + )), + DBEnum::StripeConnected => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::StripeConnected(resp)) + } + DBEnum::PaypalConnected => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::PaypalConnected(resp)) + } + DBEnum::SpRoutingConfigured => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::SPRoutingConfigured(resp)) + } + DBEnum::SpTestPayment => Ok(api::GetMetaDataResponse::SPTestPayment(data.is_some())), + DBEnum::DownloadWoocom => Ok(api::GetMetaDataResponse::DownloadWoocom(data.is_some())), + DBEnum::ConfigureWoocom => Ok(api::GetMetaDataResponse::ConfigureWoocom(data.is_some())), + DBEnum::SetupWoocomWebhook => { + Ok(api::GetMetaDataResponse::SetupWoocomWebhook(data.is_some())) + } + + DBEnum::IsMultipleConfiguration => Ok(api::GetMetaDataResponse::IsMultipleConfiguration( + data.is_some(), + )), + } +} + +async fn insert_metadata( + state: &AppState, + user: UserFromToken, + metadata_key: DBEnum, + metadata_value: types::MetaData, +) -> UserResult { + match metadata_value { + types::MetaData::ProductionAgreement(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::SetupProcessor(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::ConfigureEndpoint(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::SetupComplete(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::FirstProcessorConnected(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::SecondProcessorConnected(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::ConfiguredRouting(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::TestPayment(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::IntegrationMethod(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::IntegrationCompleted(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::StripeConnected(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::PaypalConnected(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::SPRoutingConfigured(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::SPTestPayment(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::DownloadWoocom(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::ConfigureWoocom(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::SetupWoocomWebhook(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::IsMultipleConfiguration(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + } +} + +async fn fetch_metadata( + state: &AppState, + user: &UserFromToken, + metadata_keys: Vec, +) -> UserResult> { + let mut dashboard_metadata = Vec::with_capacity(metadata_keys.len()); + let (merchant_scoped_enums, _) = utils::separate_metadata_type_based_on_scope(metadata_keys); + + if !merchant_scoped_enums.is_empty() { + let mut res = utils::get_merchant_scoped_metadata_from_db( + state, + user.merchant_id.to_owned(), + user.org_id.to_owned(), + merchant_scoped_enums, + ) + .await?; + dashboard_metadata.append(&mut res); + } + + Ok(dashboard_metadata) +} + +pub async fn backfill_metadata( + state: &AppState, + user: &UserFromToken, + key: &DBEnum, +) -> UserResult> { + let key_store = state + .store + .get_merchant_key_store_by_merchant_id( + &user.merchant_id, + &state.store.get_master_key().to_vec().into(), + ) + .await + .change_context(UserErrors::InternalServerError)?; + + match key { + DBEnum::StripeConnected => { + let mca = if let Some(stripe_connected) = get_merchant_connector_account_by_name( + state, + &user.merchant_id, + api_models::enums::RoutableConnectors::Stripe + .to_string() + .as_str(), + &key_store, + ) + .await? + { + stripe_connected + } else if let Some(stripe_test_connected) = get_merchant_connector_account_by_name( + state, + &user.merchant_id, + //TODO: Use Enum with proper feature flag + "stripe_test", + &key_store, + ) + .await? + { + stripe_test_connected + } else { + return Ok(None); + }; + + Some( + insert_metadata( + state, + user.to_owned(), + DBEnum::StripeConnected, + types::MetaData::StripeConnected(api::ProcessorConnected { + processor_id: mca.merchant_connector_id, + processor_name: mca.connector_name, + }), + ) + .await, + ) + .transpose() + } + DBEnum::PaypalConnected => { + let mca = if let Some(paypal_connected) = get_merchant_connector_account_by_name( + state, + &user.merchant_id, + api_models::enums::RoutableConnectors::Paypal + .to_string() + .as_str(), + &key_store, + ) + .await? + { + paypal_connected + } else if let Some(paypal_test_connected) = get_merchant_connector_account_by_name( + state, + &user.merchant_id, + //TODO: Use Enum with proper feature flag + "paypal_test", + &key_store, + ) + .await? + { + paypal_test_connected + } else { + return Ok(None); + }; + + Some( + insert_metadata( + state, + user.to_owned(), + DBEnum::PaypalConnected, + types::MetaData::PaypalConnected(api::ProcessorConnected { + processor_id: mca.merchant_connector_id, + processor_name: mca.connector_name, + }), + ) + .await, + ) + .transpose() + } + _ => Ok(None), + } +} + +pub async fn get_merchant_connector_account_by_name( + state: &AppState, + merchant_id: &str, + connector_name: &str, + key_store: &MerchantKeyStore, +) -> UserResult> { + state + .store + .find_merchant_connector_account_by_merchant_id_connector_name( + merchant_id, + connector_name, + key_store, + ) + .await + .map_err(|e| { + e.change_context(UserErrors::InternalServerError) + .attach_printable("DB Error Fetching DashboardMetaData") + }) + .map(|data| data.first().cloned()) +} diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs index 549bda78eda8..086a09b805c6 100644 --- a/crates/router/src/db.rs +++ b/crates/router/src/db.rs @@ -6,6 +6,7 @@ pub mod capture; pub mod cards_info; pub mod configs; pub mod customers; +pub mod dashboard_metadata; pub mod dispute; pub mod ephemeral_key; pub mod events; @@ -68,6 +69,7 @@ pub trait StorageInterface: + configs::ConfigInterface + capture::CaptureInterface + customers::CustomerInterface + + dashboard_metadata::DashboardMetadataInterface + dispute::DisputeInterface + ephemeral_key::EphemeralKeyInterface + events::EventInterface diff --git a/crates/router/src/db/dashboard_metadata.rs b/crates/router/src/db/dashboard_metadata.rs new file mode 100644 index 000000000000..2e8129398ca3 --- /dev/null +++ b/crates/router/src/db/dashboard_metadata.rs @@ -0,0 +1,184 @@ +use diesel_models::{enums, user::dashboard_metadata as storage}; +use error_stack::{IntoReport, ResultExt}; +use storage_impl::MockDb; + +use crate::{ + connection, + core::errors::{self, CustomResult}, + services::Store, +}; + +#[async_trait::async_trait] +pub trait DashboardMetadataInterface { + async fn insert_metadata( + &self, + metadata: storage::DashboardMetadataNew, + ) -> CustomResult; + + async fn find_user_scoped_dashboard_metadata( + &self, + user_id: &str, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError>; + async fn find_merchant_scoped_dashboard_metadata( + &self, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError>; +} + +#[async_trait::async_trait] +impl DashboardMetadataInterface for Store { + async fn insert_metadata( + &self, + metadata: storage::DashboardMetadataNew, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + metadata + .insert(&conn) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_user_scoped_dashboard_metadata( + &self, + user_id: &str, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_write(self).await?; + storage::DashboardMetadata::find_user_scoped_dashboard_metadata( + &conn, + user_id.to_owned(), + merchant_id.to_owned(), + org_id.to_owned(), + data_keys, + ) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_merchant_scoped_dashboard_metadata( + &self, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_write(self).await?; + storage::DashboardMetadata::find_merchant_scoped_dashboard_metadata( + &conn, + merchant_id.to_owned(), + org_id.to_owned(), + data_keys, + ) + .await + .map_err(Into::into) + .into_report() + } +} + +#[async_trait::async_trait] +impl DashboardMetadataInterface for MockDb { + async fn insert_metadata( + &self, + metadata: storage::DashboardMetadataNew, + ) -> CustomResult { + let mut dashboard_metadata = self.dashboard_metadata.lock().await; + if dashboard_metadata.iter().any(|metadata_inner| { + metadata_inner.user_id == metadata.user_id + && metadata_inner.merchant_id == metadata.merchant_id + && metadata_inner.org_id == metadata.org_id + && metadata_inner.data_key == metadata.data_key + }) { + Err(errors::StorageError::DuplicateValue { + entity: "user_id, merchant_id, org_id and data_key", + key: None, + })? + } + let metadata_new = storage::DashboardMetadata { + id: dashboard_metadata + .len() + .try_into() + .into_report() + .change_context(errors::StorageError::MockDbError)?, + user_id: metadata.user_id, + merchant_id: metadata.merchant_id, + org_id: metadata.org_id, + data_key: metadata.data_key, + data_value: metadata.data_value, + created_by: metadata.created_by, + created_at: metadata.created_at, + last_modified_by: metadata.last_modified_by, + last_modified_at: metadata.last_modified_at, + }; + dashboard_metadata.push(metadata_new.clone()); + Ok(metadata_new) + } + + async fn find_user_scoped_dashboard_metadata( + &self, + user_id: &str, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError> { + let dashboard_metadata = self.dashboard_metadata.lock().await; + let query_result = dashboard_metadata + .iter() + .filter(|metadata_inner| { + metadata_inner + .user_id + .clone() + .map(|user_id_inner| user_id_inner == user_id) + .unwrap_or(false) + && metadata_inner.merchant_id == merchant_id + && metadata_inner.org_id == org_id + && data_keys.contains(&metadata_inner.data_key) + }) + .cloned() + .collect::>(); + + if query_result.is_empty() { + return Err(errors::StorageError::ValueNotFound(format!( + "No dashboard_metadata available for user_id = {user_id},\ + merchant_id = {merchant_id}, org_id = {org_id} and data_keys = {data_keys:?}", + )) + .into()); + } + Ok(query_result) + } + + async fn find_merchant_scoped_dashboard_metadata( + &self, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError> { + let dashboard_metadata = self.dashboard_metadata.lock().await; + let query_result = dashboard_metadata + .iter() + .filter(|metadata_inner| { + metadata_inner.user_id.is_none() + && metadata_inner.merchant_id == merchant_id + && metadata_inner.org_id == org_id + && data_keys.contains(&metadata_inner.data_key) + }) + .cloned() + .collect::>(); + + if query_result.is_empty() { + return Err(errors::StorageError::ValueNotFound(format!( + "No dashboard_metadata available for merchant_id = {merchant_id},\ + org_id = {org_id} and data_keyss = {data_keys:?}", + )) + .into()); + } + Ok(query_result) + } +} diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 9cf1a7b80b8b..fcceba7fadba 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -6,6 +6,7 @@ use data_models::payments::{ payment_attempt::PaymentAttemptInterface, payment_intent::PaymentIntentInterface, }; use diesel_models::{ + enums, enums::ProcessTrackerStatus, ephemeral_key::{EphemeralKey, EphemeralKeyNew}, reverse_lookup::{ReverseLookup, ReverseLookupNew}, @@ -21,7 +22,10 @@ use scheduler::{ use storage_impl::redis::kv_store::RedisConnInterface; use time::PrimitiveDateTime; -use super::{user::UserInterface, user_role::UserRoleInterface}; +use super::{ + dashboard_metadata::DashboardMetadataInterface, user::UserInterface, + user_role::UserRoleInterface, +}; use crate::{ core::errors::{self, ProcessTrackerError}, db::{ @@ -1915,3 +1919,35 @@ impl UserRoleInterface for KafkaStore { self.diesel_store.list_user_roles_by_user_id(user_id).await } } + +#[async_trait::async_trait] +impl DashboardMetadataInterface for KafkaStore { + async fn insert_metadata( + &self, + metadata: storage::DashboardMetadataNew, + ) -> CustomResult { + self.diesel_store.insert_metadata(metadata).await + } + + async fn find_user_scoped_dashboard_metadata( + &self, + user_id: &str, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_user_scoped_dashboard_metadata(user_id, merchant_id, org_id, data_keys) + .await + } + async fn find_merchant_scoped_dashboard_metadata( + &self, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_merchant_scoped_dashboard_metadata(merchant_id, org_id, data_keys) + .await + } +} diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 2a7e1ab61905..2f8932057fb4 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -807,6 +807,11 @@ impl User { .service(web::resource("/v2/signin").route(web::post().to(user_connect_account))) .service(web::resource("/v2/signup").route(web::post().to(user_connect_account))) .service(web::resource("/change_password").route(web::post().to(change_password))) + .service( + web::resource("/data/merchant") + .route(web::post().to(set_merchant_scoped_dashboard_metadata)), + ) + .service(web::resource("/data").route(web::get().to(get_multiple_dashboard_metadata))) } } diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index c7369b9e4d52..72bc3c9cd417 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -147,9 +147,11 @@ impl From for ApiIdentifier { | Flow::GsmRuleUpdate | Flow::GsmRuleDelete => Self::Gsm, - Flow::UserConnectAccount | Flow::ChangePassword | Flow::VerifyPaymentConnector => { - Self::User - } + Flow::UserConnectAccount + | Flow::ChangePassword + | Flow::SetDashboardMetadata + | Flow::GetMutltipleDashboardMetadata + | Flow::VerifyPaymentConnector => Self::User, } } } diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index 7d3d183eda76..3f5f7815ffbc 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -1,5 +1,6 @@ use actix_web::{web, HttpRequest, HttpResponse}; -use api_models::user as user_api; +use api_models::{errors::types::ApiErrorResponse, user as user_api}; +use common_utils::errors::ReportSwitchExt; use router_env::Flow; use super::AppState; @@ -8,7 +9,9 @@ use crate::{ services::{ api, authentication::{self as auth}, + authorization::permissions::Permission, }, + utils::user::dashboard_metadata::{parse_string_to_enums, set_ip_address_if_required}, }; pub async fn user_connect_account( @@ -47,3 +50,55 @@ pub async fn change_password( )) .await } + +pub async fn set_merchant_scoped_dashboard_metadata( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::SetDashboardMetadata; + let mut payload = json_payload.into_inner(); + + if let Err(e) = common_utils::errors::ReportSwitchExt::<(), ApiErrorResponse>::switch( + set_ip_address_if_required(&mut payload, req.headers()), + ) { + return api::log_and_return_error_response(e); + } + + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + user::dashboard_metadata::set_metadata, + &auth::JWTAuth(Permission::MerchantAccountWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn get_multiple_dashboard_metadata( + state: web::Data, + req: HttpRequest, + query: web::Query, +) -> HttpResponse { + let flow = Flow::GetMutltipleDashboardMetadata; + let payload = match ReportSwitchExt::<_, ApiErrorResponse>::switch(parse_string_to_enums( + query.into_inner().keys, + )) { + Ok(payload) => payload, + Err(e) => { + return api::log_and_return_error_response(e); + } + }; + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + user::dashboard_metadata::get_multiple_metadata, + &auth::DashboardNoPermissionAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index c053b0f15448..7e723bf00c32 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -27,6 +27,8 @@ use crate::{ utils::user::password, }; +pub mod dashboard_metadata; + #[derive(Clone)] pub struct UserName(Secret); diff --git a/crates/router/src/types/domain/user/dashboard_metadata.rs b/crates/router/src/types/domain/user/dashboard_metadata.rs new file mode 100644 index 000000000000..e65379346ac9 --- /dev/null +++ b/crates/router/src/types/domain/user/dashboard_metadata.rs @@ -0,0 +1,56 @@ +use api_models::user::dashboard_metadata as api; +use diesel_models::enums::DashboardMetadata as DBEnum; +use masking::Secret; +use time::PrimitiveDateTime; + +pub enum MetaData { + ProductionAgreement(ProductionAgreementValue), + SetupProcessor(api::SetupProcessor), + ConfigureEndpoint(bool), + SetupComplete(bool), + FirstProcessorConnected(api::ProcessorConnected), + SecondProcessorConnected(api::ProcessorConnected), + ConfiguredRouting(api::ConfiguredRouting), + TestPayment(api::TestPayment), + IntegrationMethod(api::IntegrationMethod), + IntegrationCompleted(bool), + StripeConnected(api::ProcessorConnected), + PaypalConnected(api::ProcessorConnected), + SPRoutingConfigured(api::ConfiguredRouting), + SPTestPayment(bool), + DownloadWoocom(bool), + ConfigureWoocom(bool), + SetupWoocomWebhook(bool), + IsMultipleConfiguration(bool), +} + +impl From<&MetaData> for DBEnum { + fn from(value: &MetaData) -> Self { + match value { + MetaData::ProductionAgreement(_) => Self::ProductionAgreement, + MetaData::SetupProcessor(_) => Self::SetupProcessor, + MetaData::ConfigureEndpoint(_) => Self::ConfigureEndpoint, + MetaData::SetupComplete(_) => Self::SetupComplete, + MetaData::FirstProcessorConnected(_) => Self::FirstProcessorConnected, + MetaData::SecondProcessorConnected(_) => Self::SecondProcessorConnected, + MetaData::ConfiguredRouting(_) => Self::ConfiguredRouting, + MetaData::TestPayment(_) => Self::TestPayment, + MetaData::IntegrationMethod(_) => Self::IntegrationMethod, + MetaData::IntegrationCompleted(_) => Self::IntegrationCompleted, + MetaData::StripeConnected(_) => Self::StripeConnected, + MetaData::PaypalConnected(_) => Self::PaypalConnected, + MetaData::SPRoutingConfigured(_) => Self::SpRoutingConfigured, + MetaData::SPTestPayment(_) => Self::SpTestPayment, + MetaData::DownloadWoocom(_) => Self::DownloadWoocom, + MetaData::ConfigureWoocom(_) => Self::ConfigureWoocom, + MetaData::SetupWoocomWebhook(_) => Self::SetupWoocomWebhook, + MetaData::IsMultipleConfiguration(_) => Self::IsMultipleConfiguration, + } + } +} +#[derive(Debug, serde::Serialize)] +pub struct ProductionAgreementValue { + pub version: String, + pub ip_address: Secret, + pub timestamp: PrimitiveDateTime, +} diff --git a/crates/router/src/types/storage.rs b/crates/router/src/types/storage.rs index e3e19323357b..a83a405f3554 100644 --- a/crates/router/src/types/storage.rs +++ b/crates/router/src/types/storage.rs @@ -5,6 +5,7 @@ pub mod capture; pub mod cards_info; pub mod configs; pub mod customers; +pub mod dashboard_metadata; pub mod dispute; pub mod enums; pub mod ephemeral_key; @@ -42,11 +43,11 @@ pub use data_models::payments::{ }; pub use self::{ - address::*, api_keys::*, capture::*, cards_info::*, configs::*, customers::*, dispute::*, - ephemeral_key::*, events::*, file::*, gsm::*, locker_mock_up::*, mandate::*, - merchant_account::*, merchant_connector_account::*, merchant_key_store::*, payment_link::*, - payment_method::*, payout_attempt::*, payouts::*, process_tracker::*, refund::*, - reverse_lookup::*, routing_algorithm::*, user::*, user_role::*, + address::*, api_keys::*, capture::*, cards_info::*, configs::*, customers::*, + dashboard_metadata::*, dispute::*, ephemeral_key::*, events::*, file::*, gsm::*, + locker_mock_up::*, mandate::*, merchant_account::*, merchant_connector_account::*, + merchant_key_store::*, payment_link::*, payment_method::*, payout_attempt::*, payouts::*, + process_tracker::*, refund::*, reverse_lookup::*, routing_algorithm::*, user::*, user_role::*, }; use crate::types::api::routing; diff --git a/crates/router/src/types/storage/dashboard_metadata.rs b/crates/router/src/types/storage/dashboard_metadata.rs new file mode 100644 index 000000000000..d804dfb1ff8b --- /dev/null +++ b/crates/router/src/types/storage/dashboard_metadata.rs @@ -0,0 +1 @@ +pub use diesel_models::user::dashboard_metadata::*; diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs index c72e4b9feb3c..824f7f63af75 100644 --- a/crates/router/src/utils/user.rs +++ b/crates/router/src/utils/user.rs @@ -1 +1,2 @@ +pub mod dashboard_metadata; pub mod password; diff --git a/crates/router/src/utils/user/dashboard_metadata.rs b/crates/router/src/utils/user/dashboard_metadata.rs new file mode 100644 index 000000000000..5f354e613f95 --- /dev/null +++ b/crates/router/src/utils/user/dashboard_metadata.rs @@ -0,0 +1,162 @@ +use std::{net::IpAddr, str::FromStr}; + +use actix_web::http::header::HeaderMap; +use api_models::user::dashboard_metadata::{ + GetMetaDataRequest, GetMultipleMetaDataPayload, SetMetaDataRequest, +}; +use diesel_models::{ + enums::DashboardMetadata as DBEnum, + user::dashboard_metadata::{DashboardMetadata, DashboardMetadataNew}, +}; +use error_stack::{IntoReport, ResultExt}; +use masking::Secret; + +use crate::{ + core::errors::{UserErrors, UserResult}, + headers, AppState, +}; + +pub async fn insert_merchant_scoped_metadata_to_db( + state: &AppState, + user_id: String, + merchant_id: String, + org_id: String, + metadata_key: DBEnum, + metadata_value: impl serde::Serialize, +) -> UserResult { + let now = common_utils::date_time::now(); + let data_value = serde_json::to_value(metadata_value) + .into_report() + .change_context(UserErrors::InternalServerError) + .attach_printable("Error Converting Struct To Serde Value")?; + state + .store + .insert_metadata(DashboardMetadataNew { + user_id: None, + merchant_id, + org_id, + data_key: metadata_key, + data_value, + created_by: user_id.clone(), + created_at: now, + last_modified_by: user_id, + last_modified_at: now, + }) + .await + .map_err(|e| { + if e.current_context().is_db_unique_violation() { + return e.change_context(UserErrors::MetadataAlreadySet); + } + e.change_context(UserErrors::InternalServerError) + }) +} + +pub async fn get_merchant_scoped_metadata_from_db( + state: &AppState, + merchant_id: String, + org_id: String, + metadata_keys: Vec, +) -> UserResult> { + match state + .store + .find_merchant_scoped_dashboard_metadata(&merchant_id, &org_id, metadata_keys) + .await + { + Ok(data) => Ok(data), + Err(e) => { + if e.current_context().is_db_not_found() { + return Ok(Vec::with_capacity(0)); + } + Err(e + .change_context(UserErrors::InternalServerError) + .attach_printable("DB Error Fetching DashboardMetaData")) + } + } +} + +pub fn deserialize_to_response(data: Option<&DashboardMetadata>) -> UserResult> +where + T: serde::de::DeserializeOwned, +{ + data.map(|metadata| serde_json::from_value(metadata.data_value.clone())) + .transpose() + .map_err(|_| UserErrors::InternalServerError.into()) + .attach_printable("Error Serializing Metadata from DB") +} + +pub fn separate_metadata_type_based_on_scope( + metadata_keys: Vec, +) -> (Vec, Vec) { + let (mut merchant_scoped, user_scoped) = ( + Vec::with_capacity(metadata_keys.len()), + Vec::with_capacity(metadata_keys.len()), + ); + for key in metadata_keys { + match key { + DBEnum::ProductionAgreement + | DBEnum::SetupProcessor + | DBEnum::ConfigureEndpoint + | DBEnum::SetupComplete + | DBEnum::FirstProcessorConnected + | DBEnum::SecondProcessorConnected + | DBEnum::ConfiguredRouting + | DBEnum::TestPayment + | DBEnum::IntegrationMethod + | DBEnum::IntegrationCompleted + | DBEnum::StripeConnected + | DBEnum::PaypalConnected + | DBEnum::SpRoutingConfigured + | DBEnum::SpTestPayment + | DBEnum::DownloadWoocom + | DBEnum::ConfigureWoocom + | DBEnum::SetupWoocomWebhook + | DBEnum::IsMultipleConfiguration => merchant_scoped.push(key), + } + } + (merchant_scoped, user_scoped) +} + +pub fn is_backfill_required(metadata_key: &DBEnum) -> bool { + matches!( + metadata_key, + DBEnum::StripeConnected | DBEnum::PaypalConnected + ) +} + +pub fn set_ip_address_if_required( + request: &mut SetMetaDataRequest, + headers: &HeaderMap, +) -> UserResult<()> { + if let SetMetaDataRequest::ProductionAgreement(req) = request { + let ip_address_from_request: Secret = headers + .get(headers::X_FORWARDED_FOR) + .ok_or(UserErrors::IpAddressParsingFailed.into()) + .attach_printable("X-Forwarded-For header not found")? + .to_str() + .map_err(|_| UserErrors::IpAddressParsingFailed.into()) + .attach_printable("Error converting Header Value to Str")? + .split(',') + .next() + .and_then(|ip| { + let ip_addr: Result = ip.parse(); + ip_addr.ok() + }) + .ok_or(UserErrors::IpAddressParsingFailed.into()) + .attach_printable("Error Parsing header value to ip")? + .to_string() + .into(); + req.ip_address = Some(ip_address_from_request) + } + Ok(()) +} + +pub fn parse_string_to_enums(query: String) -> UserResult { + Ok(GetMultipleMetaDataPayload { + results: query + .split(',') + .map(GetMetaDataRequest::from_str) + .collect::, _>>() + .map_err(|_| UserErrors::InvalidMetadataRequest.into()) + .attach_printable("Error Parsing to DashboardMetadata enums")?, + }) +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index c254f89b4eef..7b87d2703640 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -259,6 +259,10 @@ pub enum Flow { DecisionManagerRetrieveConfig, /// Change password flow ChangePassword, + /// Set Dashboard Metadata flow + SetDashboardMetadata, + /// Get Multiple Dashboard Metadata flow + GetMutltipleDashboardMetadata, /// Payment Connector Verify VerifyPaymentConnector, } diff --git a/crates/storage_impl/src/mock_db.rs b/crates/storage_impl/src/mock_db.rs index 4cdf8e2456bb..e22d39ce70c8 100644 --- a/crates/storage_impl/src/mock_db.rs +++ b/crates/storage_impl/src/mock_db.rs @@ -43,6 +43,7 @@ pub struct MockDb { pub organizations: Arc>>, pub users: Arc>>, pub user_roles: Arc>>, + pub dashboard_metadata: Arc>>, } impl MockDb { @@ -78,6 +79,7 @@ impl MockDb { organizations: Default::default(), users: Default::default(), user_roles: Default::default(), + dashboard_metadata: Default::default(), }) } } diff --git a/migrations/2023-11-23-100644_create_dashboard_metadata_table/down.sql b/migrations/2023-11-23-100644_create_dashboard_metadata_table/down.sql new file mode 100644 index 000000000000..746fb42109e9 --- /dev/null +++ b/migrations/2023-11-23-100644_create_dashboard_metadata_table/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +DROP INDEX IF EXISTS dashboard_metadata_index; +DROP TABLE IF EXISTS dashboard_metadata; \ No newline at end of file diff --git a/migrations/2023-11-23-100644_create_dashboard_metadata_table/up.sql b/migrations/2023-11-23-100644_create_dashboard_metadata_table/up.sql new file mode 100644 index 000000000000..8296f755f543 --- /dev/null +++ b/migrations/2023-11-23-100644_create_dashboard_metadata_table/up.sql @@ -0,0 +1,15 @@ +-- Your SQL goes here +CREATE TABLE IF NOT EXISTS dashboard_metadata ( + id SERIAL PRIMARY KEY, + user_id VARCHAR(64), + merchant_id VARCHAR(64) NOT NULL, + org_id VARCHAR(64) NOT NULL, + data_key VARCHAR(64) NOT NULL, + data_value JSON NOT NULL, + created_by VARCHAR(64) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now(), + last_modified_by VARCHAR(64) NOT NULL, + last_modified_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE UNIQUE INDEX IF NOT EXISTS dashboard_metadata_index ON dashboard_metadata (COALESCE(user_id,'0'), merchant_id, org_id, data_key); \ No newline at end of file From d30b58abb5e716b70c2dadec9e6f13c9e3403b6f Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Thu, 30 Nov 2023 15:31:01 +0530 Subject: [PATCH 138/146] feat(connector): [BANKOFAMERICA] Add Required Fields for GPAY (#3014) --- crates/router/src/configs/defaults.rs | 219 ++++++++++++++++++++++++-- 1 file changed, 210 insertions(+), 9 deletions(-) diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index f5c3b46b27f2..f9bfcae1ca10 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -503,15 +503,6 @@ impl Default for super::settings::RequiredFields { value: None, } ), - ( - "payment_method_data.card.card_holder_name".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.card.card_holder_name".to_string(), - display_name: "card_holder_name".to_string(), - field_type: enums::FieldType::UserFullName, - value: None, - } - ), ( "email".to_string(), RequiredFieldInfo { @@ -2418,6 +2409,129 @@ impl Default for super::settings::RequiredFields { common: HashMap::new(), } ), + ( + enums::Connector::Bankofamerica, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from( + [ + ( + "payment_method_data.card.card_number".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_number".to_string(), + display_name: "card_number".to_string(), + field_type: enums::FieldType::UserCardNumber, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_month".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_month".to_string(), + display_name: "card_exp_month".to_string(), + field_type: enums::FieldType::UserCardExpiryMonth, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_year".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_year".to_string(), + display_name: "card_exp_year".to_string(), + field_type: enums::FieldType::UserCardExpiryYear, + value: None, + } + ), + ( + "payment_method_data.card.card_cvc".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_cvc".to_string(), + display_name: "card_cvc".to_string(), + field_type: enums::FieldType::UserCardCvc, + value: None, + } + ), + ( + "email".to_string(), + RequiredFieldInfo { + required_field: "email".to_string(), + display_name: "email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.first_name".to_string(), + display_name: "billing_first_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "billing_last_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "billing.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserAddressCity, + value: None, + } + ), + ( + "billing.address.state".to_string(), + RequiredFieldInfo { + required_field: "billing.address.state".to_string(), + display_name: "state".to_string(), + field_type: enums::FieldType::UserAddressState, + value: None, + } + ), + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ] + ), + common: HashMap::new(), + } + ), ( enums::Connector::Bluesnap, RequiredFieldFinal { @@ -4250,6 +4364,93 @@ impl Default for super::settings::RequiredFields { common: HashMap::new(), } ), + ( + enums::Connector::Bankofamerica, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from( + [ + ( + "email".to_string(), + RequiredFieldInfo { + required_field: "email".to_string(), + display_name: "email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.first_name".to_string(), + display_name: "billing_first_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "billing_last_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "billing.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserAddressCity, + value: None, + } + ), + ( + "billing.address.state".to_string(), + RequiredFieldInfo { + required_field: "billing.address.state".to_string(), + display_name: "state".to_string(), + field_type: enums::FieldType::UserAddressState, + value: None, + } + ), + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ] + ), + common: HashMap::new(), + } + ), ]), }, ), From c6cb527f07e23796c342f3562fbf3b61f1ef6801 Mon Sep 17 00:00:00 2001 From: Sarthak Soni <76486416+Sarthak1799@users.noreply.github.com> Date: Thu, 30 Nov 2023 15:41:59 +0530 Subject: [PATCH 139/146] fix(routing): Fix kgraph to exclude PM auth during construction (#3019) --- crates/router/src/core/payments/routing.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/router/src/core/payments/routing.rs b/crates/router/src/core/payments/routing.rs index 841b48b9444a..96cd65615199 100644 --- a/crates/router/src/core/payments/routing.rs +++ b/crates/router/src/core/payments/routing.rs @@ -523,8 +523,10 @@ pub async fn refresh_kgraph_cache( .await .change_context(errors::RoutingError::KgraphCacheRefreshFailed)?; - merchant_connector_accounts - .retain(|mca| mca.connector_type != storage_enums::ConnectorType::PaymentVas); + merchant_connector_accounts.retain(|mca| { + mca.connector_type != storage_enums::ConnectorType::PaymentVas + && mca.connector_type != storage_enums::ConnectorType::PaymentMethodAuth + }); #[cfg(feature = "business_profile_routing")] let merchant_connector_accounts = payments_oss::helpers::filter_mca_based_on_business_profile( From 1e60c710985b341a118bb32962bd74b406d78f69 Mon Sep 17 00:00:00 2001 From: Sahkal Poddar Date: Thu, 30 Nov 2023 16:50:18 +0530 Subject: [PATCH 140/146] refactor(postman): Fix payme postman collection for handling `order_details` (#2996) --- Cargo.lock | 11 ++++++++-- .../Payments - Create/request.json | 2 +- .../Payments - Create/request.json | 2 +- .../Payments - Create/request.json | 2 +- .../Payments - Create/request.json | 2 +- .../Payments - Create/request.json | 2 +- .../Payments - Create/request.json | 2 +- .../QuickStart/Payments - Create/request.json | 2 +- .../Payments - Create/request.json | 2 +- .../Payments - Create/request.json | 2 +- .../Payments - Create/request.json | 2 +- .../payme.postman_collection.json | 20 +++++++++---------- 12 files changed, 29 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 417e6d85db6d..e8719b29f51d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3797,6 +3797,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "mutually_exclusive_features" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d02c0b00610773bb7fc61d85e13d86c7858cbdf00e1a120bfc41bc055dbaa0e" + [[package]] name = "nanoid" version = "0.4.0" @@ -6832,11 +6838,12 @@ dependencies = [ [[package]] name = "tracing-actix-web" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a512ec11fae6c666707625e84f83e5d58f941e9ab15723289c0d380edfe48f09" +checksum = "1fe0d5feac3f4ca21ba33496bcb1ccab58cca6412b1405ae80f0581541e0ca78" dependencies = [ "actix-web", + "mutually_exclusive_features", "opentelemetry", "pin-project", "tracing", diff --git a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json index a63210df7f42..03aea095ff35 100644 --- a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json +++ b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json @@ -79,7 +79,7 @@ { "product_name": "Apple iphone 15", "quantity": 1, - "amount": 5500, + "amount": 6540, "account_name": "transaction_processing" } ] diff --git a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json index 99392fc0f916..5a651cc0f119 100644 --- a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json +++ b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json @@ -79,7 +79,7 @@ { "product_name": "Apple iphone 15", "quantity": 1, - "amount": 5500, + "amount": 6540, "account_name": "transaction_processing" } ] diff --git a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json index 90982e5acd38..54cf1b15e3db 100644 --- a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json +++ b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json @@ -69,7 +69,7 @@ { "product_name": "Apple iphone 15", "quantity": 1, - "amount": 5500, + "amount": 6540, "account_name": "transaction_processing" } ] diff --git a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/request.json b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/request.json index 0fc567f8bea0..f0915480e13e 100644 --- a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/request.json +++ b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/request.json @@ -79,7 +79,7 @@ { "product_name": "Apple iphone 15", "quantity": 1, - "amount": 5500, + "amount": 6540, "account_name": "transaction_processing" } ] diff --git a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario5-Refund full payment/Payments - Create/request.json b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario5-Refund full payment/Payments - Create/request.json index 625ae3a9d286..00b12f40997f 100644 --- a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario5-Refund full payment/Payments - Create/request.json +++ b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario5-Refund full payment/Payments - Create/request.json @@ -78,7 +78,7 @@ { "product_name": "Apple iphone 15", "quantity": 1, - "amount": 5500, + "amount": 6540, "account_name": "transaction_processing" } ] diff --git a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Create/request.json b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Create/request.json index 99392fc0f916..5a651cc0f119 100644 --- a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Create/request.json +++ b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Create/request.json @@ -79,7 +79,7 @@ { "product_name": "Apple iphone 15", "quantity": 1, - "amount": 5500, + "amount": 6540, "account_name": "transaction_processing" } ] diff --git a/postman/collection-dir/payme/Flow Testcases/QuickStart/Payments - Create/request.json b/postman/collection-dir/payme/Flow Testcases/QuickStart/Payments - Create/request.json index a99d3db4fa53..72c62f360b8d 100644 --- a/postman/collection-dir/payme/Flow Testcases/QuickStart/Payments - Create/request.json +++ b/postman/collection-dir/payme/Flow Testcases/QuickStart/Payments - Create/request.json @@ -79,7 +79,7 @@ { "product_name": "Apple iphone 15", "quantity": 1, - "amount": 5500, + "amount": 6540, "account_name": "transaction_processing" } ] diff --git a/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/request.json b/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/request.json index 0fc567f8bea0..f0915480e13e 100644 --- a/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/request.json +++ b/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/request.json @@ -79,7 +79,7 @@ { "product_name": "Apple iphone 15", "quantity": 1, - "amount": 5500, + "amount": 6540, "account_name": "transaction_processing" } ] diff --git a/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/request.json b/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/request.json index a63210df7f42..03aea095ff35 100644 --- a/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/request.json +++ b/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/request.json @@ -79,7 +79,7 @@ { "product_name": "Apple iphone 15", "quantity": 1, - "amount": 5500, + "amount": 6540, "account_name": "transaction_processing" } ] diff --git a/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/request.json b/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/request.json index a63210df7f42..03aea095ff35 100644 --- a/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/request.json +++ b/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/request.json @@ -79,7 +79,7 @@ { "product_name": "Apple iphone 15", "quantity": 1, - "amount": 5500, + "amount": 6540, "account_name": "transaction_processing" } ] diff --git a/postman/collection-json/payme.postman_collection.json b/postman/collection-json/payme.postman_collection.json index 4bca668a6af6..280a131386e5 100644 --- a/postman/collection-json/payme.postman_collection.json +++ b/postman/collection-json/payme.postman_collection.json @@ -532,7 +532,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"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://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":5500,\"account_name\":\"transaction_processing\"}]}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"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://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":6540,\"account_name\":\"transaction_processing\"}]}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -761,7 +761,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":5500,\"account_name\":\"transaction_processing\"}]}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":6540,\"account_name\":\"transaction_processing\"}]}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -1003,7 +1003,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":5500,\"account_name\":\"transaction_processing\"}]}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":6540,\"account_name\":\"transaction_processing\"}]}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -1395,7 +1395,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":5500,\"account_name\":\"transaction_processing\"}]}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":6540,\"account_name\":\"transaction_processing\"}]}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -1787,7 +1787,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":5500,\"account_name\":\"transaction_processing\"}]}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":6540,\"account_name\":\"transaction_processing\"}]}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -2189,7 +2189,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":5500,\"account_name\":\"transaction_processing\"}]}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":6540,\"account_name\":\"transaction_processing\"}]}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -3364,7 +3364,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":5500,\"account_name\":\"transaction_processing\"}]}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":6540,\"account_name\":\"transaction_processing\"}]}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -4506,7 +4506,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":5500,\"account_name\":\"transaction_processing\"}]}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":6540,\"account_name\":\"transaction_processing\"}]}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -4886,7 +4886,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":5500,\"account_name\":\"transaction_processing\"}]}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":6540,\"account_name\":\"transaction_processing\"}]}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -5147,7 +5147,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":5500,\"account_name\":\"transaction_processing\"}]}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"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\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":6540,\"account_name\":\"transaction_processing\"}]}" }, "url": { "raw": "{{baseUrl}}/payments", From 1ca2ba459495ff9340954c87a6ae3e4dce0e7b71 Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Thu, 30 Nov 2023 16:56:34 +0530 Subject: [PATCH 141/146] feat(router): make core changes in payments flow to support incremental authorization (#3009) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- connector-template/transformers.rs | 1 + crates/api_models/src/payments.rs | 6 ++++ crates/common_enums/src/enums.rs | 24 +++++++++++++++ crates/data_models/src/payments.rs | 2 ++ .../src/payments/payment_intent.rs | 14 ++++++++- crates/diesel_models/src/enums.rs | 1 + crates/diesel_models/src/payment_intent.rs | 20 ++++++++++++- crates/diesel_models/src/schema.rs | 2 ++ .../router/src/connector/aci/transformers.rs | 1 + .../src/connector/adyen/transformers.rs | 8 +++++ .../src/connector/airwallex/transformers.rs | 2 ++ .../connector/authorizedotnet/transformers.rs | 3 ++ .../src/connector/bambora/transformers.rs | 2 ++ .../connector/bankofamerica/transformers.rs | 5 ++++ .../src/connector/bitpay/transformers.rs | 1 + crates/router/src/connector/bluesnap.rs | 1 + .../src/connector/bluesnap/transformers.rs | 1 + .../router/src/connector/boku/transformers.rs | 1 + .../braintree_graphql_transformers.rs | 9 ++++++ .../src/connector/braintree/transformers.rs | 1 + .../src/connector/cashtocode/transformers.rs | 2 ++ .../src/connector/checkout/transformers.rs | 4 +++ .../src/connector/coinbase/transformers.rs | 1 + .../src/connector/cryptopay/transformers.rs | 1 + .../src/connector/cybersource/transformers.rs | 19 ++++++++---- .../src/connector/dlocal/transformers.rs | 4 +++ .../connector/dummyconnector/transformers.rs | 1 + .../src/connector/fiserv/transformers.rs | 2 ++ .../src/connector/forte/transformers.rs | 4 +++ .../src/connector/globalpay/transformers.rs | 1 + .../src/connector/globepay/transformers.rs | 2 ++ .../src/connector/gocardless/transformers.rs | 3 ++ .../src/connector/helcim/transformers.rs | 5 ++++ .../src/connector/iatapay/transformers.rs | 2 ++ .../src/connector/klarna/transformers.rs | 1 + .../src/connector/mollie/transformers.rs | 1 + .../connector/multisafepay/transformers.rs | 1 + .../src/connector/nexinets/transformers.rs | 2 ++ .../router/src/connector/nmi/transformers.rs | 5 ++++ .../router/src/connector/noon/transformers.rs | 1 + .../src/connector/nuvei/transformers.rs | 1 + .../src/connector/opayo/transformers.rs | 1 + .../src/connector/opennode/transformers.rs | 1 + .../src/connector/payeezy/transformers.rs | 1 + .../src/connector/payme/transformers.rs | 3 ++ crates/router/src/connector/paypal.rs | 1 + .../src/connector/paypal/transformers.rs | 7 +++++ .../router/src/connector/payu/transformers.rs | 4 +++ .../src/connector/powertranz/transformers.rs | 1 + .../src/connector/prophetpay/transformers.rs | 4 +++ .../src/connector/rapyd/transformers.rs | 1 + .../src/connector/shift4/transformers.rs | 2 ++ .../src/connector/square/transformers.rs | 1 + .../router/src/connector/stax/transformers.rs | 1 + .../src/connector/stripe/transformers.rs | 4 +++ .../src/connector/trustpay/transformers.rs | 5 ++++ .../router/src/connector/tsys/transformers.rs | 2 ++ .../router/src/connector/volt/transformers.rs | 2 ++ .../src/connector/worldline/transformers.rs | 2 ++ crates/router/src/connector/worldpay.rs | 3 ++ .../src/connector/worldpay/transformers.rs | 1 + .../router/src/connector/zen/transformers.rs | 2 ++ crates/router/src/core/payments/helpers.rs | 9 ++++++ .../payments/operations/payment_cancel.rs | 1 + .../payments/operations/payment_confirm.rs | 9 ++++++ .../payments/operations/payment_create.rs | 8 +++++ .../payments/operations/payment_response.rs | 14 +++++++++ .../router/src/core/payments/transformers.rs | 18 +++++++++++ crates/router/src/core/utils.rs | 30 +++++++++++++++++++ crates/router/src/types.rs | 4 +++ .../router/src/types/api/verify_connector.rs | 1 + crates/router/src/workflows/payment_sync.rs | 2 +- crates/router/tests/connectors/aci.rs | 1 + crates/router/tests/connectors/adyen.rs | 1 + crates/router/tests/connectors/bitpay.rs | 1 + crates/router/tests/connectors/cashtocode.rs | 1 + crates/router/tests/connectors/coinbase.rs | 1 + crates/router/tests/connectors/cryptopay.rs | 1 + crates/router/tests/connectors/opennode.rs | 1 + crates/router/tests/connectors/utils.rs | 2 ++ crates/router/tests/connectors/worldline.rs | 1 + .../src/mock_db/payment_intent.rs | 2 ++ .../src/payments/payment_intent.rs | 24 +++++++++++++-- .../down.sql | 3 ++ .../up.sql | 3 ++ .../down.sql | 2 ++ .../up.sql | 2 ++ openapi/openapi_spec.json | 15 ++++++++++ 88 files changed, 363 insertions(+), 11 deletions(-) create mode 100644 migrations/2023-11-28-081058_add-request_incremental_authorization-in-payment-intent/down.sql create mode 100644 migrations/2023-11-28-081058_add-request_incremental_authorization-in-payment-intent/up.sql create mode 100644 migrations/2023-11-29-063030_add-incremental_authorization_allowed-in-payment-intent/down.sql create mode 100644 migrations/2023-11-29-063030_add-incremental_authorization_allowed-in-payment-intent/up.sql diff --git a/connector-template/transformers.rs b/connector-template/transformers.rs index 3ed53a906a2e..bdbfb2e45672 100644 --- a/connector-template/transformers.rs +++ b/connector-template/transformers.rs @@ -130,6 +130,7 @@ impl TryFrom)] pub payment_type: Option, + + ///Request for an incremental authorization + pub request_incremental_authorization: Option, } impl PaymentsRequest { @@ -2210,6 +2213,9 @@ pub struct PaymentsResponse { /// Identifier of the connector ( merchant connector account ) which was chosen to make the payment pub merchant_connector_id: Option, + + /// If true incremental authorization can be performed on this payment + pub incremental_authorization_allowed: Option, } #[derive(Clone, Debug, serde::Deserialize, ToSchema, serde::Serialize)] diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 3f343965130e..8da4a2da54cc 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -12,6 +12,7 @@ pub mod diesel_exports { DbFutureUsage as FutureUsage, DbIntentStatus as IntentStatus, DbMandateStatus as MandateStatus, DbPaymentMethodIssuerCode as PaymentMethodIssuerCode, DbPaymentType as PaymentType, DbRefundStatus as RefundStatus, + DbRequestIncrementalAuthorization as RequestIncrementalAuthorization, }; } @@ -1387,6 +1388,29 @@ pub enum CountryAlpha2 { US } +#[derive( + Clone, + Debug, + Copy, + Default, + Eq, + Hash, + PartialEq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumString, +)] +#[router_derive::diesel_enum(storage_type = "db_enum")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum RequestIncrementalAuthorization { + True, + False, + #[default] + Default, +} + #[derive(Clone, Copy, Debug, Serialize, Deserialize)] #[rustfmt::skip] pub enum CountryAlpha3 { diff --git a/crates/data_models/src/payments.rs b/crates/data_models/src/payments.rs index 4e7a0923f6a9..af2076bfa10d 100644 --- a/crates/data_models/src/payments.rs +++ b/crates/data_models/src/payments.rs @@ -50,4 +50,6 @@ pub struct PaymentIntent { pub updated_by: String, pub surcharge_applicable: Option, + pub request_incremental_authorization: storage_enums::RequestIncrementalAuthorization, + pub incremental_authorization_allowed: Option, } diff --git a/crates/data_models/src/payments/payment_intent.rs b/crates/data_models/src/payments/payment_intent.rs index 2c5914f5b37f..d8f927a4e2c5 100644 --- a/crates/data_models/src/payments/payment_intent.rs +++ b/crates/data_models/src/payments/payment_intent.rs @@ -107,6 +107,8 @@ pub struct PaymentIntentNew { pub updated_by: String, pub surcharge_applicable: Option, + pub request_incremental_authorization: storage_enums::RequestIncrementalAuthorization, + pub incremental_authorization_allowed: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -116,6 +118,7 @@ pub enum PaymentIntentUpdate { amount_captured: Option, return_url: Option, updated_by: String, + incremental_authorization_allowed: Option, }, MetadataUpdate { metadata: pii::SecretSerdeValue, @@ -137,6 +140,7 @@ pub enum PaymentIntentUpdate { }, PGStatusUpdate { status: storage_enums::IntentStatus, + incremental_authorization_allowed: Option, updated_by: String, }, Update { @@ -213,6 +217,7 @@ pub struct PaymentIntentUpdateInternal { pub updated_by: String, pub surcharge_applicable: Option, + pub incremental_authorization_allowed: Option, } impl From for PaymentIntentUpdateInternal { @@ -283,10 +288,15 @@ impl From for PaymentIntentUpdateInternal { updated_by, ..Default::default() }, - PaymentIntentUpdate::PGStatusUpdate { status, updated_by } => Self { + PaymentIntentUpdate::PGStatusUpdate { + status, + updated_by, + incremental_authorization_allowed, + } => Self { status: Some(status), modified_at: Some(common_utils::date_time::now()), updated_by, + incremental_authorization_allowed, ..Default::default() }, PaymentIntentUpdate::MerchantStatusUpdate { @@ -310,6 +320,7 @@ impl From for PaymentIntentUpdateInternal { // customer_id, return_url, updated_by, + incremental_authorization_allowed, } => Self { // amount, // currency: Some(currency), @@ -319,6 +330,7 @@ impl From for PaymentIntentUpdateInternal { return_url, modified_at: Some(common_utils::date_time::now()), updated_by, + incremental_authorization_allowed, ..Default::default() }, PaymentIntentUpdate::PaymentAttemptAndAttemptCountUpdate { diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index 3ddd85f37891..3f8b37cd03f7 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -15,6 +15,7 @@ pub mod diesel_exports { DbPaymentType as PaymentType, DbPayoutStatus as PayoutStatus, DbPayoutType as PayoutType, DbProcessTrackerStatus as ProcessTrackerStatus, DbReconStatus as ReconStatus, DbRefundStatus as RefundStatus, DbRefundType as RefundType, + DbRequestIncrementalAuthorization as RequestIncrementalAuthorization, DbRoutingAlgorithmKind as RoutingAlgorithmKind, }; } diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index b6ff4fcf8d8d..8d752466103e 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -1,3 +1,4 @@ +use common_enums::RequestIncrementalAuthorization; use common_utils::pii; use diesel::{AsChangeset, Identifiable, Insertable, Queryable}; use serde::{Deserialize, Serialize}; @@ -51,6 +52,8 @@ pub struct PaymentIntent { pub updated_by: String, pub surcharge_applicable: Option, + pub request_incremental_authorization: RequestIncrementalAuthorization, + pub incremental_authorization_allowed: Option, } #[derive( @@ -106,6 +109,8 @@ pub struct PaymentIntentNew { pub updated_by: String, pub surcharge_applicable: Option, + pub request_incremental_authorization: RequestIncrementalAuthorization, + pub incremental_authorization_allowed: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -115,6 +120,7 @@ pub enum PaymentIntentUpdate { amount_captured: Option, return_url: Option, updated_by: String, + incremental_authorization_allowed: Option, }, MetadataUpdate { metadata: pii::SecretSerdeValue, @@ -137,6 +143,7 @@ pub enum PaymentIntentUpdate { PGStatusUpdate { status: storage_enums::IntentStatus, updated_by: String, + incremental_authorization_allowed: Option, }, Update { amount: i64, @@ -213,6 +220,7 @@ pub struct PaymentIntentUpdateInternal { pub updated_by: String, pub surcharge_applicable: Option, + pub incremental_authorization_allowed: Option, } impl PaymentIntentUpdate { @@ -243,6 +251,7 @@ impl PaymentIntentUpdate { payment_confirm_source, updated_by, surcharge_applicable, + incremental_authorization_allowed, } = self.into(); PaymentIntent { amount: amount.unwrap_or(source.amount), @@ -272,6 +281,8 @@ impl PaymentIntentUpdate { payment_confirm_source: payment_confirm_source.or(source.payment_confirm_source), updated_by, surcharge_applicable: surcharge_applicable.or(source.surcharge_applicable), + + incremental_authorization_allowed, ..source } } @@ -345,10 +356,15 @@ impl From for PaymentIntentUpdateInternal { updated_by, ..Default::default() }, - PaymentIntentUpdate::PGStatusUpdate { status, updated_by } => Self { + PaymentIntentUpdate::PGStatusUpdate { + status, + updated_by, + incremental_authorization_allowed, + } => Self { status: Some(status), modified_at: Some(common_utils::date_time::now()), updated_by, + incremental_authorization_allowed, ..Default::default() }, PaymentIntentUpdate::MerchantStatusUpdate { @@ -372,6 +388,7 @@ impl From for PaymentIntentUpdateInternal { // customer_id, return_url, updated_by, + incremental_authorization_allowed, } => Self { // amount, // currency: Some(currency), @@ -381,6 +398,7 @@ impl From for PaymentIntentUpdateInternal { return_url, modified_at: Some(common_utils::date_time::now()), updated_by, + incremental_authorization_allowed, ..Default::default() }, PaymentIntentUpdate::PaymentAttemptAndAttemptCountUpdate { diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 6cab6d5730d0..13b001ecc6d1 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -678,6 +678,8 @@ diesel::table! { #[max_length = 32] updated_by -> Varchar, surcharge_applicable -> Nullable, + request_incremental_authorization -> RequestIncrementalAuthorization, + incremental_authorization_allowed -> Nullable, } } diff --git a/crates/router/src/connector/aci/transformers.rs b/crates/router/src/connector/aci/transformers.rs index 66aeb3bb6b2b..9cfb657bdca8 100644 --- a/crates/router/src/connector/aci/transformers.rs +++ b/crates/router/src/connector/aci/transformers.rs @@ -733,6 +733,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.id), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index 4b3fcc851323..1793e3e07a87 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -2978,6 +2978,7 @@ impl TryFrom> connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -3011,6 +3012,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), payment_method_balance: Some(types::PaymentMethodBalance { amount: item.response.balance.value, @@ -3072,6 +3074,7 @@ pub fn get_adyen_response( connector_metadata: None, network_txn_id, connector_response_reference_id: Some(response.merchant_reference), + incremental_authorization_allowed: None, }; Ok((status, error, payments_response_data)) } @@ -3171,6 +3174,7 @@ pub fn get_redirection_response( connector_metadata, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }; Ok((status, error, payments_response_data)) } @@ -3222,6 +3226,7 @@ pub fn get_present_to_shopper_response( connector_metadata, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }; Ok((status, error, payments_response_data)) } @@ -3270,6 +3275,7 @@ pub fn get_qr_code_response( connector_metadata, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }; Ok((status, error, payments_response_data)) } @@ -3304,6 +3310,7 @@ pub fn get_redirection_error_response( connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }; Ok((status, error, payments_response_data)) @@ -3638,6 +3645,7 @@ impl TryFrom> connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), amount_captured: Some(item.response.amount.value), ..item.data diff --git a/crates/router/src/connector/airwallex/transformers.rs b/crates/router/src/connector/airwallex/transformers.rs index 3785e02d4747..2de7f6fe00ff 100644 --- a/crates/router/src/connector/airwallex/transformers.rs +++ b/crates/router/src/connector/airwallex/transformers.rs @@ -555,6 +555,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -596,6 +597,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/authorizedotnet/transformers.rs b/crates/router/src/connector/authorizedotnet/transformers.rs index 2c8a63a53e5c..30323ca4ef23 100644 --- a/crates/router/src/connector/authorizedotnet/transformers.rs +++ b/crates/router/src/connector/authorizedotnet/transformers.rs @@ -610,6 +610,7 @@ impl connector_response_reference_id: Some( transaction_response.transaction_id.clone(), ), + incremental_authorization_allowed: None, }), }, ..item.data @@ -680,6 +681,7 @@ impl connector_response_reference_id: Some( transaction_response.transaction_id.clone(), ), + incremental_authorization_allowed: None, }), }, ..item.data @@ -977,6 +979,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(transaction.transaction_id.clone()), + incremental_authorization_allowed: None, }), status: payment_status, ..item.data diff --git a/crates/router/src/connector/bambora/transformers.rs b/crates/router/src/connector/bambora/transformers.rs index e686186c901b..2d50569f9a49 100644 --- a/crates/router/src/connector/bambora/transformers.rs +++ b/crates/router/src/connector/bambora/transformers.rs @@ -215,6 +215,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(pg_response.order_number.to_string()), + incremental_authorization_allowed: None, }), ..item.data }), @@ -241,6 +242,7 @@ impl connector_response_reference_id: Some( item.data.connector_request_reference_id.to_string(), ), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index 12170deb1a00..18ec8ceb89d9 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -528,6 +528,7 @@ impl .code .unwrap_or(info_response.id), ), + incremental_authorization_allowed: None, }), ..item.data }), @@ -585,6 +586,7 @@ impl .code .unwrap_or(info_response.id), ), + incremental_authorization_allowed: None, }), ..item.data }), @@ -642,6 +644,7 @@ impl .code .unwrap_or(info_response.id), ), + incremental_authorization_allowed: None, }), ..item.data }), @@ -719,6 +722,7 @@ impl .client_reference_information .map(|cref| cref.code) .unwrap_or(Some(app_response.id)), + incremental_authorization_allowed: None, }), ..item.data }), @@ -733,6 +737,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(error_response.id), + incremental_authorization_allowed: None, }), ..item.data }), diff --git a/crates/router/src/connector/bitpay/transformers.rs b/crates/router/src/connector/bitpay/transformers.rs index 89dd2368b2b7..0ddf2dbf913b 100644 --- a/crates/router/src/connector/bitpay/transformers.rs +++ b/crates/router/src/connector/bitpay/transformers.rs @@ -178,6 +178,7 @@ impl .data .order_id .or(Some(item.response.data.id)), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/bluesnap.rs b/crates/router/src/connector/bluesnap.rs index 0bc56d4e9955..25cdcb731f11 100644 --- a/crates/router/src/connector/bluesnap.rs +++ b/crates/router/src/connector/bluesnap.rs @@ -713,6 +713,7 @@ impl ConnectorIntegration connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.transaction_id), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/boku/transformers.rs b/crates/router/src/connector/boku/transformers.rs index 3df9126fc4c0..c671560765d0 100644 --- a/crates/router/src/connector/boku/transformers.rs +++ b/crates/router/src/connector/boku/transformers.rs @@ -252,6 +252,7 @@ impl TryFrom connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -272,6 +273,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }), @@ -435,6 +437,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -452,6 +455,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }), @@ -495,6 +499,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -539,6 +544,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -1061,6 +1067,7 @@ impl TryFrom> connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -1158,6 +1165,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -1255,6 +1263,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/braintree/transformers.rs b/crates/router/src/connector/braintree/transformers.rs index dcca9c26434c..44daef94e8a6 100644 --- a/crates/router/src/connector/braintree/transformers.rs +++ b/crates/router/src/connector/braintree/transformers.rs @@ -239,6 +239,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/cashtocode/transformers.rs b/crates/router/src/connector/cashtocode/transformers.rs index cfca998e06c3..b38ca4b67132 100644 --- a/crates/router/src/connector/cashtocode/transformers.rs +++ b/crates/router/src/connector/cashtocode/transformers.rs @@ -238,6 +238,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ) } @@ -281,6 +282,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), amount_captured: Some(item.response.amount), ..item.data diff --git a/crates/router/src/connector/checkout/transformers.rs b/crates/router/src/connector/checkout/transformers.rs index 173ac0b8f585..ebe02f30d5ff 100644 --- a/crates/router/src/connector/checkout/transformers.rs +++ b/crates/router/src/connector/checkout/transformers.rs @@ -591,6 +591,7 @@ impl TryFrom> connector_response_reference_id: Some( item.response.reference.unwrap_or(item.response.id), ), + incremental_authorization_allowed: None, }; Ok(Self { status, @@ -640,6 +641,7 @@ impl TryFrom> connector_response_reference_id: Some( item.response.reference.unwrap_or(item.response.id), ), + incremental_authorization_allowed: None, }; Ok(Self { status, @@ -714,6 +716,7 @@ impl TryFrom> connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), status: response.into(), ..item.data @@ -810,6 +813,7 @@ impl TryFrom> connector_metadata: None, network_txn_id: None, connector_response_reference_id: item.response.reference, + incremental_authorization_allowed: None, }), status, amount_captured, diff --git a/crates/router/src/connector/coinbase/transformers.rs b/crates/router/src/connector/coinbase/transformers.rs index 6cc097bc9d8d..ce9bb3e871c5 100644 --- a/crates/router/src/connector/coinbase/transformers.rs +++ b/crates/router/src/connector/coinbase/transformers.rs @@ -146,6 +146,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.data.id.clone()), + incremental_authorization_allowed: None, }), |context| { Ok(types::PaymentsResponseData::TransactionUnresolvedResponse{ diff --git a/crates/router/src/connector/cryptopay/transformers.rs b/crates/router/src/connector/cryptopay/transformers.rs index 446da0761d1f..3af604c786b8 100644 --- a/crates/router/src/connector/cryptopay/transformers.rs +++ b/crates/router/src/connector/cryptopay/transformers.rs @@ -173,6 +173,7 @@ impl .data .custom_id .or(Some(item.response.data.id)), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 81df29966725..495e23e001ad 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -554,8 +554,9 @@ impl connector_mandate_id: Some(token_info.instrument_identifier.id), payment_method_id: None, }); + let status = get_payment_status(is_capture, item.response.status.into()); Ok(Self { - status: get_payment_status(is_capture, item.response.status.into()), + status, response: match item.response.error_information { Some(error) => Err(types::ErrorResponse { code: consts::NO_ERROR_CODE.to_string(), @@ -578,6 +579,9 @@ impl .client_reference_information .map(|cref| cref.code) .unwrap_or(Some(item.response.id)), + incremental_authorization_allowed: Some( + status == enums::AttemptStatus::Authorized, + ), }), }, ..item.data @@ -640,6 +644,9 @@ impl .client_reference_information .map(|cref| cref.code) .unwrap_or(Some(item.response.id)), + incremental_authorization_allowed: Some( + mandate_status == enums::AttemptStatus::Authorized, + ), }), }, ..item.data @@ -694,11 +701,12 @@ impl ) -> Result { let item = data.0; let is_capture = data.1; + let status = get_payment_status( + is_capture, + item.response.application_information.status.into(), + ); Ok(Self { - status: get_payment_status( - is_capture, - item.response.application_information.status.into(), - ), + status, response: Ok(types::PaymentsResponseData::TransactionResponse { resource_id: types::ResponseId::ConnectorTransactionId(item.response.id.clone()), redirection_data: None, @@ -710,6 +718,7 @@ impl .client_reference_information .map(|cref| cref.code) .unwrap_or(Some(item.response.id)), + incremental_authorization_allowed: Some(status == enums::AttemptStatus::Authorized), }), ..item.data }) diff --git a/crates/router/src/connector/dlocal/transformers.rs b/crates/router/src/connector/dlocal/transformers.rs index f7cfa6a868bd..92d01cfe56d4 100644 --- a/crates/router/src/connector/dlocal/transformers.rs +++ b/crates/router/src/connector/dlocal/transformers.rs @@ -329,6 +329,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: item.response.order_id.clone(), + incremental_authorization_allowed: None, }; Ok(Self { status: enums::AttemptStatus::from(item.response.status), @@ -368,6 +369,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: item.response.order_id.clone(), + incremental_authorization_allowed: None, }), ..item.data }) @@ -404,6 +406,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: item.response.order_id.clone(), + incremental_authorization_allowed: None, }), ..item.data }) @@ -440,6 +443,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.order_id.clone()), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/dummyconnector/transformers.rs b/crates/router/src/connector/dummyconnector/transformers.rs index dc707bde42cc..3c7bd2e09d9a 100644 --- a/crates/router/src/connector/dummyconnector/transformers.rs +++ b/crates/router/src/connector/dummyconnector/transformers.rs @@ -250,6 +250,7 @@ impl TryFrom connector_response_reference_id: Some( gateway_resp.transaction_processing_details.order_id, ), + incremental_authorization_allowed: None, }), ..item.data }) @@ -403,6 +404,7 @@ impl TryFrom })), network_txn_id: None, connector_response_reference_id: Some(transaction_id.to_string()), + incremental_authorization_allowed: None, }), ..item.data }) @@ -324,6 +325,7 @@ impl })), network_txn_id: None, connector_response_reference_id: Some(transaction_id.to_string()), + incremental_authorization_allowed: None, }), ..item.data }) @@ -391,6 +393,7 @@ impl TryFrom> })), network_txn_id: None, connector_response_reference_id: Some(item.response.transaction_id.to_string()), + incremental_authorization_allowed: None, }), amount_captured: None, ..item.data @@ -458,6 +461,7 @@ impl })), network_txn_id: None, connector_response_reference_id: Some(transaction_id.to_string()), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/globalpay/transformers.rs b/crates/router/src/connector/globalpay/transformers.rs index 78a83e700267..9cef564b3795 100644 --- a/crates/router/src/connector/globalpay/transformers.rs +++ b/crates/router/src/connector/globalpay/transformers.rs @@ -234,6 +234,7 @@ fn get_payment_response( connector_metadata: None, network_txn_id: None, connector_response_reference_id: response.reference, + incremental_authorization_allowed: None, }), } } diff --git a/crates/router/src/connector/globepay/transformers.rs b/crates/router/src/connector/globepay/transformers.rs index ef23f48f5197..f6adacb814de 100644 --- a/crates/router/src/connector/globepay/transformers.rs +++ b/crates/router/src/connector/globepay/transformers.rs @@ -157,6 +157,7 @@ impl connector_metadata, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -230,6 +231,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/gocardless/transformers.rs b/crates/router/src/connector/gocardless/transformers.rs index 63e199657af0..249dae370b1a 100644 --- a/crates/router/src/connector/gocardless/transformers.rs +++ b/crates/router/src/connector/gocardless/transformers.rs @@ -577,6 +577,7 @@ impl response: Ok(types::PaymentsResponseData::TransactionResponse { connector_metadata: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, resource_id: ResponseId::NoResponseId, redirection_data: None, mandate_reference, @@ -732,6 +733,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -766,6 +768,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/helcim/transformers.rs b/crates/router/src/connector/helcim/transformers.rs index 9f405e2e2ea1..dc38b2eeb253 100644 --- a/crates/router/src/connector/helcim/transformers.rs +++ b/crates/router/src/connector/helcim/transformers.rs @@ -328,6 +328,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), status: enums::AttemptStatus::from(item.response), ..item.data @@ -382,6 +383,7 @@ impl connector_metadata, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), status: enums::AttemptStatus::from(item.response), ..item.data @@ -440,6 +442,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), status: enums::AttemptStatus::from(item.response), ..item.data @@ -526,6 +529,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), status: enums::AttemptStatus::from(item.response), ..item.data @@ -588,6 +592,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), status: enums::AttemptStatus::from(item.response), ..item.data diff --git a/crates/router/src/connector/iatapay/transformers.rs b/crates/router/src/connector/iatapay/transformers.rs index 7cdfafc858b6..b6d2dee4a01b 100644 --- a/crates/router/src/connector/iatapay/transformers.rs +++ b/crates/router/src/connector/iatapay/transformers.rs @@ -286,6 +286,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: connector_response_reference_id.clone(), + incremental_authorization_allowed: None, }), |checkout_methods| { Ok(types::PaymentsResponseData::TransactionResponse { @@ -299,6 +300,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: connector_response_reference_id.clone(), + incremental_authorization_allowed: None, }) }, ), diff --git a/crates/router/src/connector/klarna/transformers.rs b/crates/router/src/connector/klarna/transformers.rs index 563410ee99d0..0816dd82ec6b 100644 --- a/crates/router/src/connector/klarna/transformers.rs +++ b/crates/router/src/connector/klarna/transformers.rs @@ -167,6 +167,7 @@ impl TryFrom> connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.order_id.clone()), + incremental_authorization_allowed: None, }), status: item.response.fraud_status.into(), ..item.data diff --git a/crates/router/src/connector/mollie/transformers.rs b/crates/router/src/connector/mollie/transformers.rs index b77077ae709f..62fb94e236a8 100644 --- a/crates/router/src/connector/mollie/transformers.rs +++ b/crates/router/src/connector/mollie/transformers.rs @@ -531,6 +531,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.id), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/multisafepay/transformers.rs b/crates/router/src/connector/multisafepay/transformers.rs index 1780b77379c7..7672566f8274 100644 --- a/crates/router/src/connector/multisafepay/transformers.rs +++ b/crates/router/src/connector/multisafepay/transformers.rs @@ -694,6 +694,7 @@ impl connector_response_reference_id: Some( payment_response.data.order_id.clone(), ), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/nexinets/transformers.rs b/crates/router/src/connector/nexinets/transformers.rs index 15cbe9a7e28e..8875abdb7868 100644 --- a/crates/router/src/connector/nexinets/transformers.rs +++ b/crates/router/src/connector/nexinets/transformers.rs @@ -372,6 +372,7 @@ impl connector_metadata: Some(connector_metadata), network_txn_id: None, connector_response_reference_id: Some(item.response.order_id), + incremental_authorization_allowed: None, }), ..item.data }) @@ -455,6 +456,7 @@ impl connector_metadata: Some(connector_metadata), network_txn_id: None, connector_response_reference_id: Some(item.response.order.order_id), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/nmi/transformers.rs b/crates/router/src/connector/nmi/transformers.rs index ff3a1e6a1c54..35c0e102020e 100644 --- a/crates/router/src/connector/nmi/transformers.rs +++ b/crates/router/src/connector/nmi/transformers.rs @@ -322,6 +322,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), enums::AttemptStatus::CaptureInitiated, ), @@ -415,6 +416,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), enums::AttemptStatus::Charged, ), @@ -470,6 +472,7 @@ impl TryFrom> connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), if let Some(diesel_models::enums::CaptureMethod::Automatic) = item.data.request.capture_method @@ -519,6 +522,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), enums::AttemptStatus::VoidInitiated, ), @@ -570,6 +574,7 @@ impl TryFrom> connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/noon/transformers.rs b/crates/router/src/connector/noon/transformers.rs index ee3a8ba8c532..b478d63e0f12 100644 --- a/crates/router/src/connector/noon/transformers.rs +++ b/crates/router/src/connector/noon/transformers.rs @@ -527,6 +527,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id, + incremental_authorization_allowed: None, }) } }, diff --git a/crates/router/src/connector/nuvei/transformers.rs b/crates/router/src/connector/nuvei/transformers.rs index 36244b8bc0d8..73e039c63395 100644 --- a/crates/router/src/connector/nuvei/transformers.rs +++ b/crates/router/src/connector/nuvei/transformers.rs @@ -1452,6 +1452,7 @@ where }, network_txn_id: None, connector_response_reference_id: response.order_id, + incremental_authorization_allowed: None, }) }, ..item.data diff --git a/crates/router/src/connector/opayo/transformers.rs b/crates/router/src/connector/opayo/transformers.rs index 5e9fb066c78d..7b633f6aa641 100644 --- a/crates/router/src/connector/opayo/transformers.rs +++ b/crates/router/src/connector/opayo/transformers.rs @@ -123,6 +123,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.transaction_id), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/opennode/transformers.rs b/crates/router/src/connector/opennode/transformers.rs index 794fc8573417..7670166fabaf 100644 --- a/crates/router/src/connector/opennode/transformers.rs +++ b/crates/router/src/connector/opennode/transformers.rs @@ -150,6 +150,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: item.response.data.order_id, + incremental_authorization_allowed: None, }) } else { Ok(types::PaymentsResponseData::TransactionUnresolvedResponse { diff --git a/crates/router/src/connector/payeezy/transformers.rs b/crates/router/src/connector/payeezy/transformers.rs index 90c58c3a9bce..0170d18ecb46 100644 --- a/crates/router/src/connector/payeezy/transformers.rs +++ b/crates/router/src/connector/payeezy/transformers.rs @@ -440,6 +440,7 @@ impl .reference .unwrap_or(item.response.transaction_id), ), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/payme/transformers.rs b/crates/router/src/connector/payme/transformers.rs index e751de20e219..e3d54881f1f2 100644 --- a/crates/router/src/connector/payme/transformers.rs +++ b/crates/router/src/connector/payme/transformers.rs @@ -262,6 +262,7 @@ impl TryFrom<&PaymePaySaleResponse> for types::PaymentsResponseData { ), network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }) } } @@ -326,6 +327,7 @@ impl From<&SaleQuery> for types::PaymentsResponseData { connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, } } } @@ -535,6 +537,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }), diff --git a/crates/router/src/connector/paypal.rs b/crates/router/src/connector/paypal.rs index 9ab19b295570..c60b20bb367d 100644 --- a/crates/router/src/connector/paypal.rs +++ b/crates/router/src/connector/paypal.rs @@ -615,6 +615,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..data.clone() }) diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs index 04328cead233..fbe6a47d2007 100644 --- a/crates/router/src/connector/paypal/transformers.rs +++ b/crates/router/src/connector/paypal/transformers.rs @@ -1174,6 +1174,7 @@ impl .invoice_id .clone() .or(Some(item.response.id)), + incremental_authorization_allowed: None, }), ..item.data }) @@ -1278,6 +1279,7 @@ impl connector_response_reference_id: Some( purchase_units.map_or(item.response.id, |item| item.invoice_id.clone()), ), + incremental_authorization_allowed: None, }), ..item.data }) @@ -1314,6 +1316,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -1363,6 +1366,7 @@ impl connector_metadata: Some(connector_meta), network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -1430,6 +1434,7 @@ impl .invoice_id .clone() .or(Some(item.response.supplementary_data.related_ids.order_id)), + incremental_authorization_allowed: None, }), ..item.data }) @@ -1531,6 +1536,7 @@ impl TryFrom> .response .invoice_id .or(Some(item.response.id)), + incremental_authorization_allowed: None, }), amount_captured: Some(amount_captured), ..item.data @@ -1581,6 +1587,7 @@ impl .response .invoice_id .or(Some(item.response.id)), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/payu/transformers.rs b/crates/router/src/connector/payu/transformers.rs index 9a2e14215c75..6edc570eb451 100644 --- a/crates/router/src/connector/payu/transformers.rs +++ b/crates/router/src/connector/payu/transformers.rs @@ -205,6 +205,7 @@ impl .response .ext_order_id .or(Some(item.response.order_id)), + incremental_authorization_allowed: None, }), amount_captured: None, ..item.data @@ -257,6 +258,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), amount_captured: None, ..item.data @@ -342,6 +344,7 @@ impl .response .ext_order_id .or(Some(item.response.order_id)), + incremental_authorization_allowed: None, }), amount_captured: None, ..item.data @@ -475,6 +478,7 @@ impl .ext_order_id .clone() .or(Some(order.order_id.clone())), + incremental_authorization_allowed: None, }), amount_captured: Some( order diff --git a/crates/router/src/connector/powertranz/transformers.rs b/crates/router/src/connector/powertranz/transformers.rs index a631a126ed3f..e0ecd81c7e58 100644 --- a/crates/router/src/connector/powertranz/transformers.rs +++ b/crates/router/src/connector/powertranz/transformers.rs @@ -328,6 +328,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.order_identifier), + incremental_authorization_allowed: None, }), Err, ); diff --git a/crates/router/src/connector/prophetpay/transformers.rs b/crates/router/src/connector/prophetpay/transformers.rs index d81b931edfc9..d05f2c3986a7 100644 --- a/crates/router/src/connector/prophetpay/transformers.rs +++ b/crates/router/src/connector/prophetpay/transformers.rs @@ -219,6 +219,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -407,6 +408,7 @@ impl connector_metadata, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -456,6 +458,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -505,6 +508,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/rapyd/transformers.rs b/crates/router/src/connector/rapyd/transformers.rs index 898b6ed6d147..193eb8198926 100644 --- a/crates/router/src/connector/rapyd/transformers.rs +++ b/crates/router/src/connector/rapyd/transformers.rs @@ -487,6 +487,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ) } diff --git a/crates/router/src/connector/shift4/transformers.rs b/crates/router/src/connector/shift4/transformers.rs index c272a5b6fc12..606da2129fb0 100644 --- a/crates/router/src/connector/shift4/transformers.rs +++ b/crates/router/src/connector/shift4/transformers.rs @@ -702,6 +702,7 @@ impl ), network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -743,6 +744,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.id), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/square/transformers.rs b/crates/router/src/connector/square/transformers.rs index 6024a20fa6ab..7343ef58bb08 100644 --- a/crates/router/src/connector/square/transformers.rs +++ b/crates/router/src/connector/square/transformers.rs @@ -401,6 +401,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: item.response.payment.reference_id, + incremental_authorization_allowed: None, }), amount_captured, ..item.data diff --git a/crates/router/src/connector/stax/transformers.rs b/crates/router/src/connector/stax/transformers.rs index 5aa0949a09cc..2fd3b3474ea4 100644 --- a/crates/router/src/connector/stax/transformers.rs +++ b/crates/router/src/connector/stax/transformers.rs @@ -367,6 +367,7 @@ impl connector_response_reference_id: Some( item.response.idempotency_id.unwrap_or(item.response.id), ), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index ae7fe59be96c..182479604539 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -2334,6 +2334,7 @@ impl connector_metadata, network_txn_id, connector_response_reference_id: Some(item.response.id), + incremental_authorization_allowed: None, }), amount_captured: item.response.amount_received, ..item.data @@ -2494,6 +2495,7 @@ impl connector_metadata, network_txn_id: None, connector_response_reference_id: Some(item.response.id.clone()), + incremental_authorization_allowed: None, }), Err, ); @@ -2535,6 +2537,7 @@ impl connector_metadata: None, network_txn_id: Option::foreign_from(item.response.latest_attempt), connector_response_reference_id: Some(item.response.id), + incremental_authorization_allowed: None, }), ..item.data }) @@ -3076,6 +3079,7 @@ impl TryFrom types::PaymentsRes connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(connector_response.transaction_id), + incremental_authorization_allowed: None, } } @@ -241,6 +242,7 @@ fn get_payments_sync_response( .transaction_id .clone(), ), + incremental_authorization_allowed: None, } } diff --git a/crates/router/src/connector/volt/transformers.rs b/crates/router/src/connector/volt/transformers.rs index 6f4c67dce8a3..cea56feb7145 100644 --- a/crates/router/src/connector/volt/transformers.rs +++ b/crates/router/src/connector/volt/transformers.rs @@ -284,6 +284,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.id), + incremental_authorization_allowed: None, }), ..item.data }) @@ -335,6 +336,7 @@ impl TryFrom TryFrom TryFrom> connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/zen/transformers.rs b/crates/router/src/connector/zen/transformers.rs index 64f6d5bf1a07..c66b098fe751 100644 --- a/crates/router/src/connector/zen/transformers.rs +++ b/crates/router/src/connector/zen/transformers.rs @@ -940,6 +940,7 @@ impl TryFrom TryFrom let payment_intent_update = storage::PaymentIntentUpdate::PGStatusUpdate { status: enums::IntentStatus::Cancelled, updated_by: storage_scheme.to_string(), + incremental_authorization_allowed: None, }; (Some(payment_intent_update), enums::AttemptStatus::Voided) } else { diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 28b6dbec96ab..d718db79a6d0 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -419,6 +419,15 @@ impl .attach_printable("Error converting feature_metadata to Value")? .or(payment_intent.feature_metadata); payment_intent.metadata = request.metadata.clone().or(payment_intent.metadata); + payment_intent.request_incremental_authorization = request + .request_incremental_authorization + .map(|request_incremental_authorization| { + core_utils::get_request_incremental_authorization_value( + Some(request_incremental_authorization), + payment_attempt.capture_method, + ) + }) + .unwrap_or(Ok(payment_intent.request_incremental_authorization))?; payment_attempt.business_sub_label = request .business_sub_label .clone() diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index c12f28e23390..ac387076d1d1 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -713,6 +713,12 @@ impl PaymentCreate { let payment_link_id = payment_link_data.map(|pl_data| pl_data.payment_link_id); + let request_incremental_authorization = + core_utils::get_request_incremental_authorization_value( + request.request_incremental_authorization, + request.capture_method, + )?; + Ok(storage::PaymentIntentNew { payment_id: payment_id.to_string(), merchant_id: merchant_account.merchant_id.to_string(), @@ -749,6 +755,8 @@ impl PaymentCreate { payment_confirm_source: None, surcharge_applicable: None, updated_by: merchant_account.storage_scheme.to_string(), + request_incremental_authorization, + incremental_authorization_allowed: None, }) } diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 2de5df38dba4..9781ad651ee2 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -418,8 +418,18 @@ async fn payment_response_update_tracker( redirection_data, connector_metadata, connector_response_reference_id, + incremental_authorization_allowed, .. } => { + payment_data + .payment_intent + .incremental_authorization_allowed = + core_utils::get_incremental_authorization_allowed_value( + incremental_authorization_allowed, + payment_data + .payment_intent + .request_incremental_authorization, + ); let connector_transaction_id = match resource_id { types::ResponseId::NoResponseId => None, types::ResponseId::ConnectorTransactionId(id) @@ -627,6 +637,7 @@ async fn payment_response_update_tracker( payment_data.payment_attempt.status, ), updated_by: storage_scheme.to_string(), + incremental_authorization_allowed: Some(false), }, Ok(_) => storage::PaymentIntentUpdate::ResponseUpdate { status: api_models::enums::IntentStatus::foreign_from( @@ -635,6 +646,9 @@ async fn payment_response_update_tracker( return_url: router_data.return_url.clone(), amount_captured, updated_by: storage_scheme.to_string(), + incremental_authorization_allowed: payment_data + .payment_intent + .incremental_authorization_allowed, }, }; diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 000bbb0fc00b..51e139c97988 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -1,6 +1,7 @@ use std::{fmt::Debug, marker::PhantomData, str::FromStr}; use api_models::payments::{FrmMessage, RequestSurchargeDetails}; +use common_enums::RequestIncrementalAuthorization; use common_utils::{consts::X_HS_LATENCY, fp_utils}; use diesel_models::ephemeral_key; use error_stack::{IntoReport, ResultExt}; @@ -80,6 +81,7 @@ where connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }); let additional_data = PaymentAdditionalData { @@ -687,6 +689,9 @@ where .set_merchant_connector_id(payment_attempt.merchant_connector_id) .set_unified_code(payment_attempt.unified_code) .set_unified_message(payment_attempt.unified_message) + .set_incremental_authorization_allowed( + payment_intent.incremental_authorization_allowed, + ) .to_owned(), headers, )) @@ -749,6 +754,7 @@ where surcharge_details, unified_code: payment_attempt.unified_code, unified_message: payment_attempt.unified_message, + incremental_authorization_allowed: payment_intent.incremental_authorization_allowed, ..Default::default() }, headers, @@ -1036,6 +1042,12 @@ impl TryFrom> for types::PaymentsAuthoriz complete_authorize_url, customer_id: None, surcharge_details: payment_data.surcharge_details, + request_incremental_authorization: matches!( + payment_data + .payment_intent + .request_incremental_authorization, + RequestIncrementalAuthorization::True | RequestIncrementalAuthorization::Default + ), }) } } @@ -1274,6 +1286,12 @@ impl TryFrom> for types::SetupMandateRequ return_url: payment_data.payment_intent.return_url, browser_info, payment_method_type: attempt.payment_method_type, + request_incremental_authorization: matches!( + payment_data + .payment_intent + .request_incremental_authorization, + RequestIncrementalAuthorization::True | RequestIncrementalAuthorization::Default + ), }) } } diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 5207e4ba8079..670c25c814ed 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -4,6 +4,7 @@ use api_models::{ enums::{DisputeStage, DisputeStatus}, payment_methods::{SurchargeDetailsResponse, SurchargeMetadata}, }; +use common_enums::RequestIncrementalAuthorization; #[cfg(feature = "payouts")] use common_utils::{crypto::Encryptable, pii::Email}; use common_utils::{ @@ -1133,3 +1134,32 @@ pub async fn get_individual_surcharge_detail_from_redis( .get_hash_field_and_deserialize(&redis_key, &value_key, "SurchargeDetailsResponse") .await } + +pub fn get_request_incremental_authorization_value( + request_incremental_authorization: Option, + capture_method: Option, +) -> RouterResult { + request_incremental_authorization + .map(|request_incremental_authorization| { + if request_incremental_authorization { + if capture_method == Some(common_enums::CaptureMethod::Automatic) { + Err(errors::ApiErrorResponse::NotSupported { message: "incremental authorization is not supported when capture_method is automatic".to_owned() }).into_report()? + } + Ok(RequestIncrementalAuthorization::True) + } else { + Ok(RequestIncrementalAuthorization::False) + } + }) + .unwrap_or(Ok(RequestIncrementalAuthorization::default())) +} + +pub fn get_incremental_authorization_allowed_value( + incremental_authorization_allowed: Option, + request_incremental_authorization: RequestIncrementalAuthorization, +) -> Option { + if request_incremental_authorization == common_enums::RequestIncrementalAuthorization::False { + Some(false) + } else { + incremental_authorization_allowed + } +} diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index c3118f0c05be..c267a54cc57b 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -381,6 +381,7 @@ pub struct PaymentsAuthorizeData { pub payment_method_type: Option, pub surcharge_details: Option, pub customer_id: Option, + pub request_incremental_authorization: bool, } #[derive(Debug, Clone, Default)] @@ -536,6 +537,7 @@ pub struct SetupMandateRequestData { pub email: Option, pub return_url: Option, pub payment_method_type: Option, + pub request_incremental_authorization: bool, } #[derive(Debug, Clone)] @@ -669,6 +671,7 @@ pub enum PaymentsResponseData { connector_metadata: Option, network_txn_id: Option, connector_response_reference_id: Option, + incremental_authorization_allowed: Option, }, MultipleCaptureResponse { // pending_capture_id_list: Vec, @@ -1200,6 +1203,7 @@ impl From<&SetupMandateRouterData> for PaymentsAuthorizeData { payment_method_type: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: data.request.request_incremental_authorization, } } } diff --git a/crates/router/src/types/api/verify_connector.rs b/crates/router/src/types/api/verify_connector.rs index 3e3511ccb98f..74b15f911b9a 100644 --- a/crates/router/src/types/api/verify_connector.rs +++ b/crates/router/src/types/api/verify_connector.rs @@ -47,6 +47,7 @@ impl VerifyConnectorData { complete_authorize_url: None, related_transaction_id: None, statement_descriptor_suffix: None, + request_incremental_authorization: false, } } diff --git a/crates/router/src/workflows/payment_sync.rs b/crates/router/src/workflows/payment_sync.rs index f2760a00582d..43567ce27e23 100644 --- a/crates/router/src/workflows/payment_sync.rs +++ b/crates/router/src/workflows/payment_sync.rs @@ -124,7 +124,7 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow { .as_ref() .is_none() { - let payment_intent_update = data_models::payments::payment_intent::PaymentIntentUpdate::PGStatusUpdate { status: api_models::enums::IntentStatus::Failed,updated_by: merchant_account.storage_scheme.to_string() }; + let payment_intent_update = data_models::payments::payment_intent::PaymentIntentUpdate::PGStatusUpdate { status: api_models::enums::IntentStatus::Failed,updated_by: merchant_account.storage_scheme.to_string(), incremental_authorization_allowed: Some(false) }; let payment_attempt_update = data_models::payments::payment_attempt::PaymentAttemptUpdate::ErrorUpdate { connector: None, diff --git a/crates/router/tests/connectors/aci.rs b/crates/router/tests/connectors/aci.rs index e12e27708f87..7ddc504956fb 100644 --- a/crates/router/tests/connectors/aci.rs +++ b/crates/router/tests/connectors/aci.rs @@ -69,6 +69,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { complete_authorize_url: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, }, response: Err(types::ErrorResponse::default()), payment_method_id: None, diff --git a/crates/router/tests/connectors/adyen.rs b/crates/router/tests/connectors/adyen.rs index 4b2cbcb7c4a9..714dc0d7d672 100644 --- a/crates/router/tests/connectors/adyen.rs +++ b/crates/router/tests/connectors/adyen.rs @@ -157,6 +157,7 @@ impl AdyenTest { complete_authorize_url: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, }) } } diff --git a/crates/router/tests/connectors/bitpay.rs b/crates/router/tests/connectors/bitpay.rs index 755427140c4f..3c9f08bf1b69 100644 --- a/crates/router/tests/connectors/bitpay.rs +++ b/crates/router/tests/connectors/bitpay.rs @@ -92,6 +92,7 @@ fn payment_method_details() -> Option { capture_method: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, }) } diff --git a/crates/router/tests/connectors/cashtocode.rs b/crates/router/tests/connectors/cashtocode.rs index 871677bb692a..a7c95936fbe8 100644 --- a/crates/router/tests/connectors/cashtocode.rs +++ b/crates/router/tests/connectors/cashtocode.rs @@ -67,6 +67,7 @@ impl CashtocodeTest { complete_authorize_url: None, customer_id: Some("John Doe".to_owned()), surcharge_details: None, + request_incremental_authorization: false, }) } diff --git a/crates/router/tests/connectors/coinbase.rs b/crates/router/tests/connectors/coinbase.rs index 512e03a5c94d..2ddb5464d4df 100644 --- a/crates/router/tests/connectors/coinbase.rs +++ b/crates/router/tests/connectors/coinbase.rs @@ -94,6 +94,7 @@ fn payment_method_details() -> Option { capture_method: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, }) } diff --git a/crates/router/tests/connectors/cryptopay.rs b/crates/router/tests/connectors/cryptopay.rs index e9c43cee3af6..11e556215c35 100644 --- a/crates/router/tests/connectors/cryptopay.rs +++ b/crates/router/tests/connectors/cryptopay.rs @@ -92,6 +92,7 @@ fn payment_method_details() -> Option { capture_method: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, }) } diff --git a/crates/router/tests/connectors/opennode.rs b/crates/router/tests/connectors/opennode.rs index 248bbb02e520..707192e01c3b 100644 --- a/crates/router/tests/connectors/opennode.rs +++ b/crates/router/tests/connectors/opennode.rs @@ -93,6 +93,7 @@ fn payment_method_details() -> Option { capture_method: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, }) } diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index f325370e737f..823b3eae497d 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -908,6 +908,7 @@ impl Default for PaymentAuthorizeType { webhook_url: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, }; Self(data) } @@ -1043,6 +1044,7 @@ pub fn get_connector_metadata( connector_metadata, network_txn_id: _, connector_response_reference_id: _, + incremental_authorization_allowed: _, }) => connector_metadata, _ => None, } diff --git a/crates/router/tests/connectors/worldline.rs b/crates/router/tests/connectors/worldline.rs index 6163949c6c58..fd697f95b754 100644 --- a/crates/router/tests/connectors/worldline.rs +++ b/crates/router/tests/connectors/worldline.rs @@ -102,6 +102,7 @@ impl WorldlineTest { complete_authorize_url: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, }) } } diff --git a/crates/storage_impl/src/mock_db/payment_intent.rs b/crates/storage_impl/src/mock_db/payment_intent.rs index 08a4a2aabeaa..a3e82c1d1044 100644 --- a/crates/storage_impl/src/mock_db/payment_intent.rs +++ b/crates/storage_impl/src/mock_db/payment_intent.rs @@ -106,6 +106,8 @@ impl PaymentIntentInterface for MockDb { payment_confirm_source: new.payment_confirm_source, updated_by: storage_scheme.to_string(), surcharge_applicable: new.surcharge_applicable, + request_incremental_authorization: new.request_incremental_authorization, + incremental_authorization_allowed: new.incremental_authorization_allowed, }; payment_intents.push(payment_intent.clone()); Ok(payment_intent) diff --git a/crates/storage_impl/src/payments/payment_intent.rs b/crates/storage_impl/src/payments/payment_intent.rs index c3b3d22ffe35..fdf9875bc1ff 100644 --- a/crates/storage_impl/src/payments/payment_intent.rs +++ b/crates/storage_impl/src/payments/payment_intent.rs @@ -97,6 +97,8 @@ impl PaymentIntentInterface for KVRouterStore { payment_confirm_source: new.payment_confirm_source, updated_by: storage_scheme.to_string(), surcharge_applicable: new.surcharge_applicable, + request_incremental_authorization: new.request_incremental_authorization, + incremental_authorization_allowed: new.incremental_authorization_allowed, }; let redis_entry = kv::TypedSql { op: kv::DBOperation::Insert { @@ -758,6 +760,8 @@ impl DataModelExt for PaymentIntentNew { payment_confirm_source: self.payment_confirm_source, updated_by: self.updated_by, surcharge_applicable: self.surcharge_applicable, + request_incremental_authorization: self.request_incremental_authorization, + incremental_authorization_allowed: self.incremental_authorization_allowed, } } @@ -798,6 +802,8 @@ impl DataModelExt for PaymentIntentNew { payment_confirm_source: storage_model.payment_confirm_source, updated_by: storage_model.updated_by, surcharge_applicable: storage_model.surcharge_applicable, + request_incremental_authorization: storage_model.request_incremental_authorization, + incremental_authorization_allowed: storage_model.incremental_authorization_allowed, } } } @@ -843,6 +849,8 @@ impl DataModelExt for PaymentIntent { payment_confirm_source: self.payment_confirm_source, updated_by: self.updated_by, surcharge_applicable: self.surcharge_applicable, + request_incremental_authorization: self.request_incremental_authorization, + incremental_authorization_allowed: self.incremental_authorization_allowed, } } @@ -884,6 +892,8 @@ impl DataModelExt for PaymentIntent { payment_confirm_source: storage_model.payment_confirm_source, updated_by: storage_model.updated_by, surcharge_applicable: storage_model.surcharge_applicable, + request_incremental_authorization: storage_model.request_incremental_authorization, + incremental_authorization_allowed: storage_model.incremental_authorization_allowed, } } } @@ -898,11 +908,13 @@ impl DataModelExt for PaymentIntentUpdate { amount_captured, return_url, updated_by, + incremental_authorization_allowed, } => DieselPaymentIntentUpdate::ResponseUpdate { status, amount_captured, return_url, updated_by, + incremental_authorization_allowed, }, Self::MetadataUpdate { metadata, @@ -937,9 +949,15 @@ impl DataModelExt for PaymentIntentUpdate { billing_address_id, updated_by, }, - Self::PGStatusUpdate { status, updated_by } => { - DieselPaymentIntentUpdate::PGStatusUpdate { status, updated_by } - } + Self::PGStatusUpdate { + status, + updated_by, + incremental_authorization_allowed, + } => DieselPaymentIntentUpdate::PGStatusUpdate { + status, + updated_by, + incremental_authorization_allowed, + }, Self::Update { amount, currency, diff --git a/migrations/2023-11-28-081058_add-request_incremental_authorization-in-payment-intent/down.sql b/migrations/2023-11-28-081058_add-request_incremental_authorization-in-payment-intent/down.sql new file mode 100644 index 000000000000..5ee12132dee6 --- /dev/null +++ b/migrations/2023-11-28-081058_add-request_incremental_authorization-in-payment-intent/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_intent DROP COLUMN IF EXISTS request_incremental_authorization; +DROP TYPE "RequestIncrementalAuthorization"; diff --git a/migrations/2023-11-28-081058_add-request_incremental_authorization-in-payment-intent/up.sql b/migrations/2023-11-28-081058_add-request_incremental_authorization-in-payment-intent/up.sql new file mode 100644 index 000000000000..2c4d68593588 --- /dev/null +++ b/migrations/2023-11-28-081058_add-request_incremental_authorization-in-payment-intent/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +CREATE TYPE "RequestIncrementalAuthorization" AS ENUM ('true', 'false', 'default'); +ALTER TABLE payment_intent ADD COLUMN IF NOT EXISTS request_incremental_authorization "RequestIncrementalAuthorization" NOT NULL DEFAULT 'false'::"RequestIncrementalAuthorization"; diff --git a/migrations/2023-11-29-063030_add-incremental_authorization_allowed-in-payment-intent/down.sql b/migrations/2023-11-29-063030_add-incremental_authorization_allowed-in-payment-intent/down.sql new file mode 100644 index 000000000000..f08165481889 --- /dev/null +++ b/migrations/2023-11-29-063030_add-incremental_authorization_allowed-in-payment-intent/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_intent DROP COLUMN IF EXISTS incremental_authorization_allowed; \ No newline at end of file diff --git a/migrations/2023-11-29-063030_add-incremental_authorization_allowed-in-payment-intent/up.sql b/migrations/2023-11-29-063030_add-incremental_authorization_allowed-in-payment-intent/up.sql new file mode 100644 index 000000000000..73fe22dd52df --- /dev/null +++ b/migrations/2023-11-29-063030_add-incremental_authorization_allowed-in-payment-intent/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE payment_intent ADD COLUMN IF NOT EXISTS incremental_authorization_allowed BOOLEAN; \ No newline at end of file diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 86dc053d2d77..f5ad99f05752 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -9721,6 +9721,11 @@ } ], "nullable": true + }, + "request_incremental_authorization": { + "type": "boolean", + "description": "Request for an incremental authorization", + "nullable": true } } }, @@ -10085,6 +10090,11 @@ } ], "nullable": true + }, + "request_incremental_authorization": { + "type": "boolean", + "description": "Request for an incremental authorization", + "nullable": true } } }, @@ -10518,6 +10528,11 @@ "type": "string", "description": "Identifier of the connector ( merchant connector account ) which was chosen to make the payment", "nullable": true + }, + "incremental_authorization_allowed": { + "type": "boolean", + "description": "If true incremental authorization can be performed on this payment", + "nullable": true } } }, From 8c37a8d857c5a58872fa2b2e194b85e755129677 Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Thu, 30 Nov 2023 18:51:51 +0530 Subject: [PATCH 142/146] fix(connector): [Trustpay] Add mapping to error code `800.100.165` and `900.100.100` (#2925) --- crates/router/src/connector/trustpay/transformers.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/router/src/connector/trustpay/transformers.rs b/crates/router/src/connector/trustpay/transformers.rs index 5f3fb865d33d..e985eff11976 100644 --- a/crates/router/src/connector/trustpay/transformers.rs +++ b/crates/router/src/connector/trustpay/transformers.rs @@ -499,6 +499,7 @@ fn is_payment_failed(payment_status: &str) -> (bool, &'static str) { true, "Transaction declined (maximum transaction frequency exceeded)", ), + "800.100.165" => (true, "Transaction declined (card lost)"), "800.100.168" => (true, "Transaction declined (restricted card)"), "800.100.170" => (true, "Transaction declined (transaction not permitted)"), "800.100.171" => (true, "transaction declined (pick up card)"), @@ -512,6 +513,10 @@ fn is_payment_failed(payment_status: &str) -> (bool, &'static str) { true, "Transaction for the same session is currently being processed, please try again later", ), + "900.100.100" => ( + true, + "Unexpected communication error with connector/acquirer", + ), "900.100.300" => (true, "Timeout, uncertain result"), _ => (false, ""), } From 2e2dbe47156695beff6c0e4c800c0036fc426ed0 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 30 Nov 2023 14:33:36 +0000 Subject: [PATCH 143/146] chore(version): v1.93.0 --- CHANGELOG.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2966b238bba..3831e3d1caf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,40 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.93.0 (2023-11-30) + +### Features + +- **connector:** [BANKOFAMERICA] Add Required Fields for GPAY ([#3014](https://github.com/juspay/hyperswitch/pull/3014)) ([`d30b58a`](https://github.com/juspay/hyperswitch/commit/d30b58abb5e716b70c2dadec9e6f13c9e3403b6f)) +- **core:** Add ability to verify connector credentials before integrating the connector ([#2986](https://github.com/juspay/hyperswitch/pull/2986)) ([`39f255b`](https://github.com/juspay/hyperswitch/commit/39f255b4b209588dec35d780078c2ab7ceb37b10)) +- **router:** Make core changes in payments flow to support incremental authorization ([#3009](https://github.com/juspay/hyperswitch/pull/3009)) ([`1ca2ba4`](https://github.com/juspay/hyperswitch/commit/1ca2ba459495ff9340954c87a6ae3e4dce0e7b71)) +- **user:** Add support for dashboard metadata ([#3000](https://github.com/juspay/hyperswitch/pull/3000)) ([`6a2e4ab`](https://github.com/juspay/hyperswitch/commit/6a2e4ab4169820f35e953a949bd2e82e7f098ed2)) + +### Bug Fixes + +- **connector:** + - Move authorised status to charged in setup mandate ([#3017](https://github.com/juspay/hyperswitch/pull/3017)) ([`663754d`](https://github.com/juspay/hyperswitch/commit/663754d629d59a17ba9d4985fe04f9404ceb16b7)) + - [Trustpay] Add mapping to error code `800.100.165` and `900.100.100` ([#2925](https://github.com/juspay/hyperswitch/pull/2925)) ([`8c37a8d`](https://github.com/juspay/hyperswitch/commit/8c37a8d857c5a58872fa2b2e194b85e755129677)) +- **core:** Error message on Refund update for `Not Implemented` Case ([#3011](https://github.com/juspay/hyperswitch/pull/3011)) ([`6b7ada1`](https://github.com/juspay/hyperswitch/commit/6b7ada1a34450ea3a7fc019375ba462a14ddd6ab)) +- **pm_list:** [Trustpay] Update Cards, Bank_redirect - blik pm type required field info for Trustpay ([#2999](https://github.com/juspay/hyperswitch/pull/2999)) ([`c05432c`](https://github.com/juspay/hyperswitch/commit/c05432c0bd70f222c2f898ce2cbb47a46364a490)) +- **router:** + - [Dlocal] connector transaction id fix ([#2872](https://github.com/juspay/hyperswitch/pull/2872)) ([`44b1f49`](https://github.com/juspay/hyperswitch/commit/44b1f4949ea06d59480670ccfa02446fa7713d13)) + - Use default value for the routing algorithm column during business profile creation ([#2791](https://github.com/juspay/hyperswitch/pull/2791)) ([`b1fe76a`](https://github.com/juspay/hyperswitch/commit/b1fe76a82b4026d6eaa3baf4356378040880a458)) +- **routing:** Fix kgraph to exclude PM auth during construction ([#3019](https://github.com/juspay/hyperswitch/pull/3019)) ([`c6cb527`](https://github.com/juspay/hyperswitch/commit/c6cb527f07e23796c342f3562fbf3b61f1ef6801)) + +### Refactors + +- **connector:** + - [Stax] change error message from NotSupported to NotImplemented ([#2879](https://github.com/juspay/hyperswitch/pull/2879)) ([`8a4dabc`](https://github.com/juspay/hyperswitch/commit/8a4dabc61df3e6012e50f785d93808ca3349be65)) + - [Volt] change error message from NotSupported to NotImplemented ([#2878](https://github.com/juspay/hyperswitch/pull/2878)) ([`de8e31b`](https://github.com/juspay/hyperswitch/commit/de8e31b70d9b3c11e268cd1deffa71918dc4270d)) + - [Adyen] Change country and issuer type to Optional for OpenBankingUk ([#2993](https://github.com/juspay/hyperswitch/pull/2993)) ([`ab3dac7`](https://github.com/juspay/hyperswitch/commit/ab3dac79b4f138cd1f60a9afc0635dcc137a4a05)) +- **postman:** Fix payme postman collection for handling `order_details` ([#2996](https://github.com/juspay/hyperswitch/pull/2996)) ([`1e60c71`](https://github.com/juspay/hyperswitch/commit/1e60c710985b341a118bb32962bd74b406d78f69)) + +**Full Changelog:** [`v1.92.0...v1.93.0`](https://github.com/juspay/hyperswitch/compare/v1.92.0...v1.93.0) + +- - - + + ## 1.92.0 (2023-11-29) ### Features From 3fa0bdf76558ec91df8d3beef3c36658cd138b37 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Thu, 30 Nov 2023 20:02:47 +0530 Subject: [PATCH 144/146] feat(user_role): Add APIs for user roles (#3013) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/events.rs | 1 + crates/api_models/src/events/user.rs | 6 +- crates/api_models/src/events/user_role.rs | 14 ++ crates/api_models/src/lib.rs | 1 + crates/api_models/src/user.rs | 17 ++ crates/api_models/src/user_role.rs | 82 ++++++ crates/router/src/consts.rs | 2 +- crates/router/src/consts/user_role.rs | 11 + crates/router/src/core.rs | 2 + crates/router/src/core/errors/user.rs | 22 +- crates/router/src/core/user.rs | 234 ++++++++++++++++-- crates/router/src/core/user_role.rs | 101 ++++++++ crates/router/src/routes.rs | 2 + crates/router/src/routes/app.rs | 13 +- crates/router/src/routes/lock_utils.rs | 10 +- crates/router/src/routes/user.rs | 66 ++++- crates/router/src/routes/user_role.rs | 84 +++++++ crates/router/src/services/authentication.rs | 3 + .../authorization/predefined_permissions.rs | 220 +++++++++++++++- crates/router/src/types/domain/user.rs | 204 ++++++++++++++- crates/router/src/utils.rs | 2 + crates/router/src/utils/user.rs | 49 ++++ crates/router/src/utils/user_role.rs | 93 +++++++ crates/router_env/src/logger/types.rs | 14 ++ 24 files changed, 1207 insertions(+), 46 deletions(-) create mode 100644 crates/api_models/src/events/user_role.rs create mode 100644 crates/api_models/src/user_role.rs create mode 100644 crates/router/src/consts/user_role.rs create mode 100644 crates/router/src/core/user_role.rs create mode 100644 crates/router/src/routes/user_role.rs create mode 100644 crates/router/src/utils/user_role.rs diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index 345f827daeac..ac7cdeb83d94 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -7,6 +7,7 @@ pub mod payouts; pub mod refund; pub mod routing; pub mod user; +pub mod user_role; use common_utils::{ events::{ApiEventMetric, ApiEventsType}, diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index edfdcf1d6652..50df0c9a584b 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -5,6 +5,7 @@ use crate::user::{ GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest, }, ChangePasswordRequest, ConnectAccountRequest, ConnectAccountResponse, + CreateInternalUserRequest, SwitchMerchantIdRequest, UserMerchantCreate, }; impl ApiEventMetric for ConnectAccountResponse { @@ -23,5 +24,8 @@ common_utils::impl_misc_api_event_type!( GetMultipleMetaDataPayload, GetMetaDataResponse, GetMetaDataRequest, - SetMetaDataRequest + SetMetaDataRequest, + SwitchMerchantIdRequest, + CreateInternalUserRequest, + UserMerchantCreate ); diff --git a/crates/api_models/src/events/user_role.rs b/crates/api_models/src/events/user_role.rs new file mode 100644 index 000000000000..aa8d13dab6df --- /dev/null +++ b/crates/api_models/src/events/user_role.rs @@ -0,0 +1,14 @@ +use common_utils::events::{ApiEventMetric, ApiEventsType}; + +use crate::user_role::{ + AuthorizationInfoResponse, GetRoleRequest, ListRolesResponse, RoleInfoResponse, + UpdateUserRoleRequest, +}; + +common_utils::impl_misc_api_event_type!( + ListRolesResponse, + RoleInfoResponse, + GetRoleRequest, + AuthorizationInfoResponse, + UpdateUserRoleRequest +); diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index 8ef40d319140..056888839a54 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -26,6 +26,7 @@ pub mod refunds; pub mod routing; pub mod surcharge_decision_configs; pub mod user; +pub mod user_role; pub mod verifications; pub mod verify_connector; pub mod webhooks; diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index 84659432aa6a..e0bfa50b4115 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -26,3 +26,20 @@ pub struct ChangePasswordRequest { pub new_password: Secret, pub old_password: Secret, } + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct SwitchMerchantIdRequest { + pub merchant_id: String, +} + +#[derive(serde::Deserialize, Debug, serde::Serialize)] +pub struct CreateInternalUserRequest { + pub name: Secret, + pub email: pii::Email, + pub password: Secret, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct UserMerchantCreate { + pub company_name: String, +} diff --git a/crates/api_models/src/user_role.rs b/crates/api_models/src/user_role.rs new file mode 100644 index 000000000000..521d17e73428 --- /dev/null +++ b/crates/api_models/src/user_role.rs @@ -0,0 +1,82 @@ +#[derive(Debug, serde::Serialize)] +pub struct ListRolesResponse(pub Vec); + +#[derive(Debug, serde::Serialize)] +pub struct RoleInfoResponse { + pub role_id: &'static str, + pub permissions: Vec, + pub role_name: &'static str, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct GetRoleRequest { + pub role_id: String, +} + +#[derive(Debug, serde::Serialize)] +pub enum Permission { + PaymentRead, + PaymentWrite, + RefundRead, + RefundWrite, + ApiKeyRead, + ApiKeyWrite, + MerchantAccountRead, + MerchantAccountWrite, + MerchantConnectorAccountRead, + MerchantConnectorAccountWrite, + ForexRead, + RoutingRead, + RoutingWrite, + DisputeRead, + DisputeWrite, + MandateRead, + MandateWrite, + FileRead, + FileWrite, + Analytics, + ThreeDsDecisionManagerWrite, + ThreeDsDecisionManagerRead, + SurchargeDecisionManagerWrite, + SurchargeDecisionManagerRead, + UsersRead, + UsersWrite, +} + +#[derive(Debug, serde::Serialize)] +pub enum PermissionModule { + Payments, + Refunds, + MerchantAccount, + Forex, + Connectors, + Routing, + Analytics, + Mandates, + Disputes, + Files, + ThreeDsDecisionManager, + SurchargeDecisionManager, +} + +#[derive(Debug, serde::Serialize)] +pub struct AuthorizationInfoResponse(pub Vec); + +#[derive(Debug, serde::Serialize)] +pub struct ModuleInfo { + pub module: PermissionModule, + pub description: &'static str, + pub permissions: Vec, +} + +#[derive(Debug, serde::Serialize)] +pub struct PermissionInfo { + pub enum_name: Permission, + pub description: &'static str, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct UpdateUserRoleRequest { + pub user_id: String, + pub role_id: String, +} diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 49c5cfacad1f..4a2d2831d103 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -1,5 +1,6 @@ #[cfg(feature = "olap")] pub mod user; +pub mod user_role; // ID generation pub(crate) const ID_LENGTH: usize = 20; @@ -64,7 +65,6 @@ pub const JWT_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24 * 2; // 2 days #[cfg(feature = "email")] pub const EMAIL_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24; // 1 day -pub const ROLE_ID_ORGANIZATION_ADMIN: &str = "org_admin"; #[cfg(feature = "olap")] pub const VERIFY_CONNECTOR_ID_PREFIX: &str = "conn_verify"; diff --git a/crates/router/src/consts/user_role.rs b/crates/router/src/consts/user_role.rs new file mode 100644 index 000000000000..ae1436bcd678 --- /dev/null +++ b/crates/router/src/consts/user_role.rs @@ -0,0 +1,11 @@ +// User Roles +pub const ROLE_ID_INTERNAL_VIEW_ONLY_USER: &str = "internal_view_only"; +pub const ROLE_ID_INTERNAL_ADMIN: &str = "internal_admin"; +pub const ROLE_ID_MERCHANT_ADMIN: &str = "merchant_admin"; +pub const ROLE_ID_ORGANIZATION_ADMIN: &str = "org_admin"; +pub const ROLE_ID_MERCHANT_VIEW_ONLY: &str = "merchant_view_only"; +pub const ROLE_ID_MERCHANT_IAM_ADMIN: &str = "merchant_iam_admin"; +pub const ROLE_ID_MERCHANT_DEVELOPER: &str = "merchant_developer"; +pub const ROLE_ID_MERCHANT_OPERATOR: &str = "merchant_operator"; +pub const ROLE_ID_MERCHANT_CUSTOMER_SUPPORT: &str = "merchant_customer_support"; +pub const INTERNAL_USER_MERCHANT_ID: &str = "juspay000"; diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index 30fe1a1ce8cb..08de9cf80384 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -25,6 +25,8 @@ pub mod routing; pub mod surcharge_decision_config; #[cfg(feature = "olap")] pub mod user; +#[cfg(feature = "olap")] +pub mod user_role; pub mod utils; #[cfg(all(feature = "olap", feature = "kms"))] pub mod verification; diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs index f5c50e28ccc6..ba600917ecca 100644 --- a/crates/router/src/core/errors/user.rs +++ b/crates/router/src/core/errors/user.rs @@ -27,16 +27,22 @@ pub enum UserErrors { MerchantAccountCreationError(String), #[error("InvalidEmailError")] InvalidEmailError, + #[error("DuplicateOrganizationId")] + DuplicateOrganizationId, #[error("MerchantIdNotFound")] MerchantIdNotFound, #[error("MetadataAlreadySet")] MetadataAlreadySet, - #[error("DuplicateOrganizationId")] - DuplicateOrganizationId, + #[error("InvalidRoleId")] + InvalidRoleId, + #[error("InvalidRoleOperation")] + InvalidRoleOperation, #[error("IpAddressParsingFailed")] IpAddressParsingFailed, #[error("InvalidMetadataRequest")] InvalidMetadataRequest, + #[error("MerchantIdParsingError")] + MerchantIdParsingError, } impl common_utils::errors::ErrorSwitch for UserErrors { @@ -95,6 +101,15 @@ impl common_utils::errors::ErrorSwitch { + AER::BadRequest(ApiError::new(sub_code, 22, "Invalid Role ID", None)) + } + Self::InvalidRoleOperation => AER::BadRequest(ApiError::new( + sub_code, + 23, + "User Role Operation Not Supported", + None, + )), Self::IpAddressParsingFailed => { AER::InternalServerError(ApiError::new(sub_code, 24, "Something Went Wrong", None)) } @@ -104,6 +119,9 @@ impl common_utils::errors::ErrorSwitch { + AER::BadRequest(ApiError::new(sub_code, 28, "Invalid Merchant Id", None)) + } } } } diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 9a199d09b8fd..8e7f6c27a7da 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -1,5 +1,5 @@ -use api_models::user as api; -use diesel_models::enums::UserStatus; +use api_models::user as user_api; +use diesel_models::{enums::UserStatus, user as storage_user}; use error_stack::{IntoReport, ResultExt}; use masking::{ExposeInterface, Secret}; use router_env::env; @@ -9,16 +9,17 @@ use crate::{ consts, db::user::UserInterface, routes::AppState, - services::{authentication::UserFromToken, ApplicationResponse}, + services::{authentication as auth, ApplicationResponse}, types::domain, + utils, }; pub mod dashboard_metadata; pub async fn connect_account( state: AppState, - request: api::ConnectAccountRequest, -) -> UserResponse { + request: user_api::ConnectAccountRequest, +) -> UserResponse { let find_user = state .store .find_user_by_email(request.email.clone().expose().expose().as_str()) @@ -34,15 +35,17 @@ pub async fn connect_account( .get_jwt_auth_token(state.clone(), user_role.org_id) .await?; - return Ok(ApplicationResponse::Json(api::ConnectAccountResponse { - token: Secret::new(jwt_token), - merchant_id: user_role.merchant_id, - name: user_from_db.get_name(), - email: user_from_db.get_email(), - verification_days_left: None, - user_role: user_role.role_id, - user_id: user_from_db.get_user_id().to_string(), - })); + return Ok(ApplicationResponse::Json( + user_api::ConnectAccountResponse { + token: Secret::new(jwt_token), + merchant_id: user_role.merchant_id, + name: user_from_db.get_name(), + email: user_from_db.get_email(), + verification_days_left: None, + user_role: user_role.role_id, + user_id: user_from_db.get_user_id().to_string(), + }, + )); } else if find_user .map_err(|e| e.current_context().is_db_not_found()) .err() @@ -64,7 +67,7 @@ pub async fn connect_account( let user_role = new_user .insert_user_role_in_db( state.clone(), - consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(), + consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(), UserStatus::Active, ) .await?; @@ -94,15 +97,17 @@ pub async fn connect_account( logger::info!(?send_email_result); } - return Ok(ApplicationResponse::Json(api::ConnectAccountResponse { - token: Secret::new(jwt_token), - merchant_id: user_role.merchant_id, - name: user_from_db.get_name(), - email: user_from_db.get_email(), - verification_days_left: None, - user_role: user_role.role_id, - user_id: user_from_db.get_user_id().to_string(), - })); + return Ok(ApplicationResponse::Json( + user_api::ConnectAccountResponse { + token: Secret::new(jwt_token), + merchant_id: user_role.merchant_id, + name: user_from_db.get_name(), + email: user_from_db.get_email(), + verification_days_left: None, + user_role: user_role.role_id, + user_id: user_from_db.get_user_id().to_string(), + }, + )); } else { Err(UserErrors::InternalServerError.into()) } @@ -110,8 +115,8 @@ pub async fn connect_account( pub async fn change_password( state: AppState, - request: api::ChangePasswordRequest, - user_from_token: UserFromToken, + request: user_api::ChangePasswordRequest, + user_from_token: auth::UserFromToken, ) -> UserResponse<()> { let user: domain::UserFromStorage = UserInterface::find_user_by_id(&*state.store, &user_from_token.user_id) @@ -139,3 +144,180 @@ pub async fn change_password( Ok(ApplicationResponse::StatusOk) } + +pub async fn create_internal_user( + state: AppState, + request: user_api::CreateInternalUserRequest, +) -> UserResponse<()> { + let new_user = domain::NewUser::try_from(request)?; + + let mut store_user: storage_user::UserNew = new_user.clone().try_into()?; + store_user.set_is_verified(true); + + let key_store = state + .store + .get_merchant_key_store_by_merchant_id( + consts::user_role::INTERNAL_USER_MERCHANT_ID, + &state.store.get_master_key().to_vec().into(), + ) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::MerchantIdNotFound) + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; + + state + .store + .find_merchant_account_by_merchant_id( + consts::user_role::INTERNAL_USER_MERCHANT_ID, + &key_store, + ) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::MerchantIdNotFound) + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; + + state + .store + .insert_user(store_user) + .await + .map_err(|e| { + if e.current_context().is_db_unique_violation() { + e.change_context(UserErrors::UserExists) + } else { + e.change_context(UserErrors::InternalServerError) + } + }) + .map(domain::user::UserFromStorage::from)?; + + new_user + .insert_user_role_in_db( + state, + consts::user_role::ROLE_ID_INTERNAL_VIEW_ONLY_USER.to_string(), + UserStatus::Active, + ) + .await?; + + Ok(ApplicationResponse::StatusOk) +} + +pub async fn switch_merchant_id( + state: AppState, + request: user_api::SwitchMerchantIdRequest, + user_from_token: auth::UserFromToken, +) -> UserResponse { + if !utils::user_role::is_internal_role(&user_from_token.role_id) { + let merchant_list = + utils::user_role::get_merchant_ids_for_user(state.clone(), &user_from_token.user_id) + .await?; + if !merchant_list.contains(&request.merchant_id) { + return Err(UserErrors::InvalidRoleOperation.into()) + .attach_printable("User doesn't have access to switch"); + } + } + + if user_from_token.merchant_id == request.merchant_id { + return Err(UserErrors::InvalidRoleOperation.into()) + .attach_printable("User switch to same merchant id."); + } + + let user = state + .store + .find_user_by_id(&user_from_token.user_id) + .await + .change_context(UserErrors::InternalServerError)?; + + let key_store = state + .store + .get_merchant_key_store_by_merchant_id( + request.merchant_id.as_str(), + &state.store.get_master_key().to_vec().into(), + ) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::MerchantIdNotFound) + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; + + let org_id = state + .store + .find_merchant_account_by_merchant_id(request.merchant_id.as_str(), &key_store) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::MerchantIdNotFound) + } else { + e.change_context(UserErrors::InternalServerError) + } + })? + .organization_id; + + let user = domain::UserFromStorage::from(user); + let user_role = state + .store + .find_user_role_by_user_id(user.get_user_id()) + .await + .change_context(UserErrors::InternalServerError)?; + + let token = Box::pin(user.get_jwt_auth_token_with_custom_merchant_id( + state.clone(), + request.merchant_id.clone(), + org_id, + )) + .await? + .into(); + + Ok(ApplicationResponse::Json( + user_api::ConnectAccountResponse { + merchant_id: request.merchant_id, + token, + name: user.get_name(), + email: user.get_email(), + user_id: user.get_user_id().to_string(), + verification_days_left: None, + user_role: user_role.role_id, + }, + )) +} + +pub async fn create_merchant_account( + state: AppState, + user_from_token: auth::UserFromToken, + req: user_api::UserMerchantCreate, +) -> UserResponse<()> { + let user_from_db: domain::UserFromStorage = + user_from_token.get_user(state.clone()).await?.into(); + + let new_user = domain::NewUser::try_from((user_from_db, req, user_from_token))?; + let new_merchant = new_user.get_new_merchant(); + new_merchant + .create_new_merchant_and_insert_in_db(state.to_owned()) + .await?; + + let role_insertion_res = new_user + .insert_user_role_in_db( + state.clone(), + consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(), + UserStatus::Active, + ) + .await; + if let Err(e) = role_insertion_res { + let _ = state + .store + .delete_merchant_account_by_merchant_id(new_merchant.get_merchant_id().as_str()) + .await; + return Err(e); + } + + Ok(ApplicationResponse::StatusOk) +} diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs new file mode 100644 index 000000000000..2b7752d1904b --- /dev/null +++ b/crates/router/src/core/user_role.rs @@ -0,0 +1,101 @@ +use api_models::user_role as user_role_api; +use diesel_models::user_role::UserRoleUpdate; +use error_stack::ResultExt; + +use crate::{ + core::errors::{UserErrors, UserResponse}, + routes::AppState, + services::{ + authentication::{self as auth}, + authorization::{info, predefined_permissions}, + ApplicationResponse, + }, + utils, +}; + +pub async fn get_authorization_info( + _state: AppState, +) -> UserResponse { + Ok(ApplicationResponse::Json( + user_role_api::AuthorizationInfoResponse( + info::get_authorization_info() + .into_iter() + .filter_map(|module| module.try_into().ok()) + .collect(), + ), + )) +} + +pub async fn list_roles(_state: AppState) -> UserResponse { + Ok(ApplicationResponse::Json(user_role_api::ListRolesResponse( + predefined_permissions::PREDEFINED_PERMISSIONS + .iter() + .filter_map(|(role_id, role_info)| { + utils::user_role::get_role_name_and_permission_response(role_info).map( + |(permissions, role_name)| user_role_api::RoleInfoResponse { + permissions, + role_id, + role_name, + }, + ) + }) + .collect(), + ))) +} + +pub async fn get_role( + _state: AppState, + role: user_role_api::GetRoleRequest, +) -> UserResponse { + let info = predefined_permissions::PREDEFINED_PERMISSIONS + .get_key_value(role.role_id.as_str()) + .and_then(|(role_id, role_info)| { + utils::user_role::get_role_name_and_permission_response(role_info).map( + |(permissions, role_name)| user_role_api::RoleInfoResponse { + permissions, + role_id, + role_name, + }, + ) + }) + .ok_or(UserErrors::InvalidRoleId)?; + + Ok(ApplicationResponse::Json(info)) +} + +pub async fn update_user_role( + state: AppState, + user_from_token: auth::UserFromToken, + req: user_role_api::UpdateUserRoleRequest, +) -> UserResponse<()> { + let merchant_id = user_from_token.merchant_id; + let role_id = req.role_id.clone(); + utils::user_role::validate_role_id(role_id.as_str())?; + + if user_from_token.user_id == req.user_id { + return Err(UserErrors::InvalidRoleOperation.into()) + .attach_printable("Admin User Changing their role"); + } + + state + .store + .update_user_role_by_user_id_merchant_id( + req.user_id.as_str(), + merchant_id.as_str(), + UserRoleUpdate::UpdateRole { + role_id, + modified_by: user_from_token.user_id, + }, + ) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + return e + .change_context(UserErrors::InvalidRoleOperation) + .attach_printable("UserId MerchantId not found"); + } + e.change_context(UserErrors::InternalServerError) + })?; + + Ok(ApplicationResponse::StatusOk) +} diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 22c2610d3255..b19ef5d7016b 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -27,6 +27,8 @@ pub mod refunds; pub mod routing; #[cfg(feature = "olap")] pub mod user; +#[cfg(feature = "olap")] +pub mod user_role; #[cfg(all(feature = "olap", feature = "kms"))] pub mod verification; #[cfg(feature = "olap")] diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 2f8932057fb4..5f0c89ed6b4c 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -23,7 +23,7 @@ use super::verification::{apple_pay_merchant_registration, retrieve_apple_pay_ve #[cfg(feature = "olap")] use super::{ admin::*, api_keys::*, disputes::*, files::*, gsm::*, locker_migration, payment_link::*, - user::*, + user::*, user_role::*, }; use super::{cache::*, health::*}; #[cfg(any(feature = "olap", feature = "oltp"))] @@ -812,6 +812,17 @@ impl User { .route(web::post().to(set_merchant_scoped_dashboard_metadata)), ) .service(web::resource("/data").route(web::get().to(get_multiple_dashboard_metadata))) + .service(web::resource("/internal_signup").route(web::post().to(internal_user_signup))) + .service(web::resource("/switch_merchant").route(web::post().to(switch_merchant_id))) + .service( + web::resource("/create_merchant") + .route(web::post().to(user_merchant_account_create)), + ) + // User Role APIs + .service(web::resource("/permission_info").route(web::get().to(get_authorization_info))) + .service(web::resource("/user/update_role").route(web::post().to(update_user_role))) + .service(web::resource("/role/list").route(web::get().to(list_roles))) + .service(web::resource("/role/{role_id}").route(web::get().to(get_role))) } } diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 72bc3c9cd417..552deb85a2e1 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -27,6 +27,7 @@ pub enum ApiIdentifier { RustLockerMigration, Gsm, User, + UserRole, } impl From for ApiIdentifier { @@ -151,7 +152,14 @@ impl From for ApiIdentifier { | Flow::ChangePassword | Flow::SetDashboardMetadata | Flow::GetMutltipleDashboardMetadata - | Flow::VerifyPaymentConnector => Self::User, + | Flow::VerifyPaymentConnector + | Flow::InternalUserSignup + | Flow::SwitchMerchant + | Flow::UserMerchantAccountCreate => Self::User, + + Flow::ListRoles | Flow::GetRole | Flow::UpdateUserRole | Flow::GetAuthorizationInfo => { + Self::UserRole + } } } } diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index 3f5f7815ffbc..89c4bd4c90ec 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -5,7 +5,7 @@ use router_env::Flow; use super::AppState; use crate::{ - core::{api_locking, user}, + core::{api_locking, user as user_core}, services::{ api, authentication::{self as auth}, @@ -26,7 +26,7 @@ pub async fn user_connect_account( state, &http_req, req_payload.clone(), - |state, _, req_body| user::connect_account(state, req_body), + |state, _, req_body| user_core::connect_account(state, req_body), &auth::NoAuth, api_locking::LockAction::NotApplicable, )) @@ -44,7 +44,7 @@ pub async fn change_password( state.clone(), &http_req, json_payload.into_inner(), - |state, user, req| user::change_password(state, req, user), + |state, user, req| user_core::change_password(state, req, user), &auth::DashboardNoPermissionAuth, api_locking::LockAction::NotApplicable, )) @@ -70,7 +70,7 @@ pub async fn set_merchant_scoped_dashboard_metadata( state, &req, payload, - user::dashboard_metadata::set_metadata, + user_core::dashboard_metadata::set_metadata, &auth::JWTAuth(Permission::MerchantAccountWrite), api_locking::LockAction::NotApplicable, )) @@ -96,9 +96,65 @@ pub async fn get_multiple_dashboard_metadata( state, &req, payload, - user::dashboard_metadata::get_multiple_metadata, + user_core::dashboard_metadata::get_multiple_metadata, &auth::DashboardNoPermissionAuth, api_locking::LockAction::NotApplicable, )) .await } + +pub async fn internal_user_signup( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::InternalUserSignup; + Box::pin(api::server_wrap( + flow, + state.clone(), + &http_req, + json_payload.into_inner(), + |state, _, req| user_core::create_internal_user(state, req), + &auth::AdminApiAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn switch_merchant_id( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::SwitchMerchant; + Box::pin(api::server_wrap( + flow, + state.clone(), + &http_req, + json_payload.into_inner(), + |state, user, req| user_core::switch_merchant_id(state, req, user), + &auth::DashboardNoPermissionAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn user_merchant_account_create( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::UserMerchantAccountCreate; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: auth::UserFromToken, json_payload| { + user_core::create_merchant_account(state, auth, json_payload) + }, + &auth::JWTAuth(Permission::MerchantAccountCreate), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/routes/user_role.rs b/crates/router/src/routes/user_role.rs new file mode 100644 index 000000000000..c96e099ab163 --- /dev/null +++ b/crates/router/src/routes/user_role.rs @@ -0,0 +1,84 @@ +use actix_web::{web, HttpRequest, HttpResponse}; +use api_models::user_role as user_role_api; +use router_env::Flow; + +use super::AppState; +use crate::{ + core::{api_locking, user_role as user_role_core}, + services::{ + api, + authentication::{self as auth}, + authorization::permissions::Permission, + }, +}; + +pub async fn get_authorization_info( + state: web::Data, + http_req: HttpRequest, +) -> HttpResponse { + let flow = Flow::GetAuthorizationInfo; + Box::pin(api::server_wrap( + flow, + state.clone(), + &http_req, + (), + |state, _: (), _| user_role_core::get_authorization_info(state), + &auth::JWTAuth(Permission::UsersRead), + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn list_roles(state: web::Data, req: HttpRequest) -> HttpResponse { + let flow = Flow::ListRoles; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + (), + |state, _: (), _| user_role_core::list_roles(state), + &auth::JWTAuth(Permission::UsersRead), + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn get_role( + state: web::Data, + req: HttpRequest, + path: web::Path, +) -> HttpResponse { + let flow = Flow::GetRole; + let request_payload = user_role_api::GetRoleRequest { + role_id: path.into_inner(), + }; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + request_payload, + |state, _: (), req| user_role_core::get_role(state, req), + &auth::JWTAuth(Permission::UsersRead), + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn update_user_role( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::UpdateUserRole; + let payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + payload, + user_role_core::update_user_role, + &auth::JWTAuth(Permission::UsersWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index b01e3762bfab..8a0cd7c729e9 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -444,6 +444,9 @@ where ) -> RouterResult<(UserFromToken, AuthenticationType)> { let payload = parse_jwt_payload::(request_headers, state).await?; + let permissions = authorization::get_permissions(&payload.role_id)?; + authorization::check_authorization(&self.0, permissions)?; + Ok(( UserFromToken { user_id: payload.user_id.clone(), diff --git a/crates/router/src/services/authorization/predefined_permissions.rs b/crates/router/src/services/authorization/predefined_permissions.rs index 89fa2c8f739c..a9f2b864d0ad 100644 --- a/crates/router/src/services/authorization/predefined_permissions.rs +++ b/crates/router/src/services/authorization/predefined_permissions.rs @@ -28,7 +28,67 @@ impl RoleInfo { pub static PREDEFINED_PERMISSIONS: Lazy> = Lazy::new(|| { let mut roles = HashMap::new(); roles.insert( - consts::ROLE_ID_ORGANIZATION_ADMIN, + consts::user_role::ROLE_ID_INTERNAL_ADMIN, + RoleInfo { + permissions: vec![ + Permission::PaymentRead, + Permission::PaymentWrite, + Permission::RefundRead, + Permission::RefundWrite, + Permission::ApiKeyRead, + Permission::ApiKeyWrite, + Permission::MerchantAccountRead, + Permission::MerchantAccountWrite, + Permission::MerchantConnectorAccountRead, + Permission::MerchantConnectorAccountWrite, + Permission::RoutingRead, + Permission::RoutingWrite, + Permission::ForexRead, + Permission::ThreeDsDecisionManagerWrite, + Permission::ThreeDsDecisionManagerRead, + Permission::SurchargeDecisionManagerWrite, + Permission::SurchargeDecisionManagerRead, + Permission::DisputeRead, + Permission::DisputeWrite, + Permission::MandateRead, + Permission::MandateWrite, + Permission::FileRead, + Permission::FileWrite, + Permission::Analytics, + Permission::UsersRead, + Permission::UsersWrite, + Permission::MerchantAccountCreate, + ], + name: None, + is_invitable: false, + }, + ); + roles.insert( + consts::user_role::ROLE_ID_INTERNAL_VIEW_ONLY_USER, + RoleInfo { + permissions: vec![ + Permission::PaymentRead, + Permission::RefundRead, + Permission::ApiKeyRead, + Permission::MerchantAccountRead, + Permission::MerchantConnectorAccountRead, + Permission::RoutingRead, + Permission::ForexRead, + Permission::ThreeDsDecisionManagerRead, + Permission::SurchargeDecisionManagerRead, + Permission::Analytics, + Permission::DisputeRead, + Permission::MandateRead, + Permission::FileRead, + Permission::UsersRead, + ], + name: None, + is_invitable: false, + }, + ); + + roles.insert( + consts::user_role::ROLE_ID_ORGANIZATION_ADMIN, RoleInfo { permissions: vec![ Permission::PaymentRead, @@ -63,6 +123,164 @@ pub static PREDEFINED_PERMISSIONS: Lazy> = Lazy: is_invitable: false, }, ); + + // MERCHANT ROLES + roles.insert( + consts::user_role::ROLE_ID_MERCHANT_ADMIN, + RoleInfo { + permissions: vec![ + Permission::PaymentRead, + Permission::PaymentWrite, + Permission::RefundRead, + Permission::RefundWrite, + Permission::ApiKeyRead, + Permission::ApiKeyWrite, + Permission::MerchantAccountRead, + Permission::MerchantAccountWrite, + Permission::MerchantConnectorAccountRead, + Permission::ForexRead, + Permission::MerchantConnectorAccountWrite, + Permission::RoutingRead, + Permission::RoutingWrite, + Permission::ThreeDsDecisionManagerWrite, + Permission::ThreeDsDecisionManagerRead, + Permission::SurchargeDecisionManagerWrite, + Permission::SurchargeDecisionManagerRead, + Permission::DisputeRead, + Permission::DisputeWrite, + Permission::MandateRead, + Permission::MandateWrite, + Permission::FileRead, + Permission::FileWrite, + Permission::Analytics, + Permission::UsersRead, + Permission::UsersWrite, + ], + name: Some("Admin"), + is_invitable: true, + }, + ); + roles.insert( + consts::user_role::ROLE_ID_MERCHANT_VIEW_ONLY, + RoleInfo { + permissions: vec![ + Permission::PaymentRead, + Permission::RefundRead, + Permission::ApiKeyRead, + Permission::MerchantAccountRead, + Permission::ForexRead, + Permission::MerchantConnectorAccountRead, + Permission::RoutingRead, + Permission::ThreeDsDecisionManagerRead, + Permission::SurchargeDecisionManagerRead, + Permission::DisputeRead, + Permission::MandateRead, + Permission::FileRead, + Permission::Analytics, + Permission::UsersRead, + ], + name: Some("View Only"), + is_invitable: true, + }, + ); + roles.insert( + consts::user_role::ROLE_ID_MERCHANT_IAM_ADMIN, + RoleInfo { + permissions: vec![ + Permission::PaymentRead, + Permission::RefundRead, + Permission::ApiKeyRead, + Permission::MerchantAccountRead, + Permission::ForexRead, + Permission::MerchantConnectorAccountRead, + Permission::RoutingRead, + Permission::ThreeDsDecisionManagerRead, + Permission::SurchargeDecisionManagerRead, + Permission::DisputeRead, + Permission::MandateRead, + Permission::FileRead, + Permission::Analytics, + Permission::UsersRead, + Permission::UsersWrite, + ], + name: Some("IAM"), + is_invitable: true, + }, + ); + roles.insert( + consts::user_role::ROLE_ID_MERCHANT_DEVELOPER, + RoleInfo { + permissions: vec![ + Permission::PaymentRead, + Permission::RefundRead, + Permission::ApiKeyRead, + Permission::ApiKeyWrite, + Permission::MerchantAccountRead, + Permission::ForexRead, + Permission::MerchantConnectorAccountRead, + Permission::RoutingRead, + Permission::ThreeDsDecisionManagerRead, + Permission::SurchargeDecisionManagerRead, + Permission::DisputeRead, + Permission::MandateRead, + Permission::FileRead, + Permission::Analytics, + Permission::UsersRead, + ], + name: Some("Developer"), + is_invitable: true, + }, + ); + roles.insert( + consts::user_role::ROLE_ID_MERCHANT_OPERATOR, + RoleInfo { + permissions: vec![ + Permission::PaymentRead, + Permission::PaymentWrite, + Permission::RefundRead, + Permission::RefundWrite, + Permission::ApiKeyRead, + Permission::MerchantAccountRead, + Permission::ForexRead, + Permission::MerchantConnectorAccountRead, + Permission::MerchantConnectorAccountWrite, + Permission::RoutingRead, + Permission::RoutingWrite, + Permission::ThreeDsDecisionManagerRead, + Permission::ThreeDsDecisionManagerWrite, + Permission::SurchargeDecisionManagerRead, + Permission::SurchargeDecisionManagerWrite, + Permission::DisputeRead, + Permission::MandateRead, + Permission::FileRead, + Permission::Analytics, + Permission::UsersRead, + ], + name: Some("Operator"), + is_invitable: true, + }, + ); + roles.insert( + consts::user_role::ROLE_ID_MERCHANT_CUSTOMER_SUPPORT, + RoleInfo { + permissions: vec![ + Permission::PaymentRead, + Permission::RefundRead, + Permission::RefundWrite, + Permission::ForexRead, + Permission::DisputeRead, + Permission::DisputeWrite, + Permission::MerchantAccountRead, + Permission::MerchantConnectorAccountRead, + Permission::MandateRead, + Permission::FileRead, + Permission::FileWrite, + Permission::Analytics, + ], + name: Some("Customer Support"), + is_invitable: true, + }, + ); roles }); diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 7e723bf00c32..0c7760f84d36 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -1,6 +1,8 @@ use std::{collections::HashSet, ops, str::FromStr}; -use api_models::{admin as admin_api, organization as api_org, user as user_api}; +use api_models::{ + admin as admin_api, organization as api_org, user as user_api, user_role as user_role_api, +}; use common_utils::pii; use diesel_models::{ enums::UserStatus, @@ -12,17 +14,21 @@ use diesel_models::{ use error_stack::{IntoReport, ResultExt}; use masking::{ExposeInterface, PeekInterface, Secret}; use once_cell::sync::Lazy; +use router_env::env; use unicode_segmentation::UnicodeSegmentation; use crate::{ - consts::user as consts, + consts, core::{ admin, errors::{UserErrors, UserResult}, }, db::StorageInterface, routes::AppState, - services::authentication::AuthToken, + services::{ + authentication::{AuthToken, UserFromToken}, + authorization::info, + }, types::transformers::ForeignFrom, utils::user::password, }; @@ -36,7 +42,7 @@ impl UserName { pub fn new(name: Secret) -> UserResult { let name = name.expose(); let is_empty_or_whitespace = name.trim().is_empty(); - let is_too_long = name.graphemes(true).count() > consts::MAX_NAME_LENGTH; + let is_too_long = name.graphemes(true).count() > consts::user::MAX_NAME_LENGTH; let forbidden_characters = ['/', '(', ')', '"', '<', '>', '\\', '{', '}']; let contains_forbidden_characters = name.chars().any(|g| forbidden_characters.contains(&g)); @@ -167,7 +173,8 @@ impl UserCompanyName { pub fn new(company_name: String) -> UserResult { let company_name = company_name.trim(); let is_empty_or_whitespace = company_name.is_empty(); - let is_too_long = company_name.graphemes(true).count() > consts::MAX_COMPANY_NAME_LENGTH; + let is_too_long = + company_name.graphemes(true).count() > consts::user::MAX_COMPANY_NAME_LENGTH; let is_all_valid_characters = company_name .chars() @@ -216,9 +223,47 @@ impl From for NewUserOrganization { } } +impl From for NewUserOrganization { + fn from(_value: user_api::CreateInternalUserRequest) -> Self { + let new_organization = api_org::OrganizationNew::new(None); + let db_organization = ForeignFrom::foreign_from(new_organization); + Self(db_organization) + } +} + +impl From for NewUserOrganization { + fn from(value: UserMerchantCreateRequestWithToken) -> Self { + Self(diesel_org::OrganizationNew { + org_id: value.2.org_id, + org_name: Some(value.1.company_name), + }) + } +} + +#[derive(Clone)] +pub struct MerchantId(String); + +impl MerchantId { + pub fn new(merchant_id: String) -> UserResult { + let merchant_id = merchant_id.trim().to_lowercase().replace(' ', "_"); + let is_empty_or_whitespace = merchant_id.is_empty(); + + let is_all_valid_characters = merchant_id.chars().all(|x| x.is_alphanumeric() || x == '_'); + if is_empty_or_whitespace || !is_all_valid_characters { + Err(UserErrors::MerchantIdParsingError.into()) + } else { + Ok(Self(merchant_id.to_string())) + } + } + + pub fn get_secret(&self) -> String { + self.0.clone() + } +} + #[derive(Clone)] pub struct NewUserMerchant { - merchant_id: String, + merchant_id: MerchantId, company_name: Option, new_organization: NewUserOrganization, } @@ -229,7 +274,7 @@ impl NewUserMerchant { } pub fn get_merchant_id(&self) -> String { - self.merchant_id.clone() + self.merchant_id.get_secret() } pub fn get_new_organization(&self) -> NewUserOrganization { @@ -293,7 +338,10 @@ impl TryFrom for NewUserMerchant { type Error = error_stack::Report; fn try_from(value: user_api::ConnectAccountRequest) -> UserResult { - let merchant_id = format!("merchant_{}", common_utils::date_time::now_unix_timestamp()); + let merchant_id = MerchantId::new(format!( + "merchant_{}", + common_utils::date_time::now_unix_timestamp() + ))?; let new_organization = NewUserOrganization::from(value); Ok(Self { @@ -304,6 +352,45 @@ impl TryFrom for NewUserMerchant { } } +impl TryFrom for NewUserMerchant { + type Error = error_stack::Report; + + fn try_from(value: user_api::CreateInternalUserRequest) -> UserResult { + let merchant_id = + MerchantId::new(consts::user_role::INTERNAL_USER_MERCHANT_ID.to_string())?; + let new_organization = NewUserOrganization::from(value); + + Ok(Self { + company_name: None, + merchant_id, + new_organization, + }) + } +} + +type UserMerchantCreateRequestWithToken = + (UserFromStorage, user_api::UserMerchantCreate, UserFromToken); + +impl TryFrom for NewUserMerchant { + type Error = error_stack::Report; + + fn try_from(value: UserMerchantCreateRequestWithToken) -> UserResult { + let merchant_id = if matches!(env::which(), env::Env::Production) { + MerchantId::new(value.1.company_name.clone())? + } else { + MerchantId::new(format!( + "merchant_{}", + common_utils::date_time::now_unix_timestamp() + ))? + }; + Ok(Self { + merchant_id, + company_name: Some(UserCompanyName::new(value.1.company_name.clone())?), + new_organization: NewUserOrganization::from(value), + }) + } +} + #[derive(Clone)] pub struct NewUser { user_id: String, @@ -428,6 +515,44 @@ impl TryFrom for NewUser { } } +impl TryFrom for NewUser { + type Error = error_stack::Report; + + fn try_from(value: user_api::CreateInternalUserRequest) -> UserResult { + let user_id = uuid::Uuid::new_v4().to_string(); + let email = value.email.clone().try_into()?; + let name = UserName::new(value.name.clone())?; + let password = UserPassword::new(value.password.clone())?; + let new_merchant = NewUserMerchant::try_from(value)?; + + Ok(Self { + user_id, + name, + email, + password, + new_merchant, + }) + } +} + +impl TryFrom for NewUser { + type Error = error_stack::Report; + + fn try_from(value: UserMerchantCreateRequestWithToken) -> Result { + let user = value.0.clone(); + let new_merchant = NewUserMerchant::try_from(value)?; + + Ok(Self { + user_id: user.0.user_id, + name: UserName::new(user.0.name)?, + email: user.0.email.clone().try_into()?, + password: UserPassword::new(user.0.password)?, + new_merchant, + }) + } +} + +#[derive(Clone)] pub struct UserFromStorage(pub storage_user::User); impl From for UserFromStorage { @@ -475,6 +600,23 @@ impl UserFromStorage { .await } + pub async fn get_jwt_auth_token_with_custom_merchant_id( + &self, + state: AppState, + merchant_id: String, + org_id: String, + ) -> UserResult { + let role_id = self.get_role_from_db(state.clone()).await?.role_id; + AuthToken::new_token( + self.0.user_id.clone(), + merchant_id, + role_id, + &state.conf, + org_id, + ) + .await + } + pub async fn get_role_from_db(&self, state: AppState) -> UserResult { state .store @@ -483,3 +625,49 @@ impl UserFromStorage { .change_context(UserErrors::InternalServerError) } } + +impl TryFrom for user_role_api::ModuleInfo { + type Error = (); + fn try_from(value: info::ModuleInfo) -> Result { + let mut permissions = Vec::with_capacity(value.permissions.len()); + for permission in value.permissions { + let permission = permission.try_into()?; + permissions.push(permission); + } + Ok(Self { + module: value.module.into(), + description: value.description, + permissions, + }) + } +} + +impl From for user_role_api::PermissionModule { + fn from(value: info::PermissionModule) -> Self { + match value { + info::PermissionModule::Payments => Self::Payments, + info::PermissionModule::Refunds => Self::Refunds, + info::PermissionModule::MerchantAccount => Self::MerchantAccount, + info::PermissionModule::Forex => Self::Forex, + info::PermissionModule::Connectors => Self::Connectors, + info::PermissionModule::Routing => Self::Routing, + info::PermissionModule::Analytics => Self::Analytics, + info::PermissionModule::Mandates => Self::Mandates, + info::PermissionModule::Disputes => Self::Disputes, + info::PermissionModule::Files => Self::Files, + info::PermissionModule::ThreeDsDecisionManager => Self::ThreeDsDecisionManager, + info::PermissionModule::SurchargeDecisionManager => Self::SurchargeDecisionManager, + } + } +} + +impl TryFrom for user_role_api::PermissionInfo { + type Error = (); + fn try_from(value: info::PermissionInfo) -> Result { + let enum_name = (&value.enum_name).try_into()?; + Ok(Self { + enum_name, + description: value.description, + }) + } +} diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index 81968cd9b628..f1590342e17c 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -7,6 +7,8 @@ pub mod storage_partitioning; #[cfg(feature = "olap")] pub mod user; #[cfg(feature = "olap")] +pub mod user_role; +#[cfg(feature = "olap")] pub mod verify_connector; use std::fmt::Debug; diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs index 824f7f63af75..4dc54ba3f708 100644 --- a/crates/router/src/utils/user.rs +++ b/crates/router/src/utils/user.rs @@ -1,2 +1,51 @@ +use error_stack::ResultExt; + +use crate::{ + core::errors::{UserErrors, UserResult}, + routes::AppState, + services::authentication::UserFromToken, + types::domain::MerchantAccount, +}; + pub mod dashboard_metadata; pub mod password; + +impl UserFromToken { + pub async fn get_merchant_account(&self, state: AppState) -> UserResult { + let key_store = state + .store + .get_merchant_key_store_by_merchant_id( + &self.merchant_id, + &state.store.get_master_key().to_vec().into(), + ) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::MerchantIdNotFound) + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; + let merchant_account = state + .store + .find_merchant_account_by_merchant_id(&self.merchant_id, &key_store) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::MerchantIdNotFound) + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; + Ok(merchant_account) + } + + pub async fn get_user(&self, state: AppState) -> UserResult { + let user = state + .store + .find_user_by_id(&self.user_id) + .await + .change_context(UserErrors::InternalServerError)?; + Ok(user) + } +} diff --git a/crates/router/src/utils/user_role.rs b/crates/router/src/utils/user_role.rs new file mode 100644 index 000000000000..0026984fdb9a --- /dev/null +++ b/crates/router/src/utils/user_role.rs @@ -0,0 +1,93 @@ +use api_models::user_role as user_role_api; +use diesel_models::enums::UserStatus; +use error_stack::ResultExt; +use router_env::logger; + +use crate::{ + consts, + core::errors::{UserErrors, UserResult}, + routes::AppState, + services::authorization::{ + permissions::Permission, + predefined_permissions::{self, RoleInfo}, + }, +}; + +pub fn is_internal_role(role_id: &str) -> bool { + role_id == consts::user_role::ROLE_ID_INTERNAL_ADMIN + || role_id == consts::user_role::ROLE_ID_INTERNAL_VIEW_ONLY_USER +} + +pub async fn get_merchant_ids_for_user(state: AppState, user_id: &str) -> UserResult> { + Ok(state + .store + .list_user_roles_by_user_id(user_id) + .await + .change_context(UserErrors::InternalServerError)? + .into_iter() + .filter_map(|ele| { + if ele.status == UserStatus::Active { + return Some(ele.merchant_id); + } + None + }) + .collect()) +} + +pub fn validate_role_id(role_id: &str) -> UserResult<()> { + if predefined_permissions::is_role_invitable(role_id) { + return Ok(()); + } + Err(UserErrors::InvalidRoleId.into()) +} + +pub fn get_role_name_and_permission_response( + role_info: &RoleInfo, +) -> Option<(Vec, &'static str)> { + role_info + .get_permissions() + .iter() + .map(TryInto::try_into) + .collect::, _>>() + .ok() + .zip(role_info.get_name()) +} + +impl TryFrom<&Permission> for user_role_api::Permission { + type Error = (); + fn try_from(value: &Permission) -> Result { + match value { + Permission::PaymentRead => Ok(Self::PaymentRead), + Permission::PaymentWrite => Ok(Self::PaymentWrite), + Permission::RefundRead => Ok(Self::RefundRead), + Permission::RefundWrite => Ok(Self::RefundWrite), + Permission::ApiKeyRead => Ok(Self::ApiKeyRead), + Permission::ApiKeyWrite => Ok(Self::ApiKeyWrite), + Permission::MerchantAccountRead => Ok(Self::MerchantAccountRead), + Permission::MerchantAccountWrite => Ok(Self::MerchantAccountWrite), + Permission::MerchantConnectorAccountRead => Ok(Self::MerchantConnectorAccountRead), + Permission::MerchantConnectorAccountWrite => Ok(Self::MerchantConnectorAccountWrite), + Permission::ForexRead => Ok(Self::ForexRead), + Permission::RoutingRead => Ok(Self::RoutingRead), + Permission::RoutingWrite => Ok(Self::RoutingWrite), + Permission::DisputeRead => Ok(Self::DisputeRead), + Permission::DisputeWrite => Ok(Self::DisputeWrite), + Permission::MandateRead => Ok(Self::MandateRead), + Permission::MandateWrite => Ok(Self::MandateWrite), + Permission::FileRead => Ok(Self::FileRead), + Permission::FileWrite => Ok(Self::FileWrite), + Permission::Analytics => Ok(Self::Analytics), + Permission::ThreeDsDecisionManagerWrite => Ok(Self::ThreeDsDecisionManagerWrite), + Permission::ThreeDsDecisionManagerRead => Ok(Self::ThreeDsDecisionManagerRead), + Permission::SurchargeDecisionManagerWrite => Ok(Self::SurchargeDecisionManagerWrite), + Permission::SurchargeDecisionManagerRead => Ok(Self::SurchargeDecisionManagerRead), + Permission::UsersRead => Ok(Self::UsersRead), + Permission::UsersWrite => Ok(Self::UsersWrite), + + Permission::MerchantAccountCreate => { + logger::error!("Invalid use of internal permission"); + Err(()) + } + } + } +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 7b87d2703640..eefdc86affad 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -265,6 +265,20 @@ pub enum Flow { GetMutltipleDashboardMetadata, /// Payment Connector Verify VerifyPaymentConnector, + /// Internal user signup + InternalUserSignup, + /// Switch merchant + SwitchMerchant, + /// Get permission info + GetAuthorizationInfo, + /// List roles + ListRoles, + /// Get role + GetRole, + /// Update user role + UpdateUserRole, + /// Create merchant account for user in a org + UserMerchantAccountCreate, } /// From 668b943403df2b3bb354dd093b8ec073a2618bda Mon Sep 17 00:00:00 2001 From: oscar2d2 Date: Thu, 30 Nov 2023 10:53:05 -0800 Subject: [PATCH 145/146] refactor(connector): [Multisafe Pay] change error message from not supported to not implemented (#2851) --- crates/router/src/connector/multisafepay/transformers.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/router/src/connector/multisafepay/transformers.rs b/crates/router/src/connector/multisafepay/transformers.rs index 7672566f8274..0a034724a629 100644 --- a/crates/router/src/connector/multisafepay/transformers.rs +++ b/crates/router/src/connector/multisafepay/transformers.rs @@ -262,10 +262,9 @@ impl TryFrom for Gateway { utils::CardIssuer::Visa => Ok(Self::Visa), utils::CardIssuer::DinersClub | utils::CardIssuer::JCB - | utils::CardIssuer::CarteBlanche => Err(errors::ConnectorError::NotSupported { - message: issuer.to_string(), - connector: "Multisafe pay", - } + | utils::CardIssuer::CarteBlanche => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Multisafe pay"), + ) .into()), } } From bc79d522c30aa036378cf1e01354c422585cc226 Mon Sep 17 00:00:00 2001 From: Brian Silah <71752651+unpervertedkid@users.noreply.github.com> Date: Thu, 30 Nov 2023 21:53:37 +0300 Subject: [PATCH 146/146] refactor(connector): [Shift4] change error message from NotSupported to NotImplemented (#2880) --- .../src/connector/shift4/transformers.rs | 89 +++++++------------ 1 file changed, 31 insertions(+), 58 deletions(-) diff --git a/crates/router/src/connector/shift4/transformers.rs b/crates/router/src/connector/shift4/transformers.rs index 606da2129fb0..ce68aad25c50 100644 --- a/crates/router/src/connector/shift4/transformers.rs +++ b/crates/router/src/connector/shift4/transformers.rs @@ -168,10 +168,9 @@ impl TryFrom<&types::RouterData { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Shift4", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Shift4"), + ) .into()) } } @@ -184,13 +183,8 @@ impl TryFrom<&api_models::payments::WalletData> for Shift4PaymentMethod { match wallet_data { payments::WalletData::AliPayRedirect(_) | payments::WalletData::ApplePay(_) - | payments::WalletData::WeChatPayRedirect(_) => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Shift4"), - ) - .into()) - } - payments::WalletData::AliPayQr(_) + | payments::WalletData::WeChatPayRedirect(_) + | payments::WalletData::AliPayQr(_) | payments::WalletData::AliPayHkRedirect(_) | payments::WalletData::MomoRedirect(_) | payments::WalletData::KakaoPayRedirect(_) @@ -212,10 +206,9 @@ impl TryFrom<&api_models::payments::WalletData> for Shift4PaymentMethod { | payments::WalletData::TouchNGoRedirect(_) | payments::WalletData::WeChatPayQr(_) | payments::WalletData::CashappQr(_) - | payments::WalletData::SwishQr(_) => Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Shift4", - } + | payments::WalletData::SwishQr(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Shift4"), + ) .into()), } } @@ -227,13 +220,8 @@ impl TryFrom<&api_models::payments::BankTransferData> for Shift4PaymentMethod { bank_transfer_data: &api_models::payments::BankTransferData, ) -> Result { match bank_transfer_data { - payments::BankTransferData::MultibancoBankTransfer { .. } => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Shift4"), - ) - .into()) - } - payments::BankTransferData::AchBankTransfer { .. } + payments::BankTransferData::MultibancoBankTransfer { .. } + | payments::BankTransferData::AchBankTransfer { .. } | payments::BankTransferData::SepaBankTransfer { .. } | payments::BankTransferData::BacsBankTransfer { .. } | payments::BankTransferData::PermataBankTransfer { .. } @@ -244,10 +232,9 @@ impl TryFrom<&api_models::payments::BankTransferData> for Shift4PaymentMethod { | payments::BankTransferData::DanamonVaBankTransfer { .. } | payments::BankTransferData::MandiriVaBankTransfer { .. } | payments::BankTransferData::Pix {} - | payments::BankTransferData::Pse {} => Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Shift4", - } + | payments::BankTransferData::Pse {} => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Shift4"), + ) .into()), } } @@ -257,11 +244,8 @@ impl TryFrom<&api_models::payments::VoucherData> for Shift4PaymentMethod { type Error = Error; fn try_from(voucher_data: &api_models::payments::VoucherData) -> Result { match voucher_data { - payments::VoucherData::Boleto(_) => Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Shift4"), - ) - .into()), - payments::VoucherData::Efecty + payments::VoucherData::Boleto(_) + | payments::VoucherData::Efecty | payments::VoucherData::PagoEfectivo | payments::VoucherData::RedCompra | payments::VoucherData::RedPagos @@ -273,10 +257,9 @@ impl TryFrom<&api_models::payments::VoucherData> for Shift4PaymentMethod { | payments::VoucherData::MiniStop(_) | payments::VoucherData::FamilyMart(_) | payments::VoucherData::Seicomart(_) - | payments::VoucherData::PayEasy(_) => Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Shift4", - } + | payments::VoucherData::PayEasy(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Shift4"), + ) .into()), } } @@ -286,15 +269,12 @@ impl TryFrom<&api_models::payments::GiftCardData> for Shift4PaymentMethod { type Error = Error; fn try_from(gift_card_data: &api_models::payments::GiftCardData) -> Result { match gift_card_data { - payments::GiftCardData::Givex(_) => Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Shift4", + payments::GiftCardData::Givex(_) | payments::GiftCardData::PaySafeCard {} => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Shift4"), + ) + .into()) } - .into()), - payments::GiftCardData::PaySafeCard {} => Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Shift4"), - ) - .into()), } } } @@ -401,10 +381,9 @@ impl TryFrom<&types::RouterData Err(errors::ConnectorError::NotSupported { - message: "Flow".to_string(), - connector: "Shift4", - } + | None => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Shift4"), + ) .into()), } } @@ -421,13 +400,8 @@ impl TryFrom<&payments::BankRedirectData> for PaymentMethodType { payments::BankRedirectData::BancontactCard { .. } | payments::BankRedirectData::Blik { .. } | payments::BankRedirectData::Trustly { .. } - | payments::BankRedirectData::Przelewy24 { .. } => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Shift4"), - ) - .into()) - } - payments::BankRedirectData::Bizum {} + | payments::BankRedirectData::Przelewy24 { .. } + | payments::BankRedirectData::Bizum {} | payments::BankRedirectData::Interac { .. } | payments::BankRedirectData::OnlineBankingCzechRepublic { .. } | payments::BankRedirectData::OnlineBankingFinland { .. } @@ -436,10 +410,9 @@ impl TryFrom<&payments::BankRedirectData> for PaymentMethodType { | payments::BankRedirectData::OpenBankingUk { .. } | payments::BankRedirectData::OnlineBankingFpx { .. } | payments::BankRedirectData::OnlineBankingThailand { .. } => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Shift4", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Shift4"), + ) .into()) } }

e9 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�H=m zobqM7QE3zEu831zRwTniz*^=+Cy63~yfkuirQ=w|c=LF!X zV^CQQNH9LLHvnb1@(r*9V(1I3ti`xxQv#r){Z912P-wZY)+fz}R^H$&db5FGce~g#PK=LRsN5xDF)JR) z91Yue9r6#!tKTf}ba^!8ik*eRtP?F>Tk78N+3uWx|5B@pUXq@l&svdIWxvn-Grdia zwOlznc5u}C_(lAQ31~+0<-vxd0DQFQwwD}kLrxNcas2u0JbovcDytKUeT)z$s)WGt zk7I)+GRG5+pdn(1x}MMKpXYbsJtxEGH7?Z%V=I@mpkH4uK+h%e8})Ht{SisgQXx`h zuDHBP^W@do@m@@R=%bKR11~%|6tuj2X*)(68~?j|JqWojxhpESyfqm~bfzP&e-wiG zpLf_wGr<0S`YQB)2<%dw|DKg(;6ce_pk&)0>?rJ?ti%&@OK@p2WhKxqw`<4=vbDpc z%L8lnDdDtc_n;#dOyngZd^gCaQ?KadKF&*-O2g9nrbf|o(Rte&+^qam@t9(&+)CNT z@G@)pbYc)rhi61&uT;^D^J9;7oz?vh6l6MdzG8zm7+wB2^2@puo?v(qstt1cV>|A}%f9f<(*if~& z#mzT~i%+_ajaMS|&GkmDWEja;f09u0-5%79>{iy43_cSy#5=G z6+5XARf2|eg;l+RT<8?K78L3kjGOhn2a8|vh-8D3fAlzNc=oOBqY;bH{DshCE48pe zt47#?w`u*dfc@g>5DfmZDDVB)p)o0TF0cqMpCcOsV zj%T&Lp{@3N7?>7~?WL3x_C@axOa+1AI%zj!J^Dyl;LOl(Xdg=NMq0Kzgg@a%nN{^(<_>;hI-^;X*oz;l?X{-~1ZgxR&z>%i z-Ej-O2~vp*zZH<1#dA@tRarj1S>vtr;Ur7pne=TbLH$@B>WmUPFtpXUO4NAZ&4&=w zk2HR;aaWZ2yo2^<&>KIe)0>ENkeL6efWm82N}B;P9rcg?mD>fexruQ#7UOqSZiVbzPzzg& zTT+H^-W;^FZnBK?_dco$Iwocq>E3~i`Y)2^y+Nk`<@NmN&!JlXc9p@KMrQ#khrwjG z{W9CL0$VG@W#aHJxOzrnU~%^4l9S+GiE5XMGxQaxl3zbSO3n^~0yZ`4BQ z1pyA(b=P@roe-=Cxv(oMn`YME?gPga`f1Fcc+YRaQ)?V=eCPM3$%}=H*GB;|ClEE6G=s%917tC%Y|?+PtG^0>jEnw0$SL@o z8>&&DNWD0ey)?jXER6$=uSU*LH6Og>X=Ab@=!{4@`me=A?80@H{s;?I*FPKuuwRI6 zx@|y0rZejZSkHC3xdR6%8i!_b+jkZV?(~WNmIKdjlE;_3sKL`8J*?;*GXwK6=~Wu6 zrJ;z}+~Tn- z9?g+LlC%lseS z#DH1Iy6*-k1U+N~oC3jN3xC%$xj;(77NUquo`Gin#HT5LJJ%+A-_)O|}6%JZ$O%QbGPCC0~63&Z2x<_OjB>uQIg zT;$#Wy1|`7y{3Y0z^ZL~D=7`I(@XQq5I-$*G^F1NnTh+LDiDuMomqCVx2kVkH#F}F zN-GO!l?qqtwJ9sR;HPE{D`nS|XYeN5;x_d}g$vRoUO9Xa?svsFBVPVbTXjleni||u zo>{#eq7<`Giu@QTX=)M|TTT7>O05tjY0!_>kz!}KsX%P2pkz(9Q_ z$AHixXX$XW`BU83d!p)lDEXTCEk-#CnH(WNe;`6<@BNQ%49*0d zw}UQ*yX8sCVQat(#MgEq_a|fc<6jQ{($l!yK^4g~D;{qRpHsxU8lv0*YPamnt+n6nBtty|*C9B8f=7mobYY#kn9`KQ zG+j&zn>Mu#;b*IknmQ=9LNMsOv{*?xwiN$UlbV7dYEqJ!z7g$u@US%P?s*!be9|=Y z>F4s5mj=0i#AYmoO22yv-%Q;8JyE>cpe;Feeqah@F-f#-U2lxvIpGCkRcqN+3WM9D z8U0PitTm$nc3u8mbZ>E^9KV8zRDSRmdH0{a%_}(~#F05an7H`O2h0WL8a)u(3(Z7D z4Y;2De)HY-!u|rzq2(4m4TekHVYyH?T!x1)Ehl;RoavWuSLV~vm61nmMjt52@@z5; zzawt!bMc^`_Lz(Ey-oTPntE3lUTVuTgz+xXle6zJF*379ly^UEu8uG-TYt1I#vc2i zKQbDBYX~mR6*OK-Yhhn>14CA7FTt}bRkM`BWrWn`lUF$r*>d_n4Pjq+B zaHM^-4z^BldU}9>wmVt$V#dw{`rffez1eGA=xzyKO5>V3i9XbI*HmBLs`Th|@i-OUFAN`grz25S8+ zVD+5{Zvv{pcc>rp-OsO4*yFbcA?+oVPcLI@R0pF2fmskJ&-z>gvgpG>oDT&XhN!pf zQBO9{0r%uVSUbz?s{fO1(V8j$o${+MjHZWlvgcl^yRIy3DfPx%gLnO7g>5-7x7Fn0 z%~54#1@U%7@_^#n3j9tFO`Mp^8Krv`bh?Ord|{<8o+J5}ph`vCv@GGLN|ie2Yp;vU zmX73lhiGi)BNmCBCQ~q;*1m=K#X~wJLW;()7SCiJhCl{{!DN((-*-eY7FS2-fsXF$yZc%SBU6rLNZA_zN6q>~3QXpG*7blKuzR7;9MN=T1)$j{%cIdOx_ zfCX*!CE1iLa-ym;dDkWqtSv$X}xt6yk>2eO2 zc}{GGWrTI_Q1`ysut+tOB2q<9JQ;d7wRLah&Q)aFfJ_5SOEddGjbqSi&E9(<*0NJ> zCbc=R?X}4HAAvp}_KW3;7J{5me-m@3G{zzAK>KO2>Svl=Ucck zOqMr6r}q^WR)!zPpp*%nZWGFP;nps32g1M}Btbc~68Yh9Yx=VY(CQn{t1+|F8=eUi zjNu@R(%_{;pUJuoJl{v0!+vJ~T?E*n|63fY5!@M0J^-X`FEv9aaP6wSL|>QOWZ$mu z#F$9-?1F2{-h0H#ENB8q5~DB?|3O?91ja*Y*rgiNIA(gw1`OD%h*5f0E)qeN)!5wG zLUS<@(cjPDWco+Cti%>Q2(QId^duI7kD#xws@>`cSe;{k4Gz7+7VhZ%eE7UH)9ftE|nsYk1J~zSk?ILzM^S=c^Z4$&{wf z8fK)8kmJzVMsh(;mhgQ^2TjFm?&M!Zz+En<1sbeAB0ON)u3-v7{O2J75(kID{lUV+*Y)Xn9`-KRn;OHK9M2Km@FgSW97=H zHdEa6(b&GK*-2Te>6!c2Q5H9&cc1W8*9!mnK_%1>h1Qh66kEX~x?J+XwTx%|QmMYA z+5KN>$zZ-@*QaQBT#*9zwPje>_4M}Sxq#Ixm+=^mfBKfgYS)O;LXGaabc1Ec&Z9=? z$`YSAGy6T}BvQ7jGOrBgXnFc#eB({yYBSbjRE^64k$X=CIUu-A1)BE*`Tup?$%$>h zOEs38_)bqOwD$SpJxQrN6FtzAg3o)PRd5qDmg!L$^7)(ul)gU_I?Vu*^|ZJS_fdGoFrn+T7!IKT2v8)n>Yj-(a}(zISBv>+JNfk}u_S8pC}S_LU2pMqad%&qC}nuT<{;;wG*+7AeUEpjyx08Fyew^ zC;Kf=`DssdRG#=%VZp7t6{6)-Pn4QX8((YDN=ZZ(@RFrH z9Bf+_apwAwdhtHbkbtkge6vz^%ugQIoitp^G%f1UYrJfzcV}b2b6j!o*`LhQ8 z0yqw$q#4yQR@jNN9{=d0jQ=JM3bz(-QNCG+34&?uDwy5cQ-K|T$XQ^_wX@m#jbK%{ zP&q}%Aae9u2LJU`-k=U}I$?cihO%=A`*>D72g4;|(ersEgjvc#>dYRDc*X(>$Bxhw zPgeUE`rW18F;pd&^H6~~AOy0(=@v5FqqmHx;u9-<%evyg3a!jLKL8oohW(m)A8Sz{ zVO65`VP9-6l{Xc2@{5ppI(jg^W!^*TkbAcl*8MxU=!AmPu8@5C{C&(&1CEXN*foDT z|L(oMZGNN1lE0}jMGLg9!bBIqLr)Jta^TOe3bPrTC(&rI0}(A1^SZ-&D~(rp$k{Mz9J7M~8v!*;++E%e| zAS0XLe2*)NJnoS0H(z0sQT&q|ZqEt6Jk((tNF#$x!uR%^}v zlxMnkYSS{R(fNFq{R73Zg?-8Xd1mIJifujTHaQ>FQQ>=EdE9!YPmvtw8#xLIi$U=; zy=Er|Fem6giVuATUL5MJIKY`LcQrh7j9!D_f&8HSEC@6P0@;M>68ay#t*P%fW&>*6 zjnCf<1Erp4ENlY9z`$nuu`IClNaJhj$U3Q;nk--q_R75vX36eGUfG9a@CPm4Gwt-# zD#yae!`xUqcYQz=gI0j4OhU&RurSLz2c-R413)z*$O7>JItPI=9>b6V?YYsrHRrPe z!@Zqn#jP;{w)Y0G4qz{$w@8gWP0OndeCRBUe%2B=VRo=4I9~GVx!bxQepdJ{95fC3 zXc)G0i&9O~@X-$;-qEL#J0-6v;KOB~^I}ihk{;)bYqb5~Yz+3BY1=o8P}hCwT1b~a z2&!$j$W=z~Y2Ef&T6?I+%1F;U)$Y_cBI0gPkyl~b>Io8)zAWDfE!GR+dJ62SpRj6x zTa;{F2m-AOK7}l?ml8Qm@xB!g`z~l_%NYiXyCr85^goQXxxH7X6&0U(83)y5BT`Iy zYCfch{L+n!-N1-fPTZnkl#cmv`nzTEbj2%=0g|)U1b?!U`?oSGcPcskjc8kofKX@?y^<-}ZhW$}-RF1JlqOy4!nTF!5p=n0-H4+~!urCbPS zW8vZv(JR^)2rDU;x0~X|MAF1?n0!^sD12Ou>C!n5^KGnH6)V*~mPLElM=2d9mk3ah z!JTdL^wy^G@gKgZ=83=UnPh*Avsj|b{CHRUoq67iwH z$w-do`b^oVNN$ExfCn{nLRW8~`?a@-5TSNwc+l&6tQSW-d8!XC3mr>Q`7`47x976H z7DS@o%4zw;YA1V95Ak~Vx`q^YW6&TV>VC(wQ-c~@p6*&K8pu@~A0eg<5}?t-+1R98 z2M$7Q8h=`l1*kaW;w7+?8l={VR2A08fU_?F1{OODDP)w-Jp4L_3ptw)&_W4dJ`rT8 z_otywq5%SM{RA=8$Qek>MI`91!H|t_h!?|nIzT~lA1w5%V!_WA-__Rx9%rS<&lUgZ zbGA2_e0?!s*!$ti)-9V$@)j1bk&zK-WMv4kl*n?Wrl|oV3T6lVK@IJDwF;Y(yu4>m z=StKW^zyf~IUcmXAPIA;!B1DJy=Bnxvk4}&2eFHd0n4vqKoFBZV1ZQZnCf0$H*Urt{#O23Pg=yNFs3S6O+^csjX%y4s#YCz)B9Lt>))>58 zWQVzT_JpY9KGyNCi2RiHS62;9d}?z(AWeexN$_7)E@`I!xKn1#WSW!*8isF& z^(;if;60`fk>h+Fz`Ke>d84#;miuUb6C@gVDwZ{Et6aapU4Akrc$25>eL7Qisk>Ir zPT2hVW1mUhSM@wO(f8u#P1ViykV%-EXO%i_YaE;UdE*Ym{bCyV_0D5xQu^ed#v7y7 z_xqda#;T(3b#mBrWtWr~IhsVoUZ@v5V;GqrAkiCLs;ZM7*LI8FPjKj{=G*j!87f() zb38fjMtpb%*8YzEqHoh+!tWG}WLJE2!aD6{9tj#t_|P=#i*2|w*F4u(JfrH8#fxtF z`fx^9tQh2Bq3bLy)wS%*<#o8?C}7q?v6M?pxAj)brg-C$mhCkTv;~D`X%9}mi{ z@_cY*8im^kKp?O5{eDCFGs^5}{2Z7t$8FhUhT}E6GJyip4gpkVkB67>2opljbOoA4oCM7EYHGKd8{1+l8q0Is9X(&=L zukMq(nd1$QTTT%)tD*6Iv~;Z_a;TbVCZ?)B!(IT-pZdgEPHg&?*h5=v6l#54 ziVYqs9u5q>Ks8ZR0K85=tFyF)k9#iu`DGpv{_KvaUupJs%>5&KSsiP^6$4$IhRWA| zFp829RgQ=qFQPhpu%dgp1a{5y&!RRpmzI_eO6CM>U~Bt7NANP>-%{>^#W>t7 z$S&c`Glwaij~r|bE!DyA>Oun;A1g5=IE^UYRFn~QUQO8Gj5xl-u|o&uZoH0_jLQ`J zNAw>ODwgZOK*c0&oP2zvp=ebuv8kVP?TIX&uKY@GnbYtUt zW9Z@9_eOnSBw}L|Dj#_DMDz<#)qk|qBacSZ^mF6%{Zq9ig~zFwT!wS>w_Ito>y=@pK(n`^1cpMCGoAPilCk9g-b^t0o+k<%3Wdze;cO$?Kt&+=eR2+;5fZ%wdDj6=jv3JW4&lK_Hqa^3 zJdqb-yU|V&E;TOeBrxEgN?Wm2w1SB+RJ-k-I!UUvyVc*Xw&1KHnzse0IB$eIV3m_- z0eWy#*#=Q`j$DgHu!D`q0hf1U_Vc1!xB()fA0+ztZrEAfPTInG>;OuJnb?lz&J$2; z@ls3UHymn4T5^PjL zF*75JgPOXvEa~MX)L%^}KYsq+u*Gd697-euU6vPElB^f%wP?@o7T_>-1uo2_3xj0U z-83(jcNDuB#FvB^=dm5pJ-=HICO+#Ab5l=uAmQ&cH=_TS7>k#i%2ZD*fjkOpY9$6AOpPZau$ z{3Fk~o8GxQqlvq;@tOCW7kvBsNHTPsa&Dy;8QiAi9{+tLTfAaD%~F=z#CInoKrRo* z^*n^ZIEmaYNyYPY^iIRo(b1tkU)JXywFK+S4>195m$&@0lT;iiFIL#uWiMBv>8Ter zm%>#D!P}F(d`(pR3LGtx(b~MyBxBDumWSyZXD;;=NlsL5XVw+qw45?H;^-{tgDsW=M$iB4|>RA{~H$ z4Y{$NfLVXCGcJoL?_R|=!E{Z55Tg-vyuxA95Lqm7zh1W{mVL&LV?+wfvou}HxYO;g&RPaO1_g-ul)2+}*1l#vkV&_`OKD!V8^V{55?&q33ne3yB z0*^CezA|}lf}vdba9|(I8JT-;A$v>NuZulkV!vNKg^E6YpWiXyn{5;l0+d8oF?)FQ64~Ipupmr0Wp167LndyY6IsXN!$sL>XUQ2Y=N7 z8_7t6U=#+^^tH2l?_VOpse7#x3*yJ=)9qU)l!QdGe@C_D%-lxm11qD&n5}N61Y^f$ zUC>#wsZt}yi#qf`{?wOz`jeZ=DaCHWT`E|Q)l2PkX;i3hGL4w?_0HhuC2O45)Re7x zg$e5Lu-nmb+y;&g@l^v#4qF< z@2d>zga?%Ho$f0Zt&=@5Z@ColeCPI!)Ns7aDuQ4GNOCZ+45lPjbF4_Eq zkRgp!1TN-t{-RhIxuLxm_bs1(NEyus)d)Zwd677T`(9k`r$`3n6yRbCX|kd7l-mL< zKI33kNw@o<&&wsJiMBSE0J;vjFM&Q%VG6*BUH=KH@UBHI)hGpw$A0vByuJkk*y>f%et`fKN;kQD$7~G6zI#TP={Z&dp57T*m zGm!d&37`h_KFH&q|56h!hn}!YVYWeEfH~{3+)`E`$$I&sa3k2FdvWY6j?x74W6-C@W^n%08&_c;%`dA}jmHGCsesKFz%#^tP})}TT>NYN;vfz1Jdsh%6r51b zE^wJ&xELB9G4ky=v-|GO!F>5Ifltcutwl|GE}WwkO5g~Y5@zTNMn$?0jSkue>IDyt z4$Ql>|HT2pZ*-oyYTQTl7@sjE0-fx*G6!c)zG?6VWPYls=)jLAEFydBm(zJ-{X^WV zX94K|8Hb;4Jx)3Kz^CEd-~RW0nPcQg71?=4r9Jgj;%`_Y>kS=w;{^S)rGFkkROPlOn81)7ycqn|5m0pl}_--A_ED( zn2KXMzsIir1>}sq-t(MNzNS)k{Wmo3U3s8gHozxKv8Q`&zl7rPWM#=GuAv)a7H8Fb z)AVF5SJ^y|ae^I~*r#4>{N49`N){Cjbi)QpR+w5f9&=2SjlM2&;?xbY?ZY|!>N3!Y zRAIMV<7Zta0-hgnpyrr&`P^aWIzSeyHjLf}hL4ez7}N8p=qO^D9+p6Qa&bS-ycaqYzre zJLYBm?foQRytY`a(8t;^k9WQ&-otUHmqn!+cTC7IF&0LS66bok+tSESN zknQ2XU=&uoFdsPQ^#U%imRZm-h8e=7?G|6QrgKeg`V)F4AQ52HHqv?}1`2W^-Q8CA97G#AH;dKikwZ6ZvUXNGqV+g#-@mF6!>9zK(#Y%ibh%q@=aKrPYLV=Fp zxn$Y342TlaIH~Rb^W*c+rJ0!Z#tu*d)xh&+GoM{u+~klid!Z78@yUmkLes;<#Ke}F zT(se|{OeqIDbkk$RH3lgMV+mk^EW*zZ26Fh==pf;)!SZA^LeD2-i-8m&0q=UMj(GY zDKA{7sByN0&n%{BCey*Mu%Jkjj{+Uu;K;Xe0k%!k7c)~ic!kwd-Q8-MuH#d;_CJPk z4^}I*?~V_ylqQ+Ayc%p7?_lPHQ9Sp&A4TrgX`Z*@^S$L74{ueQz%(Bd;&vOi*`w1# zCR*v$Qc11RcX@)4(YLKA;D4r7)Ad&0&d?alX-pVQ9<|sQ(gY>q4F+y@r9OaN<@8iP zr0(Dlvr2L{VN#{(8XjXAVs)=lsH|k1uhYJv7i(tdnP57TpdY#AoXJzxoR8ht^cNU2 zB5>rc<2CPlG$O-gpodmtmqeC(p`30FB@%G?pu8Nph|3U^-Vzr*2G}Lv%s;@T>Gv;i zgXqs4gg7{U1-4B+`3WxAPvr+8b&5V;;&jty5PB}t!UXQh#EC!bn1JBa9i*MPi(%In zyvc;DJP%vRUFZn=-htg>B{zFB!4~sIFvMw-1=@j%YwxD3bAL$n#gX5p5Gl{v80OKwn#ZON3 zX22zKe;Ifl|M@BBebnZ6#uAxiUsTiwZ@kEJOuy# zqM|~jx-So|d0RKHLwMIp3kv(pjPcFGmGlaxXNhNnOMP41cTFOGZN+`-R!oOF{pRx* z5C-gQ9`t=aL`N0;gvI48fy`I+0r7sq69zp0M%{p;>8X?dRQ`elX&UVbN_Xw;zvTHY zG*TbVHE!ww?880gd}^Bx=0k~G`jdYNZ!l!EW_xWHpM=o;PUF4S^^JC@p)y+=wSZA~ z@N=d%92Z?=6GHy^;GJ>w8LM{7Q^QKKNV&XHC5+W&$x^j~kk|03d!ALTWrk6klf$tR zGksXMFL6IQ;W&s~2{XFzNX{}>!~85vccj>vyGnE7{dViUT;2F*R`Q;_5tV&PuF-b! zfxA@!E8mk=T4En5&*og+@+wBB)YLCGAX6W6Ia6JTF8}WSJ?j0$60?KS( zPZ=$T%gDVbM#x3tHAfR#?4%-*Ljy4A18)mx77foS9414?Pqdez$;Yh;HhCzpq2C^W z_NqGW5xsMQ%AvX2(0T&^tuq-~!pN5p;~#!Hw_{op=u&nq^O>^6l&O9S*3Of1tsaXG zOpt0V*qe=1+7A##Q+F9Am_(})fZFvM*QYrdLmifX97s8&8K_J5 z%{J-`HxB35V`4hP269ydWDlZ?7Xi&yiG4=~7>KagHy4hyw!Z|bMOwE*;)Pc9QQ^+2 z;>68JN-G0HdwyZYWT|74^3$g>WoDM$j*k7#ICagFcc;pNniBmw^vuK8O>@g|fI;3s zrGRq>^p)JQD8BQry%(AWpJ9($E4`G1RYVxY+#;$?oqumbcHg@yqK_nxJqdHN62o=F zGn3fez>pc6)w+R#=L{kU;rHM-OY2y+saudq)oiR2PJ^`rQOL=nqw%`pHiCnxce9Ag zo|LhA{|RLMNt!5!(ynw_)=09{L&5LV&b}@_`P8SK9|6jbe!i20ztHKjN$(W0P3D?i zWV9Z&T}ePMH|25(^+|c^#vAsk*E^d&mNmosDBUtCVb=o+*6~>%Th(j|JNwY{ zTHv-De*nqqgsH4#uu$=$OxZC!1XE;ZC1t30=_8(G`~99mM|NH%Bixd@c>`WmvFY}; zGThiC&_GxxC=^=&Pr51OdCWTA9~7VMV%EIa-y83|Z?dGIOp$iib~?G+xv(S)!5ZU< z+4XhW=b21=ho~yjfX`d5W_WZC5gt5onXCC~(=5pGQb>-IzZi{W*3%#?I)5vh>%9BR zh5vh{QxH0M{t45vwK21G(?O86A@k=-w~rQi`!trntAG@~9%wO$e!o_8#~wWpeseBH zbM|F;YL(5xJO=U4R6jyl0>vjsh%~Y41zxJ}0>U|<*eHGMhdBqpN%_Fj{2hm|Jf8wR z7A(XfZdN2xfzqZFMB_6Orn9oP=j?|jArGFyF&14Jz&S@>w*nh>>r(=g6EqhdTol?i zp3*Z==^({&EBxq%gS6(V-4leRYztlWxO9|r27t-{b?Frp##9gfk0a$dC3R(y-N^ST zs*l{<0$St`NOXFJ^VQ5#E3^b`it2dJa~o>}tEefG&##CZOXa`nT41dPUU%7vd7O&> zoJtgZ?SuI7{E*i%AzBM{UX2PLF;p8Gq< z2li2qSx|1dvZC4Du$_sH0;&bq?ty?F_rk>p`N;Ma@%wO6AiL`y7{DFvC2F87?%!n; zula9)&J{eg8=L{URj_^~{D;d4I03_6G#DIO4%aWnsFFml11#d-b}*h_!g=ksz1#jLql94bTkoaL4vKOG zP|;N`vMM%>2s2h@`lY!owm3MX$)3-0F0^HSpR{@ttj1icVUI~9o+{zzpjirNLY zchLRR+aELEU|6VSSXSpo_f@OpTqVqX*@9EFhL7F)Iu9=x<*)|%IL&o720r1AuzJ+o z-;}8Dvpi*a)HYUHgs9Wl_jXt`(bdYj%lkZ(e8Je01aT@@JXPT)`^c{cyepo`)ukd=nC0vjhTht<$&W2?R;Y{=1; zf$>MYX3(6TP98g`#E1MghBt8;MQFZOnjYvuKsXCDA~=E1`1r#e2#j2`P2Jcw)fsQq z^76A)v8C+L(KUjtj@_AE)K+|;SKvOGPuQC z@?Gq%I^8*SiW8f52JMuQC%-QFAN=q}HYjqlW1OTBi&*kH8UMXG_iuJbSU4HtpuLiw z7-js5$!ACFzvExkTac0V%#9a4|$rLxm#h$i(CFThdR^_C3+k*PO z4)Y7$Dtgs(wPIvlDW;BJKFq%^j?p`}8n${9)klJwDUacNt8zUqveCF}j3u-SX_?o< zMb;G)UL3B;Tvx=c8(eIcmNV{OksOLk3A7(fIaIynq&rGm=!w}(znXCu$9zi$W3r2< zHS-8Q{8Zz_g+Xx3Rp=h#HTA!4Eg_~}?+-#BF#tO*EJ_|dY#(>?!@-ns_P|)Te038E z74GY+hMR8Vna1(3QLt-rXk=wAL-%rf3D6hfKyG~yN-}1LKQozoWm@k!c!h`jw+N&; zO+WyJ!X8Qq>n>e|aE4?r?HQY{uHs;&22K@?ox?^Ru-z8od8fvNp8&=L!WAYDQn@eJ zgO(@e2FERBt_3k*Kfo0+dg$c{_CNwcMXyPAQ7ijy~_$@X1}=N z{T5``M_ii%7hvF3;)=TALC{>UrG95|@9)=NF8^!8l$dbkrl=B$&S8U9AB`a&AX!NK z3)c2A`Cfa$_)opgljz`=F&QNktcB0Jg6z?G++0z{hUJ8&71H2Ei84B$JF6@k0{$GR z`rrq;8aq^>ktLi3F)Y`EwYKDR98f>aS&fXwA1&P$c^c)TNk@4}!PKz!%e`5uQqjl< zYVd2{s-NgqRQw#s-3t6IJ?LV|W>m(v#y+66C4FuLF0 zZkbfyc6?>FJ62K@%Eb|TD0-JRC2+JYDcFPaJY}$zv2MGXS0!9oi1l$Y#HC3)lq2n6 zkZFu$!NQ&@0GHc`k!ev>6W&`x3b@wS7!nE3m5*WQwKW^V>K9gCxQsuy0v_#g66X|A zdRQ3SnSO{X5syPi{sxe(q;1b1EFxeHj-cQ@=q;TB zYF~+auF*>4{q$8a(Q~ZyH=DO5>0oDxQZS=8Kq-0ZrCLMKV+AnmbJMgHq(-U}(NIh< zkjm#~?ZWwDF0ao43YK0e~uGK@>6;$o_=-z$npjtSD&5c&UHqv zdt4&!i_YFHjPwV`L|f+@mU^fgmc_v{xEu1AQMTOCLola5D6L5c zt(w%Z&4%50kv4|4;Q!HY{#4g6UE-@?frH+iYRN~sVmQZao8$F zSv#+Tg6{nLP-+u4a}_LXqeaVx2hvBWVBbnOHhG$t;4)qWm6_C=e+JSNl_y?@`<)FW zMm1&1!xB0SSF9#(pA94@;AH1djS3pem{ z9{8kb9CumUKa;sZ86MvCtG`OLw~lMEToo_W*mV@V2`RCkD@8FH52r+&lah69`bGDf zR!niM#FS&%&H35P@Uun+@T>NIVrUH>Vz@F`zv=XMkELXCaT)rziceh*k%=F1O%IlV z^)Qm1Y@$K`(K$(hCvQ>oxf)`c1m;1}UM|JYf4A8%YJPciQK09n{KbWSbxeEH>r_k< z`H{}{)V!%5oyv_2M@hd*c5)J)8B@jWdv~c!;*3dLUzbf^@=vAjNqiOJ9;cX|%*=%v zA0o-@D9#G|q&z-Zs3}Vu|1&{owRWB>xKry^)-rmux5l0i)!Bt44=pj7Gro_Us1!P? zHL+wTMOVn7X1c~qgKV!nr05!Kfh#BU)A0o8_`Tuf=4aH@__R2_E@S-j70EPxSALN` zJB8^dhHg}ak5*I4MN1Q*mY9vsO8cj4p{--9mi+Ez+JoaCZdm9!>s_iUc_PTO`aRr` zrmWG#Y$P%C=nFx;>*Eqa#i$?B<(a+JOJ{LHS}32oWYrl*4X{|o0X%G2g`nETr26YB z3ed^4w&V-)@S55WZ+Blt*GNndZ?LTA9GWz@@fqASl)a$7|4~eauwRLo75N_22$e(p zqpPu%(B_<8=P()SdB2KJb*I-(ot)=$7K2vf@E!Q5&X2?LOUNFP=6HM+m_nUWquz>4 zSy{mCDTDL+@R8G76&jsi5|;{I%3-RXIoyc6_n?yl&8lRVkOMlM_18dGzQ>mO`vNjn z8bq1QwYHDhbcP@)Io^z=GUa^-Zbh=qvQ^y~%g?N>V={_Qv+Nm!$Tb19pzx(H*WkCI zS67ewy$^Q^<*aR;hK-IdOKE&vGSBLc;4{>x^-c$Hn1)SQwgo9SYg+uoJuxOe-{rA+ z>)%LNNCde;UN!jZzBM0;uI+YyoyH?Mw;Dw=Ew;x`v@{|vfTuRdK5JPP-#=;6Y5q%y z;|)^LBeEqQRlMGP0ff_jCsq5zroHzS_L}M4N&37dzTd&{y;}?(KdqiL6poqnzQ1J? zB@rzc#XEXdTkCh;%MVerLFBv=!J0HtjMAV=Q!G@LN`<`RV_(!D@HS#bOv{XCz%~{*xabi#pe>f46Itwb7oC{bh zINh}*Oi$_BXwRu2Q)b#QwzW5Zit6DL|Jb7|7pI$)nWN4<9NNW7g6I9|?u3s_BPwNE z99$OEWD`Ih&t#oV?96fJ^i8%as^_SqX8t-XI%QXCB8{cDe#sgZ@je5*;;mLpRpVmW zAf0-EN-#elu*)3?@p6-B|LnDgmOmu8ZiqV^E)D><4>LBh=Nr zn>47l^0==QWe}@N-xiv4lz1Au+4(K9fF3d5;{xEpZDBLN&>!$U$ zIKhYTHj*{q`a#)bz3BozKwSSQL_g(`?AnouL_Z-2rCBXNNibqZ~6pR zp0IM;>(Js5+RFjithII{ku4IF+Prr{12Q~qemQA#BX5d?|NJp;{#c9yx`fCqD;C{u zD^_A=kABD<0v_d%z&@y*Oo1HbvUWnviZxeMNz$w=sL(PF8pQ&I0IteLhD{1|v0@9^X*z(%d|$*-|BfRP|5H3cnx2CT>I$0oGGEU*V6{Bn+y2l}M+e{V0uvIP z4uQRUqRgmXn{UP&eqgCLa2sI#F9>FDc#CT7V}qkjyWITawS=d`NU;$(tl4lhkpA%! zF9m~O5<3ZHbR0v@9k>mrg$5Q&f@7+DOY`T0Bn_w1cGFikmORI>Zu_ajAFmKw-d}bbXR}byQaP`P4pouA9d7~N<-gIxfIS?}VLfYYYnBlgv z0Y^Zn>f{<|bN&RF_WsX%pbJTY6!A#^6&C)tYghOf!F?+Iz0*IZ?iO}rjC}Vvx3zx} zEAxJ*V=E%E>ZEo+?8sXgn2u97{d{Y+Ep4Xm_OFIhM2^AV)qc&4pWaM<#!SlJarD>4$e%XX$o+pfNqPRxgUtnm+bUv3JlZ>$FXUO2p7 zs9&3?lBlCxRhg-o+?7N5`kcS05ML^u2h5v<_)MpHej7A9hHNc#DZ@MSDr8CVZ_Gy7 z#jX~)pIslomS4fRD8L3WPid2V|HKGKA0Fj}`0k(%iAFQFZJ;M#m%0Mo#xbW*pc2xmVSvy#S3|{_&sHc4*3%z|rPN zwpR0P$Mn&1J`SNXg@)ekq(Ft+tG`mZIeZDQ{k8BIe|akce=QDn9ZdP*ZzNO*V!VAI zXyJ|{+yZLljy#6Qmk~l64+kN^z55K<_jO4|jRF?5rEPkhMNdT@%X@3tz-W`U+p*wT zMTPJl@`7je<(&%OJ_yqzw&J1eA>mw$nfn`t6mAWRJ_*#>+Q*GlkFr+f5l4$U%F4QJ z$pK{ zt*1j*D#MwUmY=U&+6rblR;)iac=dL!O_Z!40~cnG6GX0-JxW}1DKbKcj1^lIZLh9h zUa{(q3$M6iyjE$rb34#F>;lUlp0bs%JaO%UdAN))aHgg{(C$TuA83N1x;(0x54gVq ze^YxiW?*W>rS9$PwRZ4o8%7SMF*(E91dg0Yya#siP-ys~-JJNIRPB6l8Z^!kgJa~j zn(PBo%{JgwylxE0An((_Hbe_Nhl21!Rv_P?cuG>6kWyp)b)Lnu{kUmQ<>PQy^=hv zd}g*X^9lM)RI{irhfQG>)}6Bc&_8)j>VfBw)jXk$f~zH6av8OI$M}`*zLWPG{FgoF zUjlD_Z8^CDG8io-oWKb1Ix>{60W?0u^~7f0Qhuv5IW77r zVpKVQ_o{kD#L{5&XxNt8407^&b6Y9dzdN@$s;X*Qm&kV73MJA0?)SR?d~Bh+Eo`T| z4EP+n++wlsHksw6MdL~2$fHeBvuRE-8BG*d>$k&rdq|@j$l6{ndFl@3Gs$5ovikSE zstK=;7E-r-tfmZ0isESMWoz0>HzZe_3kg{a!q1Z94!xSiXY5W~9iZkCD{;Y4gx3hWtS|R>3 ze(uKGAt;B)-k)EvobDumbZ_YRwHjh#Ps5I|d}#?PNsp9YZe_Y)C?TAJokDd+>{in3 zF_anf3jP$=2pHJM`Cd#*1oxOvoR(i@4Z37OH?b2;SzUDMabD^I#S=-Vb!}QlzV`*+ zf^9ePE9q=(y9{F71{&W9MDIh53`w@a@&(ZLrC6FgZ+Y$w!a=jlQ%ejjgZaSEd5Ay0 zhp%tmJ2s2NFyGnQqRQJkvNRZ`La+pO4)B8wa5*Pm?PuCdB-&+-{Zo|wxInEoY^SFBHC`F0i zvEat%7w>2czvSLz(r40hlQh0)*_l5WI8xzvZe{V3-F!Zsm?CQJ`DpG?f) zyO{eX3Sm9nJ6hLZa}eh3fsb$MqR87A(^@82*#WPSCe0p#mz8I`qeh0Yn0+tF(6N>` zCkYLpnAtC8o;0F)>9)JlSYRVPGa)7r)a|exoBlxrq8xT75Y@2YxI!^-<;E22mpvDO z_Mv0a7=IuL-vL3m=|1_nPxijqNC4MfyI*nBil9}Uxw4%<2PkZlR!@5YV90Qk2GyFp zOwY}{HZg_c&>~2=&$@dEC?1cD4k}??u2%Km-bZ-O;-DBX-gZ}R2X5?HYjuPbj1Z(M zS0e2C2{%_AdJK6V;9y{kSBh(0fwn(8_d;x-rNy?cAz*|TXJm@PP)};*eOib5sSt0Y zEQh7b^3RTs>(P(zq?*o#6Q{Osp7!J%mwBt~w}TzzRcg;({^JjeIAHQbOISYbN%(Xc zOozqzXB%{DOkYoS1RLt=6xAjm+#UgOxuItscK@KlB8CLHKn#lRv`@~f7La6j#C53m z=2me?`s@*X3;)Jn%p82#g#a>0y|A#`9>~U)4CC%?{spsRD7L$C>eMg=V(9muIe{&o z&jtWN__szs2FNT=rhlN%cucZB?Ar8UT!7fh5Z4>{j*AJ$!TOm&pwlIOeeVBc;>2;@ z88T^RqTcW6m;J&Hz0T4;KP}4^^kACQ$wloH`&)K_uOBI21asdX#5?vheS6rc)j6VJ z5W8I6c?{c-(L042gwRMw zF7utP3Q9rlA^aDXn8Kn;=}_T{$R~U&isR#ob8s*j`2k4KgxI{UK|mk11`Q=Zx`RE}0N!|vDBOni zrFGQopItHYIM$Ih4tqZ0_*tqyD&5@mRb)#%>3a_3=PMpXCN8QE`vQbF|08Q$%~agv6*332zg1r+-Xs9Xej6t&rGG*-*C_ ziF_^+5w7Ah4$!)Vr9rR#9_@#nXiabEp&-|DpEUZ~o&;yRf-idAUCg+uN|zA<=h@b2 z6TA$toV7rX`2sWcP{6Iv4H){ZN_v#0jz1P5mL6DuG>jFVUm)3Y!W*UE7PLvpUx3l) z{Cm{D(JCKz9P$_pHa+Pon9u(^&)ew)Y+};j^_)mq?)Giqz^qo*xFKnHHSAWoZty0E zQ$9gvK&{?8Y*X+%=S07BB{Y4}Kn0=lP)q#>Ws2oLI4#y5Cr7vKn;eLpP@Md%RVCKDPO@g+f}L8)lwceDr7GCbBcD_r_U;pKYB1g+fV%9tROq z2WxD}Me*G26!5hbO+}G5W{!Fa<2<;J{gggsDDp~ia8keInM?X6N)En%;45->AcMB$ z`Q2}psTH}!B`!bZldE`0_(|OT+2)w8IPI<9T^R4aP1b2)&id^87VGCk(r{HAe!hg) zPJc8Z>qmdBJS$bDVOFBoGikUQ!~DVZVm(f^lx4ZSdyhtKy|1(rfxoUOXnNL7i$$LS zx#=;smDNY!cAr!st8&NnGvM?;{3@UOh#95t$9^;AqRMQmbvBz3}{7EM7cii zRe2lO_x0mZF=JE(oOOU8-vOjdMffSi4~6=+Mg3B3`6UQtCUq+(+kEQP(YHA01}PF; z`O)4?&|swHkuH+~BNp)j+#{{(@ceT<1cW>xupEM|;Q{<|-A`s2;`WGHY;~*^j%Y4% z|M2V8(wl5u$SlZ29ozzXme>-C-y1-w8%P)3uc1J40oV`Diq(dERHHa^yxled(+R?xyFD*~y#-@GCZ^UR*KUo;W!>1C03*{|ZddxaY1{WCs1(_B0hO2%lYW3Rfnyi~_}$_)Lsq=~BlWt(BS zJMKTucJ1`&JK2_0mFHAp-6a(K^`Zv{Q@(=s7cDjN)Ox#wpxbs|dcO3a)o11BfbN@`VUi-Nzb%`JE z)clqTpUzBd8Kxaf)6aFqzD=7zM{rf~@tC<-of|@~y0zV*-TcwqT;u1Is?O)ptu$75 zm1Dpe%CqJMlQp^-81w>}w}Xc;Zboyuq_% z7BGw);_z+-#25Qa=K&GhanJhF#=?Hxj0|_2^<%poMZ0?}2temaIPkhlItMnjlcmO| zz8ot0S%Q9nLl)-KV&VGVH%JA337m_#|M$!P5pVC!6}awB*`kn7fn6PK=yWdj(;2d! z^5>Uz&w^PYajD+q33U1=QF;jHR~Dm^s#CQoe2pZfq@+hB<%N}amX3)ABRqG~hue`; zgo)fk$ICjkzxz(c(e{~38Z>1?W18bhNuu?b?{uUOv?)k@)*5(l+8g7x z7KsARPSC?zv#fljO>Q`#E#*Da;Wk(L3NZ#cc$vz6jdK`<|P<(m{%n6{)$w);rj^| z13dG7MSM_M8@1Qe>&-Wr>+oSD%@@36ezR60^qsk`8WzVNHK-22p3TZj+TppA_Xiq-=^E(}H@!NTU_fk}D!1)@l+X2~83U&xmiKVoJxC6? zu#gUFit1T|?s=`(|K=a_yo|9o?7MWeSqi-20bx znzc!uhkauo^5UB|6$EH&OUVi=I=P^raz1W5rG))u$kE-nQ_7s~=ps^ii_^MrmDFTQ zsTSX|@OmZna->)s!+2pF`J|+-n@|_4{kU4We-Xia<%i{?yjO+4<;ERMiVyX*ODYG1 zzB_-%B2&2|7v))YCzgNG^>EuH$ViN-v?QB9&olUp{oXYWFA4s9OIfGZbacY`cx8cI z?@*`p$;72}ycs!Vv9rk#rUf=%G(Lqf7t-&ox-p`rH$W67FL3U^_5`DDHnd zgJun#w;cNoA1bn1-HLgS7h5nWqF3wGSw?V!L$Fw{lsC7G>lN_q0YP}7%3us(0optCN#HDAwh%qx20CH=B|6ZBd3UozgOVHUzw5yIN6Yr5TtOH>4Wn}R4V|GPdh zcMKbOMS#|Dq<#+^Zsr63M=w3gQDiSU7r1b;!`!`042^lhNf?I?&9JlJs)(xuV=LU$ zcnf>`_DV5IqXgXktI4GQyty2TMxcNi{BOGe$lZ}rhNy%u7E@b-iA{nRcSAJw6){XY z**Ve_QdZy3H62*9W7`6+(?DrPP|c;MSy|BbKVjzi43YDO(kGB%e=n6 zi9-1L=U8e_-QjZHAY{D3vx^~ z1w_?M{g28V+}AK)E%aSYT1#SNarE1cwiFtMh5CBC77i=+)9{F&_vqQ!Yra+weopL1 zi$2?Rk0U)RS9i}@k8s5q-Nh3*mXPu(SQ9cjRwN#LEI{C5Z(OUy?a;XD>$z|coOvZ@ z5t7+Q2qk5@O*itz0Q+MGaR_q7W)R=gNSRS7lLMUgMVdbeQR9Aq8JE%Htpg8V&Z-X@ z9KDcO1f=0mharuLV_AZLW&a(nN2j+JG>Oso3Bc>d$X;zfPT82PeIEu(qOr6ktbMRltC4$3Y~l{Qz2BoWqb0^NW7PhOd& zqb-#qPg&bT0+|tuYy<^@s9pTK?q1xR@RtD@61B^a+|kRDf5)V!6xr%#jr(^ezv5tf zM$TXhl>HI-51>{5l~~dFOKsFy%#Lq1@58}R3)4rGSOHe^>X%zn51D*E8g#Svn&`qC zeU99(#{>FQ&;Qqe2D0~F;J~>3Cb4iv%%$~AP^Gn%QeJYeoayyTo%ujDe#0|r@n-{@ zQ6wx&JM0IC@12H;n%9v$++>TY8u$gm;l9c&{h3}{t0ZJXB(`Uo(sW|_<*fqaLDjg_ zyUz!e!-@_)t79&G5ckt@C{;vW?o(~{KH@3Vd-=0sm8`L-XiN9|t;xr_7Yeh?1??Yx z5vw7~3o$EKGaNG6Ho}P*BI)$CaOpEawKI~=BjQr;!CeENutZwxQ=zz_)52mhwxhZ0 zdz8$Czwhab>lZH08q$e%0=1ykRlA$IpMx{Ql+2SKdj9 zQ?Dm5Z?DLWM#QPv6Fyj~;aV~NSP)G#7@OdW3keHBKXg3!M!Oh&)MC~3gk1>;#I<{M)N zGw$Q?Nw4wchiF7;XqN;MKS8;pj=(~|>4qn_wBnQ--Nxk6z+l7dEuzEhFFZ`bOGOrh z=_;F7WsPhC@8su8?YLzKnbv@4>iHnNZK5zP0z~5OGq2R2@wTu{;!ze#E^3{hn;)@M zM?k8xZXhZTp`w@y1@?k5nAI#0kR$4d`{gXQ)ut(T2J*m}b&AmCpZ1*0ujB9dHd!iP zL~--{uImqT^Gz{R*B1#@)0&_bfeF-pme;!ezm@rEo&tgRvXlIF^XgQm?}r6~Gm#Yd zcbx}T@-iyk8ihVrr@9$eFAe_uuWEnA9(Xf)+53*TwVUu?1{NEi{)RE!y?ydQ`IkV3 z9nNa`z1mivr84a-+-G-G4i3y>o<8zi2YwoS;Mk!!d28b3WDRr`;sYc9%q6S);a>#= zlQ-n*qp@+r(@impu1xL-@4gofcz=ps|H|`|Du2>nHkY-12dsQ7MMjf{S))L-!nzG3 zc~WHL1GlaHDhIpDSHS~+4;!aHoWP zF~62j5pj!yErnEF?*5Drlxy~^w5cgQql}Da%aHL~UklB+Dl3J%LioyiSZ$}O=>w~? z`huFR)6U;tB?};3?S}?lSH9q_C)7}&et4z?X*gI}VA!5V^KutFA1jxoyqzG05#L2! zSSSv$q6r_^kaQUtbjDRSz5gmA{WuM>x&Dx_Sx0!G91tP^+fK-c2BC@|{2Q?_u}Oma zwZD5D{OCSv_B{x4)}h%{@A#@+cG$HwhaB1gSzS!c%YK2z(c=xyAPuyZHEf7*hM&s( zks1CC4k`udpZacHX3n0aJRU(fXo2bYO|IM#;j_QI|~*L=3}fe?hB1CBCn{^#9!|yero#o0g5SYbN4k5mIyYe-RaRg zdF-d+Y1HS=bZqN%R4;Tgz-$f&0Z8|EJ$WRq`QN7b#8Yr?7b1ps8i14#u>MELqf1V4 zY%%=nz7h{?hl;OFUgdeuaDjWW8RU3Aj2~Yy`}_V=iS)0J>8M~AN7-D46H8*cs1PYM zd)u7R@Sm&bfNgDAq-y!_u(Ji5f=GS%yX~75w=$HNbwCnl%jLsK<%r6nXgvxHg0;Vf z2gOYuMs~a3CI((#)8V!$n<19L%5BKjBIvZrmmob&Ltf(|&3gi$7oLmE zL29$!P9;eY(JtimZm=;m)Or8-y46y(v|tcMPfk~s7LP2#t6XMl8Mx16Wqlbl59!@v zAJMOY`TyC8GMKaiWk!15zdq$?>&GgEYL;66f-{ML9@pjKt^`0WSak>w?SsM6O&ige zmDQntY*Hih3N_gc?WK}M99g_!9^DbS79pVbTtGtr%zzcXbKU#)TW+l&dyE!=Q!&HA zyjJ$Ns~@KGK8WK#c7E!#$n5Q~9R6JjAQurghcEe3Ba7jO5H8aBXrtL#36ns&2(A4t z)4}@aBjgyNL9#qu{J}2-0rqT!Nm)hbLDAON*5k$2u7_;-Vl&Q-YqLk?scT`aqSsvs zb09=PIj@wA#&i3+=N6OM1`mYrIS2yD3A2mlb7alXP)IAV{{ovr)Za^Dc#Z0GfkgI# zqQb=SAXHWL;r~(f-tla{U);Fv(iSaBYot2tT5YM_(q*fyHpPe9M6HOnLe;FjRclkD zD)t_!QPgOVlG-B(F+w7_e>eI(&-Zu#;g!UFhu3w^xz78%&$woj?cipPrBp0@1J1m$ z$~y6zMRECCJvc8uDEux!rof2~z#bj|hr+A?VIYn2h72Gx#p}k4?UT@*kJd=V{>TC$ zES0}=JCm~EQ8&@`r95e2h)WFA5jK{al1HjSqY5D>!C<3XvS?CAuRuABVmJJf5_e` zznJ`zaQ^aaRC3@O-dIM_ODE47Is~6Q@tl2@mTfrLB6=wyG~J@kI0mm@DV#9;RiHp* zxujx#rRN0>hBnJv_8ci#9@4``nHGAC`K?y|p2Q6=!FHsZIu?gpa>;K2-WiL%-oNUH$cavE;(^C_A^~^ z!)BihuJ$$>#0_(H@EtoGgA*56_@8`N#P?U&OH?H1VcM9lN2Gn`bA7vHfrP+ahw_}F z&^YdsDv!H$_wF{{zHxFSDLQW@Y+#&MXH?t!b3s^Gi&8DkcKvx;EBiKg&-ZZ*@p^9_ znoZz?E-zW%>WLM{RpNq>cQLMB={v~o6Dkjtrq#T4yo=d0X4QWr(w_>mXNdjK1P_7UkP-Gw|0%wKtmvu46h@{q?=1hs=}LPk z5nq;CPO;J9DlvmoR?^>v)*us+$o}PR@}w*m5}(6f%|d+qJGa_=yP7TUf)v#N^&%X2 z-8ge*_6G_;gb%&T+GHyr|g-Mn}jQR?&?Y2P{*sZ2hDfG|F<>ZEp6Xr;?tYhI#mhXMIuq zW&KUO8w*HYAXU>va^^8UBbA<>(}(G-wh8Rt%_rIJl=IGw+Q~QNQgmttngG29>|UF~ zH3i}%HrlOV-1JnQSEs&Tue{{_fI>0oY}19vOjIXUVWj$l_13Rw8%pfs7R$Re|_rd^$HcEn279U?qje~XC4 zK&_JN0oF-ilM0_`0*E_O_EGG-jk~`kR#;gXX!NS@)-|Aqh)_|kT7^U|C{mNP|u5XW#>nI{i zxRL2GiQAr@dFY+->adzi2tjk$hb<_spdjKBu$elyKGoJk@AtLU3ST`#Z{z}wWSVty z@k>=J6u7&)gK+_?fxtZJVCyMgN`Uz_ej$MuD~gXHCtR#Yqs{&#-@=4wYw4UfbZGNS z!TuZ+lg-ClvqcH?YUIbeH|zTMjjcmWgBwL^Ir6x4<~mpBH(#gIopmn|Vp7XFp69P` z&bb~hlFB7`hSGjq?^*05f?HRRAw~S5m4FFf98(mckE%y0IiArgp)0OUC#3?*fE#73 z-B=2xg^TxxttHByRY6D=P!iszr@&?^sGe5uG8FBS{(h@YFBTbwnf8}qb-a@<@Nd=z z_*m%q?RPXrr6Oa0Y{K$Sd{LUP!rY9g)F2{YnGu4@v^HKq&`d9;`RlH~A$qGad?c6? zupSTB}#FCOq)t2U)lqxSC@sgG<_Uq!uCu4E{DFhy)2=Gn}TCx-76;l=s{Vv}hU5fI!Z zH~rW>Ql=Oz@Vc{UTw$+#5?SThiw=3#@bkOiuKd@V5aT_MV@zOgQnSMp>_}B&Z!icI zEcP=>XO8*2RnupufL+w!)#((6d$MhvmT72gqIv`=5>zvL_z}P7LG?IHmI%ll=wRSQ z-Kp;in|Bm$-~)(tb4ZMH>;bxPuHSg#L&2GgRQ+-9`PK%WO$c5|x_gQ4dew}j7pGsp z{Yc8Wkmm+nd``DwukwxcamuyN(r-FXUkmjWT1$-OFk`s)%G5Tn)G*Q zadG}0qW$h}RAcsGN>6=2<8t^gQiOq3TX4xCr|}ievQe$%Yw1M3=dKqfXH=X?|CW!| za~XT7t>9=kT^^!VL+YLDsP(S#bYw2b(;8fc_F! zHa<3Hl$@H%ba?;o3fbxe+GJB{6WjlYLA@NMtzl}xk^Pr7Thw`;}jO|$e_!SM)qr13i&+eO(%SpO!sP{Cl+Z_oE ziOpoAaiQ7F0!#@nk?bCE-)`R06KOEtZWNK@QbN*ktCe5KHE$38f{+f9PZWCe1JgJ( zS6BRJx0;rD({0Swcq-{E>JxpM_Aw)$-IU45D>{=W1D58j6sll(Cx-QLP-t5uAXdvZ z14qpz_V{5poyUCkylRFgb`p%|{2FI4ILypd0ADlXWz?T$OvM7mjjOp(w?9HQx8MM= zak*Ai{yP*aSX%Th>s|al1!$@Auc}{?L2ItNu~1DLo|p_^;a9$Zzj-al4s77h;sFE^ z5FZ3(51ecO49NFJUR`vnuSVjAHe#dVwVu9D_Lg4R*S9~AxZ6g($e~qRSjEZ1gW?sGbJ^qj1z&eBeDoZHBrd$mL}ucM$N?@ria^mUeixo@jg zZ&cpjP zJddW|61nk0arX|Y%;F|5qAbG;SQc5ig9@{c5j^PxC3kbs3QuVKROPmG<1~MUi22?y z%4&tdoYWtyUAB;9-^%4xlF=c~?-IOZ(wFah$S#r=Kb6$e+@TuG*m!-30Of2r1IG)N z#=`d_tGaWF>BjcmSeT_tHWVswDXQ{;p2|J@zyTkiX}<}$5e}aO>YU@f z0R=HUuwCA`XgoeDIk)~Nm%pS=eJts+Oq8B9Lu?ZdXpTbY>VBO=6dE{UKw6=Br3AN4 zRXECYixyfgye4>@CrR5EUszb#Ug={gu|A?G46}CSPN}9r4xS7QJOT)Z7W}C>=!O{o z(7KrwjgEC_(3R)O@)~NNB5S_Bd!t*eVL!$*_@f9mO&I<1{TE|xEJ6!$mm7=5*q?~b zW}pAkmQ}BbK)CYr8U^at`JYX@8nB=Lmy5GEN3bZeg;9lBG1XIE=ln53L8#Ahtz{H* zfG$vq-+a+g&&@j*rmOUy7XR_vr|FXERL*OPJ9)-*Zy1%u7rrXr-&q>)%4DLE2X z8lOy(GhUaD?>4R8uIi4Etk?Mcm1Um-U%+5rgrt=i%#g_D2tmBx2-KY)wE7*MPtY;6VwCw32~{ZK^qhJaWlWXuc;GHOylL z(|%fwbg2ktjr<*<9JTPg)d`esf>Kk+KM!tK(0@$j;nlqbd$!Vg_|mIGS(RNPrA6x3 zx|9fZrc8|=5d>wI+WLU=#t9yWu}?7fzX>J>_w$`K|Mv4{Zw!GoW{RISqjviL+QsAlPjr%h0^0sDtLGgjXsD=ruKtwbw>&c|&BNKgsXi;&M?zLF7 zlPA9xfAQ5GJxXkA!MK?ox3c9GnE4rzrGdrjr-Xr_-VFZ zanNcZ%(~kV?^kL1+$qwDZBunMm;(@(XZ71}!f^r*CifyC(W=TU8Kg$@UH;@T*w4a9 zfcR&_nu%pty5zxx8@LNE$v7t}-$A+#i%}?8w>>ZL(BQS|m#qOs>*LZdC4$x>!B{$y zGqHy}N!D@iscLvAd@svoaw}a@1dng>zEU(tr6g_~*l}%j*YsSGM(RUX)Z^wt;e#lt{K|I6yFeov_Kz7)V1p1=H|MWwAZ zL(xaz2k%MGoSniM5$B;iuO(z$ZS!j!`s9hFvSQmgHuvluw`vGxP#81j^*c&Bu-D|T z!|~8G+8A+GRIOrz>E-{jNkgZj#<1mTP0w8Qt8t2#lG?8ovuX1PBz5O~=$I+e&1Kp& zsXuG@O;m`f^=1J*w~4CUA35Q{fI)7PT#e5s@*g1rmZ;CN2A#OYWK~NSJQeI6%rB!t z#smERrUu08`G!uspMi7mSRTk&;Tql_Wjv>?g^1IC7^COC>JSAagGXsq-J?C0?LsO^ zos+|J>HJr`$BwKA1Hz=Wo2MYy1Al$yjDodTXA^J--Iz`96p>ZNLh30SO^D{ z^6@#$DW+#^n}~UHx$KQzHgEuCp9}|PaG!!Yyn&`aO~97}nXB3<5U9X3NZO#9N`RQy z>lK@K%vN!Ig=P|WIg>a8LeT7TX05BY$Y5oa#Yz9EBMv3jrJT{YB;HhJ&ragdwrP5@ zN>7ZSgHN~hkzKit%0mARYDuHNK#Ty@`93ss4F~|C(w@%#yhFhzXtU2b`qS*GO6& zJFK%QCu*WSXNMb_Hh0Wu;++4Gm(}}N`daN4C$YsI9-W-#dm?U-rRC&n;-yjQ*zb2% zc)Pm2kl2Pb6q`4X1apy&LivosR_GA>$MW&A8S_p%?M^rgpA^64*EU`5oF*U@(6{w6 zt+1VG8vos~mMC4n6NyP3m~=Z^%JA(QDD-tVy{fRUg>6y5B12DKPgT$Ta55g~twjF- zQ(Kv(QCQ{gcCIEAs>^=;3TzJgYKI717JCUdf;`g@~a&xR`$1qOn5}=n_nx9p8JcZ>l|e}puLTlY66WErxkk! zS^<=uU={%Tg!MR-VqlmEa9wpX852FH(r;bA8-8T@#c?&la&_|GJx#0bpxdl(x8qP! z3`f;z30cf9{J7D;!Q&4i|JRX!Vis>}Z+>#Vo0gb*Mx{oH=MFyu@8B^>d8}e@wi{0% ze=x4yF@tA{NBGNeSHec)w?b7Q<2%%xLNMc zsl9*h&_uspj(W*E-Tj?6=y(T-B#|u`u&qum}FR;ykoZr$MD;e=f%fXR5vguZ;+Eloq`4CS49x^h_3O^kqE~9*%{KZB372K zC(m6CW1Xvm$urx|GoWM<-^tUbk!2F59#+uXpX0W( zmCYx$y9(PyIT<0)zc0v9Q7>aUfe3U{3H>YB=EkL{O2 z)C=&{V;=xQ=3fyMgC)KqHC*_sPww%nqx3l`WH&=K;9YubWN-jKy&pIQ<*(v}bH#I% ze-MD5L5QEiE+60=toLirQJZt?6TKf% z^qc~)=f3AH9jYd1r!TS)KKJyV2yG<^cy@j7Lj27i@aj_Zt?ye>#(5?{=lAN>rK5Qm zUJ>Na1<+M(-?Zi@Mlrm3RLG#XtM#H&mFs?jbJ0MtBINhO7b8Qk5dh|b84}L?EvZ*Y z*6^3@Y!C8RR`U{%Eynn!uJaGMOzuRT>mfNO7gB0nJlT?Coy~C)bC$Ypm_}EQA}}s2 z%nUH8*<(4;z7;&;)w_8w;szxThmf2&S;5yrRJCqckMXF{8TjgbSFN zro4;*1d6xw@YSZy^;4CE9=u;uul!E7iJD&XybPB5o9EG>dHsg@c2DO1WNDXa)1J(` zfIUv5a|!|Es2@&PU}EXbLztQ8o|lxc;1?7)x|eFuQY>?N`$bVeK~Uf?Nf`RJ+djT# z=a#th43!MbKZLo|PeHdwNzD+Lp>TrS_I*ICW&>yL_Z*=1ulOo&`g%}s%QJR68)0N$ zor2@gB6J!^Fnz^G&EcEE`&Fpag{kicIs?K7$v!e%hN{$%wq+u<)qA%~YUt^Xzj{Tc zk`()%Nu-oS1sZH_3LF{4ZQu0Yu=iFN#1oG^i-|wU_tSHQJFfoo-wK0a!pPADn~+~D7U1*$pYH%dWeoxv+RKp_3DW6TKaYSHWx(;-D<bS4?3AE-NJGj7Epmvoyt8~aXFI(if{Dvl6yPN%@oEBNo4 zb=-PhkTLm9u%J^|!>iyb&krFE@+$6;VL!s{S3wa-4?+NI(SjL}vvI4~?;^w1Cp}P6 zT^Xk6A1{g&ZvwNg;d+xWa@6oteyljP$f5_>VplLf6IhY}E3MTCfR{xG)Js4Bzcc+2 zm;=qBXs?!5-q&~DLrM-)zO=ofB$@xw-vpgd3C~TE<|I($K6Jrc@KpgVm2z=!0Kf0R zX=a=p)C9PY`dktjK%SbkZA%<)n$&gX;xNFIJ2DI3!R)r&WFwmZaa-$saz7b#NH~d2 zs=}Sf4TTmwuqtL~h{}P1lX#O(;ZXilY!eR%v^j0gB0J#R^a!Py|Uh`qAXuUiYy-ar!o`yri_|+bEQ6?%*w~+hv+lVZNOt&(8S{ai&)egW z<-v1m$M_e2BlMG@peOO0{~f3JbLlfHdMf^iP|mPtaH>e-GQ>CO( z-6~J?eX7F#Hdm=QZP~nZC+$^am4E#rWn`7t4)u6N0P85pfvHu=!lu2%2uB!;i0{10 zq;zrs)_0c*3DQ0*qm3{t6fkLEW^g8u z+5&+a2JEd7dseJ9yYK6^r0X%Rq&DK_vu2v;H#rl<07~W<^eZ^<8Qh@;GP^{Y@UJ_6 zi?kd>sgv6~kZ3e^eSCU1^NN9YiE%0jY2Q&OsZHx5bGuCD1!P=H6kOXD0&&SarH4JF zs^*(KVtsCxU?d*Gfug_5%lGn@!jQ4xA9a8WOElQ z3gX|pRFIAW!`U!06^n=%%Y9+FzC}Q7OFQrCup3q~_%@i;o6-{7tkY+}oPx!b%j8S{ z%EqKx7ao&Hql{KD*qsc~*pbeKyN4=ckrSM5KCl4ag7p!LBlfSF_8m=94s^TXMbV^! zTO18d9OJ)}*ACvC|9IxBD1VvF9ruLAL^h`3c_yFDQ+885KmE-Te%@8dnR}?_kj0#P zgFmZNm6!20L$f>2Y^NQQqE6uR@t4nrRNg$*UOdj3dy|Bu6LB{RkM!uv#!cG8qj$n;1fc7NLw9j%Bq zCFIOw|EcDc)32du^f8xo%Ic~MQ_3v|bHpmnjji)Ph4|8_FiH5r&x6cU>|8fJ5lpcW z8wfUpzSsMK#lwvTW_Fl6Ei(!y$GG6r%2I?4&2E(Z2EWu!a(_?xQ+bh|%1y%RH=-W; zLL>_P8}$-qVy1YYkv<-r66C+U^8E1ir*LF3HJ|i%-o;O8qRP!gSR90|hR(ORkt4OW zoJAW4MD@n%w&sWkgxYgs8$ZXztGDg|_5ZU&srn5?V+10AoKBmC?N_7XVc~!qK*55X z_Z}EVm%5-~J0Ih>jdG+*QX2hCVUDY#<))8x1E+%k3InvX8_X2b^#v^6F=yNAT6nou zvtVh~lHoM7XaAjKbMQud zuftodo#T40Dk3_4T2V~2v&vG5bgoKqG(s@`_Jneo&#tz=EI9d2G+juyprl0b@|< zx-|(;^BTu}l=yh9u4Qofifz_tMs?(+X#Cm3>(w5g81%ME6*Fyg=ahT2qAmq$dUU zlK2KZ0Y$5Q^&Sa&i^Q4yp5IrEMF~9Xrk$lpX?|!z#dxVr0v8`o; zdbJN(AzA5TyI68zBKlaGL&yb%dFYiA)BCu#k$o~mDfCQ+A2p5xHtRtcu#*?@=ua<< ziY;_eWV65jWVvx{`_dTDorrG2l}(;w1MvQ2;~v<@2>9lU0(xHKzvG}rqO=XO>0G{| zM(rB40D6TiteNy;X<_z!wQp#>C$aL|YgaWr%72lWVi77cA0*yZN=99wA*e$DMW@ zErkg;T>!^)Tq02nHz;=w)RCroF_Pn!=u#DA%?nW52 z$WA7I;xo<=Ipd8e%a>s53SwqKqG?wzy5O*<7xt{)n+a62{mntn5&mWjiff-W*6v5G ze-`@AFM|4`?}A6751{5RXW z%aepATTmRu$o+4IPPDC2K+Kv=auA zO3!q-wC6pEySr&pTqACm+G4FZVg9nKrH)v;W46+tZf`Z^!!9R?l|Gaw|BAu|obA$B z-B{GEYxVM^)H(g^?(2M3oemy609Q9JpbQ2jhh7C7WNBxx5^py8ckTA?P_F=>BETJD zMX3nzlf-Q)Du!OU2s6VQ$705sV%_Xq2>VQ(AU1Lm!h+OGJ%ric)nd2BOYZ9*(Dj;4 zC{e)qXu+8Zw#EXfYP^8|nVI-ZnXJ;okDFD5ywZaMgwr;sOxni>C|?(fj~Lu=hjMUY zp)kW{(7}D-AO45PvAAdwjn1jd?rJjHpYANvZZtgG9U5!a5zIK3CC{_Y8+4)am@d?3 zbyl6zGv|aSLEMThiiy3E{rJZOx?1}-4{bS}0WC~3STQ6FP3zX7qYR9Np-U98EimYbelLU4j8V{BcHM>%>p0lIIja!r*; zEoy(_Jvk6gcVVf=bGFK@6iao}3D<w8=r(Pwfj3x6abM5xEgxrHkS zFgESofpLhQ|6ex!dNqv;tq-4W=gH~sOXA$woi>8I66<`Y6e4w%|YXDBe6R^0p_|EI`v62F{<_1e+8NTu*j%Ys=Hv98a)eKvQ5w$4Z2cOhLYxgA(7@Rj5d*} zqVikm0|xRkPln4mcZz0F+DJ~`zHm9taU0T*Dwt0#vKD1X3`1$i&u0&~fcBs;lljj) zpbx|04ex8)4s8Y9vP&$tOI3OSff%s{vkQm+$fCI5zJvZMunbRd)0YnTd2onN+4(*4 zlcqP=*u(V|wFjj!8+C!AcDBg2y#HT2$djoCi0_)+=)EtNa>chvAom4cLQm7!GvJwT zndkG0pa&mVupLz(9K5CpR6Ec2!peRA$;eWqv}2xsxfRoz^*dWDwpjXC9}pOn(_$>$ zV(1kmi7g(D@7rRP1z$g_Y`HCM$chRC-CYue*uBkH)2p3QGppnou3*ZZT}g~sivAQQ z!QA7;pYqg6NqgXVoifw>7bT>pvo8OGA||g{t1_M5r`<{!R%Z<_TX6i=$$Hj7y1cxs z?ix!GN0x!-e>*5foCa`G#_&v%7z1*U2sOu%-qT4I84nTX@3n-|i>cZq9`E)y4#>l9 zyIq&~%dg*^w~Rn`N~By93(b1bH%3XX8VXV^P>1fg8M~R9tu*!RCiY2em&X%p=+E}_ zytfwk9q~^TkJ*6FNbY~qf#BM9Sv!GS^3*mj?aF%TmxPG?teK&dK4_xqPuB@+*At|# z-;e&t!B4*H?bc^u>Qza{q&o^lrao9ciBXS^@R1V^-e|YD!Re4A zI5+ah*v;r!lz?XCbrb)Bw&dkcH%pIuq>VQkV0y2zDQ0^<`-tY8cWm&~(lPT|I5*{w zOnY|zh6;r*{hO5^v}W2`E{(49!_S|JF;+%KWx9o_FDcir`*?rUB;WncyZQ>vW$u+Z zsvdM)w=QjYA@2O|u>|_@!R-R#tKs5B=wBnVCyN|;$oO~=VGowyAMg-M6QDX1l?t0Q zDYpfp3nv=s5#Qc8{A3$!F>Q3MXO5~;%g=mmJCN;mz0u=*FqU**b(={>?VH;QruLsx zQJ67j1Hu6>InbOGw%9?FYNAIqom*AGTZvM90ajv1>awumHiMEMD4YK4iN89G(EEoAv{rvAhso6sbi z4_2;mQ5fj*=!ng1i)K1i3uSK-@NuA^+DvC^)UHnodj38}#4>$9$zmV%ZRQ(jSWW+2 z4#4=&%HRc2@Q;oVkwIttPl;Q`y<6#@Vu&{O`wLOvnuVXkAHioZXsk%zt;wgV{cGV! zj2panHRqMm=BrgH$afM!KDYmvT1ZNe$-w@ zpufD-!CRp>{|g0ZP0|u+eeT`R(5{@4;`je?`W_;ad6c<{@n;yx{chr0hvTfvae33( ztBK1mTi+=D&|ptvK9Ov~*Zul!0f7`^^K;|U+cLGXDBQ3r{EA5mdhVv`*oV20Yk6#u zai`9WIlC6X>uVu9s+R|5^*+VvY=_PHlyhs>reEi&7C zxMu?r37N>XRD>kIZFAjuE8Jk-y73WW-0~R)HtUz2oCGZfyhO*gnua#}MSd>GS}6?8 zVu?kl0Erj79EattG!|-Gdyqc4;`IG`b{e%9L~?`uT5Phx;GfKtTZc6hNz8kdx8eu4 z751b}hzB-dkz{}1!2&PVZ$!AnB{pv5yxq3OHo^|~N;VDiXi1@A>o&S9<@5(>G5gn1 ztuZ4UJR%Z|%G%d7_7#O`Q=NIcq(hHM3ZGutU8B-vsR|_V$FUR3M?V!vk3Q8Nh@@q> zlqvZYr!c}25z8>i;97u?B{eiV5smvVC?`b93bbBBxnh5ZritxWy&RT zn{q!iF;+{F<6OtI1hWC#LK{yy;nvoHFH*_;>VrQ@R;ZmWm1oCo`*!r7+Lfe?cax1X zjSpc1O|=#&?QJ7Zz^J>4WG#t+^9DC6ij(`{Y))sgxWpS#mI zpb`HehuwkIqjsCLv2u~=cO2E$hquu49m_-G}ud92%J%0ie^ z$t~4LW@|>HO2o17Hm^{sd*2H6-jt=~-2Uz`;gbL*PQH(f*uLq+dpbPS^KDshunuA- zZvatl=49+&=U?CqO%8|qWo}^QdH%Y}^R z!hC5axMG4flkmhzy8~082@L(D02H_PzoN@zpY1t93l@arS9RwWysAsAS7*^vtfgnp z1ctSnOU2HYFwY~^IN^s=($r4F0(hI|pJFAjbusFwy1pR~1EEml zLB!k#x+^hk`m+^tdM9-KOvghC&2?N8mvz8`n%SEZ`(N06Z*dp@=s@N>3RN&beR9c) z*No(I+pL8r(3TeVVu_?@BbvH$gI={5(I!19=%+Zk74hx(opQ(Gxf#AS^U5*l*`f)3 z*-phw*72)IYsJEwf(8iF4Qrm{Bac@5i^#fvqODspz<4@?)MWtTuB}? zD*iRaCyy$`4ai<9it0bA8EaPYhNk;em-cVFu34_makqjsM54-=f*c4vkmG;ipa;RUfj7>^>W?V3`QDZkB{C}&LGa>nf^r=Eq@F6yr4m1*S3n2wt9wp4{%r52xlhwNl1rGdCF#bj~WE3ap#=#Gtw zW8GJs2E)?2QfadKHyRPB&>Z;%v1TaCwvQV0&6+X}g!X?pcT8KpD9TLqyAtQZ#;!wD z-9XjB(l4+92NF556hD85a6nfI1ad$OJ1`=K%aNseq=yUzs1#*e+b_OqI~`55Tmjo* zCSSAkg&Z_<4B*9r8%qyOhla^=G|_0ee}T}?BpT4s4;)I1M)*Mh?7ty=AhU1>>Cn9H88>#!j5A9X&j=Y7w{ZL}FT7#Zn?icHYxx*&R9= z^RGRJrlZrKC&LC5P!H_sz>wURBH`uc(K$tl)6@u7V<*0`t`#%bYEkw3n!VJJQ)Ht)fy0O*N$UqZoW!c|2|0GFu z4puAY-LRZ~UzjGaQU@{O902rpCz#im=5qOH<>Kpfko&{LuHh4BPSFGo?gRTZAksVd zOInKm%Yt|D+AU)l`FGj}sBRrBsqe31X-W29{_C9)OY16teQ6CLbl>Yy?Jf^(k#VY- z#H`=E$WFaECSmpd`Q^ng3A9l-WD=^u?Dw1=68c)2Ss)&#^F_T(hd;l7-t$}h;-A;c zsJysbZ|qhCoqQotsXzI_7uhYo(`+@ZqqMB8mpxMIm*R6d9b{A7+k#`jbT2P!@s<*56?8+c(VsHR~-m=9HLQv zSS>y~PCQ#AaNEnYvS0d*y0CG6335>5G3s+Cv@aBtiJZ*f?c8}`aZX2cR%3(#&F?J%#Wl5 z#!zDM@;81cwDH4`fg47SkV6xsF?sK*!EM4iS@Q&4GwoDdqTS@tccD4kFlp=+B!3|& zo0enk%G*oFEN-hQtREXAxCA`=qk}+C_{wJ`7DREy-3CY@XUk8~46BXSNE4$Ke*ddb zQaLIaDO;w8$P1le;EGcUq3`i;3p8DS3F>6iT={&xa>Y`?S{?IhCnjH9_r+F^GD|yiH`8E>D%$bkWoaK`Mo?- zgW>Y26+|kxqiP~co4-|=wtz?i6E3C{efnHM;B7Gq7KzUB%FDHF7?8JwL~&wvzTkJ@ zj)@b*7?&Gyc-^Xbna}Cfmq|ZVU&ELNXPso)%5|=0O1F0h-CKh3OnSu>8p}#RjpR>> z>K)TA=d#=z)%4o7{5Veq1+6JzoAmgMphFE{Nq82f;nZU-CmX43`;8?iu z(WuX|OJSyWx_z=LpG)_*?LlaYTGEqQLBhxv1&Jv@MRuL5qE_$rcnQ~-!TYI>!XHvH z#dkIF?QX|tic1b$;1F>_%Kw<$>+L0@F<%t>+#h3n^k!APIv8g?UE-iWDo-_}JqW{k z2pNd3@JMhf^^3|MoMc*v0o1(SkHjJ)C(w`v|4iuaD#_yUK){}3o z?zv?v1uZjXWYFaZ1l`&5Iw7A*^ZSo;_j;kZl~mSR`rq8)$XZDHq`AYD?f3M&be0dq zuJLWKWs|-oQLj?TTU;dlJR4oYOww}dU%Ar|8S6Y?c)mpird5h& zg%;bqc^1Q&m;4>?Ztn$TgEF`Ui{GnW{q||h1M7u(b__}V zkUaln$)@W?kND!LloCtqDur>7>$gz|O;eA;l8lcZT=u9R~ z`nhnL_|5*fc(@Chq!0$O_FLti)mKj-FiLyO*KP^C<9Ru_*p)CDMqd)~MnT}ukDTnB z>c&Y09zGS`$(MR}F&`D*e~fE9E5sv^Q=KL&mKQr^trX!o#&1%C}le&$G1w+q|oPw;v*6+*S^YM+jHb4`34IP@F*y9yB2-j|59+nqO zkOj2(5t=^nUvf_P`HNWyAb;kFer2P- zIgKPD(@U6k;}!E$qVo)0puI_0qhxicZeZ+YIjo%1$oQvN7Fk+GMG@Bf=wc#$uWMTS z%-u)Al^6|_wYS!aTXEE>QqoVmRNWrb%=7Im`e^rtGR_I%k-14bzxLm^IID!^N_JAu zoq^V6(5`7$+Wfy5Mxinm^w$cx+?1V7jVApkA`y}hBgJ-rd;DQEh)6dgy%YnOw>XZ zeIKd))jKQX&<4-8BkPBpSQ_}EU7bklt9?vG%u(<+?>P>FLwfZ@v4ZD7T)qYW1giFi z-rx9c})ARE-ip4Qvdb+G-7U%tO?g+Y%+j(!{Y5c9d9or7K4Q6)qxgpx-z zkWHvSYcd(mRZwQ8yAR>Df>X>PI6H9S2h|5*W0z;lfB)P?qnz&htEjXLmZx6|Nq8x8 zofLZEF;U%SxGUHB#10DxwdMSe&eUywSx9XFr#r^9Bw6+q$Ao4HN$#TmK!@kZ^t>7iaYamOkXF!*0RuB!To0BTX!GZ(l`j$JF zY7z^J``^umj-PG*p`Hc zdoMDT*^4pN*a^oaTp7}PICBtu_!&>{T&u%)mZ8m~LZpeRVaVoCx?SZSb(OndXaRRt ziaKL!oTqqvN-^`B1_h2fabiE4wRsr8`$3i~WgN)_VO> zR*RnHyF|~lCaQu$!+yfA^L~-mXVm>B?9tqP+*KlU!94)-&S++LRmd>)zWhEX_743R z19R%zM$c+(Sq04t8u(h%;Hme?w*{K`@8P0qfjcC^b6+HcNp=KZ+8ZS**5gJ=4j2mm z%0Vggug-u!nQl{c>}c(Uj!0RKT=A8TwGAiJP*m{ZGBByfv3!H=Js-e1Y^@7zi0^@;&Xzo2fZBKVKzzExKlv^e?zOZm%G@G7iG(_*|iA~Gv!K@a{@uvFC*xEQk+%@(h{ z=d*#mmW#PuAYcl1t`W}fbb$hsr0bTcJJZQfW@hj zjjNz$!nIN|WzBn-+C|C(hD1fE2{UPKSgcdu5(+VmEO% zQCsrfY-fX$UUd4Li^l@(7lATdd=}n$7!92PmUmah?3vym>KU=aqbvCNcG8pX-wvZ3|W>dN3MT`hHtLjV;J7bQJ z_TW2m=v@hgRk5pEq%fF{o-Yoo*bauDG0ZA2fGC(*`6f&z%|y!v?*tj#^eHEAcP@CF z9*)u~uqk}=lv1@xeOUDJ;dJ=8n5cy6c5Ibz1Fc&W%S-b!Cm8J%whbPlu9NdtFa8g# zeO>^^91B>dLp5sQlhKY4Lldk`*@b>xdk_PdUi7-*pRX38{y@7Gk~q9Ev1fJqj9&`< zfz)6J&(GV5&wzj0Q5&(OYyNG>9>l>=zcBCWQB#&xOCA&|7!2s81R-EwOl(`X$AYi3 z+xUh)iwd@N>PsV&4slk+w7(a3dw0%Nr8HE5{~M$<|_deP@W z@Zc2>X;m{aJ?lwj_@BGO%lACr_W=_m_utKD{-x)0FMa{HJErTB?fHz;J)$) zt9fMgJsrPJn<(#}Ud#JPS?#gbB@P}LaH=(WMmcd9#}oQ8%=x{2t)^F3YpLZIP1X_m zu@MUc_QxttTDrqPHhoX1@L};~NQzsBwn|m1iG}n$l6k-x`Yf#8z~Y6)jml}b+S=pt zCtyPF+Weg`0bbXGH#Cg8SO!|ShHv8Gyfw`R73nKc&Ca+W=)bE8jkNhXM1fM#wO&GC z7Wk>!^w4i*v=BDRlUPs^W$$=7YxUOTv}8d^=;PYFK0TNQY*qFOlhWJXACD!Mae{@= z2cO6Sd|q85{~q99o_Eyc2hunR$jx+Un+O%{p7j-2yMQ1ar$|^IATO<;K)VZdGhEMy=@Na=bT8cEb56ohcu6K zcAc@=TSXF=V96%?&(o2a#_XKH^06unZJ+`HUNKOD0=U~2s-q~`_#*8 z^8L>|`!wkVM%SoZ>z~NQx)2j;8PHmVWiDwO!~qX~mrx3nn<+_(n#sO z+}I-cLllSTO0#g;*r%X`{^qDBDurcI8NZ)Nwpdrk)uHX{Pq!FCC_h6=^}9Fdv&*ad?0(D)WB$Iz#;K0>0b4%4PJgNfYQKrooqu`j$VRFi zT&FY@$-tL3$PY~VMRh`)%`~J)9R({lv(#9^O&(%v)~`#8La&t?zQW}GA%H;H56R28 zd5!+6VVNC#_=MHm>kXVoZrz8lgTyq*lD7nr|2Y288KAS_!Iz~_x;MN5`PBBw|3K?s zvc7o2r})hH<=>L>Rs%eq{?_F3C2(WM1>Yoo*`MtryLn>D0QI>$(&`siV4p}zu$2wl zydm{V=mI`DHs^|vo_gQN6K&%#&suE~gax5@Nq^Ne7EGl*H0sThq3r3VQyEL^CggQF z#%4~zZ(3bI+Y326u)FO~Yq)>mb=JqDs0Dfbe%c*d6ItrT!kYZHRMW9wZQ{e-^4_=K zR$BuJ)B7fu7>2zngA!P%HAbFNao_UIQ+8%Lye@yu&}SMUT0}=3|M}-F=IyTs_L##G z;@t-|!n*u2>z*0ZOyqE!C*gTmdDsF^QgMuhYt7fT*&#{)z5*}PIdQJY1^K>Xsv!Dz zrMrUMVaFrHh*OBwiTkVR;I;yu`lx%k`e_)DhgitQkX@z$nz9hrXc3IRL;Cw&tf-x8 zfN-*C!cPpJBgEHf{Hgu`a2IDt;+?5e9ZpC$X2i~t5L}x6?NDS7jZ4kqyLS8CBi$40w^9XQ3yXg&Do79P+B3vmqNsMl+Qa7vMF;Pt zWMR&g6Msgm=(-&WSI5(2c-B)EghT?I+=$7w!yXlzRW)4`Vn`Fv%d5`y8+@-dWhOJW z$5S>OW`(^&>)c;Qo8w7=RV6DeqJvF)v;Z{&0c!Z8fhQ+HaXtL6jD*>M@kPL&pX?I8 zo42#7PJRNq3j-K7BduXUxw)Gil8*KcmG3FxGGe|?k|p=Dw*%5?LbmzB?~eNDYiSCG zJU3&$(F#?2saH_J!B#-iG4$lAF_-qYucfM7Ke0j?p6^obQG?wFrpjvQ&t>W{qcOvx zy1DA#;=bMf$~(?=kzwV^9Y$Ll`DD|Yri{%ZUZeHULy}v`bv%4Pz~RbBbXf?oU%e5s2oe1BgM}eZFoA+%#j8)2 zYg7}(DL2zN9bJkveB$P1FWA^qbf-erYa&>Cq2=OhWyxp-Ft&U&+$)je1=0JNZsrM2 zI}Hc&$UrUSX&ncf$DDHC$RCHW@h#VPUS`Enaz2XRXOf)D15u*4*UGf=_$ClsPNsMx zz_oL$ZrpVO(S7iLN@^qIuHD(FAcJNMMN!#d%3q~v^ZRY=({oIvO&!lfb|rm4M(mKK zkKgGZ483Fb^xh=TL@wNgR>1?1YOrK|59RcfqU<&|V#EH}n`&G1oV1Vrj}(b56Q3S& zb;x#G#gdrohV0xVSz6fNh#ypZ|6sRR4YJ8cH1aJG*`YRTge|`-tP5;;zwscSjwLn> z>l$}FlXg<<69xnqQDjeq(ruH|_&xhq6XC?Qv$Nw47%2CU4rck4qHq0o8Wh*sLYvte zH*8lQ?Ic_KqZupQ_VI^P_~D%j`yCtZmns*|b{y;1sSE0<>@~$R9F3TU&R%3vzE+*{ zE_?eW8)GO#^)zL%11HytkQd9d-J=qQ_V|dWiF`9oZ~fXs^}M3_^^w;#?ukBEM1e6| ze=1r5rA3SS>meg5>LKjvX0C>~j!E5hw}cBKUh zS8<+gQK=!^E9*p2>mrHYrgm?`DYaNzJ1MYPZT`j{ z=@IXA)O_`&WgBt57S00fuZ(XXjE$cuDdT%7g1k93_!xA(eYGNNQ)C%rIEDr)B+UPx zA|Y1wmydDK2Lc7c@{xSF0of{CGq0oui`x|bL<748x})sO56b1QR{vTrDpqEGax9r0 z6EFR9jFAYO>F?G$3$o~6JO}+agt-tH4)<4q4gQ)vJ@4Q0DcFzC!e_>6e^}ud%2@Kr zXFj&HG71Y#XS}*^k*UbYAyrXiJAaFwGPfPcrje(?3f;jq9qR`S-6|o(J<=@7G<2V@ zJ)B;kf#ZO=afylpc;05UJmNfv<5v=7M^ z-{4UJ2s9RB}r{G#{Xq!o1d$IFN!#QUJkczb?}_}7(bJQ zv;7r@fhG0x*N>f->=58|jZ%9eo%@noutkUa)^p2bZNl`362)G7V8OZ~`u5$?Oz8ta z$D!vRkbO3Fr2W^G@Z7h_Izg6M1@wAfqG@(}SgEm{=|v1eRs`|cBqoy@=}nc2%|hOL z+cDpW*X8-Gjj zM8AM8Io;N+EyI?!jf9Ix$(aX!^RVCz-S+%##C@w+7oqC4ezBm+rtn61hS0)2`Tx4B zGYEpzagNuna;w2bvHslEVOwRZ{f8UX`k|-QL-@E~^g20g`=p;M-?Ict$euUfkHeVG zqMX+(>2kt$g%VY`XZ^=G5y#yMjt?k|)BKrfX-v4+Pr`E0HGf{<&zUO$kNZdN4g$EP z9QabZwGjcPE0UA@EgU)ug{c}l{S61u<#@2z3rVC_{l$!(D~?uw_Q)W(j$xDDe0pvw zNoMcxPgW^)!$G%adZ;BPb|h2TqW1WMOtx*!^VI2IeaKZ{6NT_@`$sqGOoO+-!Bk+? zD1GI;#$Y7Usj;OFAbfS{ZZRC_iK#XEtvJjur5=R8K3=)A!Ne3W&}YHY$RQ{50ATdF zF|H)QN6_hV?v6}4FpvfK#eY)xu@az3_?e~PzWMvf|9QVDCTbpA3BBa#v!X(kMighY zkVT!5IvPdK24TU~lysjNJ`Wwa@VhuEIIc#qdC6-q?^|)SF3-stUmt>lt}%c7Dql2G zouHZr=~(Vb5xi=A@76^kaO!)W5I(GQkB9=O~0prkGuw)Kb@S zlf7`6@oizd>OpxUU52ykxR@;Uz_#DGRv&i{$FJ~Xp@WPSU|;CjW%=!Cq3xeMW-#eT z+@aDdl1Y5UpIUSarvQ$h`{RS&(yJ_~jJ=;C-tvc9=aPPpl(WUqcSoC5xnf_`>di`r z_R^te9oGJMhrPDpsQ%tr(0guVWQ&1l5q}4+{pL$L?lcXID29s##t)t zp4}56cg^^9#aP}aI{(;x1I%s`QlWY0lH{)#*ny>7&*eTH)z*V0KjwoHxJPR2&ULC8 zCDASo1uf)AX}`F@)v;dym`@mGnM=BHUo;g-)Q%TUb*ucTv68=k=&b|Z61hV?@wxRF z-uc8U4~;h^CU;uWF#u>KlH)AvrNdaVgN+-8Q=M2*+4Ap5t4dS4P_ z&|3~epOU>!c?3Xi%=j-{=$Nff8R0wi5I+3YEFUxNn`7#Hn>oClnn*rBBr@mmyvs%? z;!hQEMp@K0Yu|Z3=%9C5H3m4$k19ya$cCmL0WMH$kRf zI!YFpH2YMA{qQ!m3Pp9jw1Nr zQdXD=zhT;zvBbN1P3fC$ZdlxhOy;jOQsc|r3l%pBhPb$bD=&-k9$jb}l;$EaA6x>d zy)B@iRoPfsaBwNkAUn)_!ff=TnHC3#-a7|h+>ly%QX>4r5y7eh(*(XVAk#?|jJpbo zoUpv_#(KhVJyzB{37y|p4a3G*Wdn^|8gQbgmZLyl!#VdCzZT!A=~{4chiWpjjG+?G^l$FS(o+kBBcUH)Cha3pIBFjcRbI z45>@FC)c6@QIgGS5nb27o1$oCMyLChHM1_9tXPYDKW2ekV!dSE{Bi7BjqLb{XvR8| zzWJZKx*JzGMs!$RC#d&g$u$v4)s~BN6ru`~CF`cA@hy4C^f8~H+-9noa~a{ zOW?2WU_2MS>;F19wKT1!MK|k{t;~OEDvR}`Zz+!jM#mb#o^@FRX+a{zxA3Jyz%_)LR3tY%xAWIe#4T$V zAKEb>e7WJ%sA1zeGbr3>yvi^S`+LkkJTld4q|sYbW=rdZw@=mDFYjZGuNlF=rep;l zd@Yo>4`RNK?o)DDz8GfwTtC?fa)79gCfX1dUkjtSc_*a&0vDYPo>b|=AaL4FT*j>t zFRj^v7VoJq9}BB62eZ6($8-Fnz590iRS&|*vpl@^2@6D4pSYSDptBwrPGTAEa5&|zip*@qN31=1d0+)U-J=eE%#+^ ztviA(9V_dyn$HpETjd&9!zcUwUP2Z&)46jExpug^8$XG#ugrOk&bT~m#-XOs_inzg z9KFJJ(jn=S|8l24fO!WJ4d}?KZZ6F3vjc$zDfkHS$|r2#<12^cd|rH4momSTC0Xzy z_ucfZ!}9+6hs<%Zh=3;Za?RGAToe#lA=Gimnq|`8&4Bp(fr?iUa3M(JX9!{he~ZRS z|LE*Wixpr|etP&IgI3eBOf#_zxtP)1L9&|}O^Eic&ju6zkYX~xVg0F8Ojm~QBR-R` z0a4WzRO%yeE^Qc#Uqe9d!J%NBczj_-?yrG4K^rrXyRWmHi0-x;{+zbCvt!PJ`fj+S z+2e9858=VQQV8=mw3CNsYSesVhKA1Q#-ZSu5M=?LrMDBV%C)VEu`C~*=y(4|xT`e+So)u%@|72-oUD;9{=6E; zd5-C*1>Kx@HRsPID8vhm{$L(H$a18T^-f|y`#~NyQeiLWi{x#k`Sa{327y}a3duS0>H{71FP`#(G!$>$Vp!h(z==$%lAKt>V6W@kZ0Id`JK+3H@>x z*j>02TnP&5$$Y%1Q1{602P#kW3pBX(3ehuK-lm3OmJ>KJ`rnpX`o5(PE2thu_4=c| zJxuhz=;qH_QB2m`qR!=wf~nmQ9s7}(FeMnL1qL*7uCsii0_8K_-r7q-V!vD;UqK{^AiAS>=%=n$e zD0Z@1^{&KU5qp-4-a|IG7rp;5Iz@6VKz9_&9F z4j+1&=zaW5cq5xKhee$}ok(#N!`MN25BKp4J$JMB+=m+}E*Nyq{kOIaU(b&dYSnaS z#jWy7kOBL(wZb0rHR}n$3X<~{pV#}D)eE6}XPX^fYYwVx%&q$@6NZikYCo$EuGD?8 zU+UQ4uU0cL--x5=$hg^<>4N44%3c~Euz7iWV)`4hbfT;Qnuqp6c}F@H$$KY)I^dr0 zQ{eZqZDKsb5d|F+&KAH(Gp&DX!ddNBYRTpwh{NtJ#x#qsh7} zhQV=G5@QdXt7;)ZtdBUI!TUjy-eFs?b)n<79RM{!jABkp;{PV{V9AA9-o?eRvOog@ zMkB3)FU{{0U}Ks&y0O0n*HrMrTFDR1t!*#Zoudj$K#CfG!+~Tapn(r?CjFIxgVe5( zZm52dQ*{sQs^Cz8IeWh^VMtKxc=qzLnrgj-_pU1 zY9<%P*Jn@zIR&-e6=CgoPGAMVM(?NS^R8)e<36)_aIVX`4~QU+RHsg`)}5ha$9DThCSvl z)=U@R4;yEz6|N!h{pZ)uHVZB897-^qj2Qm6q^*{K$NSY0*Bh|EC%-Di{M8vUp|e|* zn2o&!16dCj^TfKx%*v8jsEJkOR*60=I#H@l0( z3w45YW0OivCS&=-gjVusi1T8c1++6ht-agFA3L?olWL}4-CW3_?_vPdt3-#Cby?>P z&zM92;roBXwOjQ?Ahf06Bm6@_q^P30yeEFwjr`m#y&H+^)%WObdVFT_0aoS0%}-_# zhA}59uX^we@@K+{`2^ER@oWfcKit@lD&9WynYP-R8U`nLOhDtErklW1zi-JKAu#C$ z-*xxs^gRlLW=4V+D2dP5zTy`MhbJ?~hxhv5M@?fTxC#(F_gtLHO?p{*_9o?!RGnyg4Jhd+KoswpMH{JxI z2Uu=o@~6ugyg12xpnqQ1&CmYzih!9EtAzXF`_m6DK)HLK^{mXb|K4h~G9@b4j|HJK zQ9ab0c1uS)oD1{$F>HwiB~msHMEy|}oXCcxC(i|eQ4&jKLPd8uuJ0~O>sN)CK#T0e z=W|g_t@|08ExH^|T!~Dp43*)lUV+}l&o4U;Wj(g9&@&JN-Vk#=Fk~)GcTQGgPx%s$ z12*Sfh$q))lnwti>s+FFJ-PsfwR4W92W~vBv6fGeYdlyrLVODUjV(E=k{t z<-e5RPtM^fT9Bh_=;5IJu{GZ$p`qSocr$SMkS3sF$LLQPVkUymE8o|vF``GAULJMO z0V;zJ{=@&LL>0E@kZDv2$c0FrllB0Zg3PYNGD}?bXIfJvB#4KCzx>l`x^0BZB7<(A z8O+&RUr##m$~;^-{7*ms6YnieymS98SK!DWGza{B!CF1C&9^gTbv2>qxfLSYAC?>@L+`yh)CNQh{6RLd|Q@PS@CGs zTDBYnG^ahKtEQ+1nyQkwP9#HI{0?gN4yj43+r91yM9na(icKc@tYamvsGyVOgu%Gx z;)gCgzk0Sgmn$9f3lpfa6QPQNxzV1qnvW^}?rQO+{{P`kIVi!Ps@f~n4DUi3Ge=V2 zrh6k+H@do!B>ENp^eC(>xr5Au1_$f1SToyR>G=8LKSrbbJbaxg-5P$Ncv{0kY(8IV z@mFsG;%CTaV?vp2jD?*2yiW4BrOGcbhC}+y=kz(A*JGhB$*~UF^;f=UbTP_*YCM>B zpA_=^lTeb*1FkVlTw*e=e)M16gk}MFx1-Q6M3jC(q}hX+rq$~ zE#+8~Ld#SvOxGq$!}&}-zZAWg)g-fGQ^MIMcbE-O<8ywa_&XyJz)>hVbf zkyZNtAu&$Oc$jqYku2nXh5icrH#_;6al+uEQ2U~^nR`$q0X?#cV71JS3G+smPQ(3Z zw1S{b2~N**yYuUgd;u|}QREO&apoj0f%<%7dr(gW>|ig;-*@7Yw8MS+ZBVaZk5_h+ zIpCqr^dxvaA_(+Sk7P!~YEAlIR7@9(UtZtM6i-S5a&aCIj{@EDf$5#vvoeA&lASKM zedn0Ky1FrGhk=VrvDvOpEL=qtWYf;m zkhIA-4~Ntb%uY3wKw+->tRh|>>Vk~^}|O6MSnw? zImv7=fi-7cvHB4cw843WkI_s>y=&%PUwKU-QM^-bQ6IApe*QPH+tRQ)`;{wvUu77n z&oVGe_<9!fe7DgeNDgUMf6{$`{z*c{Mox5lK?l=buW)N?ub3X4zMSx}?Wo}r9&|sdj#?%B*5i<$OBw*;u?U&xrxtc4>{eF7PEKoa zg&P|zj6ozto~M|jue66h+c2g;LOEL@efKpQB>e%LJFT6+A!Cwc)#ou%Sb|NdYkXF;J9uC>b$=$St51n7> z6hdAYM96ix-5;hfcN)`wVZNc;)bIVI!L=fULuyXpDMf4tCB!@^1lArlGAF-Vy9egC10t+x0$doZ5Hwqli?Og?p(V;vl z{B~?7zWYU)R(lmWA&=bamM0yTR+*4bODC#S`5M-Q&VWAJ7GM?IT!Z+|f&~1I^o1=k zVqt!KxLZ_=D%i60Uyjuth8JNJ-#vo-Ib(Xi0az5e!gEq6qSw0 zuS4lf+aZ8|oR^0ForGJDhd9|ENxYLaU#107DTljc5V1$rRReNj0PB zi=aolzz$}kMldNR*lg+l(`Ogg1G!g|ysL@*IeAwpx=XRzZjcp^*dWT*t>gg{a^Op# zE<9-E8q+-EB2z`p8DUAdEmpNOJ`A{Dad$1hufmR1iig^6*&1NP5@wcr)k2~N>I?W& z8N4iVXn#aLXxASO+Z_m&@GrK%{F%qJtMuHrN%Sc6_8}rB%OEQ@_EWAxj69xwQ~4hC zggx-l1xmCW22(mc;kt#f?eM$Prj!;26FVFUD&eM$V6lNH+FGF&iy1VC%MQzO4tZ~{ z!C1EVuBfzf1h6`8sSp3W9>bux)K~4S#O2UXAMD?d{SGNlw3730VBvST;Y!HX^LDZt zObNP*%{eF=ZZXbMQ>K>=oZa=4{*nGr>>_l}^rqARtNd#cuUhlRPu>}g5B(*Aq{GUu zPyUpcnIx}Xfgh$z{um0bDZ6ieBAGJst8_;HM|H{&*4QjL(=wiDXilKR335ewAE}ZX z)BUz9-Pc$6Ra~&^q>2KiWImJ;Oj6f>9+ztCy7H1}0$Y5X^47($#~Y8M{t zN*T^y8Y$3qci9YtSUefvPj_u~l%SviVO3lmRPrWa>S?0AW4+ZqV8abUflARa}Ivdb?tW2rG$*lODi#P8ct%Ag4q5{O2k?O z*@Gjfv6wUEwor*!7^2X!Na|6b2KxKE z6>}9FDN9dUgAi3!S?i>aSu@l z9Z|Eoe2+8M!R*!ttzFXaE}zT~^a(0w)C8}qs%>hRmN^*A7KIcuh$gsumC zxb_rR4+U2NNe*TI+O@Qwzc9mu34(ykh4){DvJ1G*+v1 zmjcR(vmf6XeXi^@n9Bdm5xZ#NZF%s+bsIkzWRNN2r?b4A znTW{@D5WjF0D9S*RI`}^%z3>d#(Lb%ZAT~XfLr> zGY*i7`380XIqGp~kS*=AK#|uDtlG?1uw}{itk{ljG2F)}+opofm{0Y_D;qPMuLq#d zdGjase<#tlH96&{a4zXN7m@c^rN+KnQ3CRU{$eJkyxKz}^Xc`oF8O8` zDa`!=FG^?Gtyh-;9P`>u6Icd2#}->a$9Co5C_PnaywSkB_2mPLXp>rJDz%sfCq9S8 z(X}QmgO#A%N)Ioo@R5GgoswSukt%vyW*0hw4KU{Uyh2=OKswv8xR?}z%cmM$X8H`i z57Gf`fmeP&>m<|th68^8U z*L2sslBpU+|ISo+&yEPRe!JBV7V(2JT%<4tgJFP<){rCAxbH^}b|pEQbL&FE_nXwA zUL0-p;?CVw!%6bKu`~nPCZcaNB*l3iVy*VPxZ-Tjmh_3ro8>ea&7x~z*GF;`qK9${ zENp%_r@FCnhN-SgEv4+~<0XB&rS=+d@p{2hiK-F8%2ZB5H=Y*Kf`ehS@}|NlJU;hW2W1Ioc1-yVcRQ0nV3_f}|ba@l=}F?ImN^ z1IqkI+>B=ljih>VMQ~ToO1SPyBo`jRw^muK@!AiXG!m6_+$CR?Zp|bwHfQ{i?BZt| z^jE@k=l_A$)C*+G7_vjkv7g`E3V%;sK0@zush4h=HC7d=QH?mX`ApzVSh6ICqoR(! zIIZ5e<}*>jZ0KwDeqpBWJ`@wxb&|VZdQwwU?|JQTYNp59W%{dR0;+=`LPFzaE4BOa zk>6W=V1WCm$^J!NzF2qCLlbMP4YWJTeD_Y>>MB!jq9ItO7OC%F$_Z!&Xo$Do4AP8U z1Yf0k`IR_j#~L>qJWE{M1Vi*P{RrF=0n2JZF2KhA)I_D-;?_bZ7N!)<5+4Iuv)VqM zJ${HMFe9EE&m1%=BUVh1`qeHIJRtb56nt$uD&ZJVx z_S@A|-X)oyJOaNW3Is+MIPH{Zy-HfGJJ5BivI9d~LVJe_TU%k!=~v5FMRuMHsm1e% z`-}+7ka*e+!WQbiEdjx{LD;6jnQ9ZC_eclDsYv5Er&85)rPuzMK%71p3;6S1Eok># zh5hD2C0u_;s%D%h!+6Iea9w`(!Lr47zZ(Puo_UTs%|{jC!{R*~isHN0*U&B+u_Ckm zvgs2K7ZXUdV%5uI9M3v%;RKr-(f$=@6&vj>NdY^PWm6C`$sO35ocLDwWm2qhjwW>M zWOCjtDX@0Oj^pCB(5R70L_z-fu+E^>;eNyQIQ6my9Sv5Z#|kWHrdXF^V&K?0C;%Wn zgsN~VDfLeIwdzos;OY-)GkT1v99Ie&bHAH0<=xmwly-3Zdkm+f{gbw#? z%aBBOt;f8OW0AJzFj@)85m4?^tzqQ}EnxU;^ENV&)iYt5I=;RUTl^p_XpFM5XW#G} zjM7tRsgU%0(v+u)tor5!AK%pRKKI&lly-zp7|FG*KrfAYRFKH4)>zcj;5U-~>TnDtk4df6^8{-5^}a@4Y4XQi^lY1&+5Vl0;}5Q%XO?je z%tI1m>68AxLj5Sic%FqE;_i{+(kRoFKUPKcv)H{4y+Bv_u?-GEftvj{k30su*Vevn z|A8XzfeEdcrL9g@Xgp_ToLy${i)GC6VqeOzdI+x0qXPbu3D|+nrGig?w~u$4Y0RZ9 z4*C^U`t6)kSv|p_j?lrM1P$IZ?0sOq=a2#fIlRyIOvv7y;B_czY< z=R)ju@OMok_lw*&HV`nBEU5WH(XqE4W~~Uu^K)N;JWH`8QWlWWh=q|n>Q>SaDIQ`V z(MD>Cm^KO6Xud*N8y)ClHkCtSulI-aOvgMh-rL}Ls=(bQo{=j5Z&Vp2NoDd5dx6*O z4Zr1mOPA&gMh&$rjosb@-eIFfsexH#?U1-qk24ju93Pf1AX16O@lq-%OXtj zh{v-L59$V7g@3gAsBsg&^mr%%Ac^%_s?ydQRQ>71W{U#YH)iIsVBfiGdwyR$94@+4 zs&V=nlyDkL=p35E>Umit9~2_KCZtf=BiNpo#6!QLv(QPXC%f*s&Nb+G>bm7X_|yoe zEH4JR)PEMlJPmjBRY8#ikSDT)W}UzLrEsSc1>=dI0B}{Qjf=}}ROndzl^4XOSz=R# z9UaqylQ*i_KMw4Rsf^0cvSw;TX^Zkq^V_yO z_SD`5-%YDqZDO*iH;lR@JR<;!fes>|`*UV=uC+LI#lffOm? zOS#2KXnTuuv6~YXE1a)M4SH!cUSU(zxR~m= zdIi0TIl-)m^>EkNvYS(xnakLlAXCX!BswCH`6wEe0BxFQbjL4A9Ufc`p|Kk` z{WDbcwNbH@X5au|OL%Tq``99AY@OLWxxc^>ZYz$!I}`3D>;+=>rBNV5;drnAfPdd-1G-Z01g1cJ_y%tm{t9-i zggb&s&+r<0Kdodcb28N3g+?J3fWG5*UFLec?SS&AW>Y8IRS-0ZM|w;~#DrYBnIQpIRSNS|{w9Djx?M_jnUE?t;HLB-psv9?( zoKUuZ1I(D+@m^)nuoP4m9$(g#!YOx`I$z^2^=-tK=hJAE#}83vHf<>Ql$Ur&!=Vv| zs$Z7-oL4MGX~&itNz6u^zpqw&IYw$Hmy3DAl3sg{AVYh--ZJg(8~f{44-+zS4E zMq=*>(8n}81T3%QQ(*8Lab*8P_|*Pg@^i!zSH5#ObiDMF z^wV$;IgsM$Y*kqXSbJ-$+7(&WlA(U4TtBw>sD>`|zV7>%0cc=q)a-46GEXUObu(jT zGjSHPW-w?e=Sdy|3(~_-=;GahK2=xv^f99<#MNqa?2t)WlQS-F4QNqPY@R(VsxcWY zLxms9heYaL7}-a7s`=Bg{dds!dmkDz+EQhI#*p@TKcZa`nO}I;iKKd9UM?ucDeQvPvfw5Lb9i0y}Hk16l>dH00e%XU6W#Rb|oIlGglw zu{Zb`t9};_UFd;Vpoyx8-hT9Fh{i)pY#FRbK3cQCL>zt{#AibwZt~@+<{daKp-9eDUYfoXn}c_YxW>$mQ%`m?ew0`<{t{LaIaD&G;kNT zHNt+rN8Fa>o9K&iZ`#j(4F-Rc1!Q`=)q$U!{I+_n=hi&jU5+1AxUCu+i<63X!Q%Hx zm4U`Ir^zY4m^MFShF2Gg1BfXc0OX!ktUMFmTgb2pz%gapy{WQiwGN1GkzfHkF6RRL z*K)AWJV8-E_2rAyghI}oz_|j70|sV ztZpbVx(9DBQsSfNAhpN%JmgNv_E+*Qth?x71KOWff%vGLE9pNrx(=dI{c}WPX_IDF{YqA?hHit4akVK_ zeQi50a_jAYXnLpG2dyP6vDDeK7Uv}Zw`NKT_u8H<{5Z6))^yLLbklT($qzZc-yjdm ze|bkPr$os5(@+1J=Q65jnxIVY@SNa>yHI>KB8w<>3+>Ie;*P7(5X<+|z%_WP)$+!6 z$9M)lpV6H#>WYzQ^0X%fvO#0wosi#T*Q24kv(I9pv=l{uFk#WP-WJHD$s82rKZgvg z&C$-m$(WMq2c%rxKSRspR4|RKUzqK_QW$IgDlmOyCjG{r0uAMw#y7a*o-85;YqXjnC(_-JD|T@B4CjO zRlmb5tLXSe|_;Bks2ydNcEIdUu z7Dop%QiGso#FbiYcFI~8fVPxe!Q63?wi;W7)q7I&b(`I7%w|Vnl37>tmaf+n%6L{n zB<58+&frWVsYP=`ZLWd8Xi%_FG~)!S5BM8HCI%#bik9*h^dqSDWdN$?0hD&&f9j7_M<0aedfR_(g!EwxU5FMoG-#Lo4T`45 z%<3Ct=QwBnG=T{m-RQE$rBX@0?fPAW^dYe3FpZP~!Q&DJ7A`j&M@XC&;-r7(dnqs& z6!4NTTt||F|8Im3|0RguX!YXbg05Nmm1TdJj2cq-p*A>rLu8)VpG_(zW2;P4wU^R^ zO278=I8->NQ$BPz4)2q^*8#x$9-XXS;a@y^~LjvRYWXdP@HQ5i;D z?I5grcS?GrsOi`IU*CB4{`i7BcRHsUQF`NcUU|;X`HYp#>o!-FTx+*iaE%a3XUI1do z4rl!wcAzaO#V=<*g zCh{KnE-YXXuwi<6E7Nb48=nY1x_}>ix#SMUnZ6 z3BMx;^t-Oeq~A?o_|IaAMp~|Dp~83)#!|8`10D!bOJQ~D`r|kScMV0o#J$7x6wYA= zR01ks-iWG?rdK5uHsn&5Lb1E{ol^_(>m+Ut6^bdZK$d_ppNgm7UJ?if#k4Z99)x}jotHmgaHVXIfU_;Ro+k9~e<6Y>ye)#R7ideDfITW9vCfF{ z(@Oq|a7UW&hsldeZ-tonB~h|7=Ho^Yx0G4<7t=E&^fb?}ip*89^X605*!PsY^Vk;n z;!PFj+WUIdyV4KZnzrlCH@%;=?PqmQU8}Iw!^>COjNdGPaocKDr^Ie`abdyMqe&pE z*+s6HnKL2`p1*HwR_gMV_W#G#d%#oufAQm2g-WP|B1u`EI5fa(!l8|w)y}3jv zBW3TsviG>gHHr||zPR?ju90!cb;s|mKHuN(|M+`9>Pq*)!|Q$CXFSjIJP$Qrt*XwX z+;$bL58D0e&dBpthO1Tyxcg7O83(FjO_UJF(mfmvypkQ%EU3NKum-gH521x;9u$iwO2W)67%@kY6> z%{tfP><#QlJ9KIrXJNn^8c8uyVCAnYtu!T-EG&Isz)1F_=vvf+-`%jwRoB(bE!&&O`VsTk4xxn#qz0rq7?%vMXQcI@qaOB|qfpV46=5JkEV&+T z8az4xvgm}?-;~Gi*?m(8Ge)z#dM18iJCPK|u4C9&nA&#}h=oRXciakvD24w6ka1CZ zN=CvQVGR70ObBH^RjU-a_cBe2i~V?CK4E_i+1EtEkU50?$N2N~z!m2~SOtvK(A9xQ zB`w3p83Q;hsNy3j?g0R-8>K&3C<>UcxDSAn1zX=74P;|-hkXbWfFN5d?LeHpkcPm~ zFT%X_X$v+egAK@UVb;I`w5vK9c|ctrH_PxGaw80i?$~arvZ=5q*|Jy}G{$sOwoN5^ z&u7}>=S>#VledgjjcLkx!h@?`B=)V2?3sVgj3}#CuWiB0PVT#GrApQ|o4W6I=mg{J z-nvT*NH6<%XkAeUA2$1pQF zFE%%83QXMHRo9QKH<#LSF>kpXz#15`(f7MWk3w0FYCZRH0bdLc_ulua*Yl7GSYZxB zOmS?#=`gDH$nWCf6}c<0GQ5@UjYdN#fM_#n)( zxYf~XN+Y@bGFFgppZBFU9mLjM$>UPPYUAaiF{}<6I$Zelyw)Y?aJ!<{O!eShry{Td z>-Qv)e3|)g2HgcXVA0fy1t!`MJb|!1>2J0DT-nUNbUz{3?uXV>SsmrqvX$RcF6PQ< zVN;|&==d2v4t<03#z>AWI+d|w8(pXCHA36ot>G${f7Ok^YTQI|G4{km&kIorflUWh zmBNcIn4cH=)`{14RHfwpGPL|RUjGB|Bgnt61Kp{)GFIp*3)=2J3Ac!md0*(Xxg_WI z?aG!#Kcqmq%tY1yn=TzyRg2D_&L zaNGS}vFtdlL_S@1KUy;`JZif<$jHQ6P8F*rML#lED<9mvx*t~5cpdqfiQ6c6_^>?j z{(E3MAwN%4_*`wZ0jXDZ@s_ZE!Ac>zL|}7@XM`-Poqf_=rr}>v@=Z7K@kp6IYPg8 z4raf48muCm~!x@t@p^5j6@RB0l@;-`f_O;a^fey=TsMebQ{lL--@E z&n-6*GT~aT2v90wBfD%mAQlr^xR0-&C=OFS`*hLiqTR{0Yom>YRf2=6oMgKGCIi_o$*XKTI7E}QD;!O;#J=U)VK|A04?n8MYqwx6IAASuJ}V}VLw$dp_p z7s2sNC|J3%nghWC&~46kBIh zvnq#yscd;)UsR3OPs2@Vl_c%0-2{l~(*?PVOND1rHVSoe2dxBAR*ZkW}2+efrR1-s{H)nl&JXe&OdJDjVq}a15`EKW>@rJ80M8D(vm~p zJ`&h*^qXAmA?)a!x{|h&QE#R60RBsPy#J{nYeb~%5@^9{^78F-;a_|%_XstDxF@S8 zITO|8cqXggf0?PaX85gA)kEdz?h^etg2DN2JcIK2+tOdITl9nkM9S_F^KTe=*eE45 zx*-a%d%ChXcO4DY5!vKh#8&s>CM;f8N6)PvjT>jzFS4?-dW)O^f1Sfe&V3;}?eFx3 zG4h`S#h+z+wK$8FqVj9u9msHG9kmss2>kaUq1gw{%kH+gb?~us9gLm1N@NYit-7 zvf#a?a^P$7(SB>On9~zBngSLzzTPS=#^G+JylR1h?57ZxKQ$n z{G?k|VR+_Wo$}8e9*vG?JWmRA8@2a_u~z4!+cQE8=}B{UX*-eC5LP)>#r?AZG+*YR z`W`4?krZi^`V^vncdA~}_Q-3qvOD<@u^0yYvW}#!$?E5F$2}TKS+xud41bGZ(7-K; zNjaZJB?_3wPKUb+%UJ`)q?kw+)0;`^{E_1-_(qz6kxbSU^#?RdnnTKXIXiBCu-q#E zHrpNXd=^UW)t7?B)ED)xGKA<^uILD6J{u0;W!sF6=kAdV>nt}u_hq@SFzDlA@$QJo z9hC$vmiXfP4+WY&&Qa18P-sABij*!Y6*zv=&T+*IuhckQal5W7h1@(dS)v(~oUijl zJ%9HB7hl8AIDV7(!s`Bx{^-n#hlxEC1Zy56#-24TPO{g}BUFZ1jIe%P?{|8#ihK_( zd(p#r!3$=6we5qQ;rL(Y8heVK*yCrz5bDoZSav>{woJ~(7*VAF`(}r^PwdZiE%&9p zd{ydD-#Uy`(ml_n#7ujm5V8^S)&3+On?{spdr!`w%OF4Ru%iH(a9pCBb~!TH^FiRqz5e(yFat>cW$%V# z?}mTjKBhU_n?2B;A~kX&-W=oNaFGrzbz5AbMA-g}GgDTXJLGCAdhqu>*rbbD#;!b%geo(h2c?6r; zDD+irxNEarlSp1>&&q9&XgFJ0W@e3=miGrPr$+mc=obmihz|M zKnwRVq`BoBgof|#oz?+^xd&VvA@eKxkZnI@0t?X}D=-fayIez`9@ zUvyP-Ge=wKEG!=FhCklR60&iMd6@Qav}bur=Qv5-KthaOWi`g6p1tsR9mF0JfA2Dz zoaN#rfpWEIBc_o-fND$Tn~kDhC4XnIwSYuirfTC}W7^%pNc|5)d6dcod6G86&{t|M4_4`X7k|P*R+*OWbQ!*3{lMmviLRxEHPkpRHXiZ_#fl3j<7TfatB$g?+WYld1Gt+rcS?#c zAP}amli@4RVVm6}Da5mF&R51I9@6ky(JT`?m#WDlc=e)U_H z0>u197k!E04GpM|;rJCO;N`#JG<_;=9-fstG&DTW?r3|4pZN@%VUtBS-Hn?!BjBOf zQ;!e4x1wLj`+3%Zp{tuyBhBD$IctLPPmSsIInD)@-QYDo0X|xgbGx{KXXF*e=EeN9 zW|7A5wWVEG@7L~Gc`6Sg7DXp>1)p4vjWyvAeqvVjIOo97OZcTqFuB1Qdzq)Q$Vgi} zrFKDUK`pyrW84OAI>I%unitRC^hG<5GFUjs)9}5=&^OlG%2!jT;r)VoB=*|XH$sk* z)2Wkxj{SHew?w^VVroArGe6CKEDCH}i@>X?$c+jlXQYR+h^LgLWc0^9f+c0tJ$%NW z)U{AWdsis8&pQ${v!<>gh$j?Ag==Dk&>GRZ&Dqz>2}%Vf!80>8T;Y%2 zg_@JC|L?_oYTc(_kk2^rhnMH?aG3~W*0ftRjx1Verw2G^>%>UoPrvFyrOKN|E;9l? z`vX~;1yM|)&zH{59z##AC-eCryd?mYg@AoqZ`&87g&mTvxI?QxY(t(EU~=gFl{H7j5!B3BjSG?B^@kuD zA6x9&*xOq?wbx%y=jPu+XmnDI@4Hr7U4?vFjr?*e4M;_pCG}8CDB{E7xvcF|zQN-M zz|RC;P3)2BvU`b)dx zclZ@g@tHVIcG8=eb9Z9mpNx*WURJxT0XDv_)mj1lG2u`#p4U}XlcLG2xyfqN2Evf z2wOwn#SDyFv8Q|fY@Vnl-K4$q zQp0ko=WEUkVs8D34BXE|4Hmh9%tmY}j;{6fhXMHt_|$h#l4Ck@(W>PRSN^(fz_?qG zmE>wN7PplDi{wvtf-ro(i>K#2X)+03F|6NqR*lQ}JQa`ek=>g1R{UC(5`|&?jFC#J z!UL51fa-+|5690GX7?ohHev~L?0*;=*Z9GJPFH;3YoJdLIOZrzTkjXJZF$lijye#N zPW9l`nK^c=)?GmIA|Hc)w8boUbhI~5oQpLqC`%=DJX9)cR*(3)-PU6zi8{u^MQL&48 zjJs1ml&91a3i1`)-GXkS1aeg-{l@w`hkbkzA8mu;UJ4M%&%{eN@=&9pZ2K&aECUO6 z`V(j$TgRSDWZIkhbUYJwM#IfW9RXfAxY#@BC^_2skpH!4RLCyTiGVq_6xi5C9OmEX zuby=MA?!TfC1&UQjwYc;Jn6H%JqC!-jPK6>6ueRAie`$i5Wc|#dc1^_T{yXYo+V@- zefd;YJRk~q0DujlG&9b9f|`zk|EUPNGSN~n3$^mf4K&_^XUd@Ll__Z(kA^5?p2*eo zFn!p~eemL{Pxj;N$0o=ftz!GD!i?X|pDLN`urm*H$B3AlzLFx|7J+ACHnXX8F(v9# z=WjF1yv^Ci!JxQ@tG$V?HrSw1?Y5Xt<^ElqunKx;wD!Q6^Mbj);2NQ`7$6HS>Lf9Q;3fcf&}-$;De)%%VgB zCsrc8wa25zTlSkqsv*y!ACqQTAw`i7cyur}0BAiA3gLY_(kw0;gXVoNC}@-Rkfhk( zhM=}+m&NZFj!zIRCPD}T*WSz=nfPJijQ&HIPC$4P?ut&N3EZEN8JRqBR=oqY#0-x z(?m+Cjh7XF1h!j?M=XcZZxC&nk?f%BREQ}O=OvW70`u*UYuGbyI@j7Lk@>XFi(U6* zsgG2O?H|gp*=hx%`Mkg zFH$+t?m!fFS0Blb5bdx2*@!E3OeZz;ocOY;qH9Vf-%*aJhg7p}boc5gXwjTC(W83- z88e&iXFH4Se%>V2=umF=!0W95%+E^e7v{qK(C!@QlRu=%p*`GiulI1@im=O&Yl=Oq zx_{iH;|;xj_BJ|DpFuU&FKolFFdTSLKqBma8Jht@H9kk%mTab~|ES4k_jsGGFb|lr z*v7Q>2tgx+u4_;!qiX#Z8kM&LeV48VM(?V{-)4+rSP!3ayJ776DphV-)N|M@^Wi%c zmd>Q%8bO9oqB4`#im5fTOV@(_5dk&MyzS4uu5hOA0h?u zk#0;h8W?I8{CbIJSNwiYbG9-sTjENXRz<1nrC5IR@w$FU4yOPQHaH~l-!T<>a(THjj=gGuWLj&@u(W+@XeMC9`7PJD-7Iu}axK0^3= z^+yMMEETZEAjWS9Qa~73R@dB0&?R_n&=hbT*^fZ(4Np{j0Rq9lYp(x)pPjyvo43id z(}&7WZB2%&@tk4;#e9fm{DOU4@c0JlA&`QAw&uyLT|4%mfrQ_G_Vc8-zi5zlfh80m z)uH!m=`kAQ9|g~Hk_>ZB^ z=gaJJ`}KR@EuvHkAk#Glrk<0Qdy9R-3H?TOKL-a54Qjo0u8CYft?Bxd08yq8)JqNZ zzE2F@EA#KqiXLuO?~TT2jAvwgwXNk#6Lnhr;JfQWzO82}cBj1Y5U7x$1z5&utYPW6 zBk7kITX@041{#pzTU1C^*-@8+t4r$gBT12DI@XX8D(b^;=F#G;+^}(qiv0nJOPymQ z&XID_3I>vM58K339j@itwTLU@=GX?_q>l;esbruA`Dy%0tDP=(NH*`JSr7 z(LM=&kkkO$I){i@Pc6t2%wyhQ*?VeAbScPRdu{o?T#=X@xrRj2=D_R(S?f!DVV-sh z#f^cut0A>hsv$q1tnpJ_VG}sUZzfVVsmI&{yxcR+nP)ORY@zFc zh79nr#!7nz%^%+7v{Oa%BhvQ#wIf~TxHax*6UiG8hGL!#N)b#r8Rwfj&*IMZb16he zLf_+w!g$f|99Mg0IF=HTt9V$zBPGg;v1clK(Xyx3Q8 z=9@dScdNnOsPx+(S?)2HmQVjE#lU;(tW$$>dpE^X#Y_!Yg`;babuSz{b1_I@y!%-) z4ET%8U&sh+qQBRAT^L3xYrzk=9i!bDgunNcvKtMyalYCiLWC&W?MY5bQ9E6IQ>aAO z7AmFkcwdEG`$tt8T6Mqh`vUv%1-nWglmrol#L?XS=n!NeUb|T7)3!@=NSQ%#h{kvV z)HkPB$o7~MH5~napM+EUpNmWW`)_$P_a-^KfgIkm)ilY|*)ndamsb~^f=n3m1*k&Stjb000>&c;)<@N9-x$I>#BP4D0 zC_&-h)+IjtZXu(u_@h1Iaj(#X|&2QeDG3Ul~)Ut()Z)lNIL^wqbl z=|S@N-<^9J9==b?U>H@)v5m1apqXFd#_V8UKVDRXQ2IggyA3~3YR<-w&f(&eX65nK zyS8=@?x)i><;iUli~P&PLe!sH8NDvn;FoL=PRWDjT!YP}{#>;~LHm&}B*+)neBP+p z+O5&Em6*ufxg$PzTTPu`@^=M``-B)&^YzazS~@uakQB1)X78cFjH%$pHyF9<*biNF z81aK)px#fFRdMw*eZDUXEJsQfW7X+p8L>L|)-bqFTReibtWb@WU#rQvNxQsR$u{Bh zv-aBXdzbo3L!$JIGu&y;S_Fk2y+GbQdhz?`tmI(bI}=9xDAyLQ9hc6N@|k5$BMB1x zVF6Drx!&Om2HYBa87MJIZl8DV?yfrQR79OUt%x6A`5Q(+sS^JP{-Ag%`P`7M_cR4( zZ5NF>Jleg6u_YwDa+^W-3sRT@EV$ zJ~WF)NFTSlvRV*S{|v(FkW_^dXFudRc1w?YB#$sYAKgE$$$5hZGJ3}e7DtNsfNocm z$y*UN?Bdjagt*yLpr4@UCx`Q`CiNUlTcf-BAc40f(qT+3?icYZ`QYJx@4i%U)T~Ui zP24v1_P)j5$$P}Fg&Zpg44o)&PAH`e|7#ha&_AcnlTvDv$i|quHr_>5u#jw+V~PIj_njnlVke1qIK6JZ#?KI4=lCpUEmCPfFb+b;$Q- z1ySb#`-yT-V+9T%<9f|t=WgrKMg7e0PsvHSe-Q>~J) z5q|~4FJLy^#VIK=1t`>O!-iHQFhuU`y^INAEAOZF<@*JCE$LK5T&AIbLw4cq5JwsuEQex1VbF9*ONhh6#b|2+^hd8bS@d zt9^+lHvuQ0#kfViv2Xa0eu>&)vJ0sPL#Q&{zK}DVv678c8S?hXGJy|$t^9%QTxE${ zuD^e3?=sD&7{33I?<9HO@)YSo?VACAEdD}3dKP_8kNkDMxPs3+aK0-QOw_ZO#F2L5 zYdzo9bX@cOJWcC|yDwNz{~~4!NQE4krW=E61xds+Ajsi7@u+VbNeog;mE=fs9m3N( z|A^D;9TWCTGR9{!_u}y{y zSmTnCZeU`qGhk8|P3b%JJIsVY>zqf@0c4-YlRNPQ=%5$LL5vQ^2Z}HHo&Pf`LQcEf z=Qy>-2n^1MXg8HBZN($5KwM+mD&5?@q`qt$3 zJ@!1BNJ{wwsbAf_50eU3(#xlH%8$r(cp21rkShb(`)D1$7$|jX!-t6-CkL9!sJ@!+M z!v3>M^=_k(y^CG_Ee?rU{sIqePR910);ZGU_nD4@J&vZXy@;tR&z}0#I5^daYE`#2 ze+MD@Nqjov(bz<}4cpeg1bvh1|c8;d=95ksbieaVRmlrv2ei8QK)ra>R0TImm@0Rh4Imj^{uW?V%slvqdMA-JIPz+*^qwxDV8`-81CDG`X@wO2qYbWG-&^P*oh~ zxncDaeo(ez=6Q$)w#HGmtjZ*p9k6!l*4bm!F8<>YMX~7#)_CW^xDgM&I?hk7W6cmM zuR0#l+aP+pwIdF<6X5lZtR}CG2ZQO%$$B}}0AR!{ApQS|8VFO}D~Dv43G&$0CZwVE z=eOUJA(xsU6TvQQYVG`~;Z>~t32uNR0DMd;Ezv_|$PT1`sXK}-#-CImz5Mn0t?i$? zJThevuXf$zR(8C(*Oo`KX`MFTQ0-ojUFdmjiU7;E-rj~U(^6yahDQAy(U83dKHf`M z{w(Zk`tqi{&=wQvv$LKb-T~HUwA?hiH_lr_1Cmk%M8PvsXxSI*g9iADUsD9+@m{(} z7*6x}xxD$`(&EFCM=$yGBN*YTpP8WW=zCQWOw zv;3FJaUtp_7;{$%F<(5|aMCT1*324xvw+V zmKa6urGU4M`NeF8eAW0Dz{?Y@_2T??x-HLKY?d?F}(iS6TXGH zX@EyvfI!H5m&{dY3i=3z9lL7OLe0xf